@brainervirus/workit-opencode 1.0.9 → 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-plan/SKILL.md +14 -2
- package/dist/plugin.js +826 -266
- 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,
|
|
@@ -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)
|
|
@@ -10403,7 +10585,7 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10403
10585
|
var METHODS = {
|
|
10404
10586
|
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
10405
10587
|
"workit-behavioral-tdd": { dimensions: ["testing"] },
|
|
10406
|
-
"workit-review": { dimensions: ["review"] },
|
|
10588
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
|
|
10407
10589
|
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10408
10590
|
"workit-implement": { dimensions: ["delegation"] },
|
|
10409
10591
|
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
@@ -10465,7 +10647,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
|
10465
10647
|
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10466
10648
|
header Workit decision: <purpose>, the same label repeated in the question
|
|
10467
10649
|
text (some host UIs do not render headers), exactly two options approved and
|
|
10468
|
-
rejected; ask once, never re-ask to mint a receipt.
|
|
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,
|
|
10469
10662
|
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10470
10663
|
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10471
10664
|
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
@@ -10497,7 +10690,7 @@ function reconcileResume(view, observations = []) {
|
|
|
10497
10690
|
return candidate;
|
|
10498
10691
|
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
10499
10692
|
const blockers = [...view.task.progress.blockers];
|
|
10500
|
-
if (view.task.workers.some((entry) =>
|
|
10693
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
10501
10694
|
blockers.push({
|
|
10502
10695
|
reason: "worker state requires reconciliation",
|
|
10503
10696
|
dependentAction: "resume",
|
|
@@ -10630,6 +10823,7 @@ function shellRouteIntent(command) {
|
|
|
10630
10823
|
return null;
|
|
10631
10824
|
}
|
|
10632
10825
|
// packages/workit-core/src/core/external-action.ts
|
|
10826
|
+
import { execFileSync } from "node:child_process";
|
|
10633
10827
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
10634
10828
|
object({
|
|
10635
10829
|
operation: literal("git.branch_setup"),
|
|
@@ -10642,7 +10836,11 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
10642
10836
|
}).strict(),
|
|
10643
10837
|
object({
|
|
10644
10838
|
operation: literal("git.commit"),
|
|
10645
|
-
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()
|
|
10646
10844
|
}).strict(),
|
|
10647
10845
|
object({
|
|
10648
10846
|
operation: literal("git.push"),
|
|
@@ -10731,7 +10929,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
10731
10929
|
payload,
|
|
10732
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 } : {}
|
|
10733
10931
|
});
|
|
10734
|
-
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).";
|
|
10735
10933
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
10736
10934
|
kind: "host",
|
|
10737
10935
|
host,
|
|
@@ -10782,6 +10980,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
|
|
|
10782
10980
|
return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
|
|
10783
10981
|
return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
|
|
10784
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
|
+
};
|
|
10785
11069
|
var approvedExternalAction = (store, host, actor, operation) => {
|
|
10786
11070
|
const selected = externalActionState(store, host, actor, operation);
|
|
10787
11071
|
if (!selected.ok)
|
|
@@ -10790,6 +11074,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
|
|
|
10790
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");
|
|
10791
11075
|
return selected;
|
|
10792
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
|
+
};
|
|
10793
11094
|
var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
|
|
10794
11095
|
let steps = [];
|
|
10795
11096
|
try {
|
|
@@ -11067,7 +11368,7 @@ function resolveRequirements(input) {
|
|
|
11067
11368
|
].join(", ")}`,
|
|
11068
11369
|
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
11069
11370
|
before: "dependent_action",
|
|
11070
|
-
dependentAction:
|
|
11371
|
+
dependentAction: null,
|
|
11071
11372
|
acceptanceAllowed: false
|
|
11072
11373
|
}));
|
|
11073
11374
|
if (signals.productChoiceOpen.value === true)
|
|
@@ -11078,7 +11379,7 @@ function resolveRequirements(input) {
|
|
|
11078
11379
|
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
11079
11380
|
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
11080
11381
|
before: "dependent_action",
|
|
11081
|
-
dependentAction:
|
|
11382
|
+
dependentAction: null,
|
|
11082
11383
|
acceptanceAllowed: false
|
|
11083
11384
|
}));
|
|
11084
11385
|
if (signals.behaviorChange.value === true)
|
|
@@ -11236,59 +11537,6 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
11236
11537
|
return parsed.data;
|
|
11237
11538
|
}
|
|
11238
11539
|
|
|
11239
|
-
// packages/workit-core/src/core/workers.ts
|
|
11240
|
-
var same = (left, right) => {
|
|
11241
|
-
try {
|
|
11242
|
-
return canonicalJson(left) === canonicalJson(right);
|
|
11243
|
-
} catch {
|
|
11244
|
-
return false;
|
|
11245
|
-
}
|
|
11246
|
-
};
|
|
11247
|
-
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
11248
|
-
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
11249
|
-
const code = char.charCodeAt(0);
|
|
11250
|
-
return code < 32 || code === 127;
|
|
11251
|
-
}) && !value.split(/[\\/]/).includes("..");
|
|
11252
|
-
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
11253
|
-
var workerIdOf = (value) => value.workerId ?? null;
|
|
11254
|
-
function assertProductWriteAllowed(input) {
|
|
11255
|
-
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
11256
|
-
return failure("invalid_input", "invalid product write path");
|
|
11257
|
-
if (!input.store)
|
|
11258
|
-
return failure("permission_denied", "write authorization requires core state");
|
|
11259
|
-
const task = input.store.readTask(input.task.id);
|
|
11260
|
-
if (!task.ok)
|
|
11261
|
-
return task;
|
|
11262
|
-
const workspace = input.store.readWorkspace();
|
|
11263
|
-
if (!workspace.ok)
|
|
11264
|
-
return workspace;
|
|
11265
|
-
if (!workspace.data)
|
|
11266
|
-
return failure("not_found", "workspace not found");
|
|
11267
|
-
const workspaceRecord = workspace.data;
|
|
11268
|
-
if (task.data.status !== "active")
|
|
11269
|
-
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
11270
|
-
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
11271
|
-
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
11272
|
-
const writer = workspaceRecord.writer;
|
|
11273
|
-
if (!writer)
|
|
11274
|
-
return failure("permission_denied", "checkout has no writer owner");
|
|
11275
|
-
if (writer.state === "uncertain")
|
|
11276
|
-
return failure("recovery_required", "checkout writer requires recovery", {
|
|
11277
|
-
owner: writer.owner
|
|
11278
|
-
});
|
|
11279
|
-
const caller = callerValue(input.caller);
|
|
11280
|
-
const workerId = workerIdOf(input.caller);
|
|
11281
|
-
const owner = writer.owner;
|
|
11282
|
-
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
11283
|
-
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
11284
|
-
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
11285
|
-
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
11286
|
-
return failure("permission_denied", "only implementers can own product writes");
|
|
11287
|
-
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))))
|
|
11288
|
-
return failure("recovery_required", "worker session is not an active writer");
|
|
11289
|
-
return success(null, workspaceRecord.revision, owner);
|
|
11290
|
-
}
|
|
11291
|
-
|
|
11292
11540
|
// packages/workit-core/src/core/task-engine.ts
|
|
11293
11541
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
11294
11542
|
kind,
|
|
@@ -11419,15 +11667,20 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11419
11667
|
return failure("invalid_transition", "worker has not started");
|
|
11420
11668
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
11421
11669
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
11670
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
11422
11671
|
const nextEntry = {
|
|
11423
11672
|
...entry,
|
|
11424
11673
|
recordedAt: input.now ?? entry.recordedAt,
|
|
11425
11674
|
provenance: authority.provenance,
|
|
11426
|
-
data: {
|
|
11675
|
+
data: {
|
|
11676
|
+
...entry.data,
|
|
11677
|
+
state: terminalCancel ? "cancelling" : input.state,
|
|
11678
|
+
session: input.session
|
|
11679
|
+
}
|
|
11427
11680
|
};
|
|
11428
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;
|
|
11429
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;
|
|
11430
|
-
if (entry.data.state ===
|
|
11683
|
+
if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
|
|
11431
11684
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
11432
11685
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
11433
11686
|
taskId: task.data.id,
|
|
@@ -11797,7 +12050,7 @@ class WorkitCore {
|
|
|
11797
12050
|
activeWorkerBlocker(task, workspace) {
|
|
11798
12051
|
if (workspace.writer)
|
|
11799
12052
|
return failure("recovery_required", "writer ownership must be released first");
|
|
11800
|
-
if (task.workers.some((entry) =>
|
|
12053
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
11801
12054
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
11802
12055
|
return null;
|
|
11803
12056
|
}
|
|
@@ -12034,11 +12287,6 @@ class WorkitCore {
|
|
|
12034
12287
|
if (helper && !helper.ok)
|
|
12035
12288
|
return helper;
|
|
12036
12289
|
const evidence = input.evidence;
|
|
12037
|
-
if (helper?.ok) {
|
|
12038
|
-
const assignment = helper.data.data.assignment;
|
|
12039
|
-
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))
|
|
12040
|
-
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
12041
|
-
}
|
|
12042
12290
|
if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
|
|
12043
12291
|
return failure("invalid_input", "missing or skipped evidence requires a reason");
|
|
12044
12292
|
if (evidence.kind === "review") {
|
|
@@ -12055,6 +12303,11 @@ class WorkitCore {
|
|
|
12055
12303
|
evidence.beforeCandidateId ??= null;
|
|
12056
12304
|
evidence.candidateId ??= null;
|
|
12057
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
|
+
}
|
|
12058
12311
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
12059
12312
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
12060
12313
|
if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
|
|
@@ -12950,6 +13203,9 @@ class WorkitCore {
|
|
|
12950
13203
|
id: task.id,
|
|
12951
13204
|
revision: task.revision,
|
|
12952
13205
|
workspaceRevision: workspace.data.revision,
|
|
13206
|
+
createdAt: task.createdAt,
|
|
13207
|
+
updatedAt: task.updatedAt,
|
|
13208
|
+
runtime: task.runtime ?? null,
|
|
12953
13209
|
objective: task.intent.data.objective,
|
|
12954
13210
|
status: task.status,
|
|
12955
13211
|
closure: task.closure,
|
|
@@ -13035,7 +13291,7 @@ class WorkitCore {
|
|
|
13035
13291
|
const base = reconcileResume(effectiveView);
|
|
13036
13292
|
if (!base.ok)
|
|
13037
13293
|
return base;
|
|
13038
|
-
const needsReconciliation = view.task.workers.some((entry) =>
|
|
13294
|
+
const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
|
|
13039
13295
|
const blockers = [...base.data.blockers];
|
|
13040
13296
|
if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
|
|
13041
13297
|
blockers.push({
|
|
@@ -13102,7 +13358,8 @@ class WorkitCore {
|
|
|
13102
13358
|
...current,
|
|
13103
13359
|
status,
|
|
13104
13360
|
candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
|
|
13105
|
-
progress:
|
|
13361
|
+
progress: current.progress,
|
|
13362
|
+
pauseReason: status === "paused" ? input.reason ?? null : null
|
|
13106
13363
|
})
|
|
13107
13364
|
});
|
|
13108
13365
|
if (!changed.ok)
|
|
@@ -13138,7 +13395,7 @@ class WorkitCore {
|
|
|
13138
13395
|
return workspace;
|
|
13139
13396
|
if (!workspace.data)
|
|
13140
13397
|
return failure("not_found", "workspace not found");
|
|
13141
|
-
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)))
|
|
13142
13399
|
return failure("recovery_required", "scope revision requires worker ownership reconciliation");
|
|
13143
13400
|
this.fillRevisions(input, task.data, workspace.data);
|
|
13144
13401
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
@@ -13256,15 +13513,15 @@ function resolveGitRevision(root, value) {
|
|
|
13256
13513
|
}
|
|
13257
13514
|
function resolveInside(root, candidate) {
|
|
13258
13515
|
const base = realpathSync5(root);
|
|
13259
|
-
const target =
|
|
13260
|
-
if (target !== base && !target.startsWith(base +
|
|
13516
|
+
const target = path7.resolve(base, candidate);
|
|
13517
|
+
if (target !== base && !target.startsWith(base + path7.sep)) {
|
|
13261
13518
|
throw new Error("path must stay inside repository root");
|
|
13262
13519
|
}
|
|
13263
13520
|
let ancestor = target;
|
|
13264
|
-
while (!
|
|
13265
|
-
ancestor =
|
|
13521
|
+
while (!existsSync4(ancestor))
|
|
13522
|
+
ancestor = path7.dirname(ancestor);
|
|
13266
13523
|
const canonicalAncestor = realpathSync5(ancestor);
|
|
13267
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
13524
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
|
|
13268
13525
|
throw new Error("path must stay inside repository root");
|
|
13269
13526
|
}
|
|
13270
13527
|
return target;
|
|
@@ -13306,7 +13563,7 @@ if (false) {}
|
|
|
13306
13563
|
// packages/workit-core/src/core/logger.ts
|
|
13307
13564
|
import { appendFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
13308
13565
|
import os from "node:os";
|
|
13309
|
-
import
|
|
13566
|
+
import path8 from "node:path";
|
|
13310
13567
|
var REDACTED = "[REDACTED]";
|
|
13311
13568
|
var DEFAULT_MAX_RATE = 20;
|
|
13312
13569
|
var DEFAULT_RATE_WINDOW_MS = 1000;
|
|
@@ -13362,14 +13619,14 @@ var resolveStateDir = () => {
|
|
|
13362
13619
|
return override;
|
|
13363
13620
|
const home = os.homedir();
|
|
13364
13621
|
if (process.env.XDG_STATE_HOME)
|
|
13365
|
-
return
|
|
13622
|
+
return path8.join(process.env.XDG_STATE_HOME, "workit");
|
|
13366
13623
|
switch (os.platform()) {
|
|
13367
13624
|
case "darwin":
|
|
13368
|
-
return
|
|
13625
|
+
return path8.join(home, "Library", "Application Support", "workit");
|
|
13369
13626
|
case "win32":
|
|
13370
|
-
return
|
|
13627
|
+
return path8.join(process.env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local"), "workit");
|
|
13371
13628
|
default:
|
|
13372
|
-
return
|
|
13629
|
+
return path8.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
13373
13630
|
}
|
|
13374
13631
|
};
|
|
13375
13632
|
var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
@@ -13442,14 +13699,14 @@ var pruneOldFiles = (dir) => {
|
|
|
13442
13699
|
const files = readdirSync3(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
|
|
13443
13700
|
for (const file of files.slice(RETAINED_DAYS)) {
|
|
13444
13701
|
try {
|
|
13445
|
-
unlinkSync2(
|
|
13702
|
+
unlinkSync2(path8.join(dir, file));
|
|
13446
13703
|
} catch {}
|
|
13447
13704
|
}
|
|
13448
13705
|
} catch {}
|
|
13449
13706
|
};
|
|
13450
13707
|
var createLogger = (options = {}) => {
|
|
13451
13708
|
const stateDir = options.stateDir ?? resolveStateDir();
|
|
13452
|
-
const logDir =
|
|
13709
|
+
const logDir = path8.join(stateDir, "logs");
|
|
13453
13710
|
const now = options.now ?? (() => new Date);
|
|
13454
13711
|
const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
|
|
13455
13712
|
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
@@ -13483,7 +13740,7 @@ var createLogger = (options = {}) => {
|
|
|
13483
13740
|
const line = `${JSON.stringify(event)}
|
|
13484
13741
|
`;
|
|
13485
13742
|
mkdirSync2(logDir, { recursive: true, mode: 448 });
|
|
13486
|
-
appendFileSync(
|
|
13743
|
+
appendFileSync(path8.join(logDir, dailyFileName(date)), line, { mode: 384 });
|
|
13487
13744
|
pruneOldFiles(logDir);
|
|
13488
13745
|
} catch {
|
|
13489
13746
|
try {
|
|
@@ -13494,7 +13751,7 @@ var createLogger = (options = {}) => {
|
|
|
13494
13751
|
context: { redaction_failed: true }
|
|
13495
13752
|
};
|
|
13496
13753
|
mkdirSync2(logDir, { recursive: true, mode: 448 });
|
|
13497
|
-
appendFileSync(
|
|
13754
|
+
appendFileSync(path8.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
|
|
13498
13755
|
`, {
|
|
13499
13756
|
mode: 384
|
|
13500
13757
|
});
|
|
@@ -13577,6 +13834,24 @@ OpenCode Workit decisions use the native question header "Workit decision: <purp
|
|
|
13577
13834
|
return cached2;
|
|
13578
13835
|
};
|
|
13579
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
|
+
|
|
13580
13855
|
// packages/workit-opencode/src/tools/repo.ts
|
|
13581
13856
|
import path22 from "node:path";
|
|
13582
13857
|
import { realpathSync as realpathSync8 } from "node:fs";
|
|
@@ -25906,25 +26181,6 @@ import fs7 from "node:fs";
|
|
|
25906
26181
|
import path9 from "node:path";
|
|
25907
26182
|
import { createHash as createHash5 } from "node:crypto";
|
|
25908
26183
|
|
|
25909
|
-
// packages/workit-core/src/core/package-root.ts
|
|
25910
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
25911
|
-
import path8 from "node:path";
|
|
25912
|
-
import { fileURLToPath } from "node:url";
|
|
25913
|
-
var packageRoot = () => {
|
|
25914
|
-
let dir = path8.dirname(fileURLToPath(import.meta.url));
|
|
25915
|
-
while (true) {
|
|
25916
|
-
const parent = path8.dirname(dir);
|
|
25917
|
-
if (existsSync4(path8.join(dir, "package.json")) || parent === dir)
|
|
25918
|
-
return dir;
|
|
25919
|
-
dir = parent;
|
|
25920
|
-
}
|
|
25921
|
-
};
|
|
25922
|
-
var assetRoot = () => {
|
|
25923
|
-
const root = packageRoot();
|
|
25924
|
-
const assets = path8.join(root, "assets");
|
|
25925
|
-
return existsSync4(assets) ? assets : root;
|
|
25926
|
-
};
|
|
25927
|
-
|
|
25928
26184
|
// packages/workit-core/src/core/scripts.ts
|
|
25929
26185
|
var PLUGIN_ROOT = packageRoot();
|
|
25930
26186
|
var ASSET_ROOT = assetRoot();
|
|
@@ -26257,10 +26513,10 @@ function changelogApply({
|
|
|
26257
26513
|
}
|
|
26258
26514
|
|
|
26259
26515
|
// packages/workit-core/src/core/git.ts
|
|
26260
|
-
import { execFileSync } from "node:child_process";
|
|
26516
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
26261
26517
|
var run2 = (cwd, args) => {
|
|
26262
26518
|
try {
|
|
26263
|
-
const stdout =
|
|
26519
|
+
const stdout = execFileSync2("git", args, {
|
|
26264
26520
|
cwd,
|
|
26265
26521
|
encoding: "utf8",
|
|
26266
26522
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -26378,15 +26634,15 @@ function parseVerifyOutput(stdout) {
|
|
|
26378
26634
|
}
|
|
26379
26635
|
|
|
26380
26636
|
// packages/workit-core/src/core/branch.ts
|
|
26381
|
-
import { existsSync as
|
|
26382
|
-
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";
|
|
26383
26639
|
import path13 from "node:path";
|
|
26384
26640
|
|
|
26385
26641
|
// packages/workit-core/src/core/config.ts
|
|
26386
26642
|
import {
|
|
26387
26643
|
copyFileSync,
|
|
26388
26644
|
cpSync,
|
|
26389
|
-
existsSync as
|
|
26645
|
+
existsSync as existsSync6,
|
|
26390
26646
|
mkdirSync as mkdirSync3,
|
|
26391
26647
|
readFileSync as readFileSync4,
|
|
26392
26648
|
readdirSync as readdirSync4,
|
|
@@ -26426,11 +26682,11 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
26426
26682
|
return dir;
|
|
26427
26683
|
}
|
|
26428
26684
|
const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
|
|
26429
|
-
if (!
|
|
26685
|
+
if (!existsSync6(legacy)) {
|
|
26430
26686
|
migratedDir = dir;
|
|
26431
26687
|
return dir;
|
|
26432
26688
|
}
|
|
26433
|
-
if (!migrationFailed &&
|
|
26689
|
+
if (!migrationFailed && existsSync6(dir)) {
|
|
26434
26690
|
migratedDir = dir;
|
|
26435
26691
|
return dir;
|
|
26436
26692
|
}
|
|
@@ -26440,7 +26696,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
26440
26696
|
for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
|
|
26441
26697
|
const src = path10.join(legacy, entry.name);
|
|
26442
26698
|
const dest = path10.join(dir, entry.name);
|
|
26443
|
-
if (
|
|
26699
|
+
if (existsSync6(dest))
|
|
26444
26700
|
continue;
|
|
26445
26701
|
try {
|
|
26446
26702
|
if (entry.isDirectory())
|
|
@@ -27018,7 +27274,7 @@ var baseBranch = (cwd) => {
|
|
|
27018
27274
|
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
27019
27275
|
};
|
|
27020
27276
|
var originBaseReady = (cwd, base) => {
|
|
27021
|
-
const run = (args) =>
|
|
27277
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
27022
27278
|
try {
|
|
27023
27279
|
try {
|
|
27024
27280
|
run(["fetch", "origin", base, "--prune"]);
|
|
@@ -27026,7 +27282,7 @@ var originBaseReady = (cwd, base) => {
|
|
|
27026
27282
|
run(["fetch", "origin", "--prune"]);
|
|
27027
27283
|
}
|
|
27028
27284
|
try {
|
|
27029
|
-
|
|
27285
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
27030
27286
|
cwd,
|
|
27031
27287
|
stdio: "pipe"
|
|
27032
27288
|
});
|
|
@@ -27045,11 +27301,11 @@ var originBaseReady = (cwd, base) => {
|
|
|
27045
27301
|
}
|
|
27046
27302
|
};
|
|
27047
27303
|
var fastForwardBase = (cwd, base) => {
|
|
27048
|
-
const run = (args) =>
|
|
27304
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
27049
27305
|
try {
|
|
27050
27306
|
let hasLocalBase = true;
|
|
27051
27307
|
try {
|
|
27052
|
-
|
|
27308
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
27053
27309
|
cwd,
|
|
27054
27310
|
stdio: "pipe"
|
|
27055
27311
|
});
|
|
@@ -27090,18 +27346,17 @@ var branchSetup = ({
|
|
|
27090
27346
|
log
|
|
27091
27347
|
}) => {
|
|
27092
27348
|
const cwd = path13.resolve(workspace_root);
|
|
27093
|
-
const exec = (args) =>
|
|
27349
|
+
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
27094
27350
|
const current = gitContext(cwd).branch;
|
|
27095
27351
|
if (!current || current === "unknown")
|
|
27096
|
-
return { error: "not in a git repository" };
|
|
27352
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
27097
27353
|
const sdd = sdd_dir ?? "docs";
|
|
27098
27354
|
const manifestPath = path13.isAbsolute(sdd) ? path13.join(sdd, "manifest.json") : path13.join(cwd, sdd, "manifest.json");
|
|
27099
27355
|
const ensureManifestDir = () => {
|
|
27100
27356
|
const dir = path13.dirname(manifestPath);
|
|
27101
|
-
if (!
|
|
27357
|
+
if (!existsSync7(dir))
|
|
27102
27358
|
mkdirSync4(dir, { recursive: true, mode: 493 });
|
|
27103
27359
|
};
|
|
27104
|
-
ensureManifestDir();
|
|
27105
27360
|
const readManifest = () => {
|
|
27106
27361
|
try {
|
|
27107
27362
|
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
@@ -27119,13 +27374,16 @@ var branchSetup = ({
|
|
|
27119
27374
|
const manifest = readManifest();
|
|
27120
27375
|
const ref = manifest.stash_ref;
|
|
27121
27376
|
if (!ref)
|
|
27122
|
-
return { error: "no stash_ref in manifest" };
|
|
27377
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
27123
27378
|
journal(`pre-pop: ${String(ref)}`);
|
|
27124
27379
|
try {
|
|
27125
27380
|
exec(["stash", "pop", String(ref)]);
|
|
27126
27381
|
} catch (error) {
|
|
27127
27382
|
journal("pop: failed");
|
|
27128
|
-
return {
|
|
27383
|
+
return {
|
|
27384
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
27385
|
+
phase: "post_mutation"
|
|
27386
|
+
};
|
|
27129
27387
|
}
|
|
27130
27388
|
journal("pop: ok");
|
|
27131
27389
|
delete manifest.stash_ref;
|
|
@@ -27135,11 +27393,17 @@ var branchSetup = ({
|
|
|
27135
27393
|
}
|
|
27136
27394
|
const target = target_branch ?? "";
|
|
27137
27395
|
if (!target)
|
|
27138
|
-
return {
|
|
27396
|
+
return {
|
|
27397
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
27398
|
+
phase: "preflight"
|
|
27399
|
+
};
|
|
27139
27400
|
if (isProtected(cwd, target))
|
|
27140
|
-
return { error: `protected branch ${target}
|
|
27401
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
27141
27402
|
if (!allowedBranch(cwd, target))
|
|
27142
|
-
return {
|
|
27403
|
+
return {
|
|
27404
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
27405
|
+
phase: "preflight"
|
|
27406
|
+
};
|
|
27143
27407
|
let base;
|
|
27144
27408
|
let targetExists = true;
|
|
27145
27409
|
try {
|
|
@@ -27150,12 +27414,12 @@ var branchSetup = ({
|
|
|
27150
27414
|
if (!targetExists) {
|
|
27151
27415
|
const baseResolved = baseBranch(cwd);
|
|
27152
27416
|
if ("error" in baseResolved)
|
|
27153
|
-
return { error: baseResolved.error };
|
|
27417
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
27154
27418
|
base = baseResolved.base;
|
|
27155
27419
|
}
|
|
27156
27420
|
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
27157
27421
|
let stash_ref;
|
|
27158
|
-
const failAfterStash = (message) => {
|
|
27422
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
27159
27423
|
let suffix = "";
|
|
27160
27424
|
if (stash_ref) {
|
|
27161
27425
|
journal(`pre-pop: ${stash_ref}`);
|
|
@@ -27168,27 +27432,31 @@ var branchSetup = ({
|
|
|
27168
27432
|
suffix = " (changes preserved in stash)";
|
|
27169
27433
|
}
|
|
27170
27434
|
}
|
|
27171
|
-
return { error: `${message}${suffix}
|
|
27435
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
27172
27436
|
};
|
|
27173
27437
|
if (current !== target) {
|
|
27174
27438
|
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
27175
27439
|
if (dirty && stash !== "yes") {
|
|
27176
27440
|
return {
|
|
27177
|
-
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"
|
|
27178
27443
|
};
|
|
27179
27444
|
}
|
|
27180
27445
|
let validatedBase;
|
|
27181
27446
|
if (!targetExists && base !== undefined) {
|
|
27182
27447
|
const ready = originBaseReady(cwd, base);
|
|
27183
27448
|
if (!ready.ok)
|
|
27184
|
-
return { error: ready.error ?? "ensure-base-branch failed" };
|
|
27449
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
27185
27450
|
validatedBase = base;
|
|
27186
27451
|
}
|
|
27187
27452
|
if (dirty) {
|
|
27188
27453
|
try {
|
|
27189
27454
|
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
27190
27455
|
} catch (error) {
|
|
27191
|
-
return {
|
|
27456
|
+
return {
|
|
27457
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
27458
|
+
phase: "post_mutation"
|
|
27459
|
+
};
|
|
27192
27460
|
}
|
|
27193
27461
|
stash_ref = "stash@{0}";
|
|
27194
27462
|
journal(`stash push: ${stash_ref}`);
|
|
@@ -27199,14 +27467,14 @@ var branchSetup = ({
|
|
|
27199
27467
|
} catch (error) {
|
|
27200
27468
|
const message = error instanceof Error ? error.message : "checkout failed";
|
|
27201
27469
|
if (/worktree/i.test(message)) {
|
|
27202
|
-
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");
|
|
27203
27471
|
}
|
|
27204
27472
|
try {
|
|
27205
27473
|
let effectiveBase = base;
|
|
27206
27474
|
if (effectiveBase === undefined) {
|
|
27207
27475
|
const lateResolved = baseBranch(cwd);
|
|
27208
27476
|
if ("error" in lateResolved)
|
|
27209
|
-
return failAfterStash(lateResolved.error);
|
|
27477
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
27210
27478
|
effectiveBase = lateResolved.base;
|
|
27211
27479
|
}
|
|
27212
27480
|
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
@@ -27252,7 +27520,7 @@ var branchSetup = ({
|
|
|
27252
27520
|
};
|
|
27253
27521
|
|
|
27254
27522
|
// packages/workit-core/src/core/gitignore.ts
|
|
27255
|
-
import { existsSync as
|
|
27523
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27256
27524
|
import path14 from "node:path";
|
|
27257
27525
|
var GITIGNORE_ENTRIES = [
|
|
27258
27526
|
"# workit: SDD working state (never commit)",
|
|
@@ -27274,7 +27542,7 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
|
|
|
27274
27542
|
if (!confirmed)
|
|
27275
27543
|
return { ok: false, error: "confirmed: true required" };
|
|
27276
27544
|
const file = path14.join(workspaceRoot, ".gitignore");
|
|
27277
|
-
const existing =
|
|
27545
|
+
const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
|
|
27278
27546
|
const existingLines = new Set(existing.split(`
|
|
27279
27547
|
`).map((l) => l.trim()).filter(Boolean));
|
|
27280
27548
|
const added = [];
|
|
@@ -27293,19 +27561,19 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
|
|
|
27293
27561
|
` : "") + append.join(`
|
|
27294
27562
|
`) + `
|
|
27295
27563
|
`, "utf8");
|
|
27296
|
-
} else if (!
|
|
27564
|
+
} else if (!existsSync8(file)) {
|
|
27297
27565
|
writeFileSync4(file, "", "utf8");
|
|
27298
27566
|
}
|
|
27299
27567
|
return { ok: true, path: file, added };
|
|
27300
27568
|
};
|
|
27301
27569
|
|
|
27302
27570
|
// packages/workit-core/src/core/hygiene.ts
|
|
27303
|
-
import { existsSync as
|
|
27571
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27304
27572
|
import path15 from "node:path";
|
|
27305
27573
|
var repoRoot = assetRoot();
|
|
27306
27574
|
var templatesDir = () => path15.join(repoRoot, "templates", "hygiene");
|
|
27307
27575
|
var packageJson = (root) => {
|
|
27308
|
-
if (!
|
|
27576
|
+
if (!existsSync9(path15.join(root, "package.json")))
|
|
27309
27577
|
return null;
|
|
27310
27578
|
try {
|
|
27311
27579
|
return JSON.parse(readFileSync8(path15.join(root, "package.json"), "utf8"));
|
|
@@ -27314,7 +27582,7 @@ var packageJson = (root) => {
|
|
|
27314
27582
|
}
|
|
27315
27583
|
};
|
|
27316
27584
|
var isOpenSource = (root) => {
|
|
27317
|
-
if (
|
|
27585
|
+
if (existsSync9(path15.join(root, "LICENSE")))
|
|
27318
27586
|
return true;
|
|
27319
27587
|
const pkg = packageJson(root);
|
|
27320
27588
|
if (pkg && !pkg.private)
|
|
@@ -27342,10 +27610,10 @@ var planHygieneFiles = (root, opts = {}) => {
|
|
|
27342
27610
|
files.push("LICENSE", "CONTRIBUTING.md");
|
|
27343
27611
|
const planned = [];
|
|
27344
27612
|
for (const file of files) {
|
|
27345
|
-
if (
|
|
27613
|
+
if (existsSync9(path15.join(root, file)))
|
|
27346
27614
|
continue;
|
|
27347
27615
|
const tpl = path15.join(templatesDir(), file);
|
|
27348
|
-
if (!
|
|
27616
|
+
if (!existsSync9(tpl))
|
|
27349
27617
|
continue;
|
|
27350
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));
|
|
27351
27619
|
planned.push({ path: path15.join(root, file), content });
|
|
@@ -27365,7 +27633,7 @@ var ensureHygieneFiles = (root, opts) => {
|
|
|
27365
27633
|
|
|
27366
27634
|
// packages/workit-core/src/core/repo-context.ts
|
|
27367
27635
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
27368
|
-
import { existsSync as
|
|
27636
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
|
|
27369
27637
|
import path16 from "node:path";
|
|
27370
27638
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
27371
27639
|
try {
|
|
@@ -27532,7 +27800,7 @@ var diffStatForRange = (cwd, range) => {
|
|
|
27532
27800
|
};
|
|
27533
27801
|
var packageScripts = (cwd, keys) => {
|
|
27534
27802
|
const pkgPath = path16.join(cwd, "package.json");
|
|
27535
|
-
if (!
|
|
27803
|
+
if (!existsSync10(pkgPath))
|
|
27536
27804
|
return [];
|
|
27537
27805
|
try {
|
|
27538
27806
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
@@ -27567,7 +27835,7 @@ var documentationFiles = (cwd) => {
|
|
|
27567
27835
|
return matches.sort().slice(0, 200);
|
|
27568
27836
|
};
|
|
27569
27837
|
var readTrimmed = (file, maxLines) => {
|
|
27570
|
-
if (!
|
|
27838
|
+
if (!existsSync10(file))
|
|
27571
27839
|
return "";
|
|
27572
27840
|
const lines = readFileSync9(file, "utf8").split(`
|
|
27573
27841
|
`);
|
|
@@ -27643,7 +27911,7 @@ ${status.stderr}` : "");
|
|
|
27643
27911
|
stdout += `
|
|
27644
27912
|
`;
|
|
27645
27913
|
stdout += printSection("Recent Validation Signals");
|
|
27646
|
-
if (
|
|
27914
|
+
if (existsSync10(path16.join(cwd, "package.json"))) {
|
|
27647
27915
|
stdout += `package.json detected. Common scripts:
|
|
27648
27916
|
`;
|
|
27649
27917
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -27652,7 +27920,7 @@ ${status.stderr}` : "");
|
|
|
27652
27920
|
`) + `
|
|
27653
27921
|
`;
|
|
27654
27922
|
}
|
|
27655
|
-
if (
|
|
27923
|
+
if (existsSync10(path16.join(cwd, "Cargo.toml")) || existsSync10(path16.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
27656
27924
|
stdout += `Rust project detected.
|
|
27657
27925
|
`;
|
|
27658
27926
|
}
|
|
@@ -27704,7 +27972,7 @@ function changelogContext(root, range) {
|
|
|
27704
27972
|
`;
|
|
27705
27973
|
stdout += printSection("Existing CHANGELOG.md");
|
|
27706
27974
|
const changelogPath = path16.join(cwd, "CHANGELOG.md");
|
|
27707
|
-
if (
|
|
27975
|
+
if (existsSync10(changelogPath)) {
|
|
27708
27976
|
stdout += readTrimmed(changelogPath, 260) + `
|
|
27709
27977
|
`;
|
|
27710
27978
|
} else {
|
|
@@ -27746,7 +28014,7 @@ function docsRefreshContext(root, range) {
|
|
|
27746
28014
|
`;
|
|
27747
28015
|
stdout += printSection("README Preview");
|
|
27748
28016
|
const readme = path16.join(cwd, "README.md");
|
|
27749
|
-
if (
|
|
28017
|
+
if (existsSync10(readme)) {
|
|
27750
28018
|
stdout += readTrimmed(readme, 220) + `
|
|
27751
28019
|
`;
|
|
27752
28020
|
} else {
|
|
@@ -27755,7 +28023,7 @@ function docsRefreshContext(root, range) {
|
|
|
27755
28023
|
}
|
|
27756
28024
|
stdout += printSection("Package Scripts");
|
|
27757
28025
|
const pkgPath = path16.join(cwd, "package.json");
|
|
27758
|
-
if (
|
|
28026
|
+
if (existsSync10(pkgPath)) {
|
|
27759
28027
|
try {
|
|
27760
28028
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
27761
28029
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
@@ -27806,7 +28074,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
27806
28074
|
`;
|
|
27807
28075
|
stdout += printSection("Existing Release Files");
|
|
27808
28076
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
27809
|
-
if (
|
|
28077
|
+
if (existsSync10(path16.join(cwd, rel)))
|
|
27810
28078
|
stdout += rel + `
|
|
27811
28079
|
`;
|
|
27812
28080
|
}
|
|
@@ -27835,7 +28103,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
27835
28103
|
|
|
27836
28104
|
// packages/workit-core/src/core/verify-project.ts
|
|
27837
28105
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
27838
|
-
import { existsSync as
|
|
28106
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
|
|
27839
28107
|
import path17 from "node:path";
|
|
27840
28108
|
var commandOnPath = (name) => {
|
|
27841
28109
|
const dirs = (process.env.PATH ?? "").split(path17.delimiter);
|
|
@@ -27860,7 +28128,7 @@ var commandOnPath = (name) => {
|
|
|
27860
28128
|
};
|
|
27861
28129
|
var hasScript = (cwd, name) => {
|
|
27862
28130
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27863
|
-
if (!
|
|
28131
|
+
if (!existsSync11(pkgPath))
|
|
27864
28132
|
return false;
|
|
27865
28133
|
try {
|
|
27866
28134
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
@@ -27870,10 +28138,10 @@ var hasScript = (cwd, name) => {
|
|
|
27870
28138
|
}
|
|
27871
28139
|
};
|
|
27872
28140
|
var packageRunner = (cwd) => {
|
|
27873
|
-
if (
|
|
28141
|
+
if (existsSync11(path17.join(cwd, "pnpm-lock.yaml")))
|
|
27874
28142
|
return "pnpm";
|
|
27875
28143
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27876
|
-
if (
|
|
28144
|
+
if (existsSync11(pkgPath)) {
|
|
27877
28145
|
try {
|
|
27878
28146
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
27879
28147
|
if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
|
|
@@ -27881,7 +28149,7 @@ var packageRunner = (cwd) => {
|
|
|
27881
28149
|
}
|
|
27882
28150
|
} catch {}
|
|
27883
28151
|
}
|
|
27884
|
-
if (
|
|
28152
|
+
if (existsSync11(path17.join(cwd, "yarn.lock")))
|
|
27885
28153
|
return "yarn";
|
|
27886
28154
|
return "npm";
|
|
27887
28155
|
};
|
|
@@ -27942,7 +28210,7 @@ root: ${cwd}
|
|
|
27942
28210
|
lines.push(`dry_run: ${String(dryRun)}
|
|
27943
28211
|
`);
|
|
27944
28212
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27945
|
-
if (
|
|
28213
|
+
if (existsSync11(pkgPath)) {
|
|
27946
28214
|
const runner = packageRunner(cwd);
|
|
27947
28215
|
for (const script of ["lint", "format:check", "test", "build"]) {
|
|
27948
28216
|
if (hasScript(cwd, script)) {
|
|
@@ -27957,7 +28225,7 @@ root: ${cwd}
|
|
|
27957
28225
|
} else {
|
|
27958
28226
|
skipCheck("javascript", "package.json not found");
|
|
27959
28227
|
}
|
|
27960
|
-
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" : "";
|
|
27961
28229
|
if (cargoManifest) {
|
|
27962
28230
|
runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
|
|
27963
28231
|
runCheck("cargo clippy", "cargo", [
|
|
@@ -27973,7 +28241,7 @@ root: ${cwd}
|
|
|
27973
28241
|
} else {
|
|
27974
28242
|
skipCheck("rust", "Cargo.toml not found");
|
|
27975
28243
|
}
|
|
27976
|
-
if (
|
|
28244
|
+
if (existsSync11(path17.join(cwd, "pyproject.toml")) || existsSync11(path17.join(cwd, "pytest.ini")) || existsSync11(path17.join(cwd, "tests"))) {
|
|
27977
28245
|
if (commandOnPath("pytest"))
|
|
27978
28246
|
runCheck("pytest", "pytest", []);
|
|
27979
28247
|
else
|
|
@@ -27995,7 +28263,7 @@ root: ${cwd}
|
|
|
27995
28263
|
lines.push(`status: skipped (dry run)
|
|
27996
28264
|
`);
|
|
27997
28265
|
skipped += 1;
|
|
27998
|
-
} else if (
|
|
28266
|
+
} else if (existsSync11(changelogPath)) {
|
|
27999
28267
|
if (/## \[Unreleased\]/i.test(readFileSync10(changelogPath, "utf8"))) {
|
|
28000
28268
|
lines.push(`status: pass
|
|
28001
28269
|
`);
|
|
@@ -28330,7 +28598,7 @@ function writeFileExclusive(file, content, mode) {
|
|
|
28330
28598
|
import {
|
|
28331
28599
|
copyFileSync as copyFileSync2,
|
|
28332
28600
|
cpSync as cpSync2,
|
|
28333
|
-
existsSync as
|
|
28601
|
+
existsSync as existsSync12,
|
|
28334
28602
|
lstatSync as lstatSync4,
|
|
28335
28603
|
mkdirSync as mkdirSync5,
|
|
28336
28604
|
mkdtempSync,
|
|
@@ -29871,7 +30139,7 @@ function createRepoTools(runtime = defaultRuntime) {
|
|
|
29871
30139
|
return output(legacyScriptResult({
|
|
29872
30140
|
stdout: JSON.stringify(result),
|
|
29873
30141
|
stderr: "",
|
|
29874
|
-
exitCode:
|
|
30142
|
+
exitCode: "error" in result ? 1 : 0,
|
|
29875
30143
|
cwd: context.directory
|
|
29876
30144
|
}));
|
|
29877
30145
|
}
|
|
@@ -30519,6 +30787,94 @@ var safePath = (root, value) => {
|
|
|
30519
30787
|
return null;
|
|
30520
30788
|
}
|
|
30521
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
|
+
};
|
|
30522
30878
|
var approvedResolvedExternalAction = (content) => {
|
|
30523
30879
|
try {
|
|
30524
30880
|
const value = JSON.parse(content);
|
|
@@ -30782,24 +31138,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30782
31138
|
const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
|
|
30783
31139
|
if (!sdd_dir)
|
|
30784
31140
|
return failure("invalid_input", "sdd_dir must stay inside the workspace");
|
|
31141
|
+
const action = request.payload.action ?? "setup";
|
|
30785
31142
|
const policy = resolveBranchPolicyFor(root);
|
|
30786
|
-
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
|
+
});
|
|
30787
31149
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
30788
31150
|
if (!head)
|
|
30789
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);
|
|
30790
31154
|
const normalized = {
|
|
30791
31155
|
operation: request.operation,
|
|
30792
31156
|
payload: {
|
|
30793
31157
|
...request.payload,
|
|
30794
|
-
action
|
|
31158
|
+
action,
|
|
30795
31159
|
sdd_dir,
|
|
30796
31160
|
stash: request.payload.stash ?? "no",
|
|
30797
|
-
target_branch
|
|
31161
|
+
...target_branch ? { target_branch } : {}
|
|
30798
31162
|
}
|
|
30799
31163
|
};
|
|
30800
31164
|
return success(null, null, {
|
|
30801
31165
|
request: normalized,
|
|
30802
|
-
descriptorPayload: {
|
|
31166
|
+
descriptorPayload: {
|
|
31167
|
+
...normalized.payload,
|
|
31168
|
+
resolved: { head, dirty, ...creation }
|
|
31169
|
+
}
|
|
30803
31170
|
});
|
|
30804
31171
|
}
|
|
30805
31172
|
case "git.push": {
|
|
@@ -30813,7 +31180,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30813
31180
|
}) : failure("invalid_input", "git push requires a resolvable current branch");
|
|
30814
31181
|
}
|
|
30815
31182
|
case "git.commit": {
|
|
31183
|
+
const planSteps = request.payload.plan_steps;
|
|
31184
|
+
const planBranch = request.payload.plan_branch;
|
|
30816
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
|
+
}
|
|
30817
31214
|
const staged = gitRaw(root, [
|
|
30818
31215
|
"diff",
|
|
30819
31216
|
"--cached",
|
|
@@ -30824,7 +31221,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30824
31221
|
"HEAD"
|
|
30825
31222
|
]);
|
|
30826
31223
|
const paths = stagedPaths(root);
|
|
30827
|
-
|
|
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)
|
|
30828
31231
|
return failure("storage_error", "Git state could not be resolved");
|
|
30829
31232
|
let policy;
|
|
30830
31233
|
try {
|
|
@@ -30839,13 +31242,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30839
31242
|
`).filter(Boolean));
|
|
30840
31243
|
flavor = detected.flavor ?? "conventional";
|
|
30841
31244
|
}
|
|
30842
|
-
if (!matchCommitFlavor(
|
|
31245
|
+
if (!matchCommitFlavor(message, flavor, policy.pattern))
|
|
30843
31246
|
return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
|
|
30844
31247
|
return success(null, null, {
|
|
30845
31248
|
request,
|
|
30846
31249
|
descriptorPayload: {
|
|
30847
31250
|
...request.payload,
|
|
30848
|
-
resolved: { head, staged: sha256(staged), paths }
|
|
31251
|
+
resolved: { head, branch, staged: sha256(staged), paths }
|
|
30849
31252
|
}
|
|
30850
31253
|
});
|
|
30851
31254
|
}
|
|
@@ -31015,10 +31418,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
31015
31418
|
sdd_dir: request.payload.sdd_dir ?? "docs",
|
|
31016
31419
|
workspace_root: root
|
|
31017
31420
|
});
|
|
31018
|
-
|
|
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);
|
|
31019
31430
|
}
|
|
31020
31431
|
case "git.commit": {
|
|
31021
|
-
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]);
|
|
31022
31439
|
return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown3(request.operation);
|
|
31023
31440
|
}
|
|
31024
31441
|
case "git.push": {
|
|
@@ -31130,6 +31547,14 @@ var sessionParent = (session) => {
|
|
|
31130
31547
|
};
|
|
31131
31548
|
var freshMs = 5 * 60 * 1000;
|
|
31132
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
|
+
};
|
|
31133
31558
|
var decisionContent = (purpose, question, approvedContent) => ({
|
|
31134
31559
|
header: `Workit decision: ${purpose}`,
|
|
31135
31560
|
question,
|
|
@@ -31243,7 +31668,27 @@ class NativeReceiptStore {
|
|
|
31243
31668
|
}
|
|
31244
31669
|
return {
|
|
31245
31670
|
ok: false,
|
|
31246
|
-
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
|
+
})()
|
|
31247
31692
|
};
|
|
31248
31693
|
}
|
|
31249
31694
|
verify(observation, sessionID, purpose) {
|
|
@@ -31345,6 +31790,13 @@ var opencodeCapabilities = () => [
|
|
|
31345
31790
|
reason: "nested native task launches are denied and observed child sessions are parent-bound",
|
|
31346
31791
|
refs: [hostRef("task")]
|
|
31347
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
|
+
},
|
|
31348
31800
|
{
|
|
31349
31801
|
name: "known_product_writes",
|
|
31350
31802
|
surface: "write/edit/bash",
|
|
@@ -31363,7 +31815,8 @@ var opencodeCapabilities = () => [
|
|
|
31363
31815
|
var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
|
|
31364
31816
|
verifyDecision: ({ observation, expected, caller }) => {
|
|
31365
31817
|
const receipt = receipts.verify(observation, actor, "decision");
|
|
31366
|
-
|
|
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)
|
|
31367
31820
|
return failure("permission_denied", "native decision receipt is not bound to this session");
|
|
31368
31821
|
return success(null, null, {
|
|
31369
31822
|
kind: "host_observed",
|
|
@@ -31438,8 +31891,34 @@ var nativeDispatchFor = (directChildren, actor, generation) => ({
|
|
|
31438
31891
|
var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
|
|
31439
31892
|
const store = new TaskStore(root);
|
|
31440
31893
|
const selected = approvedExternalAction(store, "opencode", actor, operation);
|
|
31441
|
-
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
|
+
}
|
|
31442
31920
|
return selected;
|
|
31921
|
+
}
|
|
31443
31922
|
const actionRef = externalActionRef("opencode", actor, operation);
|
|
31444
31923
|
return {
|
|
31445
31924
|
taskId: selected.data.task.id,
|
|
@@ -31483,8 +31962,10 @@ var workerIdFor = (store, actor, current, directChildren) => {
|
|
|
31483
31962
|
var createWorkitTools = ({
|
|
31484
31963
|
client,
|
|
31485
31964
|
receipts = new NativeReceiptStore,
|
|
31486
|
-
directChildren = new Map
|
|
31965
|
+
directChildren = new Map,
|
|
31966
|
+
now = Date.now
|
|
31487
31967
|
} = {}) => {
|
|
31968
|
+
const actionProposals = new Map;
|
|
31488
31969
|
const make = (family) => tool({
|
|
31489
31970
|
description: `Workit ${family} operations backed by the shared task contract.`,
|
|
31490
31971
|
args: operationShapeFor(family),
|
|
@@ -31518,6 +31999,11 @@ var createWorkitTools = ({
|
|
|
31518
31999
|
let result;
|
|
31519
32000
|
if (family === "decision" && args.action === "record") {
|
|
31520
32001
|
const decision = parsed.data;
|
|
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));
|
|
31521
32007
|
const expectation = {
|
|
31522
32008
|
selectedLabel: decision.response,
|
|
31523
32009
|
decisionPurpose: decision.purpose,
|
|
@@ -31529,7 +32015,29 @@ var createWorkitTools = ({
|
|
|
31529
32015
|
const observed = receipts.consume(context.sessionID, "decision", expectation);
|
|
31530
32016
|
if (!observed.ok)
|
|
31531
32017
|
return output2(failure("permission_denied", observed.error));
|
|
31532
|
-
|
|
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);
|
|
31533
32041
|
} else {
|
|
31534
32042
|
const run = core[family];
|
|
31535
32043
|
result = run.call(core, parsed.data);
|
|
@@ -31630,6 +32138,64 @@ var createWorkitTools = ({
|
|
|
31630
32138
|
return output2(failure("external_outcome_unknown", "previous external action outcome is unknown"));
|
|
31631
32139
|
}
|
|
31632
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
|
+
}
|
|
31633
32199
|
const result = await nativeExternalActionRunner(context.directory, context.sessionID, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, context.directory, step, {
|
|
31634
32200
|
host: "opencode",
|
|
31635
32201
|
actor: context.sessionID
|
|
@@ -31729,26 +32295,14 @@ var loadProvenance = (logger, pkgUrl) => {
|
|
|
31729
32295
|
};
|
|
31730
32296
|
|
|
31731
32297
|
// packages/workit-opencode/src/plugin.ts
|
|
31732
|
-
var root =
|
|
32298
|
+
var root = fileURLToPath3(new URL("../assets/", import.meta.url));
|
|
31733
32299
|
var skillsPath = path25.join(root, "skills");
|
|
31734
32300
|
var logger = createLogger({
|
|
31735
32301
|
appLog: () => {
|
|
31736
32302
|
return;
|
|
31737
32303
|
}
|
|
31738
32304
|
});
|
|
31739
|
-
var sourceMarker = markSourcesLoaded(
|
|
31740
|
-
fileURLToPath2(import.meta.url),
|
|
31741
|
-
fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
|
|
31742
|
-
...[
|
|
31743
|
-
"task-contract.ts",
|
|
31744
|
-
"task-engine.ts",
|
|
31745
|
-
"task-evaluation.ts",
|
|
31746
|
-
"task-store.ts",
|
|
31747
|
-
"workers.ts",
|
|
31748
|
-
"authority.ts",
|
|
31749
|
-
"methods.ts"
|
|
31750
|
-
].map((file) => fileURLToPath2(new URL(`../../../workit-core/src/core/${file}`, import.meta.url)))
|
|
31751
|
-
].filter((file) => existsSync12(file)));
|
|
32305
|
+
var sourceMarker = markSourcesLoaded(pluginSourceFiles);
|
|
31752
32306
|
var staleSourcesWarned = false;
|
|
31753
32307
|
var warnStaleSources = () => {
|
|
31754
32308
|
if (staleSourcesWarned)
|
|
@@ -32067,6 +32621,12 @@ var plugin = async ({ client, directory }) => {
|
|
|
32067
32621
|
throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
|
|
32068
32622
|
return;
|
|
32069
32623
|
}
|
|
32624
|
+
if (input.tool === "question") {
|
|
32625
|
+
const issue = workitBindingQuestionIssue(output?.args?.questions);
|
|
32626
|
+
if (issue)
|
|
32627
|
+
throw new Error(issue);
|
|
32628
|
+
return;
|
|
32629
|
+
}
|
|
32070
32630
|
if (input.tool === "task") {
|
|
32071
32631
|
const listed = new TaskStore(directory).listTasks();
|
|
32072
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)))
|
|
@@ -32086,7 +32646,7 @@ var plugin = async ({ client, directory }) => {
|
|
|
32086
32646
|
config: async (config) => {
|
|
32087
32647
|
const mutable = config;
|
|
32088
32648
|
const paths = [...mutable.skills?.paths ?? []];
|
|
32089
|
-
if (
|
|
32649
|
+
if (existsSync13(skillsPath) && !paths.includes(skillsPath))
|
|
32090
32650
|
paths.push(skillsPath);
|
|
32091
32651
|
mutable.skills ??= {};
|
|
32092
32652
|
mutable.skills.paths = paths;
|