@brainervirus/workit-opencode 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/assets/skills/workit-babysit/SKILL.md +3 -1
- package/assets/skills/workit-plan/SKILL.md +14 -2
- package/dist/plugin.js +1014 -339
- package/package.json +2 -2
package/dist/plugin.js
CHANGED
|
@@ -14,14 +14,14 @@ var __export = (target, all) => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// packages/workit-opencode/src/plugin.ts
|
|
17
|
-
import { existsSync as
|
|
17
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
18
18
|
import path25 from "node:path";
|
|
19
|
-
import { fileURLToPath as
|
|
19
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
20
20
|
|
|
21
21
|
// packages/workit-core/src/core.ts
|
|
22
22
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
23
|
-
import { existsSync as
|
|
24
|
-
import
|
|
23
|
+
import { existsSync as existsSync4, realpathSync as realpathSync5 } from "node:fs";
|
|
24
|
+
import path7 from "node:path";
|
|
25
25
|
|
|
26
26
|
// packages/workit-core/src/core/task-contract.ts
|
|
27
27
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -7590,6 +7590,7 @@ var decisionSchema = object({
|
|
|
7590
7590
|
scope: scopeSchema,
|
|
7591
7591
|
presented: text,
|
|
7592
7592
|
approvedContent: text,
|
|
7593
|
+
displayed: text.optional(),
|
|
7593
7594
|
contentRefs: array(refSchema)
|
|
7594
7595
|
}).strict(),
|
|
7595
7596
|
digest,
|
|
@@ -7602,6 +7603,27 @@ var decisionSchema = object({
|
|
|
7602
7603
|
actionRef: refSchema
|
|
7603
7604
|
}).strict().nullable()
|
|
7604
7605
|
}).strict();
|
|
7606
|
+
var BINDING_QUESTION_BUDGET = 300;
|
|
7607
|
+
var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
|
|
7608
|
+
var workitBindingQuestionIssue = (questions) => {
|
|
7609
|
+
if (!Array.isArray(questions) || questions.length !== 1)
|
|
7610
|
+
return null;
|
|
7611
|
+
const question = questions[0];
|
|
7612
|
+
if (!question || typeof question !== "object")
|
|
7613
|
+
return null;
|
|
7614
|
+
if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
|
|
7615
|
+
return null;
|
|
7616
|
+
const over = (label, value) => `invalid_input: ${label} is ${value.length} characters; Workit binding questions must stay within ${BINDING_QUESTION_BUDGET}. present the item in the conversation first, then ask a short scoped question.`;
|
|
7617
|
+
if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
|
|
7618
|
+
return over("the question text", question.question);
|
|
7619
|
+
if (Array.isArray(question.options))
|
|
7620
|
+
for (const option of question.options) {
|
|
7621
|
+
const description = option.description;
|
|
7622
|
+
if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
|
|
7623
|
+
return over("an option description", description);
|
|
7624
|
+
}
|
|
7625
|
+
return null;
|
|
7626
|
+
};
|
|
7605
7627
|
var findingSchema = object({
|
|
7606
7628
|
claim: text,
|
|
7607
7629
|
consequence: text,
|
|
@@ -7628,7 +7650,7 @@ var workerReportSchema = object({
|
|
|
7628
7650
|
}).strict();
|
|
7629
7651
|
var workerSchema = object({
|
|
7630
7652
|
assignment: assignmentSchema,
|
|
7631
|
-
state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
|
|
7653
|
+
state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
|
|
7632
7654
|
session: refSchema.nullable(),
|
|
7633
7655
|
report: workerReportSchema.nullable()
|
|
7634
7656
|
}).strict();
|
|
@@ -7668,6 +7690,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
|
|
|
7668
7690
|
path: ["decisionId"]
|
|
7669
7691
|
});
|
|
7670
7692
|
});
|
|
7693
|
+
var runtimeSchema = object({
|
|
7694
|
+
createdWith: text.nullable(),
|
|
7695
|
+
updatedWith: text
|
|
7696
|
+
}).strict();
|
|
7671
7697
|
var taskRecordSchema = object({
|
|
7672
7698
|
schemaVersion: literal(1),
|
|
7673
7699
|
id,
|
|
@@ -7681,6 +7707,8 @@ var taskRecordSchema = object({
|
|
|
7681
7707
|
status: _enum(["active", "paused", "closed"]),
|
|
7682
7708
|
closure: closureSchema.nullable(),
|
|
7683
7709
|
progress: progressSchema,
|
|
7710
|
+
pauseReason: text.nullable().optional(),
|
|
7711
|
+
runtime: runtimeSchema.optional(),
|
|
7684
7712
|
assessments: array(entrySchema(assessmentSchema)),
|
|
7685
7713
|
policy: policySchema.nullable(),
|
|
7686
7714
|
policyChanges: array(policyChangeSchema),
|
|
@@ -7696,6 +7724,7 @@ var workspaceRecordSchema = object({
|
|
|
7696
7724
|
id,
|
|
7697
7725
|
revision,
|
|
7698
7726
|
root: nonEmpty,
|
|
7727
|
+
runtime: runtimeSchema.optional(),
|
|
7699
7728
|
writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
|
|
7700
7729
|
}).strict();
|
|
7701
7730
|
var capabilitySchema = object({
|
|
@@ -7723,6 +7752,9 @@ var taskSummarySchema = object({
|
|
|
7723
7752
|
id,
|
|
7724
7753
|
revision,
|
|
7725
7754
|
workspaceRevision: revision,
|
|
7755
|
+
createdAt: utc,
|
|
7756
|
+
updatedAt: utc,
|
|
7757
|
+
runtime: runtimeSchema.nullable(),
|
|
7726
7758
|
objective: text,
|
|
7727
7759
|
status: _enum(["active", "paused", "closed"]),
|
|
7728
7760
|
closure: closureSchema.nullable(),
|
|
@@ -7793,7 +7825,7 @@ var taskOperations = {
|
|
|
7793
7825
|
expectedWorkspaceRevision: revision.optional(),
|
|
7794
7826
|
outcome: outcomeSchema,
|
|
7795
7827
|
summary: text,
|
|
7796
|
-
decisionIds: array(id)
|
|
7828
|
+
decisionIds: array(id).optional()
|
|
7797
7829
|
})
|
|
7798
7830
|
};
|
|
7799
7831
|
var policyOperations = {
|
|
@@ -7885,14 +7917,15 @@ var writerOperations = {
|
|
|
7885
7917
|
...taskId,
|
|
7886
7918
|
expectedRevision: revision.optional(),
|
|
7887
7919
|
expectedWorkspaceRevision: revision.optional(),
|
|
7888
|
-
workerId: nullableId.optional()
|
|
7920
|
+
workerId: nullableId.optional(),
|
|
7921
|
+
reason: text.optional()
|
|
7889
7922
|
}),
|
|
7890
7923
|
release: operation({
|
|
7891
7924
|
action: literal("release"),
|
|
7892
7925
|
...taskId,
|
|
7893
7926
|
expectedRevision: revision.optional(),
|
|
7894
7927
|
expectedWorkspaceRevision: revision.optional(),
|
|
7895
|
-
reason: text
|
|
7928
|
+
reason: text.optional()
|
|
7896
7929
|
})
|
|
7897
7930
|
};
|
|
7898
7931
|
var stateOperations = {
|
|
@@ -8045,7 +8078,26 @@ function requirementId(input) {
|
|
|
8045
8078
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
8046
8079
|
import * as fs4 from "node:fs";
|
|
8047
8080
|
import { hostname } from "node:os";
|
|
8048
|
-
import
|
|
8081
|
+
import path4 from "node:path";
|
|
8082
|
+
|
|
8083
|
+
// packages/workit-core/src/core/package-root.ts
|
|
8084
|
+
import { existsSync } from "node:fs";
|
|
8085
|
+
import path from "node:path";
|
|
8086
|
+
import { fileURLToPath } from "node:url";
|
|
8087
|
+
var packageRoot = () => {
|
|
8088
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
8089
|
+
while (true) {
|
|
8090
|
+
const parent = path.dirname(dir);
|
|
8091
|
+
if (existsSync(path.join(dir, "package.json")) || parent === dir)
|
|
8092
|
+
return dir;
|
|
8093
|
+
dir = parent;
|
|
8094
|
+
}
|
|
8095
|
+
};
|
|
8096
|
+
var assetRoot = () => {
|
|
8097
|
+
const root = packageRoot();
|
|
8098
|
+
const assets = path.join(root, "assets");
|
|
8099
|
+
return existsSync(assets) ? assets : root;
|
|
8100
|
+
};
|
|
8049
8101
|
|
|
8050
8102
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
8051
8103
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -8096,7 +8148,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
8096
8148
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
8097
8149
|
import { randomBytes } from "node:crypto";
|
|
8098
8150
|
import fsSync from "node:fs";
|
|
8099
|
-
import
|
|
8151
|
+
import path2 from "node:path";
|
|
8100
8152
|
|
|
8101
8153
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
8102
8154
|
import fs from "node:fs";
|
|
@@ -8186,13 +8238,13 @@ ${ownershipToken}
|
|
|
8186
8238
|
return { raw, ownershipToken };
|
|
8187
8239
|
}
|
|
8188
8240
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
8189
|
-
const resolved =
|
|
8190
|
-
const lexicalRelative =
|
|
8191
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
8192
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
8241
|
+
const resolved = path2.resolve(lockPath);
|
|
8242
|
+
const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
|
|
8243
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
|
|
8244
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
|
|
8193
8245
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
8194
8246
|
}
|
|
8195
|
-
return relative.split(
|
|
8247
|
+
return relative.split(path2.sep).join(path2.posix.sep);
|
|
8196
8248
|
}
|
|
8197
8249
|
function parseSidecarLockPayload(raw, parser) {
|
|
8198
8250
|
if (parser) {
|
|
@@ -8339,7 +8391,7 @@ function getFsSafeLockConfig() {
|
|
|
8339
8391
|
|
|
8340
8392
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
8341
8393
|
import fs3 from "node:fs";
|
|
8342
|
-
import
|
|
8394
|
+
import path3 from "node:path";
|
|
8343
8395
|
|
|
8344
8396
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
8345
8397
|
function sleepSync(ms) {
|
|
@@ -8430,26 +8482,26 @@ function canonicalLockParentSync(parent) {
|
|
|
8430
8482
|
return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
|
|
8431
8483
|
}
|
|
8432
8484
|
function normalizeTargetPath(targetPath) {
|
|
8433
|
-
const resolved =
|
|
8434
|
-
fs3.mkdirSync(
|
|
8485
|
+
const resolved = path3.resolve(targetPath);
|
|
8486
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
8435
8487
|
try {
|
|
8436
|
-
return
|
|
8488
|
+
return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
|
|
8437
8489
|
} catch {
|
|
8438
8490
|
return resolved;
|
|
8439
8491
|
}
|
|
8440
8492
|
}
|
|
8441
8493
|
function boundedLockPath(lockPath, lockRoot) {
|
|
8442
|
-
const resolved =
|
|
8494
|
+
const resolved = path3.resolve(lockPath);
|
|
8443
8495
|
if (!lockRoot)
|
|
8444
8496
|
return resolved;
|
|
8445
8497
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
8446
|
-
const parent =
|
|
8498
|
+
const parent = path3.dirname(resolved);
|
|
8447
8499
|
const parentReal = canonicalLockParentSync(parent);
|
|
8448
|
-
const parentRelative =
|
|
8449
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
8500
|
+
const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
|
|
8501
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
|
|
8450
8502
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
8451
8503
|
}
|
|
8452
|
-
return
|
|
8504
|
+
return path3.join(parentReal, path3.basename(resolved));
|
|
8453
8505
|
}
|
|
8454
8506
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
8455
8507
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -8662,6 +8714,33 @@ var metadataLockSchema = object({
|
|
|
8662
8714
|
nonce: string2().min(1)
|
|
8663
8715
|
}).strict();
|
|
8664
8716
|
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8717
|
+
var cachedRuntimeVersion = null;
|
|
8718
|
+
var runtimeVersion = () => {
|
|
8719
|
+
if (cachedRuntimeVersion === null) {
|
|
8720
|
+
try {
|
|
8721
|
+
const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
|
|
8722
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
8723
|
+
} catch {
|
|
8724
|
+
cachedRuntimeVersion = "0.0.0";
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8727
|
+
return cachedRuntimeVersion;
|
|
8728
|
+
};
|
|
8729
|
+
var stampNew = (record) => ({
|
|
8730
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
8731
|
+
updatedWith: runtimeVersion()
|
|
8732
|
+
});
|
|
8733
|
+
var isNewerVersion = (candidate, current) => {
|
|
8734
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
8735
|
+
const [next, now] = [parts(candidate), parts(current)];
|
|
8736
|
+
for (let index = 0;index < 3; index += 1) {
|
|
8737
|
+
const left = next[index] ?? 0;
|
|
8738
|
+
const right = now[index] ?? 0;
|
|
8739
|
+
if (left !== right)
|
|
8740
|
+
return left > right;
|
|
8741
|
+
}
|
|
8742
|
+
return false;
|
|
8743
|
+
};
|
|
8665
8744
|
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
8666
8745
|
`;
|
|
8667
8746
|
var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
@@ -8688,12 +8767,12 @@ var sameMetadataLock = (left, right) => {
|
|
|
8688
8767
|
class TaskStore {
|
|
8689
8768
|
root;
|
|
8690
8769
|
constructor(root) {
|
|
8691
|
-
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) :
|
|
8770
|
+
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
|
|
8692
8771
|
}
|
|
8693
8772
|
readTask(taskId) {
|
|
8694
8773
|
if (!validId(taskId))
|
|
8695
8774
|
return failure("invalid_input", "task ID is invalid", { taskId });
|
|
8696
|
-
const result = this.readRecord(
|
|
8775
|
+
const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
|
|
8697
8776
|
if (!result.exists)
|
|
8698
8777
|
return failure("not_found", "task not found", { taskId });
|
|
8699
8778
|
if (result.result.ok && result.result.data.id !== taskId)
|
|
@@ -8727,7 +8806,7 @@ class TaskStore {
|
|
|
8727
8806
|
});
|
|
8728
8807
|
const tasks = [];
|
|
8729
8808
|
for (const name of names.sort()) {
|
|
8730
|
-
const item = this.readRecord(
|
|
8809
|
+
const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
|
|
8731
8810
|
if (!item.exists)
|
|
8732
8811
|
continue;
|
|
8733
8812
|
if (!item.result.ok)
|
|
@@ -8761,7 +8840,7 @@ class TaskStore {
|
|
|
8761
8840
|
if (!current.ok)
|
|
8762
8841
|
return current;
|
|
8763
8842
|
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
8764
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
8843
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
8765
8844
|
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
8766
8845
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
8767
8846
|
});
|
|
@@ -8773,11 +8852,17 @@ class TaskStore {
|
|
|
8773
8852
|
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
8774
8853
|
if (current.data && !previousWorkspaceBytes)
|
|
8775
8854
|
return failure("storage_error", "workspace snapshot disappeared during creation");
|
|
8776
|
-
const workspace = current.data ? {
|
|
8855
|
+
const workspace = current.data ? {
|
|
8856
|
+
...current.data,
|
|
8857
|
+
revision: newRevision(),
|
|
8858
|
+
root: this.root,
|
|
8859
|
+
runtime: stampNew(current.data)
|
|
8860
|
+
} : {
|
|
8777
8861
|
schemaVersion: SCHEMA_VERSION,
|
|
8778
8862
|
id: newId(),
|
|
8779
8863
|
revision: newRevision(),
|
|
8780
8864
|
root: this.root,
|
|
8865
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8781
8866
|
writer: null
|
|
8782
8867
|
};
|
|
8783
8868
|
const timestamp = value.now ?? now();
|
|
@@ -8799,6 +8884,7 @@ class TaskStore {
|
|
|
8799
8884
|
status: "active",
|
|
8800
8885
|
closure: null,
|
|
8801
8886
|
progress: { summary: "", nextAction: null, blockers: [] },
|
|
8887
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8802
8888
|
assessments: [],
|
|
8803
8889
|
policy: null,
|
|
8804
8890
|
policyChanges: [],
|
|
@@ -8829,7 +8915,7 @@ class TaskStore {
|
|
|
8829
8915
|
if (!current.ok)
|
|
8830
8916
|
return current;
|
|
8831
8917
|
if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
|
|
8832
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
8918
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
8833
8919
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
8834
8920
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
8835
8921
|
});
|
|
@@ -8837,11 +8923,17 @@ class TaskStore {
|
|
|
8837
8923
|
if (current.data && !previousWorkspaceBytes)
|
|
8838
8924
|
return failure("storage_error", "workspace snapshot disappeared during import");
|
|
8839
8925
|
const timestamp = input.now ?? now();
|
|
8840
|
-
const workspace = current.data ? {
|
|
8926
|
+
const workspace = current.data ? {
|
|
8927
|
+
...current.data,
|
|
8928
|
+
revision: newRevision(),
|
|
8929
|
+
root: this.root,
|
|
8930
|
+
runtime: stampNew(current.data)
|
|
8931
|
+
} : {
|
|
8841
8932
|
schemaVersion: SCHEMA_VERSION,
|
|
8842
8933
|
id: input.workspaceId ?? newId(),
|
|
8843
8934
|
revision: newRevision(),
|
|
8844
8935
|
root: this.root,
|
|
8936
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8845
8937
|
writer: null
|
|
8846
8938
|
};
|
|
8847
8939
|
const task = {
|
|
@@ -8849,7 +8941,11 @@ class TaskStore {
|
|
|
8849
8941
|
workspaceId: workspace.id,
|
|
8850
8942
|
revision: newRevision(),
|
|
8851
8943
|
createdAt: timestamp,
|
|
8852
|
-
updatedAt: timestamp
|
|
8944
|
+
updatedAt: timestamp,
|
|
8945
|
+
runtime: {
|
|
8946
|
+
createdWith: input.task.runtime?.createdWith ?? null,
|
|
8947
|
+
updatedWith: runtimeVersion()
|
|
8948
|
+
}
|
|
8853
8949
|
};
|
|
8854
8950
|
const validTask = taskRecordSchema.safeParse(task);
|
|
8855
8951
|
if (!validTask.success)
|
|
@@ -8898,7 +8994,8 @@ class TaskStore {
|
|
|
8898
8994
|
workspaceId: current.data.workspaceId,
|
|
8899
8995
|
createdAt: current.data.createdAt,
|
|
8900
8996
|
revision: context.revision,
|
|
8901
|
-
updatedAt: context.now
|
|
8997
|
+
updatedAt: context.now,
|
|
8998
|
+
runtime: stampNew(current.data)
|
|
8902
8999
|
};
|
|
8903
9000
|
const valid = taskRecordSchema.safeParse(record);
|
|
8904
9001
|
if (!valid.success)
|
|
@@ -8932,7 +9029,8 @@ class TaskStore {
|
|
|
8932
9029
|
...changed.data,
|
|
8933
9030
|
id: current.data.id,
|
|
8934
9031
|
root: this.root,
|
|
8935
|
-
revision: context.revision
|
|
9032
|
+
revision: context.revision,
|
|
9033
|
+
runtime: stampNew(current.data)
|
|
8936
9034
|
};
|
|
8937
9035
|
const valid = workspaceRecordSchema.safeParse(record);
|
|
8938
9036
|
if (!valid.success)
|
|
@@ -8975,7 +9073,8 @@ class TaskStore {
|
|
|
8975
9073
|
...changedWorkspace.data,
|
|
8976
9074
|
id: workspace.data.id,
|
|
8977
9075
|
root: this.root,
|
|
8978
|
-
revision: workspaceContext.revision
|
|
9076
|
+
revision: workspaceContext.revision,
|
|
9077
|
+
runtime: stampNew(workspace.data)
|
|
8979
9078
|
});
|
|
8980
9079
|
if (!nextWorkspace.success)
|
|
8981
9080
|
return failure("invalid_input", "workspace mutation produced an invalid record");
|
|
@@ -9000,7 +9099,8 @@ class TaskStore {
|
|
|
9000
9099
|
workspaceId: task.data.workspaceId,
|
|
9001
9100
|
createdAt: task.data.createdAt,
|
|
9002
9101
|
revision: taskContext.revision,
|
|
9003
|
-
updatedAt: taskContext.now
|
|
9102
|
+
updatedAt: taskContext.now,
|
|
9103
|
+
runtime: stampNew(task.data)
|
|
9004
9104
|
});
|
|
9005
9105
|
if (!nextTask.success) {
|
|
9006
9106
|
this.markUncertain(nextWorkspace.data);
|
|
@@ -9027,7 +9127,7 @@ class TaskStore {
|
|
|
9027
9127
|
if (match)
|
|
9028
9128
|
candidates.push({
|
|
9029
9129
|
target: match[1],
|
|
9030
|
-
path:
|
|
9130
|
+
path: path4.join(this.recoveryDir, name),
|
|
9031
9131
|
digest: match[3]
|
|
9032
9132
|
});
|
|
9033
9133
|
}
|
|
@@ -9220,7 +9320,8 @@ class TaskStore {
|
|
|
9220
9320
|
const value = {
|
|
9221
9321
|
...workspace,
|
|
9222
9322
|
writer: { ...workspace.writer, state: "uncertain" },
|
|
9223
|
-
revision: newRevision()
|
|
9323
|
+
revision: newRevision(),
|
|
9324
|
+
runtime: stampNew(workspace)
|
|
9224
9325
|
};
|
|
9225
9326
|
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
9226
9327
|
}
|
|
@@ -9298,7 +9399,7 @@ class TaskStore {
|
|
|
9298
9399
|
}
|
|
9299
9400
|
fs4.renameSync(temporary, file);
|
|
9300
9401
|
temporary = undefined;
|
|
9301
|
-
this.fsyncDirectory(
|
|
9402
|
+
this.fsyncDirectory(path4.dirname(file));
|
|
9302
9403
|
return success(null, null, value);
|
|
9303
9404
|
} catch (error) {
|
|
9304
9405
|
return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
@@ -9312,9 +9413,9 @@ class TaskStore {
|
|
|
9312
9413
|
}
|
|
9313
9414
|
}
|
|
9314
9415
|
saveRecovery(file, bytes) {
|
|
9315
|
-
const target =
|
|
9316
|
-
const id = target === "task" ?
|
|
9317
|
-
const destination =
|
|
9416
|
+
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
9417
|
+
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
9418
|
+
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
9318
9419
|
if (fs4.existsSync(destination)) {
|
|
9319
9420
|
if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
|
|
9320
9421
|
return;
|
|
@@ -9392,7 +9493,7 @@ class TaskStore {
|
|
|
9392
9493
|
for (const name of fs4.readdirSync(this.tasksDir)) {
|
|
9393
9494
|
if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
|
|
9394
9495
|
return null;
|
|
9395
|
-
const parsed = this.parseBytes(fs4.readFileSync(
|
|
9496
|
+
const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
|
|
9396
9497
|
if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
|
|
9397
9498
|
return null;
|
|
9398
9499
|
ids.add(parsed.data.workspaceId);
|
|
@@ -9434,7 +9535,12 @@ class TaskStore {
|
|
|
9434
9535
|
if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
9435
9536
|
return failure("unsupported_version", "unsupported snapshot schema version");
|
|
9436
9537
|
const parsed = schema.safeParse(value);
|
|
9437
|
-
|
|
9538
|
+
if (parsed.success)
|
|
9539
|
+
return success(null, null, parsed.data);
|
|
9540
|
+
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
9541
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
9542
|
+
return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
9543
|
+
return failure("recovery_required", "snapshot does not satisfy its schema");
|
|
9438
9544
|
}
|
|
9439
9545
|
processStart(pid) {
|
|
9440
9546
|
try {
|
|
@@ -9444,43 +9550,98 @@ class TaskStore {
|
|
|
9444
9550
|
}
|
|
9445
9551
|
}
|
|
9446
9552
|
conflict(expected, actual) {
|
|
9447
|
-
return failure("revision_conflict", "snapshot revision does not match", {
|
|
9553
|
+
return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
9448
9554
|
expectedRevision: expected,
|
|
9449
9555
|
actualRevision: actual
|
|
9450
9556
|
});
|
|
9451
9557
|
}
|
|
9452
9558
|
taskPath(taskId) {
|
|
9453
|
-
return
|
|
9559
|
+
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
9454
9560
|
}
|
|
9455
9561
|
get workitDir() {
|
|
9456
|
-
return
|
|
9562
|
+
return path4.join(this.root, ".workit");
|
|
9457
9563
|
}
|
|
9458
9564
|
get tasksDir() {
|
|
9459
|
-
return
|
|
9565
|
+
return path4.join(this.workitDir, "tasks");
|
|
9460
9566
|
}
|
|
9461
9567
|
get recoveryDir() {
|
|
9462
|
-
return
|
|
9568
|
+
return path4.join(this.workitDir, "recovery");
|
|
9463
9569
|
}
|
|
9464
9570
|
get workspacePath() {
|
|
9465
|
-
return
|
|
9571
|
+
return path4.join(this.workitDir, "workspace.json");
|
|
9466
9572
|
}
|
|
9467
9573
|
get lockPath() {
|
|
9468
|
-
return
|
|
9574
|
+
return path4.join(this.workitDir, "metadata.lock");
|
|
9469
9575
|
}
|
|
9470
9576
|
get gitignorePath() {
|
|
9471
|
-
return
|
|
9577
|
+
return path4.join(this.workitDir, ".gitignore");
|
|
9472
9578
|
}
|
|
9473
9579
|
}
|
|
9580
|
+
// packages/workit-core/src/core/workers.ts
|
|
9581
|
+
var same = (left, right) => {
|
|
9582
|
+
try {
|
|
9583
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
9584
|
+
} catch {
|
|
9585
|
+
return false;
|
|
9586
|
+
}
|
|
9587
|
+
};
|
|
9588
|
+
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
9589
|
+
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
9590
|
+
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
9591
|
+
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
9592
|
+
const code = char.charCodeAt(0);
|
|
9593
|
+
return code < 32 || code === 127;
|
|
9594
|
+
}) && !value.split(/[\\/]/).includes("..");
|
|
9595
|
+
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
9596
|
+
var workerIdOf = (value) => value.workerId ?? null;
|
|
9597
|
+
function assertProductWriteAllowed(input) {
|
|
9598
|
+
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
9599
|
+
return failure("invalid_input", "invalid product write path");
|
|
9600
|
+
if (!input.store)
|
|
9601
|
+
return failure("permission_denied", "write authorization requires core state");
|
|
9602
|
+
const task = input.store.readTask(input.task.id);
|
|
9603
|
+
if (!task.ok)
|
|
9604
|
+
return task;
|
|
9605
|
+
const workspace = input.store.readWorkspace();
|
|
9606
|
+
if (!workspace.ok)
|
|
9607
|
+
return workspace;
|
|
9608
|
+
if (!workspace.data)
|
|
9609
|
+
return failure("not_found", "workspace not found");
|
|
9610
|
+
const workspaceRecord = workspace.data;
|
|
9611
|
+
if (task.data.status !== "active")
|
|
9612
|
+
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
9613
|
+
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
9614
|
+
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
9615
|
+
const writer = workspaceRecord.writer;
|
|
9616
|
+
if (!writer)
|
|
9617
|
+
return failure("permission_denied", "checkout has no writer owner");
|
|
9618
|
+
if (writer.state === "uncertain")
|
|
9619
|
+
return failure("recovery_required", "checkout writer requires recovery", {
|
|
9620
|
+
owner: writer.owner
|
|
9621
|
+
});
|
|
9622
|
+
const caller = callerValue(input.caller);
|
|
9623
|
+
const workerId = workerIdOf(input.caller);
|
|
9624
|
+
const owner = writer.owner;
|
|
9625
|
+
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
9626
|
+
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
9627
|
+
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
9628
|
+
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
9629
|
+
return failure("permission_denied", "only implementers can own product writes");
|
|
9630
|
+
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))))
|
|
9631
|
+
return failure("recovery_required", "worker session is not an active writer");
|
|
9632
|
+
return success(null, workspaceRecord.revision, owner);
|
|
9633
|
+
}
|
|
9634
|
+
|
|
9474
9635
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9475
9636
|
import { createHash as createHash4 } from "node:crypto";
|
|
9476
9637
|
import * as fs6 from "node:fs";
|
|
9477
9638
|
import { spawnSync } from "node:child_process";
|
|
9478
|
-
import
|
|
9639
|
+
import path6 from "node:path";
|
|
9479
9640
|
|
|
9480
9641
|
// packages/workit-core/src/core/authority.ts
|
|
9481
9642
|
import { createHash as createHash3 } from "node:crypto";
|
|
9482
9643
|
import * as fs5 from "node:fs";
|
|
9483
|
-
import
|
|
9644
|
+
import path5 from "node:path";
|
|
9484
9645
|
var verifiedAuthorities = new WeakMap;
|
|
9485
9646
|
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
9486
9647
|
const token = {};
|
|
@@ -9607,20 +9768,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
9607
9768
|
continue;
|
|
9608
9769
|
if (!reference.digest)
|
|
9609
9770
|
return failure("invalid_input", "document references require a byte digest");
|
|
9610
|
-
const target =
|
|
9611
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
9612
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
9771
|
+
const target = path5.resolve(root, reference.path);
|
|
9772
|
+
if (target !== root && !target.startsWith(`${root}${path5.sep}`))
|
|
9773
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
9774
|
+
fields: [
|
|
9775
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
9776
|
+
]
|
|
9777
|
+
});
|
|
9613
9778
|
try {
|
|
9614
|
-
const relative =
|
|
9779
|
+
const relative = path5.relative(root, target);
|
|
9615
9780
|
let current = root;
|
|
9616
|
-
for (const segment of relative.split(
|
|
9781
|
+
for (const segment of relative.split(path5.sep)) {
|
|
9617
9782
|
if (!segment)
|
|
9618
9783
|
continue;
|
|
9619
|
-
current =
|
|
9784
|
+
current = path5.join(current, segment);
|
|
9620
9785
|
const stat = fs5.lstatSync(current);
|
|
9621
9786
|
const real = fs5.realpathSync(current);
|
|
9622
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
9623
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
9787
|
+
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
9788
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
9789
|
+
fields: [
|
|
9790
|
+
{
|
|
9791
|
+
path: "contentRefs.path",
|
|
9792
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
9793
|
+
}
|
|
9794
|
+
]
|
|
9795
|
+
});
|
|
9624
9796
|
if (stat.isSymbolicLink())
|
|
9625
9797
|
return failure("invalid_input", "document reference uses a symlink");
|
|
9626
9798
|
if (current === target && !stat.isFile())
|
|
@@ -9967,14 +10139,14 @@ var verifyDecisionContent = verifyContentRefs;
|
|
|
9967
10139
|
|
|
9968
10140
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9969
10141
|
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
9970
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
9971
|
-
var relative = (root, candidate) =>
|
|
10142
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
10143
|
+
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
9972
10144
|
var canonicalPath = (value) => {
|
|
9973
10145
|
if (typeof value !== "string" || !value)
|
|
9974
10146
|
return null;
|
|
9975
10147
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
9976
10148
|
return null;
|
|
9977
|
-
const normalized =
|
|
10149
|
+
const normalized = path6.posix.normalize(value);
|
|
9978
10150
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
9979
10151
|
return null;
|
|
9980
10152
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -10022,14 +10194,14 @@ var gitPaths = (root) => {
|
|
|
10022
10194
|
git: /not a git repository/i.test(stderr) ? false : true
|
|
10023
10195
|
};
|
|
10024
10196
|
}
|
|
10025
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10197
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
10026
10198
|
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
10027
10199
|
if (stagedDeleted.status !== 0) {
|
|
10028
10200
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
10029
10201
|
if (!/not a git repository/i.test(stderr))
|
|
10030
10202
|
return { paths, uncertain: true, git: true };
|
|
10031
10203
|
} else if (stagedDeleted.stdout) {
|
|
10032
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10204
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
10033
10205
|
}
|
|
10034
10206
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
10035
10207
|
};
|
|
@@ -10044,7 +10216,7 @@ var walk = (root, directory, output) => {
|
|
|
10044
10216
|
for (const name of names) {
|
|
10045
10217
|
if (name === ".git" || name === ".workit")
|
|
10046
10218
|
continue;
|
|
10047
|
-
const target =
|
|
10219
|
+
const target = path6.join(directory, name);
|
|
10048
10220
|
let stat;
|
|
10049
10221
|
try {
|
|
10050
10222
|
stat = fs6.lstatSync(target);
|
|
@@ -10062,17 +10234,17 @@ var walk = (root, directory, output) => {
|
|
|
10062
10234
|
var scopeRoots = (root, scope) => {
|
|
10063
10235
|
const roots = [];
|
|
10064
10236
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
10065
|
-
const target =
|
|
10237
|
+
const target = path6.resolve(root, item);
|
|
10066
10238
|
if (!inside(root, target))
|
|
10067
|
-
return failure("invalid_input",
|
|
10239
|
+
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` }] });
|
|
10068
10240
|
let ancestor = target;
|
|
10069
10241
|
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
10070
|
-
ancestor =
|
|
10242
|
+
ancestor = path6.dirname(ancestor);
|
|
10071
10243
|
try {
|
|
10072
10244
|
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
10073
|
-
return failure("invalid_input",
|
|
10245
|
+
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` }] });
|
|
10074
10246
|
} catch {
|
|
10075
|
-
return failure("invalid_input",
|
|
10247
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
10076
10248
|
}
|
|
10077
10249
|
roots.push(target);
|
|
10078
10250
|
}
|
|
@@ -10087,7 +10259,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10087
10259
|
}
|
|
10088
10260
|
const normalizedScope = canonicalScope(scope);
|
|
10089
10261
|
if (!normalizedScope)
|
|
10090
|
-
return failure("invalid_input", "candidate scope is invalid");
|
|
10262
|
+
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" }] });
|
|
10091
10263
|
const roots = scopeRoots(checkout, normalizedScope);
|
|
10092
10264
|
if (!roots.ok)
|
|
10093
10265
|
return roots;
|
|
@@ -10119,7 +10291,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10119
10291
|
for (const item of [...names].sort()) {
|
|
10120
10292
|
if (!scopeMatches(item, normalizedScope))
|
|
10121
10293
|
continue;
|
|
10122
|
-
const target =
|
|
10294
|
+
const target = path6.join(checkout, item);
|
|
10123
10295
|
try {
|
|
10124
10296
|
const stat = fs6.lstatSync(target);
|
|
10125
10297
|
if (stat.isSymbolicLink()) {
|
|
@@ -10276,7 +10448,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
10276
10448
|
return true;
|
|
10277
10449
|
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
10278
10450
|
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
10279
|
-
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
|
|
10451
|
+
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
|
|
10280
10452
|
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
10281
10453
|
});
|
|
10282
10454
|
if (passed.length) {
|
|
@@ -10372,7 +10544,17 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10372
10544
|
const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
|
|
10373
10545
|
if (requestedOutcome !== "stopped" && blocking.length)
|
|
10374
10546
|
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
10375
|
-
requirementIds: blocking.map((item) => item.requirementId)
|
|
10547
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
10548
|
+
requirements: blocking.map((item) => {
|
|
10549
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
10550
|
+
return {
|
|
10551
|
+
requirementId: item.requirementId,
|
|
10552
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
10553
|
+
reason: item.reason,
|
|
10554
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
10555
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
10556
|
+
};
|
|
10557
|
+
})
|
|
10376
10558
|
});
|
|
10377
10559
|
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
10378
10560
|
if (requestedOutcome === "verified" && accepted.length)
|
|
@@ -10399,6 +10581,106 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10399
10581
|
});
|
|
10400
10582
|
}
|
|
10401
10583
|
|
|
10584
|
+
// packages/workit-core/src/core/methods.ts
|
|
10585
|
+
var METHODS = {
|
|
10586
|
+
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
10587
|
+
"workit-behavioral-tdd": { dimensions: ["testing"] },
|
|
10588
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
|
|
10589
|
+
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10590
|
+
"workit-implement": { dimensions: ["delegation"] },
|
|
10591
|
+
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
10592
|
+
"workit-handoff": { ruleIds: ["durable-handoff"] },
|
|
10593
|
+
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
10594
|
+
};
|
|
10595
|
+
var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
10596
|
+
var assuranceFor = (id, definition, capabilities) => {
|
|
10597
|
+
const keys = new Set([
|
|
10598
|
+
id,
|
|
10599
|
+
...definition.dimensions ?? [],
|
|
10600
|
+
...definition.ruleIds ?? []
|
|
10601
|
+
]);
|
|
10602
|
+
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
10603
|
+
if (id === "workit-review" && matches.length === 0)
|
|
10604
|
+
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
10605
|
+
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
10606
|
+
return {
|
|
10607
|
+
assurance: "unavailable",
|
|
10608
|
+
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
10609
|
+
};
|
|
10610
|
+
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
10611
|
+
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
10612
|
+
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
10613
|
+
};
|
|
10614
|
+
function selectMethods(policy, capabilities) {
|
|
10615
|
+
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
10616
|
+
const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
|
|
10617
|
+
if (requirements.length === 0)
|
|
10618
|
+
return [];
|
|
10619
|
+
const capability = assuranceFor(id, definition, capabilities);
|
|
10620
|
+
const reasons = [
|
|
10621
|
+
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
10622
|
+
...capability.reasons
|
|
10623
|
+
];
|
|
10624
|
+
if (capability.assurance === "unavailable")
|
|
10625
|
+
reasons.unshift("required capability is unavailable");
|
|
10626
|
+
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
10627
|
+
});
|
|
10628
|
+
}
|
|
10629
|
+
var invariantBootstrap = () => `
|
|
10630
|
+
Workit keeps one accountable lead and one shared task state. Inspect current task
|
|
10631
|
+
state before acting. An empty task.list means no session yet, not permission to
|
|
10632
|
+
skip Workit; for user-requested product, debug, or behavior work with no active
|
|
10633
|
+
or paused task, run task.start then policy.assess before other product mutations.
|
|
10634
|
+
Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
|
|
10635
|
+
to the current records; explicit values are still concurrency-checked, so never
|
|
10636
|
+
copy revisions between calls. Happy path: task.list, task.start {intent} with no
|
|
10637
|
+
revisions, policy.assess {assessment: {facts, signals, consequences,
|
|
10638
|
+
verification}} where facts are inferred or observed with file refs,
|
|
10639
|
+
writer.acquire {taskId} before product writes, evidence.record {evidence} where
|
|
10640
|
+
check and review kinds auto-bind the current tree and need no digests. Record
|
|
10641
|
+
self-review requirements with kind review and reviewContext matching your own
|
|
10642
|
+
host session; the lead may also record checks. Fresh-context review requires a
|
|
10643
|
+
session distinct from the task creator and every other evidence recorder. Record
|
|
10644
|
+
verification evidence last: later evidence or commits can reopen fixed findings
|
|
10645
|
+
for re-verification, so verify, resolve, then close with nothing in between.
|
|
10646
|
+
decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
10647
|
+
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10648
|
+
header Workit decision: <purpose>, the same label repeated in the question
|
|
10649
|
+
text (some host UIs do not render headers), exactly two options approved and
|
|
10650
|
+
rejected; ask once, never re-ask to mint a receipt. Before asking, present the
|
|
10651
|
+
item in the conversation (durable artifacts: complete digest plus exact path;
|
|
10652
|
+
inline plans: the content) and keep the question to one short scoped sentence;
|
|
10653
|
+
never ask to approve something unseen. An answer with custom text is steering,
|
|
10654
|
+
not approval: adjust, re-present, then ask a new scoped question. After the user
|
|
10655
|
+
approves a spec or plan, execute the remaining tasks continuously with one
|
|
10656
|
+
atomic commit per task and do not ask whether to continue; stop only for a new
|
|
10657
|
+
product decision, a failed safety or verification gate, a conflicting edit, or
|
|
10658
|
+
missing authority. Record the plan's commit list once through the plan-scoped
|
|
10659
|
+
action approval (git.commit with plan_steps and plan_branch) so each listed
|
|
10660
|
+
commit executes without a new question; an unlisted message needs a fresh
|
|
10661
|
+
approval. task.close {outcome,
|
|
10662
|
+
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10663
|
+
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10664
|
+
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
10665
|
+
without a bound candidate goes stale; findings close only as fixed with passing
|
|
10666
|
+
verification, dismissed with supporting evidence, or deferred under an approved
|
|
10667
|
+
limitation. Use only the
|
|
10668
|
+
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
10669
|
+
and state changes. Authority is bounded by the requested scope, current
|
|
10670
|
+
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
10671
|
+
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
10672
|
+
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
10673
|
+
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
10674
|
+
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
10675
|
+
forgotten items mid-task → load workit-steer before responding; about to open
|
|
10676
|
+
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
10677
|
+
workit-green-run; small-looking change before close or merge → load
|
|
10678
|
+
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
10679
|
+
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
10680
|
+
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
10681
|
+
act from memory of it.
|
|
10682
|
+
`.trim();
|
|
10683
|
+
|
|
10402
10684
|
// packages/workit-core/src/core/task-context.ts
|
|
10403
10685
|
function reconcileResume(view, observations = []) {
|
|
10404
10686
|
if (observations.length > 0)
|
|
@@ -10408,7 +10690,7 @@ function reconcileResume(view, observations = []) {
|
|
|
10408
10690
|
return candidate;
|
|
10409
10691
|
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
10410
10692
|
const blockers = [...view.task.progress.blockers];
|
|
10411
|
-
if (view.task.workers.some((entry) =>
|
|
10693
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
10412
10694
|
blockers.push({
|
|
10413
10695
|
reason: "worker state requires reconciliation",
|
|
10414
10696
|
dependentAction: "resume",
|
|
@@ -10458,6 +10740,11 @@ function compactTaskContext(view) {
|
|
|
10458
10740
|
digest: entry.data.digest,
|
|
10459
10741
|
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
10460
10742
|
}));
|
|
10743
|
+
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
10744
|
+
id: method.id,
|
|
10745
|
+
assurance: method.assurance,
|
|
10746
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
10747
|
+
})) : [];
|
|
10461
10748
|
const gaps = Array.from(new Set([
|
|
10462
10749
|
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
10463
10750
|
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
@@ -10467,15 +10754,18 @@ function compactTaskContext(view) {
|
|
|
10467
10754
|
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
10468
10755
|
status: view.task.status,
|
|
10469
10756
|
decisions,
|
|
10757
|
+
methods,
|
|
10470
10758
|
gaps,
|
|
10471
10759
|
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
10472
10760
|
};
|
|
10473
10761
|
let encoded = JSON.stringify(context);
|
|
10474
|
-
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
|
|
10762
|
+
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
10475
10763
|
if (context.gaps.length)
|
|
10476
10764
|
context.gaps.pop();
|
|
10477
|
-
else
|
|
10765
|
+
else if (context.decisions.length)
|
|
10478
10766
|
context.decisions.pop();
|
|
10767
|
+
else
|
|
10768
|
+
context.methods.pop();
|
|
10479
10769
|
encoded = JSON.stringify(context);
|
|
10480
10770
|
}
|
|
10481
10771
|
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
@@ -10491,51 +10781,49 @@ var exportDigest = (bundle) => sha256({
|
|
|
10491
10781
|
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
10492
10782
|
task: bundle.task
|
|
10493
10783
|
});
|
|
10494
|
-
// packages/workit-core/src/core/
|
|
10495
|
-
var
|
|
10496
|
-
Workit
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
10536
|
-
act from memory of it.
|
|
10537
|
-
`.trim();
|
|
10784
|
+
// packages/workit-core/src/core/route-intent.ts
|
|
10785
|
+
var GUIDANCE = {
|
|
10786
|
+
"git.branch_setup": "use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
|
|
10787
|
+
"hosting.pull_request": "use the Workit hosting.pull_request action (workit_external_action in hosts, or `workit action hosting.pull_request --payload '{...}' --confirm --json` in the CLI), then drive the created PR through workit-babysit"
|
|
10788
|
+
};
|
|
10789
|
+
var unbalancedQuotes = (segment) => {
|
|
10790
|
+
const text = segment.replace(/\\./gu, "");
|
|
10791
|
+
return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
|
|
10792
|
+
};
|
|
10793
|
+
var segmentTokens = (segment) => {
|
|
10794
|
+
if (segment.includes("`") || segment.includes("$("))
|
|
10795
|
+
return null;
|
|
10796
|
+
if (unbalancedQuotes(segment))
|
|
10797
|
+
return null;
|
|
10798
|
+
const parts = segment.trim().split(/\s+/u);
|
|
10799
|
+
while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
|
|
10800
|
+
parts.shift();
|
|
10801
|
+
return parts.length > 0 ? parts : null;
|
|
10802
|
+
};
|
|
10803
|
+
var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
|
|
10804
|
+
function shellRouteIntent(command) {
|
|
10805
|
+
if (typeof command !== "string" || command.trim() === "")
|
|
10806
|
+
return null;
|
|
10807
|
+
for (const raw of command.split(/&&|\|\||;|\n/u)) {
|
|
10808
|
+
const parts = segmentTokens(raw);
|
|
10809
|
+
if (!parts)
|
|
10810
|
+
continue;
|
|
10811
|
+
const head = unquote(parts[0]);
|
|
10812
|
+
const second = unquote(parts[1] ?? "");
|
|
10813
|
+
const third = unquote(parts[2] ?? "");
|
|
10814
|
+
if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
|
|
10815
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
10816
|
+
if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
|
|
10817
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
10818
|
+
if (head === "gh" && second === "pr" && third === "create")
|
|
10819
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
10820
|
+
if (head === "glab" && second === "mr" && third === "create")
|
|
10821
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
10822
|
+
}
|
|
10823
|
+
return null;
|
|
10824
|
+
}
|
|
10538
10825
|
// packages/workit-core/src/core/external-action.ts
|
|
10826
|
+
import { execFileSync } from "node:child_process";
|
|
10539
10827
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
10540
10828
|
object({
|
|
10541
10829
|
operation: literal("git.branch_setup"),
|
|
@@ -10548,7 +10836,11 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
10548
10836
|
}).strict(),
|
|
10549
10837
|
object({
|
|
10550
10838
|
operation: literal("git.commit"),
|
|
10551
|
-
payload: object({
|
|
10839
|
+
payload: object({
|
|
10840
|
+
message: string2().min(1).optional(),
|
|
10841
|
+
plan_steps: array(string2().min(1)).optional(),
|
|
10842
|
+
plan_branch: string2().min(1).optional()
|
|
10843
|
+
}).strict()
|
|
10552
10844
|
}).strict(),
|
|
10553
10845
|
object({
|
|
10554
10846
|
operation: literal("git.push"),
|
|
@@ -10637,7 +10929,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
10637
10929
|
payload,
|
|
10638
10930
|
...payload && typeof payload === "object" && !Array.isArray(payload) && "resolved" in payload && payload.resolved && typeof payload.resolved === "object" && Array.isArray(payload.resolved.steps) ? { steps: payload.resolved.steps } : {}
|
|
10639
10931
|
});
|
|
10640
|
-
var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch
|
|
10932
|
+
var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch(required unless reapply_stash; the working branch to create or switch to),stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
|
|
10641
10933
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
10642
10934
|
kind: "host",
|
|
10643
10935
|
host,
|
|
@@ -10688,6 +10980,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
|
|
|
10688
10980
|
return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
|
|
10689
10981
|
return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
|
|
10690
10982
|
};
|
|
10983
|
+
var planCommitDescriptor = (content) => {
|
|
10984
|
+
try {
|
|
10985
|
+
const value = JSON.parse(content);
|
|
10986
|
+
if (value.operation !== "git.commit" || !value.payload)
|
|
10987
|
+
return null;
|
|
10988
|
+
const steps = value.payload.plan_steps;
|
|
10989
|
+
const branch = value.payload.plan_branch;
|
|
10990
|
+
if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
|
|
10991
|
+
return null;
|
|
10992
|
+
return { steps, branch };
|
|
10993
|
+
} catch {
|
|
10994
|
+
return null;
|
|
10995
|
+
}
|
|
10996
|
+
};
|
|
10997
|
+
var approvedPlanCommit = (store, host, actor, message) => {
|
|
10998
|
+
if (typeof message !== "string" || !message)
|
|
10999
|
+
return failure("invalid_input", "commit message is required");
|
|
11000
|
+
const listed = store.listTasks();
|
|
11001
|
+
const workspace = store.readWorkspace();
|
|
11002
|
+
if (!listed.ok || !workspace.ok)
|
|
11003
|
+
return failure("storage_error", "external action state is unavailable");
|
|
11004
|
+
if (!workspace.data)
|
|
11005
|
+
return failure("not_found", "workspace not found");
|
|
11006
|
+
const matches = listed.data.flatMap((task) => {
|
|
11007
|
+
if (task.status !== "active" || task.workspaceId !== workspace.data.id)
|
|
11008
|
+
return [];
|
|
11009
|
+
if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
|
|
11010
|
+
return [];
|
|
11011
|
+
return task.decisions.flatMap((entry) => {
|
|
11012
|
+
const decision = entry.data;
|
|
11013
|
+
if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
|
|
11014
|
+
return [];
|
|
11015
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
|
|
11016
|
+
return [];
|
|
11017
|
+
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
11018
|
+
if (!plan)
|
|
11019
|
+
return [];
|
|
11020
|
+
const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
|
|
11021
|
+
if (plan.steps[completed.length] !== message)
|
|
11022
|
+
return [];
|
|
11023
|
+
return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
|
|
11024
|
+
});
|
|
11025
|
+
});
|
|
11026
|
+
if (matches.length !== 1)
|
|
11027
|
+
return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
|
|
11028
|
+
return success(workspace.data.revision, null, matches[0]);
|
|
11029
|
+
};
|
|
11030
|
+
var commitMessageFromDescriptor = (operation) => {
|
|
11031
|
+
try {
|
|
11032
|
+
const descriptor = JSON.parse(operation);
|
|
11033
|
+
return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
|
|
11034
|
+
} catch {
|
|
11035
|
+
return;
|
|
11036
|
+
}
|
|
11037
|
+
};
|
|
11038
|
+
var planCommitBinding = (store, host, actor, operation) => {
|
|
11039
|
+
const message = commitMessageFromDescriptor(operation);
|
|
11040
|
+
if (!message)
|
|
11041
|
+
return null;
|
|
11042
|
+
const plan = approvedPlanCommit(store, host, actor, message);
|
|
11043
|
+
if (!plan.ok)
|
|
11044
|
+
return null;
|
|
11045
|
+
let branch;
|
|
11046
|
+
try {
|
|
11047
|
+
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
11048
|
+
cwd: store.root,
|
|
11049
|
+
encoding: "utf8",
|
|
11050
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11051
|
+
}).trim();
|
|
11052
|
+
} catch {
|
|
11053
|
+
return null;
|
|
11054
|
+
}
|
|
11055
|
+
if (!branch || branch !== plan.data.branch)
|
|
11056
|
+
return null;
|
|
11057
|
+
const workspace = store.readWorkspace();
|
|
11058
|
+
if (!workspace.ok || !workspace.data)
|
|
11059
|
+
return null;
|
|
11060
|
+
return {
|
|
11061
|
+
taskId: plan.data.task.id,
|
|
11062
|
+
decisionId: plan.data.entry.id,
|
|
11063
|
+
binding: plan.data.entry.data.binding,
|
|
11064
|
+
expectedRevision: plan.data.task.revision,
|
|
11065
|
+
expectedWorkspaceRevision: workspace.data.revision,
|
|
11066
|
+
step: message
|
|
11067
|
+
};
|
|
11068
|
+
};
|
|
10691
11069
|
var approvedExternalAction = (store, host, actor, operation) => {
|
|
10692
11070
|
const selected = externalActionState(store, host, actor, operation);
|
|
10693
11071
|
if (!selected.ok)
|
|
@@ -10696,6 +11074,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
|
|
|
10696
11074
|
return failure(selected.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", selected.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
|
|
10697
11075
|
return selected;
|
|
10698
11076
|
};
|
|
11077
|
+
var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
|
|
11078
|
+
const prior = priorExternalAction(store, host, actor, operation, payload);
|
|
11079
|
+
if (!prior.ok)
|
|
11080
|
+
return success(null, null, null);
|
|
11081
|
+
let baseline;
|
|
11082
|
+
try {
|
|
11083
|
+
const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
|
|
11084
|
+
baseline = parsed.payload?.resolved;
|
|
11085
|
+
} catch {
|
|
11086
|
+
return success(null, null, null);
|
|
11087
|
+
}
|
|
11088
|
+
if (baseline === undefined || baseline === null)
|
|
11089
|
+
return success(null, null, null);
|
|
11090
|
+
if (canonicalJson(baseline) === canonicalJson(currentResolved))
|
|
11091
|
+
return success(null, null, null);
|
|
11092
|
+
return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
|
|
11093
|
+
};
|
|
10699
11094
|
var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
|
|
10700
11095
|
let steps = [];
|
|
10701
11096
|
try {
|
|
@@ -10973,7 +11368,7 @@ function resolveRequirements(input) {
|
|
|
10973
11368
|
].join(", ")}`,
|
|
10974
11369
|
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
10975
11370
|
before: "dependent_action",
|
|
10976
|
-
dependentAction:
|
|
11371
|
+
dependentAction: null,
|
|
10977
11372
|
acceptanceAllowed: false
|
|
10978
11373
|
}));
|
|
10979
11374
|
if (signals.productChoiceOpen.value === true)
|
|
@@ -10984,7 +11379,7 @@ function resolveRequirements(input) {
|
|
|
10984
11379
|
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
10985
11380
|
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
10986
11381
|
before: "dependent_action",
|
|
10987
|
-
dependentAction:
|
|
11382
|
+
dependentAction: null,
|
|
10988
11383
|
acceptanceAllowed: false
|
|
10989
11384
|
}));
|
|
10990
11385
|
if (signals.behaviorChange.value === true)
|
|
@@ -11142,59 +11537,6 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
11142
11537
|
return parsed.data;
|
|
11143
11538
|
}
|
|
11144
11539
|
|
|
11145
|
-
// packages/workit-core/src/core/workers.ts
|
|
11146
|
-
var same = (left, right) => {
|
|
11147
|
-
try {
|
|
11148
|
-
return canonicalJson(left) === canonicalJson(right);
|
|
11149
|
-
} catch {
|
|
11150
|
-
return false;
|
|
11151
|
-
}
|
|
11152
|
-
};
|
|
11153
|
-
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
11154
|
-
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
11155
|
-
const code = char.charCodeAt(0);
|
|
11156
|
-
return code < 32 || code === 127;
|
|
11157
|
-
}) && !value.split(/[\\/]/).includes("..");
|
|
11158
|
-
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
11159
|
-
var workerIdOf = (value) => value.workerId ?? null;
|
|
11160
|
-
function assertProductWriteAllowed(input) {
|
|
11161
|
-
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
11162
|
-
return failure("invalid_input", "invalid product write path");
|
|
11163
|
-
if (!input.store)
|
|
11164
|
-
return failure("permission_denied", "write authorization requires core state");
|
|
11165
|
-
const task = input.store.readTask(input.task.id);
|
|
11166
|
-
if (!task.ok)
|
|
11167
|
-
return task;
|
|
11168
|
-
const workspace = input.store.readWorkspace();
|
|
11169
|
-
if (!workspace.ok)
|
|
11170
|
-
return workspace;
|
|
11171
|
-
if (!workspace.data)
|
|
11172
|
-
return failure("not_found", "workspace not found");
|
|
11173
|
-
const workspaceRecord = workspace.data;
|
|
11174
|
-
if (task.data.status !== "active")
|
|
11175
|
-
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
11176
|
-
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
11177
|
-
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
11178
|
-
const writer = workspaceRecord.writer;
|
|
11179
|
-
if (!writer)
|
|
11180
|
-
return failure("permission_denied", "checkout has no writer owner");
|
|
11181
|
-
if (writer.state === "uncertain")
|
|
11182
|
-
return failure("recovery_required", "checkout writer requires recovery", {
|
|
11183
|
-
owner: writer.owner
|
|
11184
|
-
});
|
|
11185
|
-
const caller = callerValue(input.caller);
|
|
11186
|
-
const workerId = workerIdOf(input.caller);
|
|
11187
|
-
const owner = writer.owner;
|
|
11188
|
-
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
11189
|
-
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
11190
|
-
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
11191
|
-
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
11192
|
-
return failure("permission_denied", "only implementers can own product writes");
|
|
11193
|
-
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))))
|
|
11194
|
-
return failure("recovery_required", "worker session is not an active writer");
|
|
11195
|
-
return success(null, workspaceRecord.revision, owner);
|
|
11196
|
-
}
|
|
11197
|
-
|
|
11198
11540
|
// packages/workit-core/src/core/task-engine.ts
|
|
11199
11541
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
11200
11542
|
kind,
|
|
@@ -11313,10 +11655,10 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11313
11655
|
const notStarted = input.dispatch === true;
|
|
11314
11656
|
if (notStarted && (input.state !== "stopped" || input.session !== null))
|
|
11315
11657
|
return failure("invalid_input", "a never-dispatched worker stops with no session");
|
|
11316
|
-
if (notStarted &&
|
|
11317
|
-
return failure("invalid_transition", "worker is no longer awaiting dispatch");
|
|
11318
|
-
if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
|
|
11658
|
+
if (notStarted && entry.data.session !== null)
|
|
11319
11659
|
return failure("invalid_transition", "worker already has an observed session");
|
|
11660
|
+
if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
|
|
11661
|
+
return failure("invalid_transition", "worker launch was not claimed");
|
|
11320
11662
|
if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
|
|
11321
11663
|
return failure("invalid_input", "running worker observations require a session");
|
|
11322
11664
|
if (entry.data.session && !sameValue(entry.data.session, input.session))
|
|
@@ -11325,15 +11667,20 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11325
11667
|
return failure("invalid_transition", "worker has not started");
|
|
11326
11668
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
11327
11669
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
11670
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
11328
11671
|
const nextEntry = {
|
|
11329
11672
|
...entry,
|
|
11330
11673
|
recordedAt: input.now ?? entry.recordedAt,
|
|
11331
11674
|
provenance: authority.provenance,
|
|
11332
|
-
data: {
|
|
11675
|
+
data: {
|
|
11676
|
+
...entry.data,
|
|
11677
|
+
state: terminalCancel ? "cancelling" : input.state,
|
|
11678
|
+
session: input.session
|
|
11679
|
+
}
|
|
11333
11680
|
};
|
|
11334
11681
|
const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
|
|
11335
11682
|
const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
|
|
11336
|
-
if (entry.data.state ===
|
|
11683
|
+
if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
|
|
11337
11684
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
11338
11685
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
11339
11686
|
taskId: task.data.id,
|
|
@@ -11703,7 +12050,7 @@ class WorkitCore {
|
|
|
11703
12050
|
activeWorkerBlocker(task, workspace) {
|
|
11704
12051
|
if (workspace.writer)
|
|
11705
12052
|
return failure("recovery_required", "writer ownership must be released first");
|
|
11706
|
-
if (task.workers.some((entry) =>
|
|
12053
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
11707
12054
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
11708
12055
|
return null;
|
|
11709
12056
|
}
|
|
@@ -11940,11 +12287,6 @@ class WorkitCore {
|
|
|
11940
12287
|
if (helper && !helper.ok)
|
|
11941
12288
|
return helper;
|
|
11942
12289
|
const evidence = input.evidence;
|
|
11943
|
-
if (helper?.ok) {
|
|
11944
|
-
const assignment = helper.data.data.assignment;
|
|
11945
|
-
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))
|
|
11946
|
-
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
11947
|
-
}
|
|
11948
12290
|
if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
|
|
11949
12291
|
return failure("invalid_input", "missing or skipped evidence requires a reason");
|
|
11950
12292
|
if (evidence.kind === "review") {
|
|
@@ -11961,6 +12303,11 @@ class WorkitCore {
|
|
|
11961
12303
|
evidence.beforeCandidateId ??= null;
|
|
11962
12304
|
evidence.candidateId ??= null;
|
|
11963
12305
|
}
|
|
12306
|
+
if (helper?.ok) {
|
|
12307
|
+
const assignment = helper.data.data.assignment;
|
|
12308
|
+
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))
|
|
12309
|
+
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
12310
|
+
}
|
|
11964
12311
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
11965
12312
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
11966
12313
|
if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
|
|
@@ -12467,7 +12814,12 @@ class WorkitCore {
|
|
|
12467
12814
|
return failure("invalid_transition", "worker is not awaiting dispatch");
|
|
12468
12815
|
return success(mutation.revision, null, {
|
|
12469
12816
|
...current,
|
|
12470
|
-
workers: current.workers.map((item) => item.id === input.workerId ? {
|
|
12817
|
+
workers: current.workers.map((item) => item.id === input.workerId ? {
|
|
12818
|
+
...item,
|
|
12819
|
+
recordedAt: mutation.now,
|
|
12820
|
+
provenance: attested.data,
|
|
12821
|
+
data: { ...item.data, state: "dispatching" }
|
|
12822
|
+
} : item)
|
|
12471
12823
|
});
|
|
12472
12824
|
}
|
|
12473
12825
|
});
|
|
@@ -12851,6 +13203,9 @@ class WorkitCore {
|
|
|
12851
13203
|
id: task.id,
|
|
12852
13204
|
revision: task.revision,
|
|
12853
13205
|
workspaceRevision: workspace.data.revision,
|
|
13206
|
+
createdAt: task.createdAt,
|
|
13207
|
+
updatedAt: task.updatedAt,
|
|
13208
|
+
runtime: task.runtime ?? null,
|
|
12854
13209
|
objective: task.intent.data.objective,
|
|
12855
13210
|
status: task.status,
|
|
12856
13211
|
closure: task.closure,
|
|
@@ -12936,7 +13291,7 @@ class WorkitCore {
|
|
|
12936
13291
|
const base = reconcileResume(effectiveView);
|
|
12937
13292
|
if (!base.ok)
|
|
12938
13293
|
return base;
|
|
12939
|
-
const needsReconciliation = view.task.workers.some((entry) =>
|
|
13294
|
+
const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
|
|
12940
13295
|
const blockers = [...base.data.blockers];
|
|
12941
13296
|
if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
|
|
12942
13297
|
blockers.push({
|
|
@@ -13003,7 +13358,8 @@ class WorkitCore {
|
|
|
13003
13358
|
...current,
|
|
13004
13359
|
status,
|
|
13005
13360
|
candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
|
|
13006
|
-
progress:
|
|
13361
|
+
progress: current.progress,
|
|
13362
|
+
pauseReason: status === "paused" ? input.reason ?? null : null
|
|
13007
13363
|
})
|
|
13008
13364
|
});
|
|
13009
13365
|
if (!changed.ok)
|
|
@@ -13039,7 +13395,7 @@ class WorkitCore {
|
|
|
13039
13395
|
return workspace;
|
|
13040
13396
|
if (!workspace.data)
|
|
13041
13397
|
return failure("not_found", "workspace not found");
|
|
13042
|
-
if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state
|
|
13398
|
+
if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
13043
13399
|
return failure("recovery_required", "scope revision requires worker ownership reconciliation");
|
|
13044
13400
|
this.fillRevisions(input, task.data, workspace.data);
|
|
13045
13401
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
@@ -13157,15 +13513,15 @@ function resolveGitRevision(root, value) {
|
|
|
13157
13513
|
}
|
|
13158
13514
|
function resolveInside(root, candidate) {
|
|
13159
13515
|
const base = realpathSync5(root);
|
|
13160
|
-
const target =
|
|
13161
|
-
if (target !== base && !target.startsWith(base +
|
|
13516
|
+
const target = path7.resolve(base, candidate);
|
|
13517
|
+
if (target !== base && !target.startsWith(base + path7.sep)) {
|
|
13162
13518
|
throw new Error("path must stay inside repository root");
|
|
13163
13519
|
}
|
|
13164
13520
|
let ancestor = target;
|
|
13165
|
-
while (!
|
|
13166
|
-
ancestor =
|
|
13521
|
+
while (!existsSync4(ancestor))
|
|
13522
|
+
ancestor = path7.dirname(ancestor);
|
|
13167
13523
|
const canonicalAncestor = realpathSync5(ancestor);
|
|
13168
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
13524
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
|
|
13169
13525
|
throw new Error("path must stay inside repository root");
|
|
13170
13526
|
}
|
|
13171
13527
|
return target;
|
|
@@ -13207,7 +13563,7 @@ if (false) {}
|
|
|
13207
13563
|
// packages/workit-core/src/core/logger.ts
|
|
13208
13564
|
import { appendFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
13209
13565
|
import os from "node:os";
|
|
13210
|
-
import
|
|
13566
|
+
import path8 from "node:path";
|
|
13211
13567
|
var REDACTED = "[REDACTED]";
|
|
13212
13568
|
var DEFAULT_MAX_RATE = 20;
|
|
13213
13569
|
var DEFAULT_RATE_WINDOW_MS = 1000;
|
|
@@ -13263,14 +13619,14 @@ var resolveStateDir = () => {
|
|
|
13263
13619
|
return override;
|
|
13264
13620
|
const home = os.homedir();
|
|
13265
13621
|
if (process.env.XDG_STATE_HOME)
|
|
13266
|
-
return
|
|
13622
|
+
return path8.join(process.env.XDG_STATE_HOME, "workit");
|
|
13267
13623
|
switch (os.platform()) {
|
|
13268
13624
|
case "darwin":
|
|
13269
|
-
return
|
|
13625
|
+
return path8.join(home, "Library", "Application Support", "workit");
|
|
13270
13626
|
case "win32":
|
|
13271
|
-
return
|
|
13627
|
+
return path8.join(process.env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local"), "workit");
|
|
13272
13628
|
default:
|
|
13273
|
-
return
|
|
13629
|
+
return path8.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
13274
13630
|
}
|
|
13275
13631
|
};
|
|
13276
13632
|
var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
@@ -13343,14 +13699,14 @@ var pruneOldFiles = (dir) => {
|
|
|
13343
13699
|
const files = readdirSync3(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
|
|
13344
13700
|
for (const file of files.slice(RETAINED_DAYS)) {
|
|
13345
13701
|
try {
|
|
13346
|
-
unlinkSync2(
|
|
13702
|
+
unlinkSync2(path8.join(dir, file));
|
|
13347
13703
|
} catch {}
|
|
13348
13704
|
}
|
|
13349
13705
|
} catch {}
|
|
13350
13706
|
};
|
|
13351
13707
|
var createLogger = (options = {}) => {
|
|
13352
13708
|
const stateDir = options.stateDir ?? resolveStateDir();
|
|
13353
|
-
const logDir =
|
|
13709
|
+
const logDir = path8.join(stateDir, "logs");
|
|
13354
13710
|
const now = options.now ?? (() => new Date);
|
|
13355
13711
|
const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
|
|
13356
13712
|
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
@@ -13384,7 +13740,7 @@ var createLogger = (options = {}) => {
|
|
|
13384
13740
|
const line = `${JSON.stringify(event)}
|
|
13385
13741
|
`;
|
|
13386
13742
|
mkdirSync2(logDir, { recursive: true, mode: 448 });
|
|
13387
|
-
appendFileSync(
|
|
13743
|
+
appendFileSync(path8.join(logDir, dailyFileName(date)), line, { mode: 384 });
|
|
13388
13744
|
pruneOldFiles(logDir);
|
|
13389
13745
|
} catch {
|
|
13390
13746
|
try {
|
|
@@ -13395,7 +13751,7 @@ var createLogger = (options = {}) => {
|
|
|
13395
13751
|
context: { redaction_failed: true }
|
|
13396
13752
|
};
|
|
13397
13753
|
mkdirSync2(logDir, { recursive: true, mode: 448 });
|
|
13398
|
-
appendFileSync(
|
|
13754
|
+
appendFileSync(path8.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
|
|
13399
13755
|
`, {
|
|
13400
13756
|
mode: 384
|
|
13401
13757
|
});
|
|
@@ -13478,6 +13834,24 @@ OpenCode Workit decisions use the native question header "Workit decision: <purp
|
|
|
13478
13834
|
return cached2;
|
|
13479
13835
|
};
|
|
13480
13836
|
|
|
13837
|
+
// packages/workit-opencode/src/stale-sources.ts
|
|
13838
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
13839
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
13840
|
+
var pluginSourceFiles = [
|
|
13841
|
+
fileURLToPath2(import.meta.url),
|
|
13842
|
+
fileURLToPath2(new URL("./plugin.ts", import.meta.url)),
|
|
13843
|
+
fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
|
|
13844
|
+
...[
|
|
13845
|
+
"task-contract.ts",
|
|
13846
|
+
"task-engine.ts",
|
|
13847
|
+
"task-evaluation.ts",
|
|
13848
|
+
"task-store.ts",
|
|
13849
|
+
"workers.ts",
|
|
13850
|
+
"authority.ts",
|
|
13851
|
+
"methods.ts"
|
|
13852
|
+
].map((file) => fileURLToPath2(new URL(`../../workit-core/src/core/${file}`, import.meta.url)))
|
|
13853
|
+
].filter((file) => existsSync5(file));
|
|
13854
|
+
|
|
13481
13855
|
// packages/workit-opencode/src/tools/repo.ts
|
|
13482
13856
|
import path22 from "node:path";
|
|
13483
13857
|
import { realpathSync as realpathSync8 } from "node:fs";
|
|
@@ -25807,25 +26181,6 @@ import fs7 from "node:fs";
|
|
|
25807
26181
|
import path9 from "node:path";
|
|
25808
26182
|
import { createHash as createHash5 } from "node:crypto";
|
|
25809
26183
|
|
|
25810
|
-
// packages/workit-core/src/core/package-root.ts
|
|
25811
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
25812
|
-
import path8 from "node:path";
|
|
25813
|
-
import { fileURLToPath } from "node:url";
|
|
25814
|
-
var packageRoot = () => {
|
|
25815
|
-
let dir = path8.dirname(fileURLToPath(import.meta.url));
|
|
25816
|
-
while (true) {
|
|
25817
|
-
const parent = path8.dirname(dir);
|
|
25818
|
-
if (existsSync4(path8.join(dir, "package.json")) || parent === dir)
|
|
25819
|
-
return dir;
|
|
25820
|
-
dir = parent;
|
|
25821
|
-
}
|
|
25822
|
-
};
|
|
25823
|
-
var assetRoot = () => {
|
|
25824
|
-
const root = packageRoot();
|
|
25825
|
-
const assets = path8.join(root, "assets");
|
|
25826
|
-
return existsSync4(assets) ? assets : root;
|
|
25827
|
-
};
|
|
25828
|
-
|
|
25829
26184
|
// packages/workit-core/src/core/scripts.ts
|
|
25830
26185
|
var PLUGIN_ROOT = packageRoot();
|
|
25831
26186
|
var ASSET_ROOT = assetRoot();
|
|
@@ -26158,10 +26513,10 @@ function changelogApply({
|
|
|
26158
26513
|
}
|
|
26159
26514
|
|
|
26160
26515
|
// packages/workit-core/src/core/git.ts
|
|
26161
|
-
import { execFileSync } from "node:child_process";
|
|
26516
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
26162
26517
|
var run2 = (cwd, args) => {
|
|
26163
26518
|
try {
|
|
26164
|
-
const stdout =
|
|
26519
|
+
const stdout = execFileSync2("git", args, {
|
|
26165
26520
|
cwd,
|
|
26166
26521
|
encoding: "utf8",
|
|
26167
26522
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -26279,15 +26634,15 @@ function parseVerifyOutput(stdout) {
|
|
|
26279
26634
|
}
|
|
26280
26635
|
|
|
26281
26636
|
// packages/workit-core/src/core/branch.ts
|
|
26282
|
-
import { existsSync as
|
|
26283
|
-
import { execFileSync as
|
|
26637
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26638
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
26284
26639
|
import path13 from "node:path";
|
|
26285
26640
|
|
|
26286
26641
|
// packages/workit-core/src/core/config.ts
|
|
26287
26642
|
import {
|
|
26288
26643
|
copyFileSync,
|
|
26289
26644
|
cpSync,
|
|
26290
|
-
existsSync as
|
|
26645
|
+
existsSync as existsSync6,
|
|
26291
26646
|
mkdirSync as mkdirSync3,
|
|
26292
26647
|
readFileSync as readFileSync4,
|
|
26293
26648
|
readdirSync as readdirSync4,
|
|
@@ -26327,11 +26682,11 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
26327
26682
|
return dir;
|
|
26328
26683
|
}
|
|
26329
26684
|
const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
|
|
26330
|
-
if (!
|
|
26685
|
+
if (!existsSync6(legacy)) {
|
|
26331
26686
|
migratedDir = dir;
|
|
26332
26687
|
return dir;
|
|
26333
26688
|
}
|
|
26334
|
-
if (!migrationFailed &&
|
|
26689
|
+
if (!migrationFailed && existsSync6(dir)) {
|
|
26335
26690
|
migratedDir = dir;
|
|
26336
26691
|
return dir;
|
|
26337
26692
|
}
|
|
@@ -26341,7 +26696,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
26341
26696
|
for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
|
|
26342
26697
|
const src = path10.join(legacy, entry.name);
|
|
26343
26698
|
const dest = path10.join(dir, entry.name);
|
|
26344
|
-
if (
|
|
26699
|
+
if (existsSync6(dest))
|
|
26345
26700
|
continue;
|
|
26346
26701
|
try {
|
|
26347
26702
|
if (entry.isDirectory())
|
|
@@ -26919,7 +27274,7 @@ var baseBranch = (cwd) => {
|
|
|
26919
27274
|
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
26920
27275
|
};
|
|
26921
27276
|
var originBaseReady = (cwd, base) => {
|
|
26922
|
-
const run = (args) =>
|
|
27277
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
26923
27278
|
try {
|
|
26924
27279
|
try {
|
|
26925
27280
|
run(["fetch", "origin", base, "--prune"]);
|
|
@@ -26927,7 +27282,7 @@ var originBaseReady = (cwd, base) => {
|
|
|
26927
27282
|
run(["fetch", "origin", "--prune"]);
|
|
26928
27283
|
}
|
|
26929
27284
|
try {
|
|
26930
|
-
|
|
27285
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
26931
27286
|
cwd,
|
|
26932
27287
|
stdio: "pipe"
|
|
26933
27288
|
});
|
|
@@ -26946,11 +27301,11 @@ var originBaseReady = (cwd, base) => {
|
|
|
26946
27301
|
}
|
|
26947
27302
|
};
|
|
26948
27303
|
var fastForwardBase = (cwd, base) => {
|
|
26949
|
-
const run = (args) =>
|
|
27304
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
26950
27305
|
try {
|
|
26951
27306
|
let hasLocalBase = true;
|
|
26952
27307
|
try {
|
|
26953
|
-
|
|
27308
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
26954
27309
|
cwd,
|
|
26955
27310
|
stdio: "pipe"
|
|
26956
27311
|
});
|
|
@@ -26991,18 +27346,17 @@ var branchSetup = ({
|
|
|
26991
27346
|
log
|
|
26992
27347
|
}) => {
|
|
26993
27348
|
const cwd = path13.resolve(workspace_root);
|
|
26994
|
-
const exec = (args) =>
|
|
27349
|
+
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
26995
27350
|
const current = gitContext(cwd).branch;
|
|
26996
27351
|
if (!current || current === "unknown")
|
|
26997
|
-
return { error: "not in a git repository" };
|
|
27352
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
26998
27353
|
const sdd = sdd_dir ?? "docs";
|
|
26999
27354
|
const manifestPath = path13.isAbsolute(sdd) ? path13.join(sdd, "manifest.json") : path13.join(cwd, sdd, "manifest.json");
|
|
27000
27355
|
const ensureManifestDir = () => {
|
|
27001
27356
|
const dir = path13.dirname(manifestPath);
|
|
27002
|
-
if (!
|
|
27357
|
+
if (!existsSync7(dir))
|
|
27003
27358
|
mkdirSync4(dir, { recursive: true, mode: 493 });
|
|
27004
27359
|
};
|
|
27005
|
-
ensureManifestDir();
|
|
27006
27360
|
const readManifest = () => {
|
|
27007
27361
|
try {
|
|
27008
27362
|
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
@@ -27020,13 +27374,16 @@ var branchSetup = ({
|
|
|
27020
27374
|
const manifest = readManifest();
|
|
27021
27375
|
const ref = manifest.stash_ref;
|
|
27022
27376
|
if (!ref)
|
|
27023
|
-
return { error: "no stash_ref in manifest" };
|
|
27377
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
27024
27378
|
journal(`pre-pop: ${String(ref)}`);
|
|
27025
27379
|
try {
|
|
27026
27380
|
exec(["stash", "pop", String(ref)]);
|
|
27027
27381
|
} catch (error) {
|
|
27028
27382
|
journal("pop: failed");
|
|
27029
|
-
return {
|
|
27383
|
+
return {
|
|
27384
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
27385
|
+
phase: "post_mutation"
|
|
27386
|
+
};
|
|
27030
27387
|
}
|
|
27031
27388
|
journal("pop: ok");
|
|
27032
27389
|
delete manifest.stash_ref;
|
|
@@ -27036,11 +27393,17 @@ var branchSetup = ({
|
|
|
27036
27393
|
}
|
|
27037
27394
|
const target = target_branch ?? "";
|
|
27038
27395
|
if (!target)
|
|
27039
|
-
return {
|
|
27396
|
+
return {
|
|
27397
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
27398
|
+
phase: "preflight"
|
|
27399
|
+
};
|
|
27040
27400
|
if (isProtected(cwd, target))
|
|
27041
|
-
return { error: `protected branch ${target}
|
|
27401
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
27042
27402
|
if (!allowedBranch(cwd, target))
|
|
27043
|
-
return {
|
|
27403
|
+
return {
|
|
27404
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
27405
|
+
phase: "preflight"
|
|
27406
|
+
};
|
|
27044
27407
|
let base;
|
|
27045
27408
|
let targetExists = true;
|
|
27046
27409
|
try {
|
|
@@ -27051,12 +27414,12 @@ var branchSetup = ({
|
|
|
27051
27414
|
if (!targetExists) {
|
|
27052
27415
|
const baseResolved = baseBranch(cwd);
|
|
27053
27416
|
if ("error" in baseResolved)
|
|
27054
|
-
return { error: baseResolved.error };
|
|
27417
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
27055
27418
|
base = baseResolved.base;
|
|
27056
27419
|
}
|
|
27057
27420
|
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
27058
27421
|
let stash_ref;
|
|
27059
|
-
const failAfterStash = (message) => {
|
|
27422
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
27060
27423
|
let suffix = "";
|
|
27061
27424
|
if (stash_ref) {
|
|
27062
27425
|
journal(`pre-pop: ${stash_ref}`);
|
|
@@ -27069,27 +27432,31 @@ var branchSetup = ({
|
|
|
27069
27432
|
suffix = " (changes preserved in stash)";
|
|
27070
27433
|
}
|
|
27071
27434
|
}
|
|
27072
|
-
return { error: `${message}${suffix}
|
|
27435
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
27073
27436
|
};
|
|
27074
27437
|
if (current !== target) {
|
|
27075
27438
|
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
27076
27439
|
if (dirty && stash !== "yes") {
|
|
27077
27440
|
return {
|
|
27078
|
-
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
|
|
27441
|
+
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
|
|
27442
|
+
phase: "preflight"
|
|
27079
27443
|
};
|
|
27080
27444
|
}
|
|
27081
27445
|
let validatedBase;
|
|
27082
27446
|
if (!targetExists && base !== undefined) {
|
|
27083
27447
|
const ready = originBaseReady(cwd, base);
|
|
27084
27448
|
if (!ready.ok)
|
|
27085
|
-
return { error: ready.error ?? "ensure-base-branch failed" };
|
|
27449
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
27086
27450
|
validatedBase = base;
|
|
27087
27451
|
}
|
|
27088
27452
|
if (dirty) {
|
|
27089
27453
|
try {
|
|
27090
27454
|
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
27091
27455
|
} catch (error) {
|
|
27092
|
-
return {
|
|
27456
|
+
return {
|
|
27457
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
27458
|
+
phase: "post_mutation"
|
|
27459
|
+
};
|
|
27093
27460
|
}
|
|
27094
27461
|
stash_ref = "stash@{0}";
|
|
27095
27462
|
journal(`stash push: ${stash_ref}`);
|
|
@@ -27100,14 +27467,14 @@ var branchSetup = ({
|
|
|
27100
27467
|
} catch (error) {
|
|
27101
27468
|
const message = error instanceof Error ? error.message : "checkout failed";
|
|
27102
27469
|
if (/worktree/i.test(message)) {
|
|
27103
|
-
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)
|
|
27470
|
+
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
|
|
27104
27471
|
}
|
|
27105
27472
|
try {
|
|
27106
27473
|
let effectiveBase = base;
|
|
27107
27474
|
if (effectiveBase === undefined) {
|
|
27108
27475
|
const lateResolved = baseBranch(cwd);
|
|
27109
27476
|
if ("error" in lateResolved)
|
|
27110
|
-
return failAfterStash(lateResolved.error);
|
|
27477
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
27111
27478
|
effectiveBase = lateResolved.base;
|
|
27112
27479
|
}
|
|
27113
27480
|
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
@@ -27153,7 +27520,7 @@ var branchSetup = ({
|
|
|
27153
27520
|
};
|
|
27154
27521
|
|
|
27155
27522
|
// packages/workit-core/src/core/gitignore.ts
|
|
27156
|
-
import { existsSync as
|
|
27523
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27157
27524
|
import path14 from "node:path";
|
|
27158
27525
|
var GITIGNORE_ENTRIES = [
|
|
27159
27526
|
"# workit: SDD working state (never commit)",
|
|
@@ -27175,7 +27542,7 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
|
|
|
27175
27542
|
if (!confirmed)
|
|
27176
27543
|
return { ok: false, error: "confirmed: true required" };
|
|
27177
27544
|
const file = path14.join(workspaceRoot, ".gitignore");
|
|
27178
|
-
const existing =
|
|
27545
|
+
const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
|
|
27179
27546
|
const existingLines = new Set(existing.split(`
|
|
27180
27547
|
`).map((l) => l.trim()).filter(Boolean));
|
|
27181
27548
|
const added = [];
|
|
@@ -27194,19 +27561,19 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
|
|
|
27194
27561
|
` : "") + append.join(`
|
|
27195
27562
|
`) + `
|
|
27196
27563
|
`, "utf8");
|
|
27197
|
-
} else if (!
|
|
27564
|
+
} else if (!existsSync8(file)) {
|
|
27198
27565
|
writeFileSync4(file, "", "utf8");
|
|
27199
27566
|
}
|
|
27200
27567
|
return { ok: true, path: file, added };
|
|
27201
27568
|
};
|
|
27202
27569
|
|
|
27203
27570
|
// packages/workit-core/src/core/hygiene.ts
|
|
27204
|
-
import { existsSync as
|
|
27571
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27205
27572
|
import path15 from "node:path";
|
|
27206
27573
|
var repoRoot = assetRoot();
|
|
27207
27574
|
var templatesDir = () => path15.join(repoRoot, "templates", "hygiene");
|
|
27208
27575
|
var packageJson = (root) => {
|
|
27209
|
-
if (!
|
|
27576
|
+
if (!existsSync9(path15.join(root, "package.json")))
|
|
27210
27577
|
return null;
|
|
27211
27578
|
try {
|
|
27212
27579
|
return JSON.parse(readFileSync8(path15.join(root, "package.json"), "utf8"));
|
|
@@ -27215,7 +27582,7 @@ var packageJson = (root) => {
|
|
|
27215
27582
|
}
|
|
27216
27583
|
};
|
|
27217
27584
|
var isOpenSource = (root) => {
|
|
27218
|
-
if (
|
|
27585
|
+
if (existsSync9(path15.join(root, "LICENSE")))
|
|
27219
27586
|
return true;
|
|
27220
27587
|
const pkg = packageJson(root);
|
|
27221
27588
|
if (pkg && !pkg.private)
|
|
@@ -27243,10 +27610,10 @@ var planHygieneFiles = (root, opts = {}) => {
|
|
|
27243
27610
|
files.push("LICENSE", "CONTRIBUTING.md");
|
|
27244
27611
|
const planned = [];
|
|
27245
27612
|
for (const file of files) {
|
|
27246
|
-
if (
|
|
27613
|
+
if (existsSync9(path15.join(root, file)))
|
|
27247
27614
|
continue;
|
|
27248
27615
|
const tpl = path15.join(templatesDir(), file);
|
|
27249
|
-
if (!
|
|
27616
|
+
if (!existsSync9(tpl))
|
|
27250
27617
|
continue;
|
|
27251
27618
|
const content = readFileSync8(tpl, "utf8").replace(/<PROJECT>/g, path15.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
|
|
27252
27619
|
planned.push({ path: path15.join(root, file), content });
|
|
@@ -27266,7 +27633,7 @@ var ensureHygieneFiles = (root, opts) => {
|
|
|
27266
27633
|
|
|
27267
27634
|
// packages/workit-core/src/core/repo-context.ts
|
|
27268
27635
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
27269
|
-
import { existsSync as
|
|
27636
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
|
|
27270
27637
|
import path16 from "node:path";
|
|
27271
27638
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
27272
27639
|
try {
|
|
@@ -27433,7 +27800,7 @@ var diffStatForRange = (cwd, range) => {
|
|
|
27433
27800
|
};
|
|
27434
27801
|
var packageScripts = (cwd, keys) => {
|
|
27435
27802
|
const pkgPath = path16.join(cwd, "package.json");
|
|
27436
|
-
if (!
|
|
27803
|
+
if (!existsSync10(pkgPath))
|
|
27437
27804
|
return [];
|
|
27438
27805
|
try {
|
|
27439
27806
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
@@ -27468,7 +27835,7 @@ var documentationFiles = (cwd) => {
|
|
|
27468
27835
|
return matches.sort().slice(0, 200);
|
|
27469
27836
|
};
|
|
27470
27837
|
var readTrimmed = (file, maxLines) => {
|
|
27471
|
-
if (!
|
|
27838
|
+
if (!existsSync10(file))
|
|
27472
27839
|
return "";
|
|
27473
27840
|
const lines = readFileSync9(file, "utf8").split(`
|
|
27474
27841
|
`);
|
|
@@ -27544,7 +27911,7 @@ ${status.stderr}` : "");
|
|
|
27544
27911
|
stdout += `
|
|
27545
27912
|
`;
|
|
27546
27913
|
stdout += printSection("Recent Validation Signals");
|
|
27547
|
-
if (
|
|
27914
|
+
if (existsSync10(path16.join(cwd, "package.json"))) {
|
|
27548
27915
|
stdout += `package.json detected. Common scripts:
|
|
27549
27916
|
`;
|
|
27550
27917
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -27553,7 +27920,7 @@ ${status.stderr}` : "");
|
|
|
27553
27920
|
`) + `
|
|
27554
27921
|
`;
|
|
27555
27922
|
}
|
|
27556
|
-
if (
|
|
27923
|
+
if (existsSync10(path16.join(cwd, "Cargo.toml")) || existsSync10(path16.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
27557
27924
|
stdout += `Rust project detected.
|
|
27558
27925
|
`;
|
|
27559
27926
|
}
|
|
@@ -27605,7 +27972,7 @@ function changelogContext(root, range) {
|
|
|
27605
27972
|
`;
|
|
27606
27973
|
stdout += printSection("Existing CHANGELOG.md");
|
|
27607
27974
|
const changelogPath = path16.join(cwd, "CHANGELOG.md");
|
|
27608
|
-
if (
|
|
27975
|
+
if (existsSync10(changelogPath)) {
|
|
27609
27976
|
stdout += readTrimmed(changelogPath, 260) + `
|
|
27610
27977
|
`;
|
|
27611
27978
|
} else {
|
|
@@ -27647,7 +28014,7 @@ function docsRefreshContext(root, range) {
|
|
|
27647
28014
|
`;
|
|
27648
28015
|
stdout += printSection("README Preview");
|
|
27649
28016
|
const readme = path16.join(cwd, "README.md");
|
|
27650
|
-
if (
|
|
28017
|
+
if (existsSync10(readme)) {
|
|
27651
28018
|
stdout += readTrimmed(readme, 220) + `
|
|
27652
28019
|
`;
|
|
27653
28020
|
} else {
|
|
@@ -27656,7 +28023,7 @@ function docsRefreshContext(root, range) {
|
|
|
27656
28023
|
}
|
|
27657
28024
|
stdout += printSection("Package Scripts");
|
|
27658
28025
|
const pkgPath = path16.join(cwd, "package.json");
|
|
27659
|
-
if (
|
|
28026
|
+
if (existsSync10(pkgPath)) {
|
|
27660
28027
|
try {
|
|
27661
28028
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
27662
28029
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
@@ -27707,7 +28074,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
27707
28074
|
`;
|
|
27708
28075
|
stdout += printSection("Existing Release Files");
|
|
27709
28076
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
27710
|
-
if (
|
|
28077
|
+
if (existsSync10(path16.join(cwd, rel)))
|
|
27711
28078
|
stdout += rel + `
|
|
27712
28079
|
`;
|
|
27713
28080
|
}
|
|
@@ -27736,7 +28103,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
27736
28103
|
|
|
27737
28104
|
// packages/workit-core/src/core/verify-project.ts
|
|
27738
28105
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
27739
|
-
import { existsSync as
|
|
28106
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
|
|
27740
28107
|
import path17 from "node:path";
|
|
27741
28108
|
var commandOnPath = (name) => {
|
|
27742
28109
|
const dirs = (process.env.PATH ?? "").split(path17.delimiter);
|
|
@@ -27761,7 +28128,7 @@ var commandOnPath = (name) => {
|
|
|
27761
28128
|
};
|
|
27762
28129
|
var hasScript = (cwd, name) => {
|
|
27763
28130
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27764
|
-
if (!
|
|
28131
|
+
if (!existsSync11(pkgPath))
|
|
27765
28132
|
return false;
|
|
27766
28133
|
try {
|
|
27767
28134
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
@@ -27771,10 +28138,10 @@ var hasScript = (cwd, name) => {
|
|
|
27771
28138
|
}
|
|
27772
28139
|
};
|
|
27773
28140
|
var packageRunner = (cwd) => {
|
|
27774
|
-
if (
|
|
28141
|
+
if (existsSync11(path17.join(cwd, "pnpm-lock.yaml")))
|
|
27775
28142
|
return "pnpm";
|
|
27776
28143
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27777
|
-
if (
|
|
28144
|
+
if (existsSync11(pkgPath)) {
|
|
27778
28145
|
try {
|
|
27779
28146
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
27780
28147
|
if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
|
|
@@ -27782,7 +28149,7 @@ var packageRunner = (cwd) => {
|
|
|
27782
28149
|
}
|
|
27783
28150
|
} catch {}
|
|
27784
28151
|
}
|
|
27785
|
-
if (
|
|
28152
|
+
if (existsSync11(path17.join(cwd, "yarn.lock")))
|
|
27786
28153
|
return "yarn";
|
|
27787
28154
|
return "npm";
|
|
27788
28155
|
};
|
|
@@ -27843,7 +28210,7 @@ root: ${cwd}
|
|
|
27843
28210
|
lines.push(`dry_run: ${String(dryRun)}
|
|
27844
28211
|
`);
|
|
27845
28212
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27846
|
-
if (
|
|
28213
|
+
if (existsSync11(pkgPath)) {
|
|
27847
28214
|
const runner = packageRunner(cwd);
|
|
27848
28215
|
for (const script of ["lint", "format:check", "test", "build"]) {
|
|
27849
28216
|
if (hasScript(cwd, script)) {
|
|
@@ -27858,7 +28225,7 @@ root: ${cwd}
|
|
|
27858
28225
|
} else {
|
|
27859
28226
|
skipCheck("javascript", "package.json not found");
|
|
27860
28227
|
}
|
|
27861
|
-
const cargoManifest =
|
|
28228
|
+
const cargoManifest = existsSync11(path17.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync11(path17.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
|
|
27862
28229
|
if (cargoManifest) {
|
|
27863
28230
|
runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
|
|
27864
28231
|
runCheck("cargo clippy", "cargo", [
|
|
@@ -27874,7 +28241,7 @@ root: ${cwd}
|
|
|
27874
28241
|
} else {
|
|
27875
28242
|
skipCheck("rust", "Cargo.toml not found");
|
|
27876
28243
|
}
|
|
27877
|
-
if (
|
|
28244
|
+
if (existsSync11(path17.join(cwd, "pyproject.toml")) || existsSync11(path17.join(cwd, "pytest.ini")) || existsSync11(path17.join(cwd, "tests"))) {
|
|
27878
28245
|
if (commandOnPath("pytest"))
|
|
27879
28246
|
runCheck("pytest", "pytest", []);
|
|
27880
28247
|
else
|
|
@@ -27896,7 +28263,7 @@ root: ${cwd}
|
|
|
27896
28263
|
lines.push(`status: skipped (dry run)
|
|
27897
28264
|
`);
|
|
27898
28265
|
skipped += 1;
|
|
27899
|
-
} else if (
|
|
28266
|
+
} else if (existsSync11(changelogPath)) {
|
|
27900
28267
|
if (/## \[Unreleased\]/i.test(readFileSync10(changelogPath, "utf8"))) {
|
|
27901
28268
|
lines.push(`status: pass
|
|
27902
28269
|
`);
|
|
@@ -28231,7 +28598,7 @@ function writeFileExclusive(file, content, mode) {
|
|
|
28231
28598
|
import {
|
|
28232
28599
|
copyFileSync as copyFileSync2,
|
|
28233
28600
|
cpSync as cpSync2,
|
|
28234
|
-
existsSync as
|
|
28601
|
+
existsSync as existsSync12,
|
|
28235
28602
|
lstatSync as lstatSync4,
|
|
28236
28603
|
mkdirSync as mkdirSync5,
|
|
28237
28604
|
mkdtempSync,
|
|
@@ -29772,7 +30139,7 @@ function createRepoTools(runtime = defaultRuntime) {
|
|
|
29772
30139
|
return output(legacyScriptResult({
|
|
29773
30140
|
stdout: JSON.stringify(result),
|
|
29774
30141
|
stderr: "",
|
|
29775
|
-
exitCode:
|
|
30142
|
+
exitCode: "error" in result ? 1 : 0,
|
|
29776
30143
|
cwd: context.directory
|
|
29777
30144
|
}));
|
|
29778
30145
|
}
|
|
@@ -30420,6 +30787,94 @@ var safePath = (root, value) => {
|
|
|
30420
30787
|
return null;
|
|
30421
30788
|
}
|
|
30422
30789
|
};
|
|
30790
|
+
var branchCreationBaseline = (root, target, base) => {
|
|
30791
|
+
const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
|
|
30792
|
+
if (exists)
|
|
30793
|
+
return { base_branch: base, target_exists: true, remote_base: null };
|
|
30794
|
+
const remote = run4(root, ["ls-remote", "origin", `refs/heads/${base}`]);
|
|
30795
|
+
const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
|
|
30796
|
+
return {
|
|
30797
|
+
base_branch: base,
|
|
30798
|
+
target_exists: false,
|
|
30799
|
+
remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
|
|
30800
|
+
};
|
|
30801
|
+
};
|
|
30802
|
+
var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
|
|
30803
|
+
var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
|
|
30804
|
+
var actionProposalQuestion = (request, descriptorPayload) => {
|
|
30805
|
+
const payload = descriptorPayload ?? {};
|
|
30806
|
+
const resolved = payload.resolved ?? {};
|
|
30807
|
+
const payloadTarget = String(payload.target_branch ?? "");
|
|
30808
|
+
if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
|
|
30809
|
+
const steps = payload.plan_steps;
|
|
30810
|
+
return {
|
|
30811
|
+
presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
|
|
30812
|
+
approvedText: `Approve the ${steps.length} listed plan commits.`
|
|
30813
|
+
};
|
|
30814
|
+
}
|
|
30815
|
+
switch (request.operation) {
|
|
30816
|
+
case "git.branch_setup": {
|
|
30817
|
+
if (request.payload.action === "reapply_stash")
|
|
30818
|
+
return {
|
|
30819
|
+
presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
|
|
30820
|
+
approvedText: "Reapply the recorded stash."
|
|
30821
|
+
};
|
|
30822
|
+
const target = String(payload.target_branch ?? "");
|
|
30823
|
+
if (resolved.target_exists === true)
|
|
30824
|
+
return {
|
|
30825
|
+
presented: `Workit decision: action — Switch to branch \`${target}\`?`,
|
|
30826
|
+
approvedText: `Switch to \`${target}\`.`
|
|
30827
|
+
};
|
|
30828
|
+
const base = String(resolved.base_branch ?? "the base branch");
|
|
30829
|
+
return {
|
|
30830
|
+
presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
|
|
30831
|
+
approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
|
|
30832
|
+
};
|
|
30833
|
+
}
|
|
30834
|
+
case "git.commit": {
|
|
30835
|
+
const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
|
|
30836
|
+
return {
|
|
30837
|
+
presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
|
|
30838
|
+
approvedText: `Commit as "${String(request.payload.message)}".`
|
|
30839
|
+
};
|
|
30840
|
+
}
|
|
30841
|
+
case "git.push":
|
|
30842
|
+
return {
|
|
30843
|
+
presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
|
|
30844
|
+
approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
|
|
30845
|
+
};
|
|
30846
|
+
case "hosting.pull_request":
|
|
30847
|
+
return {
|
|
30848
|
+
presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
|
|
30849
|
+
approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
|
|
30850
|
+
};
|
|
30851
|
+
case "changelog.apply":
|
|
30852
|
+
return {
|
|
30853
|
+
presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
|
|
30854
|
+
approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
|
|
30855
|
+
};
|
|
30856
|
+
case "youtrack.update":
|
|
30857
|
+
return {
|
|
30858
|
+
presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
|
|
30859
|
+
approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
|
|
30860
|
+
};
|
|
30861
|
+
case "youtrack.time":
|
|
30862
|
+
return {
|
|
30863
|
+
presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
|
|
30864
|
+
approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
|
|
30865
|
+
};
|
|
30866
|
+
case "youtrack.meeting":
|
|
30867
|
+
return {
|
|
30868
|
+
presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
|
|
30869
|
+
approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
|
|
30870
|
+
};
|
|
30871
|
+
default:
|
|
30872
|
+
return {
|
|
30873
|
+
presented: `Workit decision: action — Approve ${request.operation}?`,
|
|
30874
|
+
approvedText: `Approve ${request.operation}.`
|
|
30875
|
+
};
|
|
30876
|
+
}
|
|
30877
|
+
};
|
|
30423
30878
|
var approvedResolvedExternalAction = (content) => {
|
|
30424
30879
|
try {
|
|
30425
30880
|
const value = JSON.parse(content);
|
|
@@ -30683,24 +31138,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30683
31138
|
const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
|
|
30684
31139
|
if (!sdd_dir)
|
|
30685
31140
|
return failure("invalid_input", "sdd_dir must stay inside the workspace");
|
|
31141
|
+
const action = request.payload.action ?? "setup";
|
|
30686
31142
|
const policy = resolveBranchPolicyFor(root);
|
|
30687
|
-
const target_branch = request.payload.target_branch
|
|
31143
|
+
const target_branch = request.payload.target_branch;
|
|
31144
|
+
if (action !== "reapply_stash" && !target_branch)
|
|
31145
|
+
return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
|
|
31146
|
+
outcome: "not_started",
|
|
31147
|
+
fields: [{ path: "target_branch", reason: "required for action=setup" }]
|
|
31148
|
+
});
|
|
30688
31149
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
30689
31150
|
if (!head)
|
|
30690
31151
|
return failure("storage_error", "current Git commit could not be resolved");
|
|
31152
|
+
const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
|
|
31153
|
+
const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
|
|
30691
31154
|
const normalized = {
|
|
30692
31155
|
operation: request.operation,
|
|
30693
31156
|
payload: {
|
|
30694
31157
|
...request.payload,
|
|
30695
|
-
action
|
|
31158
|
+
action,
|
|
30696
31159
|
sdd_dir,
|
|
30697
31160
|
stash: request.payload.stash ?? "no",
|
|
30698
|
-
target_branch
|
|
31161
|
+
...target_branch ? { target_branch } : {}
|
|
30699
31162
|
}
|
|
30700
31163
|
};
|
|
30701
31164
|
return success(null, null, {
|
|
30702
31165
|
request: normalized,
|
|
30703
|
-
descriptorPayload: {
|
|
31166
|
+
descriptorPayload: {
|
|
31167
|
+
...normalized.payload,
|
|
31168
|
+
resolved: { head, dirty, ...creation }
|
|
31169
|
+
}
|
|
30704
31170
|
});
|
|
30705
31171
|
}
|
|
30706
31172
|
case "git.push": {
|
|
@@ -30714,7 +31180,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30714
31180
|
}) : failure("invalid_input", "git push requires a resolvable current branch");
|
|
30715
31181
|
}
|
|
30716
31182
|
case "git.commit": {
|
|
31183
|
+
const planSteps = request.payload.plan_steps;
|
|
31184
|
+
const planBranch = request.payload.plan_branch;
|
|
30717
31185
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
31186
|
+
if (!head)
|
|
31187
|
+
return failure("storage_error", "Git state could not be resolved");
|
|
31188
|
+
const branch = gitValue(root, ["branch", "--show-current"]);
|
|
31189
|
+
if (planSteps !== undefined || planBranch !== undefined) {
|
|
31190
|
+
if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
|
|
31191
|
+
return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
|
|
31192
|
+
outcome: "not_started",
|
|
31193
|
+
fields: [{ path: "plan_steps", reason: "unique strings" }]
|
|
31194
|
+
});
|
|
31195
|
+
if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
|
|
31196
|
+
return failure("invalid_input", "plan_branch must name the current working branch", {
|
|
31197
|
+
outcome: "not_started",
|
|
31198
|
+
fields: [{ path: "plan_branch", reason: "current branch" }]
|
|
31199
|
+
});
|
|
31200
|
+
if (isProtectedBranch2(root, planBranch))
|
|
31201
|
+
return failure("invalid_input", "plan_branch must not be a protected branch", {
|
|
31202
|
+
outcome: "not_started",
|
|
31203
|
+
fields: [{ path: "plan_branch", reason: "protected branch" }]
|
|
31204
|
+
});
|
|
31205
|
+
return success(null, null, {
|
|
31206
|
+
request,
|
|
31207
|
+
descriptorPayload: {
|
|
31208
|
+
plan_steps: planSteps,
|
|
31209
|
+
plan_branch: planBranch,
|
|
31210
|
+
resolved: { head, branch, steps: planSteps }
|
|
31211
|
+
}
|
|
31212
|
+
});
|
|
31213
|
+
}
|
|
30718
31214
|
const staged = gitRaw(root, [
|
|
30719
31215
|
"diff",
|
|
30720
31216
|
"--cached",
|
|
@@ -30725,7 +31221,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30725
31221
|
"HEAD"
|
|
30726
31222
|
]);
|
|
30727
31223
|
const paths = stagedPaths(root);
|
|
30728
|
-
|
|
31224
|
+
const message = request.payload.message;
|
|
31225
|
+
if (typeof message !== "string" || !message)
|
|
31226
|
+
return failure("invalid_input", "git.commit requires a commit message", {
|
|
31227
|
+
outcome: "not_started",
|
|
31228
|
+
fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
|
|
31229
|
+
});
|
|
31230
|
+
if (staged === null || paths === null)
|
|
30729
31231
|
return failure("storage_error", "Git state could not be resolved");
|
|
30730
31232
|
let policy;
|
|
30731
31233
|
try {
|
|
@@ -30740,13 +31242,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30740
31242
|
`).filter(Boolean));
|
|
30741
31243
|
flavor = detected.flavor ?? "conventional";
|
|
30742
31244
|
}
|
|
30743
|
-
if (!matchCommitFlavor(
|
|
31245
|
+
if (!matchCommitFlavor(message, flavor, policy.pattern))
|
|
30744
31246
|
return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
|
|
30745
31247
|
return success(null, null, {
|
|
30746
31248
|
request,
|
|
30747
31249
|
descriptorPayload: {
|
|
30748
31250
|
...request.payload,
|
|
30749
|
-
resolved: { head, staged: sha256(staged), paths }
|
|
31251
|
+
resolved: { head, branch, staged: sha256(staged), paths }
|
|
30750
31252
|
}
|
|
30751
31253
|
});
|
|
30752
31254
|
}
|
|
@@ -30916,10 +31418,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
30916
31418
|
sdd_dir: request.payload.sdd_dir ?? "docs",
|
|
30917
31419
|
workspace_root: root
|
|
30918
31420
|
});
|
|
30919
|
-
|
|
31421
|
+
if ("error" in result) {
|
|
31422
|
+
if (result.phase === "preflight")
|
|
31423
|
+
return failure("invalid_input", result.error, {
|
|
31424
|
+
outcome: "not_started",
|
|
31425
|
+
operation: request.operation
|
|
31426
|
+
});
|
|
31427
|
+
return unknown3(request.operation);
|
|
31428
|
+
}
|
|
31429
|
+
return success(null, null, result);
|
|
30920
31430
|
}
|
|
30921
31431
|
case "git.commit": {
|
|
30922
|
-
const
|
|
31432
|
+
const message = request.payload.message;
|
|
31433
|
+
if (typeof message !== "string" || !message)
|
|
31434
|
+
return failure("invalid_input", "git.commit requires a commit message", {
|
|
31435
|
+
outcome: "not_started",
|
|
31436
|
+
fields: [{ path: "message", reason: "required" }]
|
|
31437
|
+
});
|
|
31438
|
+
const result = run4(root, ["commit", "-m", message]);
|
|
30923
31439
|
return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown3(request.operation);
|
|
30924
31440
|
}
|
|
30925
31441
|
case "git.push": {
|
|
@@ -31031,6 +31547,14 @@ var sessionParent = (session) => {
|
|
|
31031
31547
|
};
|
|
31032
31548
|
var freshMs = 5 * 60 * 1000;
|
|
31033
31549
|
var rejectedDescription = "Reject this decision";
|
|
31550
|
+
var isSelfAuthorizingActionContent = (content) => {
|
|
31551
|
+
try {
|
|
31552
|
+
const value = JSON.parse(content);
|
|
31553
|
+
return typeof value.operation === "string" && value.operation.length > 0;
|
|
31554
|
+
} catch {
|
|
31555
|
+
return /^[a-z][a-z_]*\.[a-z_]+$/.test(content);
|
|
31556
|
+
}
|
|
31557
|
+
};
|
|
31034
31558
|
var decisionContent = (purpose, question, approvedContent) => ({
|
|
31035
31559
|
header: `Workit decision: ${purpose}`,
|
|
31036
31560
|
question,
|
|
@@ -31040,11 +31564,12 @@ var decisionContent = (purpose, question, approvedContent) => ({
|
|
|
31040
31564
|
]
|
|
31041
31565
|
});
|
|
31042
31566
|
var decisionOptions = (options) => Array.isArray(options) && options.length === 2 && options.every((option) => typeof option === "object" && option !== null && typeof option.label === "string" && typeof option.description === "string" && Object.keys(option).length === 2 && Object.keys(option).every((key) => key === "label" || key === "description")) ? options : null;
|
|
31567
|
+
var normalizeReceiptLabel = (label) => label.trim().replace(/(?:\s*[([]\s*[^()[\]]*\s*[)\]]\s*)+$/u, "").trim().toLowerCase();
|
|
31043
31568
|
var purposeForQuestion = (question) => {
|
|
31044
31569
|
const header = typeof question.header === "string" ? question.header.trim() : "";
|
|
31045
31570
|
const options = decisionOptions(question.options);
|
|
31046
31571
|
const decisionPurpose = header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1];
|
|
31047
|
-
if (decisionPurpose && options !== null && options[0].label === "approved" && options[1].label === "rejected"
|
|
31572
|
+
if (decisionPurpose && options !== null && normalizeReceiptLabel(options[0].label) === "approved" && normalizeReceiptLabel(options[1].label) === "rejected")
|
|
31048
31573
|
return "decision";
|
|
31049
31574
|
return;
|
|
31050
31575
|
};
|
|
@@ -31053,6 +31578,10 @@ class NativeReceiptStore {
|
|
|
31053
31578
|
#receipts = new Map;
|
|
31054
31579
|
#observations = new WeakSet;
|
|
31055
31580
|
#pending = new Map;
|
|
31581
|
+
#now;
|
|
31582
|
+
constructor(options = {}) {
|
|
31583
|
+
this.#now = options.now ?? Date.now;
|
|
31584
|
+
}
|
|
31056
31585
|
recordRequest(requestID, sessionID, callID, questions) {
|
|
31057
31586
|
if (typeof requestID !== "string" || !requestID)
|
|
31058
31587
|
return;
|
|
@@ -31090,7 +31619,7 @@ class NativeReceiptStore {
|
|
|
31090
31619
|
const options = decisionOptions(question.options);
|
|
31091
31620
|
if (!options)
|
|
31092
31621
|
return false;
|
|
31093
|
-
const selected = options
|
|
31622
|
+
const selected = options.find((option) => normalizeReceiptLabel(option.label) === normalizeReceiptLabel(answer));
|
|
31094
31623
|
if (!selected)
|
|
31095
31624
|
return false;
|
|
31096
31625
|
const decisionPurpose = typeof question.header === "string" ? question.header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1] : undefined;
|
|
@@ -31106,7 +31635,7 @@ class NativeReceiptStore {
|
|
|
31106
31635
|
question: content.question,
|
|
31107
31636
|
purpose,
|
|
31108
31637
|
contentDigest: sha256(canonicalJson(content)),
|
|
31109
|
-
recordedAt:
|
|
31638
|
+
recordedAt: this.#now()
|
|
31110
31639
|
};
|
|
31111
31640
|
const queue = this.#receipts.get(sessionID) ?? [];
|
|
31112
31641
|
queue.push(receipt);
|
|
@@ -31126,15 +31655,12 @@ class NativeReceiptStore {
|
|
|
31126
31655
|
const receipt = queue[i];
|
|
31127
31656
|
if (receipt.purpose !== purpose)
|
|
31128
31657
|
continue;
|
|
31129
|
-
if (expected.callID !== undefined && receipt.callID !== expected.callID || expected.selectedLabel !== undefined && receipt.selectedLabel !== expected.selectedLabel || expected.selectedDescription !== undefined && receipt.selectedDescription !== expected.selectedDescription || expected.decisionPurpose !== undefined && receipt.decisionPurpose !== expected.decisionPurpose || expected.contentDigest !== undefined && receipt.contentDigest !== expected.contentDigest || expected.question !== undefined && receipt.question !== expected.question)
|
|
31130
|
-
|
|
31131
|
-
ok: false,
|
|
31132
|
-
error: "permission_denied: native question receipt does not match the requested content " + "(binding questions must be asked receipt-shaped: header `Workit decision: <purpose>` " + "with the same label in the question text and exactly approved/rejected options)"
|
|
31133
|
-
};
|
|
31658
|
+
if (expected.callID !== undefined && receipt.callID !== expected.callID || expected.selectedLabel !== undefined && normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel) || expected.selectedDescription !== undefined && receipt.selectedDescription !== expected.selectedDescription || expected.decisionPurpose !== undefined && receipt.decisionPurpose !== expected.decisionPurpose || expected.contentDigest !== undefined && receipt.contentDigest !== expected.contentDigest || expected.question !== undefined && receipt.question !== expected.question)
|
|
31659
|
+
continue;
|
|
31134
31660
|
queue.splice(i, 1);
|
|
31135
31661
|
if (!queue.length)
|
|
31136
31662
|
this.#receipts.delete(sessionID);
|
|
31137
|
-
if (
|
|
31663
|
+
if (this.#now() - receipt.recordedAt > freshMs)
|
|
31138
31664
|
return { ok: false, error: "permission_denied: native question receipt is stale" };
|
|
31139
31665
|
const observation = { receipt };
|
|
31140
31666
|
this.#observations.add(observation);
|
|
@@ -31142,7 +31668,27 @@ class NativeReceiptStore {
|
|
|
31142
31668
|
}
|
|
31143
31669
|
return {
|
|
31144
31670
|
ok: false,
|
|
31145
|
-
error:
|
|
31671
|
+
error: (() => {
|
|
31672
|
+
const samePurpose = queue.filter((receipt) => receipt.purpose === purpose);
|
|
31673
|
+
const base = "permission_denied: no matching native question receipt for this purpose";
|
|
31674
|
+
if (samePurpose.length) {
|
|
31675
|
+
const closest = samePurpose[samePurpose.length - 1];
|
|
31676
|
+
const reasons = [];
|
|
31677
|
+
if (expected.selectedLabel !== undefined && normalizeReceiptLabel(closest.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel))
|
|
31678
|
+
reasons.push(`selected answer was "${closest.selectedLabel}"`);
|
|
31679
|
+
if (expected.selectedDescription !== undefined && closest.selectedDescription !== expected.selectedDescription)
|
|
31680
|
+
reasons.push("approved option description did not match");
|
|
31681
|
+
if (expected.decisionPurpose !== undefined && closest.decisionPurpose !== expected.decisionPurpose)
|
|
31682
|
+
reasons.push(`purpose was "${closest.decisionPurpose}"`);
|
|
31683
|
+
if (expected.contentDigest !== undefined && closest.contentDigest !== expected.contentDigest)
|
|
31684
|
+
reasons.push("question or approved content differed from the asked receipt");
|
|
31685
|
+
if (this.#now() - closest.recordedAt > freshMs)
|
|
31686
|
+
reasons.push("receipt is stale (older than 5 minutes)");
|
|
31687
|
+
if (reasons.length)
|
|
31688
|
+
return `${base} (closest ${purpose} receipt: ${reasons.join("; ")})`;
|
|
31689
|
+
}
|
|
31690
|
+
return `${base}; if the user already settled this choice in conversation, do not ask ` + `again — record the settled choice in task progress and reassess so the ` + `requirement can retire. Otherwise ask once, receipt-shaped: header ` + `\`Workit decision: <purpose>\`, the same label repeated in the question text, ` + `with exactly approved/rejected options`;
|
|
31691
|
+
})()
|
|
31146
31692
|
};
|
|
31147
31693
|
}
|
|
31148
31694
|
verify(observation, sessionID, purpose) {
|
|
@@ -31244,6 +31790,13 @@ var opencodeCapabilities = () => [
|
|
|
31244
31790
|
reason: "nested native task launches are denied and observed child sessions are parent-bound",
|
|
31245
31791
|
refs: [hostRef("task")]
|
|
31246
31792
|
},
|
|
31793
|
+
{
|
|
31794
|
+
name: "fresh-context-review",
|
|
31795
|
+
surface: "task",
|
|
31796
|
+
assurance: "agent_guided",
|
|
31797
|
+
reason: "independent review runs as a native direct-child session; evidence evaluation enforces creator and duplicate-reviewer exclusion",
|
|
31798
|
+
refs: [hostRef("task")]
|
|
31799
|
+
},
|
|
31247
31800
|
{
|
|
31248
31801
|
name: "known_product_writes",
|
|
31249
31802
|
surface: "write/edit/bash",
|
|
@@ -31262,7 +31815,8 @@ var opencodeCapabilities = () => [
|
|
|
31262
31815
|
var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
|
|
31263
31816
|
verifyDecision: ({ observation, expected, caller }) => {
|
|
31264
31817
|
const receipt = receipts.verify(observation, actor, "decision");
|
|
31265
|
-
|
|
31818
|
+
const expectedApproved = typeof expected.binding.displayed === "string" ? expected.binding.displayed : expected.binding.approvedContent;
|
|
31819
|
+
if (!receipt || caller.host !== "opencode" || caller.actor !== actor || normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.response) || expected.response === "approved" && receipt.selectedDescription !== expectedApproved || receipt.decisionPurpose !== expected.purpose || receipt.contentDigest !== sha256(canonicalJson(decisionContent(expected.purpose, expected.binding.presented, expectedApproved))) || receipt.question !== expected.binding.presented)
|
|
31266
31820
|
return failure("permission_denied", "native decision receipt is not bound to this session");
|
|
31267
31821
|
return success(null, null, {
|
|
31268
31822
|
kind: "host_observed",
|
|
@@ -31337,8 +31891,34 @@ var nativeDispatchFor = (directChildren, actor, generation) => ({
|
|
|
31337
31891
|
var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
|
|
31338
31892
|
const store = new TaskStore(root);
|
|
31339
31893
|
const selected = approvedExternalAction(store, "opencode", actor, operation);
|
|
31340
|
-
if (!selected.ok)
|
|
31894
|
+
if (!selected.ok) {
|
|
31895
|
+
const plan = planCommitBinding(store, "opencode", actor, operation);
|
|
31896
|
+
if (plan) {
|
|
31897
|
+
const actionRef = externalActionRef("opencode", actor, operation);
|
|
31898
|
+
return {
|
|
31899
|
+
taskId: plan.taskId,
|
|
31900
|
+
decisionId: plan.decisionId,
|
|
31901
|
+
actionRef,
|
|
31902
|
+
expectedRevision: plan.expectedRevision,
|
|
31903
|
+
expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
|
|
31904
|
+
binding: plan.binding,
|
|
31905
|
+
step: plan.step,
|
|
31906
|
+
refresh: () => {
|
|
31907
|
+
const task = store.readTask(plan.taskId);
|
|
31908
|
+
const freshWorkspace = store.readWorkspace();
|
|
31909
|
+
if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
|
|
31910
|
+
throw new Error("external action state changed");
|
|
31911
|
+
return {
|
|
31912
|
+
expectedRevision: task.data.revision,
|
|
31913
|
+
expectedWorkspaceRevision: freshWorkspace.data.revision
|
|
31914
|
+
};
|
|
31915
|
+
},
|
|
31916
|
+
reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
|
|
31917
|
+
settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
|
|
31918
|
+
};
|
|
31919
|
+
}
|
|
31341
31920
|
return selected;
|
|
31921
|
+
}
|
|
31342
31922
|
const actionRef = externalActionRef("opencode", actor, operation);
|
|
31343
31923
|
return {
|
|
31344
31924
|
taskId: selected.data.task.id,
|
|
@@ -31382,8 +31962,10 @@ var workerIdFor = (store, actor, current, directChildren) => {
|
|
|
31382
31962
|
var createWorkitTools = ({
|
|
31383
31963
|
client,
|
|
31384
31964
|
receipts = new NativeReceiptStore,
|
|
31385
|
-
directChildren = new Map
|
|
31965
|
+
directChildren = new Map,
|
|
31966
|
+
now = Date.now
|
|
31386
31967
|
} = {}) => {
|
|
31968
|
+
const actionProposals = new Map;
|
|
31387
31969
|
const make = (family) => tool({
|
|
31388
31970
|
description: `Workit ${family} operations backed by the shared task contract.`,
|
|
31389
31971
|
args: operationShapeFor(family),
|
|
@@ -31417,16 +31999,45 @@ var createWorkitTools = ({
|
|
|
31417
31999
|
let result;
|
|
31418
32000
|
if (family === "decision" && args.action === "record") {
|
|
31419
32001
|
const decision = parsed.data;
|
|
31420
|
-
const
|
|
32002
|
+
const budgetIssue = workitBindingQuestionIssue([
|
|
32003
|
+
decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent)
|
|
32004
|
+
]);
|
|
32005
|
+
if (budgetIssue)
|
|
32006
|
+
return output2(failure("invalid_input", budgetIssue));
|
|
32007
|
+
const expectation = {
|
|
31421
32008
|
selectedLabel: decision.response,
|
|
31422
|
-
selectedDescription: decision.response === "approved" ? decision.binding.approvedContent : rejectedDescription,
|
|
31423
32009
|
decisionPurpose: decision.purpose,
|
|
31424
32010
|
contentDigest: sha256(canonicalJson(decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent))),
|
|
31425
32011
|
question: decision.binding.presented
|
|
31426
|
-
}
|
|
32012
|
+
};
|
|
32013
|
+
if (decision.response === "approved")
|
|
32014
|
+
expectation.selectedDescription = decision.binding.approvedContent;
|
|
32015
|
+
const observed = receipts.consume(context.sessionID, "decision", expectation);
|
|
31427
32016
|
if (!observed.ok)
|
|
31428
32017
|
return output2(failure("permission_denied", observed.error));
|
|
31429
|
-
|
|
32018
|
+
let recordInput = parsed.data;
|
|
32019
|
+
if (decision.purpose === "action" && decision.response === "approved") {
|
|
32020
|
+
const queue = actionProposals.get(context.sessionID) ?? [];
|
|
32021
|
+
const textMatches = queue.filter((pending) => pending.presented === decision.binding.presented && pending.approvedText === decision.binding.approvedContent);
|
|
32022
|
+
const matches = textMatches.filter((pending) => now() - pending.createdAt <= 5 * 60 * 1000);
|
|
32023
|
+
if (matches.length > 1)
|
|
32024
|
+
return output2(failure("invalid_input", "multiple action proposals match this approval; resolve the action again"));
|
|
32025
|
+
const pending = matches[0];
|
|
32026
|
+
if (pending) {
|
|
32027
|
+
actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
|
|
32028
|
+
recordInput = {
|
|
32029
|
+
...recordInput,
|
|
32030
|
+
binding: {
|
|
32031
|
+
...recordInput.binding,
|
|
32032
|
+
approvedContent: pending.descriptor,
|
|
32033
|
+
displayed: pending.approvedText
|
|
32034
|
+
}
|
|
32035
|
+
};
|
|
32036
|
+
} else if (!isSelfAuthorizingActionContent(decision.binding.approvedContent)) {
|
|
32037
|
+
return output2(failure("invalid_input", textMatches.length > 0 ? "action proposal expired; resolve the action again for a fresh proposal" : "no matching action proposal; resolve the action through the action tool first"));
|
|
32038
|
+
}
|
|
32039
|
+
}
|
|
32040
|
+
result = core.observeDecision(recordInput, observed.observation);
|
|
31430
32041
|
} else {
|
|
31431
32042
|
const run = core[family];
|
|
31432
32043
|
result = run.call(core, parsed.data);
|
|
@@ -31527,6 +32138,64 @@ var createWorkitTools = ({
|
|
|
31527
32138
|
return output2(failure("external_outcome_unknown", "previous external action outcome is unknown"));
|
|
31528
32139
|
}
|
|
31529
32140
|
const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
|
|
32141
|
+
const drift = resolved.data.request.operation === "git.branch_setup" ? priorResolvedDrift(store, "opencode", context.sessionID, resolved.data.request.operation, resolved.data.request.payload, resolved.data.descriptorPayload.resolved) : success(null, null, null);
|
|
32142
|
+
if (!drift.ok)
|
|
32143
|
+
return output2(drift);
|
|
32144
|
+
const localOperation = [
|
|
32145
|
+
"git.branch_setup",
|
|
32146
|
+
"git.commit",
|
|
32147
|
+
"git.push",
|
|
32148
|
+
"hosting.pull_request",
|
|
32149
|
+
"changelog.apply"
|
|
32150
|
+
].includes(resolved.data.request.operation);
|
|
32151
|
+
if (localOperation) {
|
|
32152
|
+
const writer = assertLocalExternalActionWriter(context.directory, {
|
|
32153
|
+
host: "opencode",
|
|
32154
|
+
actor: context.sessionID
|
|
32155
|
+
});
|
|
32156
|
+
if (!writer.ok)
|
|
32157
|
+
return output2(failure("needs_input", "writer ownership is required before this action", {
|
|
32158
|
+
outcome: "not_started",
|
|
32159
|
+
operation: resolved.data.request.operation,
|
|
32160
|
+
guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
|
|
32161
|
+
}));
|
|
32162
|
+
}
|
|
32163
|
+
const selected = approvedExternalAction(store, "opencode", context.sessionID, descriptor);
|
|
32164
|
+
let planAuthorized = false;
|
|
32165
|
+
if (!selected.ok) {
|
|
32166
|
+
const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
|
|
32167
|
+
const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
|
|
32168
|
+
if (typeof commitMessage === "string" && commitMessage) {
|
|
32169
|
+
const plan = approvedPlanCommit(store, "opencode", context.sessionID, commitMessage);
|
|
32170
|
+
planAuthorized = plan.ok && plan.data.branch === currentBranch;
|
|
32171
|
+
}
|
|
32172
|
+
}
|
|
32173
|
+
if (!selected.ok && !planAuthorized) {
|
|
32174
|
+
if (selected.error.startsWith("no approved action")) {
|
|
32175
|
+
const proposal = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
|
|
32176
|
+
const pending = actionProposals.get(context.sessionID) ?? [];
|
|
32177
|
+
pending.push({
|
|
32178
|
+
descriptor,
|
|
32179
|
+
presented: proposal.presented,
|
|
32180
|
+
approvedText: proposal.approvedText,
|
|
32181
|
+
createdAt: now()
|
|
32182
|
+
});
|
|
32183
|
+
if (pending.length > 8)
|
|
32184
|
+
pending.shift();
|
|
32185
|
+
actionProposals.set(context.sessionID, pending);
|
|
32186
|
+
return output2(failure("needs_input", proposal.presented, {
|
|
32187
|
+
outcome: "not_started",
|
|
32188
|
+
operation: resolved.data.request.operation,
|
|
32189
|
+
proposal: {
|
|
32190
|
+
presented: proposal.presented,
|
|
32191
|
+
approvedContent: proposal.approvedText,
|
|
32192
|
+
descriptorDigest: sha256(descriptor)
|
|
32193
|
+
},
|
|
32194
|
+
guidance: "Ask one native question with header `Workit decision: action`, the presented text as the question text, and exactly two options: approved (description = the proposal approvedContent) and rejected (description = `Reject this decision`). Then call decision.record with the same presented/approvedContent and the native receipt; the adapter binds the exact descriptor. Never show the raw descriptor."
|
|
32195
|
+
}));
|
|
32196
|
+
}
|
|
32197
|
+
return output2(selected);
|
|
32198
|
+
}
|
|
31530
32199
|
const result = await nativeExternalActionRunner(context.directory, context.sessionID, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, context.directory, step, {
|
|
31531
32200
|
host: "opencode",
|
|
31532
32201
|
actor: context.sessionID
|
|
@@ -31626,26 +32295,14 @@ var loadProvenance = (logger, pkgUrl) => {
|
|
|
31626
32295
|
};
|
|
31627
32296
|
|
|
31628
32297
|
// packages/workit-opencode/src/plugin.ts
|
|
31629
|
-
var root =
|
|
32298
|
+
var root = fileURLToPath3(new URL("../assets/", import.meta.url));
|
|
31630
32299
|
var skillsPath = path25.join(root, "skills");
|
|
31631
32300
|
var logger = createLogger({
|
|
31632
32301
|
appLog: () => {
|
|
31633
32302
|
return;
|
|
31634
32303
|
}
|
|
31635
32304
|
});
|
|
31636
|
-
var sourceMarker = markSourcesLoaded(
|
|
31637
|
-
fileURLToPath2(import.meta.url),
|
|
31638
|
-
fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
|
|
31639
|
-
...[
|
|
31640
|
-
"task-contract.ts",
|
|
31641
|
-
"task-engine.ts",
|
|
31642
|
-
"task-evaluation.ts",
|
|
31643
|
-
"task-store.ts",
|
|
31644
|
-
"workers.ts",
|
|
31645
|
-
"authority.ts",
|
|
31646
|
-
"methods.ts"
|
|
31647
|
-
].map((file) => fileURLToPath2(new URL(`../../../workit-core/src/core/${file}`, import.meta.url)))
|
|
31648
|
-
].filter((file) => existsSync12(file)));
|
|
32305
|
+
var sourceMarker = markSourcesLoaded(pluginSourceFiles);
|
|
31649
32306
|
var staleSourcesWarned = false;
|
|
31650
32307
|
var warnStaleSources = () => {
|
|
31651
32308
|
if (staleSourcesWarned)
|
|
@@ -31685,7 +32342,6 @@ var plugin = async ({ client, directory }) => {
|
|
|
31685
32342
|
const lifecycleBindings = new Map;
|
|
31686
32343
|
const unresolvedTaskLaunches = new Set;
|
|
31687
32344
|
const dispatches = new Map;
|
|
31688
|
-
const bootstrapped = new Set;
|
|
31689
32345
|
try {
|
|
31690
32346
|
logger.info(EVENT.initialization, { host: "opencode", plugin_root: root });
|
|
31691
32347
|
logger.info(EVENT.provenance, loadProvenance(logger, new URL("../package.json", import.meta.url)));
|
|
@@ -31713,16 +32369,20 @@ var plugin = async ({ client, directory }) => {
|
|
|
31713
32369
|
return [...items].sort((a, b) => a.entry.recordedAt < b.entry.recordedAt ? -1 : a.entry.recordedAt > b.entry.recordedAt ? 1 : a.entry.id < b.entry.id ? -1 : 1)[0];
|
|
31714
32370
|
};
|
|
31715
32371
|
const prepareDispatch = (coordinator, callID) => {
|
|
31716
|
-
dispatches.
|
|
32372
|
+
if (dispatches.has(coordinator))
|
|
32373
|
+
return "unsettled";
|
|
31717
32374
|
const store = new TaskStore(directory);
|
|
31718
32375
|
const workspace = store.readWorkspace();
|
|
31719
32376
|
const listed = store.listTasks();
|
|
31720
32377
|
if (!workspace.ok || !workspace.data || !listed.ok)
|
|
31721
|
-
return;
|
|
31722
|
-
const
|
|
32378
|
+
return "unmanaged";
|
|
32379
|
+
const owned = listed.data.filter((task) => task.status === "active" && task.workspaceId === workspace.data?.id && task.intent.provenance.session?.kind === "host" && task.intent.provenance.session.host === "opencode" && task.intent.provenance.session.handle === coordinator);
|
|
32380
|
+
if (owned.length === 0)
|
|
32381
|
+
return "unmanaged";
|
|
32382
|
+
const eligible = owned.flatMap((task) => task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === coordinator).map((entry) => ({ task, entry })));
|
|
31723
32383
|
const next = oldestOfSingleTask(eligible);
|
|
31724
32384
|
if (!next)
|
|
31725
|
-
return;
|
|
32385
|
+
return "unbound";
|
|
31726
32386
|
const generation = {
|
|
31727
32387
|
coordinator,
|
|
31728
32388
|
callID,
|
|
@@ -31745,7 +32405,7 @@ var plugin = async ({ client, directory }) => {
|
|
|
31745
32405
|
observation: { stage: "prepare", sessionID: coordinator, callID }
|
|
31746
32406
|
});
|
|
31747
32407
|
if (!prepared.ok)
|
|
31748
|
-
return;
|
|
32408
|
+
return "unbound";
|
|
31749
32409
|
dispatches.set(coordinator, {
|
|
31750
32410
|
generation,
|
|
31751
32411
|
dispatch: prepared.data,
|
|
@@ -31753,6 +32413,7 @@ var plugin = async ({ client, directory }) => {
|
|
|
31753
32413
|
taskId: next.task.id,
|
|
31754
32414
|
workerId: next.entry.id
|
|
31755
32415
|
});
|
|
32416
|
+
return "prepared";
|
|
31756
32417
|
};
|
|
31757
32418
|
const commitDispatchStart = (coordinator, childID) => {
|
|
31758
32419
|
const pending = dispatches.get(coordinator);
|
|
@@ -31888,7 +32549,7 @@ var plugin = async ({ client, directory }) => {
|
|
|
31888
32549
|
const listed = store.listTasks();
|
|
31889
32550
|
if (!workspace.ok || !workspace.data || !listed.ok)
|
|
31890
32551
|
return;
|
|
31891
|
-
const candidates = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === info.parentID).map((entry) => ({ task, entry })) : []);
|
|
32552
|
+
const candidates = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => (entry.data.state === "assigned" || entry.data.state === "dispatching") && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === info.parentID).map((entry) => ({ task, entry })) : []);
|
|
31892
32553
|
const next = oldestOfSingleTask(candidates);
|
|
31893
32554
|
if (!next)
|
|
31894
32555
|
return;
|
|
@@ -31951,8 +32612,21 @@ var plugin = async ({ client, directory }) => {
|
|
|
31951
32612
|
unresolvedTaskLaunches.add(input.sessionID);
|
|
31952
32613
|
}
|
|
31953
32614
|
},
|
|
31954
|
-
"tool.execute.before": async (input,
|
|
32615
|
+
"tool.execute.before": async (input, output) => {
|
|
31955
32616
|
warnStaleSources();
|
|
32617
|
+
if (input.tool === "bash") {
|
|
32618
|
+
const command = output?.args?.command;
|
|
32619
|
+
const route = typeof command === "string" ? shellRouteIntent(command) : null;
|
|
32620
|
+
if (route)
|
|
32621
|
+
throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
|
|
32622
|
+
return;
|
|
32623
|
+
}
|
|
32624
|
+
if (input.tool === "question") {
|
|
32625
|
+
const issue = workitBindingQuestionIssue(output?.args?.questions);
|
|
32626
|
+
if (issue)
|
|
32627
|
+
throw new Error(issue);
|
|
32628
|
+
return;
|
|
32629
|
+
}
|
|
31956
32630
|
if (input.tool === "task") {
|
|
31957
32631
|
const listed = new TaskStore(directory).listTasks();
|
|
31958
32632
|
if (listed.ok && listed.data.some((task) => task.workers.some((worker) => (worker.data.state === "cancelling" || worker.data.state === "unknown") && worker.provenance.session?.kind === "host" && worker.provenance.session.handle === input.sessionID)))
|
|
@@ -31962,13 +32636,17 @@ var plugin = async ({ client, directory }) => {
|
|
|
31962
32636
|
const session = await sessionData2(client, input.sessionID);
|
|
31963
32637
|
if (!client || !trustedSession(directory, input.sessionID, session) || session.parentID)
|
|
31964
32638
|
throw new Error("delegation_lineage_denied: native task workers must be direct children of the coordinator");
|
|
31965
|
-
prepareDispatch(input.sessionID, input.callID);
|
|
32639
|
+
const prepared = prepareDispatch(input.sessionID, input.callID);
|
|
32640
|
+
if (prepared === "unsettled")
|
|
32641
|
+
throw new Error("recovery_required: a previous native task launch is unsettled; wait for it to settle or reconcile its worker before launching again");
|
|
32642
|
+
if (prepared === "unbound")
|
|
32643
|
+
throw new Error("recovery_required: no assigned Workit worker is attributable to this coordinator; assign one with workit_worker before launching a native task");
|
|
31966
32644
|
}
|
|
31967
32645
|
},
|
|
31968
32646
|
config: async (config) => {
|
|
31969
32647
|
const mutable = config;
|
|
31970
32648
|
const paths = [...mutable.skills?.paths ?? []];
|
|
31971
|
-
if (
|
|
32649
|
+
if (existsSync13(skillsPath) && !paths.includes(skillsPath))
|
|
31972
32650
|
paths.push(skillsPath);
|
|
31973
32651
|
mutable.skills ??= {};
|
|
31974
32652
|
mutable.skills.paths = paths;
|
|
@@ -32011,8 +32689,6 @@ var plugin = async ({ client, directory }) => {
|
|
|
32011
32689
|
if (!first || !first.parts.length)
|
|
32012
32690
|
return;
|
|
32013
32691
|
const sessionID = first.info.sessionID;
|
|
32014
|
-
if (bootstrapped.has(sessionID))
|
|
32015
|
-
return;
|
|
32016
32692
|
const anchor = first.parts[0];
|
|
32017
32693
|
const session = await sessionData2(client, sessionID);
|
|
32018
32694
|
if (!trustedSession(directory, sessionID, session))
|
|
@@ -32037,7 +32713,6 @@ var plugin = async ({ client, directory }) => {
|
|
|
32037
32713
|
text: `<workit-worker-context>${workerContext}</workit-worker-context>`
|
|
32038
32714
|
});
|
|
32039
32715
|
}
|
|
32040
|
-
bootstrapped.add(sessionID);
|
|
32041
32716
|
}
|
|
32042
32717
|
};
|
|
32043
32718
|
};
|