@brainervirus/workit-opencode 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/workit-plan/SKILL.md +14 -2
- package/dist/plugin.js +955 -279
- 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,10 +7590,12 @@ var decisionSchema = object({
|
|
|
7590
7590
|
scope: scopeSchema,
|
|
7591
7591
|
presented: text,
|
|
7592
7592
|
approvedContent: text,
|
|
7593
|
-
|
|
7593
|
+
displayed: text.optional(),
|
|
7594
|
+
contentRefs: array(refSchema),
|
|
7595
|
+
statedChoice: object({ ref: text, text }).strict().optional()
|
|
7594
7596
|
}).strict(),
|
|
7595
7597
|
digest,
|
|
7596
|
-
response: _enum(["approved", "rejected"]),
|
|
7598
|
+
response: _enum(["approved", "rejected", "stated"]),
|
|
7597
7599
|
requirementIds: array(digest),
|
|
7598
7600
|
revoked: object({ at: utc, reason: text }).strict().nullable(),
|
|
7599
7601
|
consumption: object({
|
|
@@ -7602,6 +7604,27 @@ var decisionSchema = object({
|
|
|
7602
7604
|
actionRef: refSchema
|
|
7603
7605
|
}).strict().nullable()
|
|
7604
7606
|
}).strict();
|
|
7607
|
+
var BINDING_QUESTION_BUDGET = 300;
|
|
7608
|
+
var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
|
|
7609
|
+
var workitBindingQuestionIssue = (questions) => {
|
|
7610
|
+
if (!Array.isArray(questions) || questions.length !== 1)
|
|
7611
|
+
return null;
|
|
7612
|
+
const question = questions[0];
|
|
7613
|
+
if (!question || typeof question !== "object")
|
|
7614
|
+
return null;
|
|
7615
|
+
if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
|
|
7616
|
+
return null;
|
|
7617
|
+
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.`;
|
|
7618
|
+
if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
|
|
7619
|
+
return over("the question text", question.question);
|
|
7620
|
+
if (Array.isArray(question.options))
|
|
7621
|
+
for (const option of question.options) {
|
|
7622
|
+
const description = option.description;
|
|
7623
|
+
if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
|
|
7624
|
+
return over("an option description", description);
|
|
7625
|
+
}
|
|
7626
|
+
return null;
|
|
7627
|
+
};
|
|
7605
7628
|
var findingSchema = object({
|
|
7606
7629
|
claim: text,
|
|
7607
7630
|
consequence: text,
|
|
@@ -7668,6 +7691,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
|
|
|
7668
7691
|
path: ["decisionId"]
|
|
7669
7692
|
});
|
|
7670
7693
|
});
|
|
7694
|
+
var runtimeSchema = object({
|
|
7695
|
+
createdWith: text.nullable(),
|
|
7696
|
+
updatedWith: text
|
|
7697
|
+
}).strict();
|
|
7671
7698
|
var taskRecordSchema = object({
|
|
7672
7699
|
schemaVersion: literal(1),
|
|
7673
7700
|
id,
|
|
@@ -7681,6 +7708,8 @@ var taskRecordSchema = object({
|
|
|
7681
7708
|
status: _enum(["active", "paused", "closed"]),
|
|
7682
7709
|
closure: closureSchema.nullable(),
|
|
7683
7710
|
progress: progressSchema,
|
|
7711
|
+
pauseReason: text.nullable().optional(),
|
|
7712
|
+
runtime: runtimeSchema.optional(),
|
|
7684
7713
|
assessments: array(entrySchema(assessmentSchema)),
|
|
7685
7714
|
policy: policySchema.nullable(),
|
|
7686
7715
|
policyChanges: array(policyChangeSchema),
|
|
@@ -7696,6 +7725,7 @@ var workspaceRecordSchema = object({
|
|
|
7696
7725
|
id,
|
|
7697
7726
|
revision,
|
|
7698
7727
|
root: nonEmpty,
|
|
7728
|
+
runtime: runtimeSchema.optional(),
|
|
7699
7729
|
writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
|
|
7700
7730
|
}).strict();
|
|
7701
7731
|
var capabilitySchema = object({
|
|
@@ -7723,6 +7753,9 @@ var taskSummarySchema = object({
|
|
|
7723
7753
|
id,
|
|
7724
7754
|
revision,
|
|
7725
7755
|
workspaceRevision: revision,
|
|
7756
|
+
createdAt: utc,
|
|
7757
|
+
updatedAt: utc,
|
|
7758
|
+
runtime: runtimeSchema.nullable(),
|
|
7726
7759
|
objective: text,
|
|
7727
7760
|
status: _enum(["active", "paused", "closed"]),
|
|
7728
7761
|
closure: closureSchema.nullable(),
|
|
@@ -7793,7 +7826,7 @@ var taskOperations = {
|
|
|
7793
7826
|
expectedWorkspaceRevision: revision.optional(),
|
|
7794
7827
|
outcome: outcomeSchema,
|
|
7795
7828
|
summary: text,
|
|
7796
|
-
decisionIds: array(id)
|
|
7829
|
+
decisionIds: array(id).optional()
|
|
7797
7830
|
})
|
|
7798
7831
|
};
|
|
7799
7832
|
var policyOperations = {
|
|
@@ -7885,14 +7918,15 @@ var writerOperations = {
|
|
|
7885
7918
|
...taskId,
|
|
7886
7919
|
expectedRevision: revision.optional(),
|
|
7887
7920
|
expectedWorkspaceRevision: revision.optional(),
|
|
7888
|
-
workerId: nullableId.optional()
|
|
7921
|
+
workerId: nullableId.optional(),
|
|
7922
|
+
reason: text.optional()
|
|
7889
7923
|
}),
|
|
7890
7924
|
release: operation({
|
|
7891
7925
|
action: literal("release"),
|
|
7892
7926
|
...taskId,
|
|
7893
7927
|
expectedRevision: revision.optional(),
|
|
7894
7928
|
expectedWorkspaceRevision: revision.optional(),
|
|
7895
|
-
reason: text
|
|
7929
|
+
reason: text.optional()
|
|
7896
7930
|
})
|
|
7897
7931
|
};
|
|
7898
7932
|
var stateOperations = {
|
|
@@ -8045,7 +8079,26 @@ function requirementId(input) {
|
|
|
8045
8079
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
8046
8080
|
import * as fs4 from "node:fs";
|
|
8047
8081
|
import { hostname } from "node:os";
|
|
8048
|
-
import
|
|
8082
|
+
import path4 from "node:path";
|
|
8083
|
+
|
|
8084
|
+
// packages/workit-core/src/core/package-root.ts
|
|
8085
|
+
import { existsSync } from "node:fs";
|
|
8086
|
+
import path from "node:path";
|
|
8087
|
+
import { fileURLToPath } from "node:url";
|
|
8088
|
+
var packageRoot = () => {
|
|
8089
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
8090
|
+
while (true) {
|
|
8091
|
+
const parent = path.dirname(dir);
|
|
8092
|
+
if (existsSync(path.join(dir, "package.json")) || parent === dir)
|
|
8093
|
+
return dir;
|
|
8094
|
+
dir = parent;
|
|
8095
|
+
}
|
|
8096
|
+
};
|
|
8097
|
+
var assetRoot = () => {
|
|
8098
|
+
const root = packageRoot();
|
|
8099
|
+
const assets = path.join(root, "assets");
|
|
8100
|
+
return existsSync(assets) ? assets : root;
|
|
8101
|
+
};
|
|
8049
8102
|
|
|
8050
8103
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
8051
8104
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -8096,7 +8149,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
8096
8149
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
8097
8150
|
import { randomBytes } from "node:crypto";
|
|
8098
8151
|
import fsSync from "node:fs";
|
|
8099
|
-
import
|
|
8152
|
+
import path2 from "node:path";
|
|
8100
8153
|
|
|
8101
8154
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
8102
8155
|
import fs from "node:fs";
|
|
@@ -8186,13 +8239,13 @@ ${ownershipToken}
|
|
|
8186
8239
|
return { raw, ownershipToken };
|
|
8187
8240
|
}
|
|
8188
8241
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
8189
|
-
const resolved =
|
|
8190
|
-
const lexicalRelative =
|
|
8191
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
8192
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
8242
|
+
const resolved = path2.resolve(lockPath);
|
|
8243
|
+
const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
|
|
8244
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
|
|
8245
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
|
|
8193
8246
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
8194
8247
|
}
|
|
8195
|
-
return relative.split(
|
|
8248
|
+
return relative.split(path2.sep).join(path2.posix.sep);
|
|
8196
8249
|
}
|
|
8197
8250
|
function parseSidecarLockPayload(raw, parser) {
|
|
8198
8251
|
if (parser) {
|
|
@@ -8339,7 +8392,7 @@ function getFsSafeLockConfig() {
|
|
|
8339
8392
|
|
|
8340
8393
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
8341
8394
|
import fs3 from "node:fs";
|
|
8342
|
-
import
|
|
8395
|
+
import path3 from "node:path";
|
|
8343
8396
|
|
|
8344
8397
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
8345
8398
|
function sleepSync(ms) {
|
|
@@ -8430,26 +8483,26 @@ function canonicalLockParentSync(parent) {
|
|
|
8430
8483
|
return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
|
|
8431
8484
|
}
|
|
8432
8485
|
function normalizeTargetPath(targetPath) {
|
|
8433
|
-
const resolved =
|
|
8434
|
-
fs3.mkdirSync(
|
|
8486
|
+
const resolved = path3.resolve(targetPath);
|
|
8487
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
8435
8488
|
try {
|
|
8436
|
-
return
|
|
8489
|
+
return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
|
|
8437
8490
|
} catch {
|
|
8438
8491
|
return resolved;
|
|
8439
8492
|
}
|
|
8440
8493
|
}
|
|
8441
8494
|
function boundedLockPath(lockPath, lockRoot) {
|
|
8442
|
-
const resolved =
|
|
8495
|
+
const resolved = path3.resolve(lockPath);
|
|
8443
8496
|
if (!lockRoot)
|
|
8444
8497
|
return resolved;
|
|
8445
8498
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
8446
|
-
const parent =
|
|
8499
|
+
const parent = path3.dirname(resolved);
|
|
8447
8500
|
const parentReal = canonicalLockParentSync(parent);
|
|
8448
|
-
const parentRelative =
|
|
8449
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
8501
|
+
const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
|
|
8502
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
|
|
8450
8503
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
8451
8504
|
}
|
|
8452
|
-
return
|
|
8505
|
+
return path3.join(parentReal, path3.basename(resolved));
|
|
8453
8506
|
}
|
|
8454
8507
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
8455
8508
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -8662,6 +8715,33 @@ var metadataLockSchema = object({
|
|
|
8662
8715
|
nonce: string2().min(1)
|
|
8663
8716
|
}).strict();
|
|
8664
8717
|
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8718
|
+
var cachedRuntimeVersion = null;
|
|
8719
|
+
var runtimeVersion = () => {
|
|
8720
|
+
if (cachedRuntimeVersion === null) {
|
|
8721
|
+
try {
|
|
8722
|
+
const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
|
|
8723
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
8724
|
+
} catch {
|
|
8725
|
+
cachedRuntimeVersion = "0.0.0";
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
return cachedRuntimeVersion;
|
|
8729
|
+
};
|
|
8730
|
+
var stampNew = (record) => ({
|
|
8731
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
8732
|
+
updatedWith: runtimeVersion()
|
|
8733
|
+
});
|
|
8734
|
+
var isNewerVersion = (candidate, current) => {
|
|
8735
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
8736
|
+
const [next, now] = [parts(candidate), parts(current)];
|
|
8737
|
+
for (let index = 0;index < 3; index += 1) {
|
|
8738
|
+
const left = next[index] ?? 0;
|
|
8739
|
+
const right = now[index] ?? 0;
|
|
8740
|
+
if (left !== right)
|
|
8741
|
+
return left > right;
|
|
8742
|
+
}
|
|
8743
|
+
return false;
|
|
8744
|
+
};
|
|
8665
8745
|
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
8666
8746
|
`;
|
|
8667
8747
|
var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
@@ -8688,12 +8768,12 @@ var sameMetadataLock = (left, right) => {
|
|
|
8688
8768
|
class TaskStore {
|
|
8689
8769
|
root;
|
|
8690
8770
|
constructor(root) {
|
|
8691
|
-
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) :
|
|
8771
|
+
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
|
|
8692
8772
|
}
|
|
8693
8773
|
readTask(taskId) {
|
|
8694
8774
|
if (!validId(taskId))
|
|
8695
8775
|
return failure("invalid_input", "task ID is invalid", { taskId });
|
|
8696
|
-
const result = this.readRecord(
|
|
8776
|
+
const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
|
|
8697
8777
|
if (!result.exists)
|
|
8698
8778
|
return failure("not_found", "task not found", { taskId });
|
|
8699
8779
|
if (result.result.ok && result.result.data.id !== taskId)
|
|
@@ -8727,7 +8807,7 @@ class TaskStore {
|
|
|
8727
8807
|
});
|
|
8728
8808
|
const tasks = [];
|
|
8729
8809
|
for (const name of names.sort()) {
|
|
8730
|
-
const item = this.readRecord(
|
|
8810
|
+
const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
|
|
8731
8811
|
if (!item.exists)
|
|
8732
8812
|
continue;
|
|
8733
8813
|
if (!item.result.ok)
|
|
@@ -8761,7 +8841,7 @@ class TaskStore {
|
|
|
8761
8841
|
if (!current.ok)
|
|
8762
8842
|
return current;
|
|
8763
8843
|
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
8764
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
8844
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
8765
8845
|
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
8766
8846
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
8767
8847
|
});
|
|
@@ -8773,11 +8853,17 @@ class TaskStore {
|
|
|
8773
8853
|
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
8774
8854
|
if (current.data && !previousWorkspaceBytes)
|
|
8775
8855
|
return failure("storage_error", "workspace snapshot disappeared during creation");
|
|
8776
|
-
const workspace = current.data ? {
|
|
8856
|
+
const workspace = current.data ? {
|
|
8857
|
+
...current.data,
|
|
8858
|
+
revision: newRevision(),
|
|
8859
|
+
root: this.root,
|
|
8860
|
+
runtime: stampNew(current.data)
|
|
8861
|
+
} : {
|
|
8777
8862
|
schemaVersion: SCHEMA_VERSION,
|
|
8778
8863
|
id: newId(),
|
|
8779
8864
|
revision: newRevision(),
|
|
8780
8865
|
root: this.root,
|
|
8866
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8781
8867
|
writer: null
|
|
8782
8868
|
};
|
|
8783
8869
|
const timestamp = value.now ?? now();
|
|
@@ -8799,6 +8885,7 @@ class TaskStore {
|
|
|
8799
8885
|
status: "active",
|
|
8800
8886
|
closure: null,
|
|
8801
8887
|
progress: { summary: "", nextAction: null, blockers: [] },
|
|
8888
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8802
8889
|
assessments: [],
|
|
8803
8890
|
policy: null,
|
|
8804
8891
|
policyChanges: [],
|
|
@@ -8829,7 +8916,7 @@ class TaskStore {
|
|
|
8829
8916
|
if (!current.ok)
|
|
8830
8917
|
return current;
|
|
8831
8918
|
if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
|
|
8832
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
8919
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
8833
8920
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
8834
8921
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
8835
8922
|
});
|
|
@@ -8837,11 +8924,17 @@ class TaskStore {
|
|
|
8837
8924
|
if (current.data && !previousWorkspaceBytes)
|
|
8838
8925
|
return failure("storage_error", "workspace snapshot disappeared during import");
|
|
8839
8926
|
const timestamp = input.now ?? now();
|
|
8840
|
-
const workspace = current.data ? {
|
|
8927
|
+
const workspace = current.data ? {
|
|
8928
|
+
...current.data,
|
|
8929
|
+
revision: newRevision(),
|
|
8930
|
+
root: this.root,
|
|
8931
|
+
runtime: stampNew(current.data)
|
|
8932
|
+
} : {
|
|
8841
8933
|
schemaVersion: SCHEMA_VERSION,
|
|
8842
8934
|
id: input.workspaceId ?? newId(),
|
|
8843
8935
|
revision: newRevision(),
|
|
8844
8936
|
root: this.root,
|
|
8937
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
8845
8938
|
writer: null
|
|
8846
8939
|
};
|
|
8847
8940
|
const task = {
|
|
@@ -8849,7 +8942,11 @@ class TaskStore {
|
|
|
8849
8942
|
workspaceId: workspace.id,
|
|
8850
8943
|
revision: newRevision(),
|
|
8851
8944
|
createdAt: timestamp,
|
|
8852
|
-
updatedAt: timestamp
|
|
8945
|
+
updatedAt: timestamp,
|
|
8946
|
+
runtime: {
|
|
8947
|
+
createdWith: input.task.runtime?.createdWith ?? null,
|
|
8948
|
+
updatedWith: runtimeVersion()
|
|
8949
|
+
}
|
|
8853
8950
|
};
|
|
8854
8951
|
const validTask = taskRecordSchema.safeParse(task);
|
|
8855
8952
|
if (!validTask.success)
|
|
@@ -8898,7 +8995,8 @@ class TaskStore {
|
|
|
8898
8995
|
workspaceId: current.data.workspaceId,
|
|
8899
8996
|
createdAt: current.data.createdAt,
|
|
8900
8997
|
revision: context.revision,
|
|
8901
|
-
updatedAt: context.now
|
|
8998
|
+
updatedAt: context.now,
|
|
8999
|
+
runtime: stampNew(current.data)
|
|
8902
9000
|
};
|
|
8903
9001
|
const valid = taskRecordSchema.safeParse(record);
|
|
8904
9002
|
if (!valid.success)
|
|
@@ -8932,7 +9030,8 @@ class TaskStore {
|
|
|
8932
9030
|
...changed.data,
|
|
8933
9031
|
id: current.data.id,
|
|
8934
9032
|
root: this.root,
|
|
8935
|
-
revision: context.revision
|
|
9033
|
+
revision: context.revision,
|
|
9034
|
+
runtime: stampNew(current.data)
|
|
8936
9035
|
};
|
|
8937
9036
|
const valid = workspaceRecordSchema.safeParse(record);
|
|
8938
9037
|
if (!valid.success)
|
|
@@ -8975,7 +9074,8 @@ class TaskStore {
|
|
|
8975
9074
|
...changedWorkspace.data,
|
|
8976
9075
|
id: workspace.data.id,
|
|
8977
9076
|
root: this.root,
|
|
8978
|
-
revision: workspaceContext.revision
|
|
9077
|
+
revision: workspaceContext.revision,
|
|
9078
|
+
runtime: stampNew(workspace.data)
|
|
8979
9079
|
});
|
|
8980
9080
|
if (!nextWorkspace.success)
|
|
8981
9081
|
return failure("invalid_input", "workspace mutation produced an invalid record");
|
|
@@ -9000,7 +9100,8 @@ class TaskStore {
|
|
|
9000
9100
|
workspaceId: task.data.workspaceId,
|
|
9001
9101
|
createdAt: task.data.createdAt,
|
|
9002
9102
|
revision: taskContext.revision,
|
|
9003
|
-
updatedAt: taskContext.now
|
|
9103
|
+
updatedAt: taskContext.now,
|
|
9104
|
+
runtime: stampNew(task.data)
|
|
9004
9105
|
});
|
|
9005
9106
|
if (!nextTask.success) {
|
|
9006
9107
|
this.markUncertain(nextWorkspace.data);
|
|
@@ -9027,7 +9128,7 @@ class TaskStore {
|
|
|
9027
9128
|
if (match)
|
|
9028
9129
|
candidates.push({
|
|
9029
9130
|
target: match[1],
|
|
9030
|
-
path:
|
|
9131
|
+
path: path4.join(this.recoveryDir, name),
|
|
9031
9132
|
digest: match[3]
|
|
9032
9133
|
});
|
|
9033
9134
|
}
|
|
@@ -9220,7 +9321,8 @@ class TaskStore {
|
|
|
9220
9321
|
const value = {
|
|
9221
9322
|
...workspace,
|
|
9222
9323
|
writer: { ...workspace.writer, state: "uncertain" },
|
|
9223
|
-
revision: newRevision()
|
|
9324
|
+
revision: newRevision(),
|
|
9325
|
+
runtime: stampNew(workspace)
|
|
9224
9326
|
};
|
|
9225
9327
|
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
9226
9328
|
}
|
|
@@ -9298,7 +9400,7 @@ class TaskStore {
|
|
|
9298
9400
|
}
|
|
9299
9401
|
fs4.renameSync(temporary, file);
|
|
9300
9402
|
temporary = undefined;
|
|
9301
|
-
this.fsyncDirectory(
|
|
9403
|
+
this.fsyncDirectory(path4.dirname(file));
|
|
9302
9404
|
return success(null, null, value);
|
|
9303
9405
|
} catch (error) {
|
|
9304
9406
|
return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
@@ -9312,9 +9414,9 @@ class TaskStore {
|
|
|
9312
9414
|
}
|
|
9313
9415
|
}
|
|
9314
9416
|
saveRecovery(file, bytes) {
|
|
9315
|
-
const target =
|
|
9316
|
-
const id = target === "task" ?
|
|
9317
|
-
const destination =
|
|
9417
|
+
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
9418
|
+
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
9419
|
+
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
9318
9420
|
if (fs4.existsSync(destination)) {
|
|
9319
9421
|
if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
|
|
9320
9422
|
return;
|
|
@@ -9392,7 +9494,7 @@ class TaskStore {
|
|
|
9392
9494
|
for (const name of fs4.readdirSync(this.tasksDir)) {
|
|
9393
9495
|
if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
|
|
9394
9496
|
return null;
|
|
9395
|
-
const parsed = this.parseBytes(fs4.readFileSync(
|
|
9497
|
+
const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
|
|
9396
9498
|
if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
|
|
9397
9499
|
return null;
|
|
9398
9500
|
ids.add(parsed.data.workspaceId);
|
|
@@ -9434,7 +9536,12 @@ class TaskStore {
|
|
|
9434
9536
|
if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
9435
9537
|
return failure("unsupported_version", "unsupported snapshot schema version");
|
|
9436
9538
|
const parsed = schema.safeParse(value);
|
|
9437
|
-
|
|
9539
|
+
if (parsed.success)
|
|
9540
|
+
return success(null, null, parsed.data);
|
|
9541
|
+
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
9542
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
9543
|
+
return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
9544
|
+
return failure("recovery_required", "snapshot does not satisfy its schema");
|
|
9438
9545
|
}
|
|
9439
9546
|
processStart(pid) {
|
|
9440
9547
|
try {
|
|
@@ -9444,43 +9551,98 @@ class TaskStore {
|
|
|
9444
9551
|
}
|
|
9445
9552
|
}
|
|
9446
9553
|
conflict(expected, actual) {
|
|
9447
|
-
return failure("revision_conflict", "snapshot revision does not match", {
|
|
9554
|
+
return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
9448
9555
|
expectedRevision: expected,
|
|
9449
9556
|
actualRevision: actual
|
|
9450
9557
|
});
|
|
9451
9558
|
}
|
|
9452
9559
|
taskPath(taskId) {
|
|
9453
|
-
return
|
|
9560
|
+
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
9454
9561
|
}
|
|
9455
9562
|
get workitDir() {
|
|
9456
|
-
return
|
|
9563
|
+
return path4.join(this.root, ".workit");
|
|
9457
9564
|
}
|
|
9458
9565
|
get tasksDir() {
|
|
9459
|
-
return
|
|
9566
|
+
return path4.join(this.workitDir, "tasks");
|
|
9460
9567
|
}
|
|
9461
9568
|
get recoveryDir() {
|
|
9462
|
-
return
|
|
9569
|
+
return path4.join(this.workitDir, "recovery");
|
|
9463
9570
|
}
|
|
9464
9571
|
get workspacePath() {
|
|
9465
|
-
return
|
|
9572
|
+
return path4.join(this.workitDir, "workspace.json");
|
|
9466
9573
|
}
|
|
9467
9574
|
get lockPath() {
|
|
9468
|
-
return
|
|
9575
|
+
return path4.join(this.workitDir, "metadata.lock");
|
|
9469
9576
|
}
|
|
9470
9577
|
get gitignorePath() {
|
|
9471
|
-
return
|
|
9578
|
+
return path4.join(this.workitDir, ".gitignore");
|
|
9472
9579
|
}
|
|
9473
9580
|
}
|
|
9581
|
+
// packages/workit-core/src/core/workers.ts
|
|
9582
|
+
var same = (left, right) => {
|
|
9583
|
+
try {
|
|
9584
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
9585
|
+
} catch {
|
|
9586
|
+
return false;
|
|
9587
|
+
}
|
|
9588
|
+
};
|
|
9589
|
+
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
9590
|
+
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
9591
|
+
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
9592
|
+
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
9593
|
+
const code = char.charCodeAt(0);
|
|
9594
|
+
return code < 32 || code === 127;
|
|
9595
|
+
}) && !value.split(/[\\/]/).includes("..");
|
|
9596
|
+
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
9597
|
+
var workerIdOf = (value) => value.workerId ?? null;
|
|
9598
|
+
function assertProductWriteAllowed(input) {
|
|
9599
|
+
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
9600
|
+
return failure("invalid_input", "invalid product write path");
|
|
9601
|
+
if (!input.store)
|
|
9602
|
+
return failure("permission_denied", "write authorization requires core state");
|
|
9603
|
+
const task = input.store.readTask(input.task.id);
|
|
9604
|
+
if (!task.ok)
|
|
9605
|
+
return task;
|
|
9606
|
+
const workspace = input.store.readWorkspace();
|
|
9607
|
+
if (!workspace.ok)
|
|
9608
|
+
return workspace;
|
|
9609
|
+
if (!workspace.data)
|
|
9610
|
+
return failure("not_found", "workspace not found");
|
|
9611
|
+
const workspaceRecord = workspace.data;
|
|
9612
|
+
if (task.data.status !== "active")
|
|
9613
|
+
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
9614
|
+
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
9615
|
+
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
9616
|
+
const writer = workspaceRecord.writer;
|
|
9617
|
+
if (!writer)
|
|
9618
|
+
return failure("permission_denied", "checkout has no writer owner");
|
|
9619
|
+
if (writer.state === "uncertain")
|
|
9620
|
+
return failure("recovery_required", "checkout writer requires recovery", {
|
|
9621
|
+
owner: writer.owner
|
|
9622
|
+
});
|
|
9623
|
+
const caller = callerValue(input.caller);
|
|
9624
|
+
const workerId = workerIdOf(input.caller);
|
|
9625
|
+
const owner = writer.owner;
|
|
9626
|
+
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
9627
|
+
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
9628
|
+
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
9629
|
+
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
9630
|
+
return failure("permission_denied", "only implementers can own product writes");
|
|
9631
|
+
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))))
|
|
9632
|
+
return failure("recovery_required", "worker session is not an active writer");
|
|
9633
|
+
return success(null, workspaceRecord.revision, owner);
|
|
9634
|
+
}
|
|
9635
|
+
|
|
9474
9636
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9475
9637
|
import { createHash as createHash4 } from "node:crypto";
|
|
9476
9638
|
import * as fs6 from "node:fs";
|
|
9477
9639
|
import { spawnSync } from "node:child_process";
|
|
9478
|
-
import
|
|
9640
|
+
import path6 from "node:path";
|
|
9479
9641
|
|
|
9480
9642
|
// packages/workit-core/src/core/authority.ts
|
|
9481
9643
|
import { createHash as createHash3 } from "node:crypto";
|
|
9482
9644
|
import * as fs5 from "node:fs";
|
|
9483
|
-
import
|
|
9645
|
+
import path5 from "node:path";
|
|
9484
9646
|
var verifiedAuthorities = new WeakMap;
|
|
9485
9647
|
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
9486
9648
|
const token = {};
|
|
@@ -9607,20 +9769,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
9607
9769
|
continue;
|
|
9608
9770
|
if (!reference.digest)
|
|
9609
9771
|
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"
|
|
9772
|
+
const target = path5.resolve(root, reference.path);
|
|
9773
|
+
if (target !== root && !target.startsWith(`${root}${path5.sep}`))
|
|
9774
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
9775
|
+
fields: [
|
|
9776
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
9777
|
+
]
|
|
9778
|
+
});
|
|
9613
9779
|
try {
|
|
9614
|
-
const relative =
|
|
9780
|
+
const relative = path5.relative(root, target);
|
|
9615
9781
|
let current = root;
|
|
9616
|
-
for (const segment of relative.split(
|
|
9782
|
+
for (const segment of relative.split(path5.sep)) {
|
|
9617
9783
|
if (!segment)
|
|
9618
9784
|
continue;
|
|
9619
|
-
current =
|
|
9785
|
+
current = path5.join(current, segment);
|
|
9620
9786
|
const stat = fs5.lstatSync(current);
|
|
9621
9787
|
const real = fs5.realpathSync(current);
|
|
9622
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
9623
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
9788
|
+
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
9789
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
9790
|
+
fields: [
|
|
9791
|
+
{
|
|
9792
|
+
path: "contentRefs.path",
|
|
9793
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
9794
|
+
}
|
|
9795
|
+
]
|
|
9796
|
+
});
|
|
9624
9797
|
if (stat.isSymbolicLink())
|
|
9625
9798
|
return failure("invalid_input", "document reference uses a symlink");
|
|
9626
9799
|
if (current === target && !stat.isFile())
|
|
@@ -9967,14 +10140,14 @@ var verifyDecisionContent = verifyContentRefs;
|
|
|
9967
10140
|
|
|
9968
10141
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9969
10142
|
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
9970
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
9971
|
-
var relative = (root, candidate) =>
|
|
10143
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
10144
|
+
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
9972
10145
|
var canonicalPath = (value) => {
|
|
9973
10146
|
if (typeof value !== "string" || !value)
|
|
9974
10147
|
return null;
|
|
9975
10148
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
9976
10149
|
return null;
|
|
9977
|
-
const normalized =
|
|
10150
|
+
const normalized = path6.posix.normalize(value);
|
|
9978
10151
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
9979
10152
|
return null;
|
|
9980
10153
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -10022,14 +10195,14 @@ var gitPaths = (root) => {
|
|
|
10022
10195
|
git: /not a git repository/i.test(stderr) ? false : true
|
|
10023
10196
|
};
|
|
10024
10197
|
}
|
|
10025
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10198
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
10026
10199
|
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
10027
10200
|
if (stagedDeleted.status !== 0) {
|
|
10028
10201
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
10029
10202
|
if (!/not a git repository/i.test(stderr))
|
|
10030
10203
|
return { paths, uncertain: true, git: true };
|
|
10031
10204
|
} else if (stagedDeleted.stdout) {
|
|
10032
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10205
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
10033
10206
|
}
|
|
10034
10207
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
10035
10208
|
};
|
|
@@ -10044,7 +10217,7 @@ var walk = (root, directory, output) => {
|
|
|
10044
10217
|
for (const name of names) {
|
|
10045
10218
|
if (name === ".git" || name === ".workit")
|
|
10046
10219
|
continue;
|
|
10047
|
-
const target =
|
|
10220
|
+
const target = path6.join(directory, name);
|
|
10048
10221
|
let stat;
|
|
10049
10222
|
try {
|
|
10050
10223
|
stat = fs6.lstatSync(target);
|
|
@@ -10062,17 +10235,17 @@ var walk = (root, directory, output) => {
|
|
|
10062
10235
|
var scopeRoots = (root, scope) => {
|
|
10063
10236
|
const roots = [];
|
|
10064
10237
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
10065
|
-
const target =
|
|
10238
|
+
const target = path6.resolve(root, item);
|
|
10066
10239
|
if (!inside(root, target))
|
|
10067
|
-
return failure("invalid_input",
|
|
10240
|
+
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
10241
|
let ancestor = target;
|
|
10069
10242
|
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
10070
|
-
ancestor =
|
|
10243
|
+
ancestor = path6.dirname(ancestor);
|
|
10071
10244
|
try {
|
|
10072
10245
|
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
10073
|
-
return failure("invalid_input",
|
|
10246
|
+
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
10247
|
} catch {
|
|
10075
|
-
return failure("invalid_input",
|
|
10248
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
10076
10249
|
}
|
|
10077
10250
|
roots.push(target);
|
|
10078
10251
|
}
|
|
@@ -10087,7 +10260,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10087
10260
|
}
|
|
10088
10261
|
const normalizedScope = canonicalScope(scope);
|
|
10089
10262
|
if (!normalizedScope)
|
|
10090
|
-
return failure("invalid_input", "candidate scope is invalid");
|
|
10263
|
+
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
10264
|
const roots = scopeRoots(checkout, normalizedScope);
|
|
10092
10265
|
if (!roots.ok)
|
|
10093
10266
|
return roots;
|
|
@@ -10119,7 +10292,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10119
10292
|
for (const item of [...names].sort()) {
|
|
10120
10293
|
if (!scopeMatches(item, normalizedScope))
|
|
10121
10294
|
continue;
|
|
10122
|
-
const target =
|
|
10295
|
+
const target = path6.join(checkout, item);
|
|
10123
10296
|
try {
|
|
10124
10297
|
const stat = fs6.lstatSync(target);
|
|
10125
10298
|
if (stat.isSymbolicLink()) {
|
|
@@ -10241,7 +10414,7 @@ function evaluateEvidence(task, candidate) {
|
|
|
10241
10414
|
}
|
|
10242
10415
|
var scopeCovers2 = scopeCovers;
|
|
10243
10416
|
var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
|
|
10244
|
-
var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && data.response === "approved" && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
|
|
10417
|
+
var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
|
|
10245
10418
|
var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
|
|
10246
10419
|
if (kind !== "passed")
|
|
10247
10420
|
return false;
|
|
@@ -10276,7 +10449,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
10276
10449
|
return true;
|
|
10277
10450
|
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
10278
10451
|
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
10279
|
-
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
|
|
10452
|
+
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
|
|
10280
10453
|
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
10281
10454
|
});
|
|
10282
10455
|
if (passed.length) {
|
|
@@ -10317,7 +10490,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
10317
10490
|
status: "satisfied",
|
|
10318
10491
|
evidenceIds: [],
|
|
10319
10492
|
decisionIds: decisions.map((decision) => decision.id),
|
|
10320
|
-
reason: "an applicable approved decision satisfies the requirement"
|
|
10493
|
+
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
10321
10494
|
};
|
|
10322
10495
|
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
10323
10496
|
if (limitations.length)
|
|
@@ -10372,7 +10545,17 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10372
10545
|
const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
|
|
10373
10546
|
if (requestedOutcome !== "stopped" && blocking.length)
|
|
10374
10547
|
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
10375
|
-
requirementIds: blocking.map((item) => item.requirementId)
|
|
10548
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
10549
|
+
requirements: blocking.map((item) => {
|
|
10550
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
10551
|
+
return {
|
|
10552
|
+
requirementId: item.requirementId,
|
|
10553
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
10554
|
+
reason: item.reason,
|
|
10555
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
10556
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
10557
|
+
};
|
|
10558
|
+
})
|
|
10376
10559
|
});
|
|
10377
10560
|
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
10378
10561
|
if (requestedOutcome === "verified" && accepted.length)
|
|
@@ -10403,7 +10586,7 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10403
10586
|
var METHODS = {
|
|
10404
10587
|
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
10405
10588
|
"workit-behavioral-tdd": { dimensions: ["testing"] },
|
|
10406
|
-
"workit-review": { dimensions: ["review"] },
|
|
10589
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
|
|
10407
10590
|
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10408
10591
|
"workit-implement": { dimensions: ["delegation"] },
|
|
10409
10592
|
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
@@ -10465,7 +10648,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
|
10465
10648
|
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10466
10649
|
header Workit decision: <purpose>, the same label repeated in the question
|
|
10467
10650
|
text (some host UIs do not render headers), exactly two options approved and
|
|
10468
|
-
rejected; ask once, never re-ask to mint a receipt.
|
|
10651
|
+
rejected; ask once, never re-ask to mint a receipt. Before asking, present the
|
|
10652
|
+
item in the conversation (durable artifacts: complete digest plus exact path;
|
|
10653
|
+
inline plans: the content) and keep the question to one short scoped sentence;
|
|
10654
|
+
never ask to approve something unseen. An answer with custom text is steering,
|
|
10655
|
+
not approval: adjust, re-present, then ask a new scoped question. After the user
|
|
10656
|
+
approves a spec or plan, execute the remaining tasks continuously with one
|
|
10657
|
+
atomic commit per task and do not ask whether to continue; stop only for a new
|
|
10658
|
+
product decision, a failed safety or verification gate, a conflicting edit, or
|
|
10659
|
+
missing authority. Record the plan's commit list once through the plan-scoped
|
|
10660
|
+
action approval (git.commit with plan_steps and plan_branch) so each listed
|
|
10661
|
+
commit executes without a new question; an unlisted message needs a fresh
|
|
10662
|
+
approval. task.close {outcome,
|
|
10469
10663
|
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10470
10664
|
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10471
10665
|
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
@@ -10497,7 +10691,7 @@ function reconcileResume(view, observations = []) {
|
|
|
10497
10691
|
return candidate;
|
|
10498
10692
|
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
10499
10693
|
const blockers = [...view.task.progress.blockers];
|
|
10500
|
-
if (view.task.workers.some((entry) =>
|
|
10694
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
10501
10695
|
blockers.push({
|
|
10502
10696
|
reason: "worker state requires reconciliation",
|
|
10503
10697
|
dependentAction: "resume",
|
|
@@ -10629,7 +10823,25 @@ function shellRouteIntent(command) {
|
|
|
10629
10823
|
}
|
|
10630
10824
|
return null;
|
|
10631
10825
|
}
|
|
10826
|
+
function hasLiveWorkitTask(dir) {
|
|
10827
|
+
try {
|
|
10828
|
+
if (typeof dir !== "string" || dir === "")
|
|
10829
|
+
return false;
|
|
10830
|
+
const listed = new TaskStore(dir).listTasks();
|
|
10831
|
+
if (!listed.ok)
|
|
10832
|
+
return false;
|
|
10833
|
+
return listed.data.some((task) => task.status === "active" || task.status === "paused");
|
|
10834
|
+
} catch {
|
|
10835
|
+
return false;
|
|
10836
|
+
}
|
|
10837
|
+
}
|
|
10838
|
+
function shouldDenyShellRoute(dir, command) {
|
|
10839
|
+
if (!hasLiveWorkitTask(dir))
|
|
10840
|
+
return null;
|
|
10841
|
+
return shellRouteIntent(command);
|
|
10842
|
+
}
|
|
10632
10843
|
// packages/workit-core/src/core/external-action.ts
|
|
10844
|
+
import { execFileSync } from "node:child_process";
|
|
10633
10845
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
10634
10846
|
object({
|
|
10635
10847
|
operation: literal("git.branch_setup"),
|
|
@@ -10642,7 +10854,11 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
10642
10854
|
}).strict(),
|
|
10643
10855
|
object({
|
|
10644
10856
|
operation: literal("git.commit"),
|
|
10645
|
-
payload: object({
|
|
10857
|
+
payload: object({
|
|
10858
|
+
message: string2().min(1).optional(),
|
|
10859
|
+
plan_steps: array(string2().min(1)).optional(),
|
|
10860
|
+
plan_branch: string2().min(1).optional()
|
|
10861
|
+
}).strict()
|
|
10646
10862
|
}).strict(),
|
|
10647
10863
|
object({
|
|
10648
10864
|
operation: literal("git.push"),
|
|
@@ -10731,7 +10947,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
10731
10947
|
payload,
|
|
10732
10948
|
...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
10949
|
});
|
|
10734
|
-
var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch
|
|
10950
|
+
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
10951
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
10736
10952
|
kind: "host",
|
|
10737
10953
|
host,
|
|
@@ -10782,14 +10998,185 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
|
|
|
10782
10998
|
return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
|
|
10783
10999
|
return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
|
|
10784
11000
|
};
|
|
11001
|
+
var planCommitDescriptor = (content) => {
|
|
11002
|
+
try {
|
|
11003
|
+
const value = JSON.parse(content);
|
|
11004
|
+
if (value.operation !== "git.commit" || !value.payload)
|
|
11005
|
+
return null;
|
|
11006
|
+
const steps = value.payload.plan_steps;
|
|
11007
|
+
const branch = value.payload.plan_branch;
|
|
11008
|
+
if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
|
|
11009
|
+
return null;
|
|
11010
|
+
return { steps, branch };
|
|
11011
|
+
} catch {
|
|
11012
|
+
return null;
|
|
11013
|
+
}
|
|
11014
|
+
};
|
|
11015
|
+
var approvedPlanCommit = (store, host, actor, message) => {
|
|
11016
|
+
if (typeof message !== "string" || !message)
|
|
11017
|
+
return failure("invalid_input", "commit message is required");
|
|
11018
|
+
const listed = store.listTasks();
|
|
11019
|
+
const workspace = store.readWorkspace();
|
|
11020
|
+
if (!listed.ok || !workspace.ok)
|
|
11021
|
+
return failure("storage_error", "external action state is unavailable");
|
|
11022
|
+
if (!workspace.data)
|
|
11023
|
+
return failure("not_found", "workspace not found");
|
|
11024
|
+
const matches = listed.data.flatMap((task) => {
|
|
11025
|
+
if (task.status !== "active" || task.workspaceId !== workspace.data.id)
|
|
11026
|
+
return [];
|
|
11027
|
+
if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
|
|
11028
|
+
return [];
|
|
11029
|
+
return task.decisions.flatMap((entry) => {
|
|
11030
|
+
const decision = entry.data;
|
|
11031
|
+
if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
|
|
11032
|
+
return [];
|
|
11033
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
|
|
11034
|
+
return [];
|
|
11035
|
+
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
11036
|
+
if (!plan)
|
|
11037
|
+
return [];
|
|
11038
|
+
const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
|
|
11039
|
+
if (plan.steps[completed.length] !== message)
|
|
11040
|
+
return [];
|
|
11041
|
+
return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
|
|
11042
|
+
});
|
|
11043
|
+
});
|
|
11044
|
+
if (matches.length !== 1)
|
|
11045
|
+
return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
|
|
11046
|
+
return success(workspace.data.revision, null, matches[0]);
|
|
11047
|
+
};
|
|
11048
|
+
var commitMessageFromDescriptor = (operation) => {
|
|
11049
|
+
try {
|
|
11050
|
+
const descriptor = JSON.parse(operation);
|
|
11051
|
+
return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
|
|
11052
|
+
} catch {
|
|
11053
|
+
return;
|
|
11054
|
+
}
|
|
11055
|
+
};
|
|
11056
|
+
var planCommitBinding = (store, host, actor, operation) => {
|
|
11057
|
+
const message = commitMessageFromDescriptor(operation);
|
|
11058
|
+
if (!message)
|
|
11059
|
+
return null;
|
|
11060
|
+
const plan = approvedPlanCommit(store, host, actor, message);
|
|
11061
|
+
if (!plan.ok)
|
|
11062
|
+
return null;
|
|
11063
|
+
let branch;
|
|
11064
|
+
try {
|
|
11065
|
+
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
11066
|
+
cwd: store.root,
|
|
11067
|
+
encoding: "utf8",
|
|
11068
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11069
|
+
}).trim();
|
|
11070
|
+
} catch {
|
|
11071
|
+
return null;
|
|
11072
|
+
}
|
|
11073
|
+
if (!branch || branch !== plan.data.branch)
|
|
11074
|
+
return null;
|
|
11075
|
+
const workspace = store.readWorkspace();
|
|
11076
|
+
if (!workspace.ok || !workspace.data)
|
|
11077
|
+
return null;
|
|
11078
|
+
return {
|
|
11079
|
+
taskId: plan.data.task.id,
|
|
11080
|
+
decisionId: plan.data.entry.id,
|
|
11081
|
+
binding: plan.data.entry.data.binding,
|
|
11082
|
+
expectedRevision: plan.data.task.revision,
|
|
11083
|
+
expectedWorkspaceRevision: workspace.data.revision,
|
|
11084
|
+
step: message
|
|
11085
|
+
};
|
|
11086
|
+
};
|
|
10785
11087
|
var approvedExternalAction = (store, host, actor, operation) => {
|
|
10786
11088
|
const selected = externalActionState(store, host, actor, operation);
|
|
10787
|
-
if (
|
|
11089
|
+
if (selected.ok) {
|
|
11090
|
+
if (selected.data.entry.data.consumption !== null)
|
|
11091
|
+
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");
|
|
10788
11092
|
return selected;
|
|
10789
|
-
|
|
10790
|
-
|
|
11093
|
+
}
|
|
11094
|
+
const intent = approvedBranchSetupIntent(store, host, actor, operation);
|
|
11095
|
+
if (intent.ok) {
|
|
11096
|
+
if (intent.data.entry.data.consumption !== null)
|
|
11097
|
+
return failure(intent.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", intent.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
|
|
11098
|
+
return intent;
|
|
11099
|
+
}
|
|
10791
11100
|
return selected;
|
|
10792
11101
|
};
|
|
11102
|
+
var branchSetupIntent = (descriptor) => {
|
|
11103
|
+
try {
|
|
11104
|
+
const value = JSON.parse(descriptor);
|
|
11105
|
+
if (value.operation !== "git.branch_setup" || !value.payload)
|
|
11106
|
+
return null;
|
|
11107
|
+
const { target_branch, resolved } = value.payload;
|
|
11108
|
+
if (typeof target_branch !== "string" || !target_branch || !resolved)
|
|
11109
|
+
return null;
|
|
11110
|
+
if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
|
|
11111
|
+
return null;
|
|
11112
|
+
return {
|
|
11113
|
+
target: target_branch,
|
|
11114
|
+
base: resolved.base_branch,
|
|
11115
|
+
remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
|
|
11116
|
+
targetExists: resolved.target_exists
|
|
11117
|
+
};
|
|
11118
|
+
} catch {
|
|
11119
|
+
return null;
|
|
11120
|
+
}
|
|
11121
|
+
};
|
|
11122
|
+
var approvedBranchSetupIntent = (store, host, actor, operation) => {
|
|
11123
|
+
const current = branchSetupIntent(operation);
|
|
11124
|
+
if (!current)
|
|
11125
|
+
return failure("permission_denied", "no approved action is bound to this session");
|
|
11126
|
+
const listed = store.listTasks();
|
|
11127
|
+
const workspace = store.readWorkspace();
|
|
11128
|
+
if (!listed.ok || !workspace.ok)
|
|
11129
|
+
return failure("storage_error", "external action state is unavailable");
|
|
11130
|
+
if (!workspace.data)
|
|
11131
|
+
return failure("not_found", "workspace not found");
|
|
11132
|
+
const candidates = listed.data.flatMap((task) => {
|
|
11133
|
+
if (task.status !== "active" || task.workspaceId !== workspace.data.id || task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
|
|
11134
|
+
return [];
|
|
11135
|
+
return task.decisions.filter((entry) => entry.data.purpose === "action" && entry.data.response === "approved" && entry.data.revoked === null).map((entry) => ({ task, entry }));
|
|
11136
|
+
});
|
|
11137
|
+
const matches = candidates.filter(({ entry }) => {
|
|
11138
|
+
const intent = branchSetupIntent(entry.data.binding.approvedContent);
|
|
11139
|
+
return intent !== null && intent.target === current.target && intent.base === current.base && intent.remoteBase === current.remoteBase && intent.targetExists === current.targetExists;
|
|
11140
|
+
});
|
|
11141
|
+
if (matches.length !== 1)
|
|
11142
|
+
return failure("permission_denied", matches.length === 0 ? "no approved action is bound to this session" : "multiple approved actions match this session");
|
|
11143
|
+
return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
|
|
11144
|
+
};
|
|
11145
|
+
var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
|
|
11146
|
+
const prior = priorExternalAction(store, host, actor, operation, payload);
|
|
11147
|
+
if (!prior.ok)
|
|
11148
|
+
return success(null, null, null);
|
|
11149
|
+
let baseline;
|
|
11150
|
+
try {
|
|
11151
|
+
const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
|
|
11152
|
+
baseline = parsed.payload?.resolved;
|
|
11153
|
+
} catch {
|
|
11154
|
+
return success(null, null, null);
|
|
11155
|
+
}
|
|
11156
|
+
if (baseline === undefined || baseline === null)
|
|
11157
|
+
return success(null, null, null);
|
|
11158
|
+
if (operation === "git.branch_setup") {
|
|
11159
|
+
const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
|
|
11160
|
+
let after = null;
|
|
11161
|
+
try {
|
|
11162
|
+
after = branchSetupIntent(JSON.stringify({
|
|
11163
|
+
operation,
|
|
11164
|
+
payload: { ...payload, resolved: currentResolved }
|
|
11165
|
+
}));
|
|
11166
|
+
} catch {
|
|
11167
|
+
after = null;
|
|
11168
|
+
}
|
|
11169
|
+
if (before && after) {
|
|
11170
|
+
const moved = before.target !== after.target ? "target_branch" : before.base !== after.base ? "base_branch" : before.remoteBase !== after.remoteBase ? "remote_base" : before.targetExists !== after.targetExists ? "target_exists" : null;
|
|
11171
|
+
if (moved === null)
|
|
11172
|
+
return success(null, null, null);
|
|
11173
|
+
return failure("invalid_input", `repository state changed since approval (${moved} moved); show the current state and approve the action again`, { outcome: "not_started", operation });
|
|
11174
|
+
}
|
|
11175
|
+
}
|
|
11176
|
+
if (canonicalJson(baseline) === canonicalJson(currentResolved))
|
|
11177
|
+
return success(null, null, null);
|
|
11178
|
+
return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
|
|
11179
|
+
};
|
|
10793
11180
|
var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
|
|
10794
11181
|
let steps = [];
|
|
10795
11182
|
try {
|
|
@@ -11067,7 +11454,7 @@ function resolveRequirements(input) {
|
|
|
11067
11454
|
].join(", ")}`,
|
|
11068
11455
|
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
11069
11456
|
before: "dependent_action",
|
|
11070
|
-
dependentAction:
|
|
11457
|
+
dependentAction: null,
|
|
11071
11458
|
acceptanceAllowed: false
|
|
11072
11459
|
}));
|
|
11073
11460
|
if (signals.productChoiceOpen.value === true)
|
|
@@ -11078,7 +11465,7 @@ function resolveRequirements(input) {
|
|
|
11078
11465
|
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
11079
11466
|
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
11080
11467
|
before: "dependent_action",
|
|
11081
|
-
dependentAction:
|
|
11468
|
+
dependentAction: null,
|
|
11082
11469
|
acceptanceAllowed: false
|
|
11083
11470
|
}));
|
|
11084
11471
|
if (signals.behaviorChange.value === true)
|
|
@@ -11236,59 +11623,6 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
11236
11623
|
return parsed.data;
|
|
11237
11624
|
}
|
|
11238
11625
|
|
|
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
11626
|
// packages/workit-core/src/core/task-engine.ts
|
|
11293
11627
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
11294
11628
|
kind,
|
|
@@ -11419,15 +11753,20 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11419
11753
|
return failure("invalid_transition", "worker has not started");
|
|
11420
11754
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
11421
11755
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
11756
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
11422
11757
|
const nextEntry = {
|
|
11423
11758
|
...entry,
|
|
11424
11759
|
recordedAt: input.now ?? entry.recordedAt,
|
|
11425
11760
|
provenance: authority.provenance,
|
|
11426
|
-
data: {
|
|
11761
|
+
data: {
|
|
11762
|
+
...entry.data,
|
|
11763
|
+
state: terminalCancel ? "cancelling" : input.state,
|
|
11764
|
+
session: input.session
|
|
11765
|
+
}
|
|
11427
11766
|
};
|
|
11428
11767
|
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
11768
|
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 ===
|
|
11769
|
+
if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
|
|
11431
11770
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
11432
11771
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
11433
11772
|
taskId: task.data.id,
|
|
@@ -11797,7 +12136,7 @@ class WorkitCore {
|
|
|
11797
12136
|
activeWorkerBlocker(task, workspace) {
|
|
11798
12137
|
if (workspace.writer)
|
|
11799
12138
|
return failure("recovery_required", "writer ownership must be released first");
|
|
11800
|
-
if (task.workers.some((entry) =>
|
|
12139
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
11801
12140
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
11802
12141
|
return null;
|
|
11803
12142
|
}
|
|
@@ -12034,11 +12373,6 @@ class WorkitCore {
|
|
|
12034
12373
|
if (helper && !helper.ok)
|
|
12035
12374
|
return helper;
|
|
12036
12375
|
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
12376
|
if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
|
|
12043
12377
|
return failure("invalid_input", "missing or skipped evidence requires a reason");
|
|
12044
12378
|
if (evidence.kind === "review") {
|
|
@@ -12055,6 +12389,11 @@ class WorkitCore {
|
|
|
12055
12389
|
evidence.beforeCandidateId ??= null;
|
|
12056
12390
|
evidence.candidateId ??= null;
|
|
12057
12391
|
}
|
|
12392
|
+
if (helper?.ok) {
|
|
12393
|
+
const assignment = helper.data.data.assignment;
|
|
12394
|
+
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))
|
|
12395
|
+
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
12396
|
+
}
|
|
12058
12397
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
12059
12398
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
12060
12399
|
if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
|
|
@@ -12108,7 +12447,12 @@ class WorkitCore {
|
|
|
12108
12447
|
const input = parsed.data;
|
|
12109
12448
|
if (nativeRequired && input.action !== "record")
|
|
12110
12449
|
return failure("permission_denied", "native observation is only valid for recording decisions");
|
|
12111
|
-
|
|
12450
|
+
const stated = input.action === "record" && input.response === "stated";
|
|
12451
|
+
if (stated && input.purpose === "action")
|
|
12452
|
+
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
12453
|
+
if (stated && !input.binding?.statedChoice)
|
|
12454
|
+
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
12455
|
+
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
12112
12456
|
return failure("permission_denied", "native decision observation is required");
|
|
12113
12457
|
const task = this.store.readTask(input.taskId);
|
|
12114
12458
|
if (!task.ok)
|
|
@@ -12141,7 +12485,7 @@ class WorkitCore {
|
|
|
12141
12485
|
consumption: null
|
|
12142
12486
|
};
|
|
12143
12487
|
const data = { ...base, digest: decisionDigest(base) };
|
|
12144
|
-
const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
|
|
12488
|
+
const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
|
|
12145
12489
|
observation: nativeObservation,
|
|
12146
12490
|
expected: {
|
|
12147
12491
|
taskId: task.data.id,
|
|
@@ -12950,6 +13294,9 @@ class WorkitCore {
|
|
|
12950
13294
|
id: task.id,
|
|
12951
13295
|
revision: task.revision,
|
|
12952
13296
|
workspaceRevision: workspace.data.revision,
|
|
13297
|
+
createdAt: task.createdAt,
|
|
13298
|
+
updatedAt: task.updatedAt,
|
|
13299
|
+
runtime: task.runtime ?? null,
|
|
12953
13300
|
objective: task.intent.data.objective,
|
|
12954
13301
|
status: task.status,
|
|
12955
13302
|
closure: task.closure,
|
|
@@ -13035,7 +13382,7 @@ class WorkitCore {
|
|
|
13035
13382
|
const base = reconcileResume(effectiveView);
|
|
13036
13383
|
if (!base.ok)
|
|
13037
13384
|
return base;
|
|
13038
|
-
const needsReconciliation = view.task.workers.some((entry) =>
|
|
13385
|
+
const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
|
|
13039
13386
|
const blockers = [...base.data.blockers];
|
|
13040
13387
|
if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
|
|
13041
13388
|
blockers.push({
|
|
@@ -13102,7 +13449,8 @@ class WorkitCore {
|
|
|
13102
13449
|
...current,
|
|
13103
13450
|
status,
|
|
13104
13451
|
candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
|
|
13105
|
-
progress:
|
|
13452
|
+
progress: current.progress,
|
|
13453
|
+
pauseReason: status === "paused" ? input.reason ?? null : null
|
|
13106
13454
|
})
|
|
13107
13455
|
});
|
|
13108
13456
|
if (!changed.ok)
|
|
@@ -13138,7 +13486,7 @@ class WorkitCore {
|
|
|
13138
13486
|
return workspace;
|
|
13139
13487
|
if (!workspace.data)
|
|
13140
13488
|
return failure("not_found", "workspace not found");
|
|
13141
|
-
if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state
|
|
13489
|
+
if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
13142
13490
|
return failure("recovery_required", "scope revision requires worker ownership reconciliation");
|
|
13143
13491
|
this.fillRevisions(input, task.data, workspace.data);
|
|
13144
13492
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
@@ -13256,15 +13604,15 @@ function resolveGitRevision(root, value) {
|
|
|
13256
13604
|
}
|
|
13257
13605
|
function resolveInside(root, candidate) {
|
|
13258
13606
|
const base = realpathSync5(root);
|
|
13259
|
-
const target =
|
|
13260
|
-
if (target !== base && !target.startsWith(base +
|
|
13607
|
+
const target = path7.resolve(base, candidate);
|
|
13608
|
+
if (target !== base && !target.startsWith(base + path7.sep)) {
|
|
13261
13609
|
throw new Error("path must stay inside repository root");
|
|
13262
13610
|
}
|
|
13263
13611
|
let ancestor = target;
|
|
13264
|
-
while (!
|
|
13265
|
-
ancestor =
|
|
13612
|
+
while (!existsSync4(ancestor))
|
|
13613
|
+
ancestor = path7.dirname(ancestor);
|
|
13266
13614
|
const canonicalAncestor = realpathSync5(ancestor);
|
|
13267
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
13615
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
|
|
13268
13616
|
throw new Error("path must stay inside repository root");
|
|
13269
13617
|
}
|
|
13270
13618
|
return target;
|
|
@@ -13306,7 +13654,7 @@ if (false) {}
|
|
|
13306
13654
|
// packages/workit-core/src/core/logger.ts
|
|
13307
13655
|
import { appendFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
13308
13656
|
import os from "node:os";
|
|
13309
|
-
import
|
|
13657
|
+
import path8 from "node:path";
|
|
13310
13658
|
var REDACTED = "[REDACTED]";
|
|
13311
13659
|
var DEFAULT_MAX_RATE = 20;
|
|
13312
13660
|
var DEFAULT_RATE_WINDOW_MS = 1000;
|
|
@@ -13362,14 +13710,14 @@ var resolveStateDir = () => {
|
|
|
13362
13710
|
return override;
|
|
13363
13711
|
const home = os.homedir();
|
|
13364
13712
|
if (process.env.XDG_STATE_HOME)
|
|
13365
|
-
return
|
|
13713
|
+
return path8.join(process.env.XDG_STATE_HOME, "workit");
|
|
13366
13714
|
switch (os.platform()) {
|
|
13367
13715
|
case "darwin":
|
|
13368
|
-
return
|
|
13716
|
+
return path8.join(home, "Library", "Application Support", "workit");
|
|
13369
13717
|
case "win32":
|
|
13370
|
-
return
|
|
13718
|
+
return path8.join(process.env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local"), "workit");
|
|
13371
13719
|
default:
|
|
13372
|
-
return
|
|
13720
|
+
return path8.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
13373
13721
|
}
|
|
13374
13722
|
};
|
|
13375
13723
|
var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
@@ -13442,14 +13790,14 @@ var pruneOldFiles = (dir) => {
|
|
|
13442
13790
|
const files = readdirSync3(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
|
|
13443
13791
|
for (const file of files.slice(RETAINED_DAYS)) {
|
|
13444
13792
|
try {
|
|
13445
|
-
unlinkSync2(
|
|
13793
|
+
unlinkSync2(path8.join(dir, file));
|
|
13446
13794
|
} catch {}
|
|
13447
13795
|
}
|
|
13448
13796
|
} catch {}
|
|
13449
13797
|
};
|
|
13450
13798
|
var createLogger = (options = {}) => {
|
|
13451
13799
|
const stateDir = options.stateDir ?? resolveStateDir();
|
|
13452
|
-
const logDir =
|
|
13800
|
+
const logDir = path8.join(stateDir, "logs");
|
|
13453
13801
|
const now = options.now ?? (() => new Date);
|
|
13454
13802
|
const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
|
|
13455
13803
|
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
@@ -13483,7 +13831,7 @@ var createLogger = (options = {}) => {
|
|
|
13483
13831
|
const line = `${JSON.stringify(event)}
|
|
13484
13832
|
`;
|
|
13485
13833
|
mkdirSync2(logDir, { recursive: true, mode: 448 });
|
|
13486
|
-
appendFileSync(
|
|
13834
|
+
appendFileSync(path8.join(logDir, dailyFileName(date)), line, { mode: 384 });
|
|
13487
13835
|
pruneOldFiles(logDir);
|
|
13488
13836
|
} catch {
|
|
13489
13837
|
try {
|
|
@@ -13494,7 +13842,7 @@ var createLogger = (options = {}) => {
|
|
|
13494
13842
|
context: { redaction_failed: true }
|
|
13495
13843
|
};
|
|
13496
13844
|
mkdirSync2(logDir, { recursive: true, mode: 448 });
|
|
13497
|
-
appendFileSync(
|
|
13845
|
+
appendFileSync(path8.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
|
|
13498
13846
|
`, {
|
|
13499
13847
|
mode: 384
|
|
13500
13848
|
});
|
|
@@ -13577,6 +13925,24 @@ OpenCode Workit decisions use the native question header "Workit decision: <purp
|
|
|
13577
13925
|
return cached2;
|
|
13578
13926
|
};
|
|
13579
13927
|
|
|
13928
|
+
// packages/workit-opencode/src/stale-sources.ts
|
|
13929
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
13930
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
13931
|
+
var pluginSourceFiles = [
|
|
13932
|
+
fileURLToPath2(import.meta.url),
|
|
13933
|
+
fileURLToPath2(new URL("./plugin.ts", import.meta.url)),
|
|
13934
|
+
fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
|
|
13935
|
+
...[
|
|
13936
|
+
"task-contract.ts",
|
|
13937
|
+
"task-engine.ts",
|
|
13938
|
+
"task-evaluation.ts",
|
|
13939
|
+
"task-store.ts",
|
|
13940
|
+
"workers.ts",
|
|
13941
|
+
"authority.ts",
|
|
13942
|
+
"methods.ts"
|
|
13943
|
+
].map((file) => fileURLToPath2(new URL(`../../workit-core/src/core/${file}`, import.meta.url)))
|
|
13944
|
+
].filter((file) => existsSync5(file));
|
|
13945
|
+
|
|
13580
13946
|
// packages/workit-opencode/src/tools/repo.ts
|
|
13581
13947
|
import path22 from "node:path";
|
|
13582
13948
|
import { realpathSync as realpathSync8 } from "node:fs";
|
|
@@ -25906,25 +26272,6 @@ import fs7 from "node:fs";
|
|
|
25906
26272
|
import path9 from "node:path";
|
|
25907
26273
|
import { createHash as createHash5 } from "node:crypto";
|
|
25908
26274
|
|
|
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
26275
|
// packages/workit-core/src/core/scripts.ts
|
|
25929
26276
|
var PLUGIN_ROOT = packageRoot();
|
|
25930
26277
|
var ASSET_ROOT = assetRoot();
|
|
@@ -26257,10 +26604,10 @@ function changelogApply({
|
|
|
26257
26604
|
}
|
|
26258
26605
|
|
|
26259
26606
|
// packages/workit-core/src/core/git.ts
|
|
26260
|
-
import { execFileSync } from "node:child_process";
|
|
26607
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
26261
26608
|
var run2 = (cwd, args) => {
|
|
26262
26609
|
try {
|
|
26263
|
-
const stdout =
|
|
26610
|
+
const stdout = execFileSync2("git", args, {
|
|
26264
26611
|
cwd,
|
|
26265
26612
|
encoding: "utf8",
|
|
26266
26613
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -26378,15 +26725,15 @@ function parseVerifyOutput(stdout) {
|
|
|
26378
26725
|
}
|
|
26379
26726
|
|
|
26380
26727
|
// packages/workit-core/src/core/branch.ts
|
|
26381
|
-
import { existsSync as
|
|
26382
|
-
import { execFileSync as
|
|
26728
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26729
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
26383
26730
|
import path13 from "node:path";
|
|
26384
26731
|
|
|
26385
26732
|
// packages/workit-core/src/core/config.ts
|
|
26386
26733
|
import {
|
|
26387
26734
|
copyFileSync,
|
|
26388
26735
|
cpSync,
|
|
26389
|
-
existsSync as
|
|
26736
|
+
existsSync as existsSync6,
|
|
26390
26737
|
mkdirSync as mkdirSync3,
|
|
26391
26738
|
readFileSync as readFileSync4,
|
|
26392
26739
|
readdirSync as readdirSync4,
|
|
@@ -26426,11 +26773,11 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
26426
26773
|
return dir;
|
|
26427
26774
|
}
|
|
26428
26775
|
const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
|
|
26429
|
-
if (!
|
|
26776
|
+
if (!existsSync6(legacy)) {
|
|
26430
26777
|
migratedDir = dir;
|
|
26431
26778
|
return dir;
|
|
26432
26779
|
}
|
|
26433
|
-
if (!migrationFailed &&
|
|
26780
|
+
if (!migrationFailed && existsSync6(dir)) {
|
|
26434
26781
|
migratedDir = dir;
|
|
26435
26782
|
return dir;
|
|
26436
26783
|
}
|
|
@@ -26440,7 +26787,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
26440
26787
|
for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
|
|
26441
26788
|
const src = path10.join(legacy, entry.name);
|
|
26442
26789
|
const dest = path10.join(dir, entry.name);
|
|
26443
|
-
if (
|
|
26790
|
+
if (existsSync6(dest))
|
|
26444
26791
|
continue;
|
|
26445
26792
|
try {
|
|
26446
26793
|
if (entry.isDirectory())
|
|
@@ -27018,7 +27365,7 @@ var baseBranch = (cwd) => {
|
|
|
27018
27365
|
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
27019
27366
|
};
|
|
27020
27367
|
var originBaseReady = (cwd, base) => {
|
|
27021
|
-
const run = (args) =>
|
|
27368
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
27022
27369
|
try {
|
|
27023
27370
|
try {
|
|
27024
27371
|
run(["fetch", "origin", base, "--prune"]);
|
|
@@ -27026,7 +27373,7 @@ var originBaseReady = (cwd, base) => {
|
|
|
27026
27373
|
run(["fetch", "origin", "--prune"]);
|
|
27027
27374
|
}
|
|
27028
27375
|
try {
|
|
27029
|
-
|
|
27376
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
27030
27377
|
cwd,
|
|
27031
27378
|
stdio: "pipe"
|
|
27032
27379
|
});
|
|
@@ -27045,11 +27392,11 @@ var originBaseReady = (cwd, base) => {
|
|
|
27045
27392
|
}
|
|
27046
27393
|
};
|
|
27047
27394
|
var fastForwardBase = (cwd, base) => {
|
|
27048
|
-
const run = (args) =>
|
|
27395
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
27049
27396
|
try {
|
|
27050
27397
|
let hasLocalBase = true;
|
|
27051
27398
|
try {
|
|
27052
|
-
|
|
27399
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
27053
27400
|
cwd,
|
|
27054
27401
|
stdio: "pipe"
|
|
27055
27402
|
});
|
|
@@ -27090,18 +27437,17 @@ var branchSetup = ({
|
|
|
27090
27437
|
log
|
|
27091
27438
|
}) => {
|
|
27092
27439
|
const cwd = path13.resolve(workspace_root);
|
|
27093
|
-
const exec = (args) =>
|
|
27440
|
+
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
27094
27441
|
const current = gitContext(cwd).branch;
|
|
27095
27442
|
if (!current || current === "unknown")
|
|
27096
|
-
return { error: "not in a git repository" };
|
|
27443
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
27097
27444
|
const sdd = sdd_dir ?? "docs";
|
|
27098
27445
|
const manifestPath = path13.isAbsolute(sdd) ? path13.join(sdd, "manifest.json") : path13.join(cwd, sdd, "manifest.json");
|
|
27099
27446
|
const ensureManifestDir = () => {
|
|
27100
27447
|
const dir = path13.dirname(manifestPath);
|
|
27101
|
-
if (!
|
|
27448
|
+
if (!existsSync7(dir))
|
|
27102
27449
|
mkdirSync4(dir, { recursive: true, mode: 493 });
|
|
27103
27450
|
};
|
|
27104
|
-
ensureManifestDir();
|
|
27105
27451
|
const readManifest = () => {
|
|
27106
27452
|
try {
|
|
27107
27453
|
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
@@ -27119,13 +27465,16 @@ var branchSetup = ({
|
|
|
27119
27465
|
const manifest = readManifest();
|
|
27120
27466
|
const ref = manifest.stash_ref;
|
|
27121
27467
|
if (!ref)
|
|
27122
|
-
return { error: "no stash_ref in manifest" };
|
|
27468
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
27123
27469
|
journal(`pre-pop: ${String(ref)}`);
|
|
27124
27470
|
try {
|
|
27125
27471
|
exec(["stash", "pop", String(ref)]);
|
|
27126
27472
|
} catch (error) {
|
|
27127
27473
|
journal("pop: failed");
|
|
27128
|
-
return {
|
|
27474
|
+
return {
|
|
27475
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
27476
|
+
phase: "post_mutation"
|
|
27477
|
+
};
|
|
27129
27478
|
}
|
|
27130
27479
|
journal("pop: ok");
|
|
27131
27480
|
delete manifest.stash_ref;
|
|
@@ -27135,11 +27484,17 @@ var branchSetup = ({
|
|
|
27135
27484
|
}
|
|
27136
27485
|
const target = target_branch ?? "";
|
|
27137
27486
|
if (!target)
|
|
27138
|
-
return {
|
|
27487
|
+
return {
|
|
27488
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
27489
|
+
phase: "preflight"
|
|
27490
|
+
};
|
|
27139
27491
|
if (isProtected(cwd, target))
|
|
27140
|
-
return { error: `protected branch ${target}
|
|
27492
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
27141
27493
|
if (!allowedBranch(cwd, target))
|
|
27142
|
-
return {
|
|
27494
|
+
return {
|
|
27495
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
27496
|
+
phase: "preflight"
|
|
27497
|
+
};
|
|
27143
27498
|
let base;
|
|
27144
27499
|
let targetExists = true;
|
|
27145
27500
|
try {
|
|
@@ -27150,12 +27505,12 @@ var branchSetup = ({
|
|
|
27150
27505
|
if (!targetExists) {
|
|
27151
27506
|
const baseResolved = baseBranch(cwd);
|
|
27152
27507
|
if ("error" in baseResolved)
|
|
27153
|
-
return { error: baseResolved.error };
|
|
27508
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
27154
27509
|
base = baseResolved.base;
|
|
27155
27510
|
}
|
|
27156
27511
|
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
27157
27512
|
let stash_ref;
|
|
27158
|
-
const failAfterStash = (message) => {
|
|
27513
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
27159
27514
|
let suffix = "";
|
|
27160
27515
|
if (stash_ref) {
|
|
27161
27516
|
journal(`pre-pop: ${stash_ref}`);
|
|
@@ -27168,27 +27523,31 @@ var branchSetup = ({
|
|
|
27168
27523
|
suffix = " (changes preserved in stash)";
|
|
27169
27524
|
}
|
|
27170
27525
|
}
|
|
27171
|
-
return { error: `${message}${suffix}
|
|
27526
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
27172
27527
|
};
|
|
27173
27528
|
if (current !== target) {
|
|
27174
27529
|
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
27175
27530
|
if (dirty && stash !== "yes") {
|
|
27176
27531
|
return {
|
|
27177
|
-
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
|
|
27532
|
+
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
|
|
27533
|
+
phase: "preflight"
|
|
27178
27534
|
};
|
|
27179
27535
|
}
|
|
27180
27536
|
let validatedBase;
|
|
27181
27537
|
if (!targetExists && base !== undefined) {
|
|
27182
27538
|
const ready = originBaseReady(cwd, base);
|
|
27183
27539
|
if (!ready.ok)
|
|
27184
|
-
return { error: ready.error ?? "ensure-base-branch failed" };
|
|
27540
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
27185
27541
|
validatedBase = base;
|
|
27186
27542
|
}
|
|
27187
27543
|
if (dirty) {
|
|
27188
27544
|
try {
|
|
27189
27545
|
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
27190
27546
|
} catch (error) {
|
|
27191
|
-
return {
|
|
27547
|
+
return {
|
|
27548
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
27549
|
+
phase: "post_mutation"
|
|
27550
|
+
};
|
|
27192
27551
|
}
|
|
27193
27552
|
stash_ref = "stash@{0}";
|
|
27194
27553
|
journal(`stash push: ${stash_ref}`);
|
|
@@ -27199,14 +27558,14 @@ var branchSetup = ({
|
|
|
27199
27558
|
} catch (error) {
|
|
27200
27559
|
const message = error instanceof Error ? error.message : "checkout failed";
|
|
27201
27560
|
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)
|
|
27561
|
+
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
|
|
27203
27562
|
}
|
|
27204
27563
|
try {
|
|
27205
27564
|
let effectiveBase = base;
|
|
27206
27565
|
if (effectiveBase === undefined) {
|
|
27207
27566
|
const lateResolved = baseBranch(cwd);
|
|
27208
27567
|
if ("error" in lateResolved)
|
|
27209
|
-
return failAfterStash(lateResolved.error);
|
|
27568
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
27210
27569
|
effectiveBase = lateResolved.base;
|
|
27211
27570
|
}
|
|
27212
27571
|
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
@@ -27252,7 +27611,7 @@ var branchSetup = ({
|
|
|
27252
27611
|
};
|
|
27253
27612
|
|
|
27254
27613
|
// packages/workit-core/src/core/gitignore.ts
|
|
27255
|
-
import { existsSync as
|
|
27614
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27256
27615
|
import path14 from "node:path";
|
|
27257
27616
|
var GITIGNORE_ENTRIES = [
|
|
27258
27617
|
"# workit: SDD working state (never commit)",
|
|
@@ -27274,7 +27633,7 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
|
|
|
27274
27633
|
if (!confirmed)
|
|
27275
27634
|
return { ok: false, error: "confirmed: true required" };
|
|
27276
27635
|
const file = path14.join(workspaceRoot, ".gitignore");
|
|
27277
|
-
const existing =
|
|
27636
|
+
const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
|
|
27278
27637
|
const existingLines = new Set(existing.split(`
|
|
27279
27638
|
`).map((l) => l.trim()).filter(Boolean));
|
|
27280
27639
|
const added = [];
|
|
@@ -27293,19 +27652,19 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
|
|
|
27293
27652
|
` : "") + append.join(`
|
|
27294
27653
|
`) + `
|
|
27295
27654
|
`, "utf8");
|
|
27296
|
-
} else if (!
|
|
27655
|
+
} else if (!existsSync8(file)) {
|
|
27297
27656
|
writeFileSync4(file, "", "utf8");
|
|
27298
27657
|
}
|
|
27299
27658
|
return { ok: true, path: file, added };
|
|
27300
27659
|
};
|
|
27301
27660
|
|
|
27302
27661
|
// packages/workit-core/src/core/hygiene.ts
|
|
27303
|
-
import { existsSync as
|
|
27662
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27304
27663
|
import path15 from "node:path";
|
|
27305
27664
|
var repoRoot = assetRoot();
|
|
27306
27665
|
var templatesDir = () => path15.join(repoRoot, "templates", "hygiene");
|
|
27307
27666
|
var packageJson = (root) => {
|
|
27308
|
-
if (!
|
|
27667
|
+
if (!existsSync9(path15.join(root, "package.json")))
|
|
27309
27668
|
return null;
|
|
27310
27669
|
try {
|
|
27311
27670
|
return JSON.parse(readFileSync8(path15.join(root, "package.json"), "utf8"));
|
|
@@ -27314,7 +27673,7 @@ var packageJson = (root) => {
|
|
|
27314
27673
|
}
|
|
27315
27674
|
};
|
|
27316
27675
|
var isOpenSource = (root) => {
|
|
27317
|
-
if (
|
|
27676
|
+
if (existsSync9(path15.join(root, "LICENSE")))
|
|
27318
27677
|
return true;
|
|
27319
27678
|
const pkg = packageJson(root);
|
|
27320
27679
|
if (pkg && !pkg.private)
|
|
@@ -27342,10 +27701,10 @@ var planHygieneFiles = (root, opts = {}) => {
|
|
|
27342
27701
|
files.push("LICENSE", "CONTRIBUTING.md");
|
|
27343
27702
|
const planned = [];
|
|
27344
27703
|
for (const file of files) {
|
|
27345
|
-
if (
|
|
27704
|
+
if (existsSync9(path15.join(root, file)))
|
|
27346
27705
|
continue;
|
|
27347
27706
|
const tpl = path15.join(templatesDir(), file);
|
|
27348
|
-
if (!
|
|
27707
|
+
if (!existsSync9(tpl))
|
|
27349
27708
|
continue;
|
|
27350
27709
|
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
27710
|
planned.push({ path: path15.join(root, file), content });
|
|
@@ -27365,7 +27724,7 @@ var ensureHygieneFiles = (root, opts) => {
|
|
|
27365
27724
|
|
|
27366
27725
|
// packages/workit-core/src/core/repo-context.ts
|
|
27367
27726
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
27368
|
-
import { existsSync as
|
|
27727
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
|
|
27369
27728
|
import path16 from "node:path";
|
|
27370
27729
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
27371
27730
|
try {
|
|
@@ -27532,7 +27891,7 @@ var diffStatForRange = (cwd, range) => {
|
|
|
27532
27891
|
};
|
|
27533
27892
|
var packageScripts = (cwd, keys) => {
|
|
27534
27893
|
const pkgPath = path16.join(cwd, "package.json");
|
|
27535
|
-
if (!
|
|
27894
|
+
if (!existsSync10(pkgPath))
|
|
27536
27895
|
return [];
|
|
27537
27896
|
try {
|
|
27538
27897
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
@@ -27567,7 +27926,7 @@ var documentationFiles = (cwd) => {
|
|
|
27567
27926
|
return matches.sort().slice(0, 200);
|
|
27568
27927
|
};
|
|
27569
27928
|
var readTrimmed = (file, maxLines) => {
|
|
27570
|
-
if (!
|
|
27929
|
+
if (!existsSync10(file))
|
|
27571
27930
|
return "";
|
|
27572
27931
|
const lines = readFileSync9(file, "utf8").split(`
|
|
27573
27932
|
`);
|
|
@@ -27643,7 +28002,7 @@ ${status.stderr}` : "");
|
|
|
27643
28002
|
stdout += `
|
|
27644
28003
|
`;
|
|
27645
28004
|
stdout += printSection("Recent Validation Signals");
|
|
27646
|
-
if (
|
|
28005
|
+
if (existsSync10(path16.join(cwd, "package.json"))) {
|
|
27647
28006
|
stdout += `package.json detected. Common scripts:
|
|
27648
28007
|
`;
|
|
27649
28008
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -27652,7 +28011,7 @@ ${status.stderr}` : "");
|
|
|
27652
28011
|
`) + `
|
|
27653
28012
|
`;
|
|
27654
28013
|
}
|
|
27655
|
-
if (
|
|
28014
|
+
if (existsSync10(path16.join(cwd, "Cargo.toml")) || existsSync10(path16.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
27656
28015
|
stdout += `Rust project detected.
|
|
27657
28016
|
`;
|
|
27658
28017
|
}
|
|
@@ -27704,7 +28063,7 @@ function changelogContext(root, range) {
|
|
|
27704
28063
|
`;
|
|
27705
28064
|
stdout += printSection("Existing CHANGELOG.md");
|
|
27706
28065
|
const changelogPath = path16.join(cwd, "CHANGELOG.md");
|
|
27707
|
-
if (
|
|
28066
|
+
if (existsSync10(changelogPath)) {
|
|
27708
28067
|
stdout += readTrimmed(changelogPath, 260) + `
|
|
27709
28068
|
`;
|
|
27710
28069
|
} else {
|
|
@@ -27746,7 +28105,7 @@ function docsRefreshContext(root, range) {
|
|
|
27746
28105
|
`;
|
|
27747
28106
|
stdout += printSection("README Preview");
|
|
27748
28107
|
const readme = path16.join(cwd, "README.md");
|
|
27749
|
-
if (
|
|
28108
|
+
if (existsSync10(readme)) {
|
|
27750
28109
|
stdout += readTrimmed(readme, 220) + `
|
|
27751
28110
|
`;
|
|
27752
28111
|
} else {
|
|
@@ -27755,7 +28114,7 @@ function docsRefreshContext(root, range) {
|
|
|
27755
28114
|
}
|
|
27756
28115
|
stdout += printSection("Package Scripts");
|
|
27757
28116
|
const pkgPath = path16.join(cwd, "package.json");
|
|
27758
|
-
if (
|
|
28117
|
+
if (existsSync10(pkgPath)) {
|
|
27759
28118
|
try {
|
|
27760
28119
|
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
27761
28120
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
@@ -27806,7 +28165,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
27806
28165
|
`;
|
|
27807
28166
|
stdout += printSection("Existing Release Files");
|
|
27808
28167
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
27809
|
-
if (
|
|
28168
|
+
if (existsSync10(path16.join(cwd, rel)))
|
|
27810
28169
|
stdout += rel + `
|
|
27811
28170
|
`;
|
|
27812
28171
|
}
|
|
@@ -27835,7 +28194,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
27835
28194
|
|
|
27836
28195
|
// packages/workit-core/src/core/verify-project.ts
|
|
27837
28196
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
27838
|
-
import { existsSync as
|
|
28197
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
|
|
27839
28198
|
import path17 from "node:path";
|
|
27840
28199
|
var commandOnPath = (name) => {
|
|
27841
28200
|
const dirs = (process.env.PATH ?? "").split(path17.delimiter);
|
|
@@ -27860,7 +28219,7 @@ var commandOnPath = (name) => {
|
|
|
27860
28219
|
};
|
|
27861
28220
|
var hasScript = (cwd, name) => {
|
|
27862
28221
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27863
|
-
if (!
|
|
28222
|
+
if (!existsSync11(pkgPath))
|
|
27864
28223
|
return false;
|
|
27865
28224
|
try {
|
|
27866
28225
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
@@ -27870,10 +28229,10 @@ var hasScript = (cwd, name) => {
|
|
|
27870
28229
|
}
|
|
27871
28230
|
};
|
|
27872
28231
|
var packageRunner = (cwd) => {
|
|
27873
|
-
if (
|
|
28232
|
+
if (existsSync11(path17.join(cwd, "pnpm-lock.yaml")))
|
|
27874
28233
|
return "pnpm";
|
|
27875
28234
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27876
|
-
if (
|
|
28235
|
+
if (existsSync11(pkgPath)) {
|
|
27877
28236
|
try {
|
|
27878
28237
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
27879
28238
|
if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
|
|
@@ -27881,7 +28240,7 @@ var packageRunner = (cwd) => {
|
|
|
27881
28240
|
}
|
|
27882
28241
|
} catch {}
|
|
27883
28242
|
}
|
|
27884
|
-
if (
|
|
28243
|
+
if (existsSync11(path17.join(cwd, "yarn.lock")))
|
|
27885
28244
|
return "yarn";
|
|
27886
28245
|
return "npm";
|
|
27887
28246
|
};
|
|
@@ -27942,7 +28301,7 @@ root: ${cwd}
|
|
|
27942
28301
|
lines.push(`dry_run: ${String(dryRun)}
|
|
27943
28302
|
`);
|
|
27944
28303
|
const pkgPath = path17.join(cwd, "package.json");
|
|
27945
|
-
if (
|
|
28304
|
+
if (existsSync11(pkgPath)) {
|
|
27946
28305
|
const runner = packageRunner(cwd);
|
|
27947
28306
|
for (const script of ["lint", "format:check", "test", "build"]) {
|
|
27948
28307
|
if (hasScript(cwd, script)) {
|
|
@@ -27957,7 +28316,7 @@ root: ${cwd}
|
|
|
27957
28316
|
} else {
|
|
27958
28317
|
skipCheck("javascript", "package.json not found");
|
|
27959
28318
|
}
|
|
27960
|
-
const cargoManifest =
|
|
28319
|
+
const cargoManifest = existsSync11(path17.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync11(path17.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
|
|
27961
28320
|
if (cargoManifest) {
|
|
27962
28321
|
runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
|
|
27963
28322
|
runCheck("cargo clippy", "cargo", [
|
|
@@ -27973,7 +28332,7 @@ root: ${cwd}
|
|
|
27973
28332
|
} else {
|
|
27974
28333
|
skipCheck("rust", "Cargo.toml not found");
|
|
27975
28334
|
}
|
|
27976
|
-
if (
|
|
28335
|
+
if (existsSync11(path17.join(cwd, "pyproject.toml")) || existsSync11(path17.join(cwd, "pytest.ini")) || existsSync11(path17.join(cwd, "tests"))) {
|
|
27977
28336
|
if (commandOnPath("pytest"))
|
|
27978
28337
|
runCheck("pytest", "pytest", []);
|
|
27979
28338
|
else
|
|
@@ -27995,7 +28354,7 @@ root: ${cwd}
|
|
|
27995
28354
|
lines.push(`status: skipped (dry run)
|
|
27996
28355
|
`);
|
|
27997
28356
|
skipped += 1;
|
|
27998
|
-
} else if (
|
|
28357
|
+
} else if (existsSync11(changelogPath)) {
|
|
27999
28358
|
if (/## \[Unreleased\]/i.test(readFileSync10(changelogPath, "utf8"))) {
|
|
28000
28359
|
lines.push(`status: pass
|
|
28001
28360
|
`);
|
|
@@ -28330,7 +28689,7 @@ function writeFileExclusive(file, content, mode) {
|
|
|
28330
28689
|
import {
|
|
28331
28690
|
copyFileSync as copyFileSync2,
|
|
28332
28691
|
cpSync as cpSync2,
|
|
28333
|
-
existsSync as
|
|
28692
|
+
existsSync as existsSync12,
|
|
28334
28693
|
lstatSync as lstatSync4,
|
|
28335
28694
|
mkdirSync as mkdirSync5,
|
|
28336
28695
|
mkdtempSync,
|
|
@@ -29871,7 +30230,7 @@ function createRepoTools(runtime = defaultRuntime) {
|
|
|
29871
30230
|
return output(legacyScriptResult({
|
|
29872
30231
|
stdout: JSON.stringify(result),
|
|
29873
30232
|
stderr: "",
|
|
29874
|
-
exitCode:
|
|
30233
|
+
exitCode: "error" in result ? 1 : 0,
|
|
29875
30234
|
cwd: context.directory
|
|
29876
30235
|
}));
|
|
29877
30236
|
}
|
|
@@ -30519,6 +30878,101 @@ var safePath = (root, value) => {
|
|
|
30519
30878
|
return null;
|
|
30520
30879
|
}
|
|
30521
30880
|
};
|
|
30881
|
+
var branchCreationBaseline = (root, target, base) => {
|
|
30882
|
+
const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
|
|
30883
|
+
if (exists)
|
|
30884
|
+
return { base_branch: base, target_exists: true, remote_base: null };
|
|
30885
|
+
const remote = run4(root, ["ls-remote", "origin", `refs/heads/${base}`]);
|
|
30886
|
+
const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
|
|
30887
|
+
return {
|
|
30888
|
+
base_branch: base,
|
|
30889
|
+
target_exists: false,
|
|
30890
|
+
remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
|
|
30891
|
+
};
|
|
30892
|
+
};
|
|
30893
|
+
var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
|
|
30894
|
+
var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
|
|
30895
|
+
var actionProposalQuestion = (request, descriptorPayload) => {
|
|
30896
|
+
const payload = descriptorPayload ?? {};
|
|
30897
|
+
const resolved = payload.resolved ?? {};
|
|
30898
|
+
const payloadTarget = String(payload.target_branch ?? "");
|
|
30899
|
+
if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
|
|
30900
|
+
const steps = payload.plan_steps;
|
|
30901
|
+
return {
|
|
30902
|
+
presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
|
|
30903
|
+
approvedText: `Approve the ${steps.length} listed plan commits.`
|
|
30904
|
+
};
|
|
30905
|
+
}
|
|
30906
|
+
switch (request.operation) {
|
|
30907
|
+
case "git.branch_setup": {
|
|
30908
|
+
if (request.payload.action === "reapply_stash")
|
|
30909
|
+
return {
|
|
30910
|
+
presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
|
|
30911
|
+
approvedText: "Reapply the recorded stash."
|
|
30912
|
+
};
|
|
30913
|
+
const target = String(payload.target_branch ?? "");
|
|
30914
|
+
const stashes = payload.stash === "yes" && resolved.dirty === true;
|
|
30915
|
+
if (resolved.target_exists === true)
|
|
30916
|
+
return stashes ? {
|
|
30917
|
+
presented: `Workit decision: action — Stash the dirty working tree and switch to branch \`${target}\`?`,
|
|
30918
|
+
approvedText: `Stash and switch to \`${target}\`.`
|
|
30919
|
+
} : {
|
|
30920
|
+
presented: `Workit decision: action — Switch to branch \`${target}\`?`,
|
|
30921
|
+
approvedText: `Switch to \`${target}\`.`
|
|
30922
|
+
};
|
|
30923
|
+
const base = String(resolved.base_branch ?? "the base branch");
|
|
30924
|
+
return stashes ? {
|
|
30925
|
+
presented: `Workit decision: action — Stash the dirty working tree and create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
|
|
30926
|
+
approvedText: `Stash and create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
|
|
30927
|
+
} : {
|
|
30928
|
+
presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
|
|
30929
|
+
approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
|
|
30930
|
+
};
|
|
30931
|
+
}
|
|
30932
|
+
case "git.commit": {
|
|
30933
|
+
const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
|
|
30934
|
+
return {
|
|
30935
|
+
presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
|
|
30936
|
+
approvedText: `Commit as "${String(request.payload.message)}".`
|
|
30937
|
+
};
|
|
30938
|
+
}
|
|
30939
|
+
case "git.push":
|
|
30940
|
+
return {
|
|
30941
|
+
presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
|
|
30942
|
+
approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
|
|
30943
|
+
};
|
|
30944
|
+
case "hosting.pull_request":
|
|
30945
|
+
return {
|
|
30946
|
+
presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
|
|
30947
|
+
approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
|
|
30948
|
+
};
|
|
30949
|
+
case "changelog.apply":
|
|
30950
|
+
return {
|
|
30951
|
+
presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
|
|
30952
|
+
approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
|
|
30953
|
+
};
|
|
30954
|
+
case "youtrack.update":
|
|
30955
|
+
return {
|
|
30956
|
+
presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
|
|
30957
|
+
approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
|
|
30958
|
+
};
|
|
30959
|
+
case "youtrack.time":
|
|
30960
|
+
return {
|
|
30961
|
+
presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
|
|
30962
|
+
approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
|
|
30963
|
+
};
|
|
30964
|
+
case "youtrack.meeting":
|
|
30965
|
+
return {
|
|
30966
|
+
presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
|
|
30967
|
+
approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
|
|
30968
|
+
};
|
|
30969
|
+
default:
|
|
30970
|
+
return {
|
|
30971
|
+
presented: `Workit decision: action — Approve ${request.operation}?`,
|
|
30972
|
+
approvedText: `Approve ${request.operation}.`
|
|
30973
|
+
};
|
|
30974
|
+
}
|
|
30975
|
+
};
|
|
30522
30976
|
var approvedResolvedExternalAction = (content) => {
|
|
30523
30977
|
try {
|
|
30524
30978
|
const value = JSON.parse(content);
|
|
@@ -30782,24 +31236,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30782
31236
|
const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
|
|
30783
31237
|
if (!sdd_dir)
|
|
30784
31238
|
return failure("invalid_input", "sdd_dir must stay inside the workspace");
|
|
31239
|
+
const action = request.payload.action ?? "setup";
|
|
30785
31240
|
const policy = resolveBranchPolicyFor(root);
|
|
30786
|
-
const target_branch = request.payload.target_branch
|
|
31241
|
+
const target_branch = request.payload.target_branch;
|
|
31242
|
+
if (action !== "reapply_stash" && !target_branch)
|
|
31243
|
+
return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
|
|
31244
|
+
outcome: "not_started",
|
|
31245
|
+
fields: [{ path: "target_branch", reason: "required for action=setup" }]
|
|
31246
|
+
});
|
|
30787
31247
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
30788
31248
|
if (!head)
|
|
30789
31249
|
return failure("storage_error", "current Git commit could not be resolved");
|
|
31250
|
+
const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
|
|
31251
|
+
const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
|
|
30790
31252
|
const normalized = {
|
|
30791
31253
|
operation: request.operation,
|
|
30792
31254
|
payload: {
|
|
30793
31255
|
...request.payload,
|
|
30794
|
-
action
|
|
31256
|
+
action,
|
|
30795
31257
|
sdd_dir,
|
|
30796
31258
|
stash: request.payload.stash ?? "no",
|
|
30797
|
-
target_branch
|
|
31259
|
+
...target_branch ? { target_branch } : {}
|
|
30798
31260
|
}
|
|
30799
31261
|
};
|
|
30800
31262
|
return success(null, null, {
|
|
30801
31263
|
request: normalized,
|
|
30802
|
-
descriptorPayload: {
|
|
31264
|
+
descriptorPayload: {
|
|
31265
|
+
...normalized.payload,
|
|
31266
|
+
resolved: { head, dirty, ...creation }
|
|
31267
|
+
}
|
|
30803
31268
|
});
|
|
30804
31269
|
}
|
|
30805
31270
|
case "git.push": {
|
|
@@ -30813,7 +31278,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30813
31278
|
}) : failure("invalid_input", "git push requires a resolvable current branch");
|
|
30814
31279
|
}
|
|
30815
31280
|
case "git.commit": {
|
|
31281
|
+
const planSteps = request.payload.plan_steps;
|
|
31282
|
+
const planBranch = request.payload.plan_branch;
|
|
30816
31283
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
31284
|
+
if (!head)
|
|
31285
|
+
return failure("storage_error", "Git state could not be resolved");
|
|
31286
|
+
const branch = gitValue(root, ["branch", "--show-current"]);
|
|
31287
|
+
if (planSteps !== undefined || planBranch !== undefined) {
|
|
31288
|
+
if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
|
|
31289
|
+
return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
|
|
31290
|
+
outcome: "not_started",
|
|
31291
|
+
fields: [{ path: "plan_steps", reason: "unique strings" }]
|
|
31292
|
+
});
|
|
31293
|
+
if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
|
|
31294
|
+
return failure("invalid_input", "plan_branch must name the current working branch", {
|
|
31295
|
+
outcome: "not_started",
|
|
31296
|
+
fields: [{ path: "plan_branch", reason: "current branch" }]
|
|
31297
|
+
});
|
|
31298
|
+
if (isProtectedBranch2(root, planBranch))
|
|
31299
|
+
return failure("invalid_input", "plan_branch must not be a protected branch", {
|
|
31300
|
+
outcome: "not_started",
|
|
31301
|
+
fields: [{ path: "plan_branch", reason: "protected branch" }]
|
|
31302
|
+
});
|
|
31303
|
+
return success(null, null, {
|
|
31304
|
+
request,
|
|
31305
|
+
descriptorPayload: {
|
|
31306
|
+
plan_steps: planSteps,
|
|
31307
|
+
plan_branch: planBranch,
|
|
31308
|
+
resolved: { head, branch, steps: planSteps }
|
|
31309
|
+
}
|
|
31310
|
+
});
|
|
31311
|
+
}
|
|
30817
31312
|
const staged = gitRaw(root, [
|
|
30818
31313
|
"diff",
|
|
30819
31314
|
"--cached",
|
|
@@ -30824,7 +31319,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30824
31319
|
"HEAD"
|
|
30825
31320
|
]);
|
|
30826
31321
|
const paths = stagedPaths(root);
|
|
30827
|
-
|
|
31322
|
+
const message = request.payload.message;
|
|
31323
|
+
if (typeof message !== "string" || !message)
|
|
31324
|
+
return failure("invalid_input", "git.commit requires a commit message", {
|
|
31325
|
+
outcome: "not_started",
|
|
31326
|
+
fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
|
|
31327
|
+
});
|
|
31328
|
+
if (staged === null || paths === null)
|
|
30828
31329
|
return failure("storage_error", "Git state could not be resolved");
|
|
30829
31330
|
let policy;
|
|
30830
31331
|
try {
|
|
@@ -30839,13 +31340,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
30839
31340
|
`).filter(Boolean));
|
|
30840
31341
|
flavor = detected.flavor ?? "conventional";
|
|
30841
31342
|
}
|
|
30842
|
-
if (!matchCommitFlavor(
|
|
31343
|
+
if (!matchCommitFlavor(message, flavor, policy.pattern))
|
|
30843
31344
|
return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
|
|
30844
31345
|
return success(null, null, {
|
|
30845
31346
|
request,
|
|
30846
31347
|
descriptorPayload: {
|
|
30847
31348
|
...request.payload,
|
|
30848
|
-
resolved: { head, staged: sha256(staged), paths }
|
|
31349
|
+
resolved: { head, branch, staged: sha256(staged), paths }
|
|
30849
31350
|
}
|
|
30850
31351
|
});
|
|
30851
31352
|
}
|
|
@@ -31015,10 +31516,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
31015
31516
|
sdd_dir: request.payload.sdd_dir ?? "docs",
|
|
31016
31517
|
workspace_root: root
|
|
31017
31518
|
});
|
|
31018
|
-
|
|
31519
|
+
if ("error" in result) {
|
|
31520
|
+
if (result.phase === "preflight")
|
|
31521
|
+
return failure("invalid_input", result.error, {
|
|
31522
|
+
outcome: "not_started",
|
|
31523
|
+
operation: request.operation
|
|
31524
|
+
});
|
|
31525
|
+
return unknown3(request.operation);
|
|
31526
|
+
}
|
|
31527
|
+
return success(null, null, result);
|
|
31019
31528
|
}
|
|
31020
31529
|
case "git.commit": {
|
|
31021
|
-
const
|
|
31530
|
+
const message = request.payload.message;
|
|
31531
|
+
if (typeof message !== "string" || !message)
|
|
31532
|
+
return failure("invalid_input", "git.commit requires a commit message", {
|
|
31533
|
+
outcome: "not_started",
|
|
31534
|
+
fields: [{ path: "message", reason: "required" }]
|
|
31535
|
+
});
|
|
31536
|
+
const result = run4(root, ["commit", "-m", message]);
|
|
31022
31537
|
return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown3(request.operation);
|
|
31023
31538
|
}
|
|
31024
31539
|
case "git.push": {
|
|
@@ -31128,8 +31643,15 @@ var sessionParent = (session) => {
|
|
|
31128
31643
|
const parentID = session.parentID;
|
|
31129
31644
|
return typeof parentID === "string" && parentID.length > 0 ? parentID : null;
|
|
31130
31645
|
};
|
|
31131
|
-
var freshMs = 5 * 60 * 1000;
|
|
31132
31646
|
var rejectedDescription = "Reject this decision";
|
|
31647
|
+
var isSelfAuthorizingActionContent = (content) => {
|
|
31648
|
+
try {
|
|
31649
|
+
const value = JSON.parse(content);
|
|
31650
|
+
return typeof value.operation === "string" && value.operation.length > 0;
|
|
31651
|
+
} catch {
|
|
31652
|
+
return /^[a-z][a-z_]*\.[a-z_]+$/.test(content);
|
|
31653
|
+
}
|
|
31654
|
+
};
|
|
31133
31655
|
var decisionContent = (purpose, question, approvedContent) => ({
|
|
31134
31656
|
header: `Workit decision: ${purpose}`,
|
|
31135
31657
|
question,
|
|
@@ -31235,15 +31757,31 @@ class NativeReceiptStore {
|
|
|
31235
31757
|
queue.splice(i, 1);
|
|
31236
31758
|
if (!queue.length)
|
|
31237
31759
|
this.#receipts.delete(sessionID);
|
|
31238
|
-
if (this.#now() - receipt.recordedAt > freshMs)
|
|
31239
|
-
return { ok: false, error: "permission_denied: native question receipt is stale" };
|
|
31240
31760
|
const observation = { receipt };
|
|
31241
31761
|
this.#observations.add(observation);
|
|
31242
31762
|
return { ok: true, receipt, observation };
|
|
31243
31763
|
}
|
|
31244
31764
|
return {
|
|
31245
31765
|
ok: false,
|
|
31246
|
-
error:
|
|
31766
|
+
error: (() => {
|
|
31767
|
+
const samePurpose = queue.filter((receipt) => receipt.purpose === purpose);
|
|
31768
|
+
const base = "permission_denied: no matching native question receipt for this purpose";
|
|
31769
|
+
if (samePurpose.length) {
|
|
31770
|
+
const closest = samePurpose[samePurpose.length - 1];
|
|
31771
|
+
const reasons = [];
|
|
31772
|
+
if (expected.selectedLabel !== undefined && normalizeReceiptLabel(closest.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel))
|
|
31773
|
+
reasons.push(`selected answer was "${closest.selectedLabel}"`);
|
|
31774
|
+
if (expected.selectedDescription !== undefined && closest.selectedDescription !== expected.selectedDescription)
|
|
31775
|
+
reasons.push("approved option description did not match");
|
|
31776
|
+
if (expected.decisionPurpose !== undefined && closest.decisionPurpose !== expected.decisionPurpose)
|
|
31777
|
+
reasons.push(`purpose was "${closest.decisionPurpose}"`);
|
|
31778
|
+
if (expected.contentDigest !== undefined && closest.contentDigest !== expected.contentDigest)
|
|
31779
|
+
reasons.push("question or approved content differed from the asked receipt");
|
|
31780
|
+
if (reasons.length)
|
|
31781
|
+
return `${base} (closest ${purpose} receipt: ${reasons.join("; ")})`;
|
|
31782
|
+
}
|
|
31783
|
+
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`;
|
|
31784
|
+
})()
|
|
31247
31785
|
};
|
|
31248
31786
|
}
|
|
31249
31787
|
verify(observation, sessionID, purpose) {
|
|
@@ -31345,6 +31883,13 @@ var opencodeCapabilities = () => [
|
|
|
31345
31883
|
reason: "nested native task launches are denied and observed child sessions are parent-bound",
|
|
31346
31884
|
refs: [hostRef("task")]
|
|
31347
31885
|
},
|
|
31886
|
+
{
|
|
31887
|
+
name: "fresh-context-review",
|
|
31888
|
+
surface: "task",
|
|
31889
|
+
assurance: "agent_guided",
|
|
31890
|
+
reason: "independent review runs as a native direct-child session; evidence evaluation enforces creator and duplicate-reviewer exclusion",
|
|
31891
|
+
refs: [hostRef("task")]
|
|
31892
|
+
},
|
|
31348
31893
|
{
|
|
31349
31894
|
name: "known_product_writes",
|
|
31350
31895
|
surface: "write/edit/bash",
|
|
@@ -31363,7 +31908,8 @@ var opencodeCapabilities = () => [
|
|
|
31363
31908
|
var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
|
|
31364
31909
|
verifyDecision: ({ observation, expected, caller }) => {
|
|
31365
31910
|
const receipt = receipts.verify(observation, actor, "decision");
|
|
31366
|
-
|
|
31911
|
+
const expectedApproved = typeof expected.binding.displayed === "string" ? expected.binding.displayed : expected.binding.approvedContent;
|
|
31912
|
+
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
31913
|
return failure("permission_denied", "native decision receipt is not bound to this session");
|
|
31368
31914
|
return success(null, null, {
|
|
31369
31915
|
kind: "host_observed",
|
|
@@ -31438,8 +31984,34 @@ var nativeDispatchFor = (directChildren, actor, generation) => ({
|
|
|
31438
31984
|
var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
|
|
31439
31985
|
const store = new TaskStore(root);
|
|
31440
31986
|
const selected = approvedExternalAction(store, "opencode", actor, operation);
|
|
31441
|
-
if (!selected.ok)
|
|
31987
|
+
if (!selected.ok) {
|
|
31988
|
+
const plan = planCommitBinding(store, "opencode", actor, operation);
|
|
31989
|
+
if (plan) {
|
|
31990
|
+
const actionRef = externalActionRef("opencode", actor, operation);
|
|
31991
|
+
return {
|
|
31992
|
+
taskId: plan.taskId,
|
|
31993
|
+
decisionId: plan.decisionId,
|
|
31994
|
+
actionRef,
|
|
31995
|
+
expectedRevision: plan.expectedRevision,
|
|
31996
|
+
expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
|
|
31997
|
+
binding: plan.binding,
|
|
31998
|
+
step: plan.step,
|
|
31999
|
+
refresh: () => {
|
|
32000
|
+
const task = store.readTask(plan.taskId);
|
|
32001
|
+
const freshWorkspace = store.readWorkspace();
|
|
32002
|
+
if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
|
|
32003
|
+
throw new Error("external action state changed");
|
|
32004
|
+
return {
|
|
32005
|
+
expectedRevision: task.data.revision,
|
|
32006
|
+
expectedWorkspaceRevision: freshWorkspace.data.revision
|
|
32007
|
+
};
|
|
32008
|
+
},
|
|
32009
|
+
reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
|
|
32010
|
+
settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
|
|
32011
|
+
};
|
|
32012
|
+
}
|
|
31442
32013
|
return selected;
|
|
32014
|
+
}
|
|
31443
32015
|
const actionRef = externalActionRef("opencode", actor, operation);
|
|
31444
32016
|
return {
|
|
31445
32017
|
taskId: selected.data.task.id,
|
|
@@ -31485,6 +32057,7 @@ var createWorkitTools = ({
|
|
|
31485
32057
|
receipts = new NativeReceiptStore,
|
|
31486
32058
|
directChildren = new Map
|
|
31487
32059
|
} = {}) => {
|
|
32060
|
+
const actionProposals = new Map;
|
|
31488
32061
|
const make = (family) => tool({
|
|
31489
32062
|
description: `Workit ${family} operations backed by the shared task contract.`,
|
|
31490
32063
|
args: operationShapeFor(family),
|
|
@@ -31518,6 +32091,15 @@ var createWorkitTools = ({
|
|
|
31518
32091
|
let result;
|
|
31519
32092
|
if (family === "decision" && args.action === "record") {
|
|
31520
32093
|
const decision = parsed.data;
|
|
32094
|
+
if (decision.response === "stated") {
|
|
32095
|
+
result = core.observeDecision(parsed.data, undefined);
|
|
32096
|
+
return output2(result);
|
|
32097
|
+
}
|
|
32098
|
+
const budgetIssue = workitBindingQuestionIssue([
|
|
32099
|
+
decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent)
|
|
32100
|
+
]);
|
|
32101
|
+
if (budgetIssue)
|
|
32102
|
+
return output2(failure("invalid_input", budgetIssue));
|
|
31521
32103
|
const expectation = {
|
|
31522
32104
|
selectedLabel: decision.response,
|
|
31523
32105
|
decisionPurpose: decision.purpose,
|
|
@@ -31529,7 +32111,37 @@ var createWorkitTools = ({
|
|
|
31529
32111
|
const observed = receipts.consume(context.sessionID, "decision", expectation);
|
|
31530
32112
|
if (!observed.ok)
|
|
31531
32113
|
return output2(failure("permission_denied", observed.error));
|
|
31532
|
-
|
|
32114
|
+
let recordInput = parsed.data;
|
|
32115
|
+
if (decision.purpose === "action" && decision.response === "approved") {
|
|
32116
|
+
const queue = actionProposals.get(context.sessionID) ?? [];
|
|
32117
|
+
const textMatches = queue.filter((pending) => pending.presented === decision.binding.presented && pending.approvedText === decision.binding.approvedContent);
|
|
32118
|
+
if (textMatches.length > 1 && new Set(textMatches.map((pending) => pending.descriptor)).size > 1)
|
|
32119
|
+
return output2(failure("invalid_input", "multiple action proposals match this approval; resolve the action again"));
|
|
32120
|
+
let bound = false;
|
|
32121
|
+
const pending = textMatches[0];
|
|
32122
|
+
if (pending) {
|
|
32123
|
+
const fresh = resolveExternalActionRequest(context.directory, pending.request);
|
|
32124
|
+
const freshDescriptor = fresh.ok ? externalActionDescriptor(pending.request.operation, fresh.data.descriptorPayload) : null;
|
|
32125
|
+
if (freshDescriptor === pending.descriptor) {
|
|
32126
|
+
actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
|
|
32127
|
+
recordInput = {
|
|
32128
|
+
...recordInput,
|
|
32129
|
+
binding: {
|
|
32130
|
+
...recordInput.binding,
|
|
32131
|
+
approvedContent: pending.descriptor,
|
|
32132
|
+
displayed: pending.approvedText
|
|
32133
|
+
}
|
|
32134
|
+
};
|
|
32135
|
+
bound = true;
|
|
32136
|
+
} else if (fresh.ok) {
|
|
32137
|
+
actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
|
|
32138
|
+
}
|
|
32139
|
+
}
|
|
32140
|
+
if (!bound && !isSelfAuthorizingActionContent(decision.binding.approvedContent)) {
|
|
32141
|
+
return output2(failure("invalid_input", textMatches.length > 0 ? "repository state changed since the proposal; resolve the action again for a fresh proposal" : "no matching action proposal; resolve the action through the action tool first"));
|
|
32142
|
+
}
|
|
32143
|
+
}
|
|
32144
|
+
result = core.observeDecision(recordInput, observed.observation);
|
|
31533
32145
|
} else {
|
|
31534
32146
|
const run = core[family];
|
|
31535
32147
|
result = run.call(core, parsed.data);
|
|
@@ -31565,9 +32177,17 @@ var createWorkitTools = ({
|
|
|
31565
32177
|
if (earlySession !== null && sessionParent(earlySession) !== undefined)
|
|
31566
32178
|
return output2(failure("permission_denied", "child sessions cannot run external actions"));
|
|
31567
32179
|
}
|
|
31568
|
-
|
|
32180
|
+
let resolved = resolveExternalActionRequest(context.directory, parsed.data);
|
|
31569
32181
|
if (!resolved.ok)
|
|
31570
32182
|
return output2(resolved);
|
|
32183
|
+
if (resolved.ok && resolved.data.request.operation === "git.branch_setup" && resolved.data.descriptorPayload.resolved?.dirty === true && resolved.data.descriptorPayload.stash !== "yes") {
|
|
32184
|
+
const upgraded = resolveExternalActionRequest(context.directory, {
|
|
32185
|
+
...parsed.data,
|
|
32186
|
+
payload: { ...parsed.data.payload, stash: "yes" }
|
|
32187
|
+
});
|
|
32188
|
+
if (upgraded.ok)
|
|
32189
|
+
resolved = upgraded;
|
|
32190
|
+
}
|
|
31571
32191
|
if (resolved.data.request.operation === "context.read")
|
|
31572
32192
|
return output2(await executeResolvedExternalAction(resolved.data, context.directory));
|
|
31573
32193
|
if (!client)
|
|
@@ -31630,6 +32250,68 @@ var createWorkitTools = ({
|
|
|
31630
32250
|
return output2(failure("external_outcome_unknown", "previous external action outcome is unknown"));
|
|
31631
32251
|
}
|
|
31632
32252
|
const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
|
|
32253
|
+
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);
|
|
32254
|
+
if (!drift.ok)
|
|
32255
|
+
return output2(drift);
|
|
32256
|
+
const localOperation = [
|
|
32257
|
+
"git.branch_setup",
|
|
32258
|
+
"git.commit",
|
|
32259
|
+
"git.push",
|
|
32260
|
+
"hosting.pull_request",
|
|
32261
|
+
"changelog.apply"
|
|
32262
|
+
].includes(resolved.data.request.operation);
|
|
32263
|
+
if (localOperation) {
|
|
32264
|
+
const writer = assertLocalExternalActionWriter(context.directory, {
|
|
32265
|
+
host: "opencode",
|
|
32266
|
+
actor: context.sessionID
|
|
32267
|
+
});
|
|
32268
|
+
if (!writer.ok)
|
|
32269
|
+
return output2(failure("needs_input", "writer ownership is required before this action", {
|
|
32270
|
+
outcome: "not_started",
|
|
32271
|
+
operation: resolved.data.request.operation,
|
|
32272
|
+
guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
|
|
32273
|
+
}));
|
|
32274
|
+
}
|
|
32275
|
+
const selected = approvedExternalAction(store, "opencode", context.sessionID, descriptor);
|
|
32276
|
+
let planAuthorized = false;
|
|
32277
|
+
if (!selected.ok) {
|
|
32278
|
+
const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
|
|
32279
|
+
const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
|
|
32280
|
+
if (typeof commitMessage === "string" && commitMessage) {
|
|
32281
|
+
const plan = approvedPlanCommit(store, "opencode", context.sessionID, commitMessage);
|
|
32282
|
+
planAuthorized = plan.ok && plan.data.branch === currentBranch;
|
|
32283
|
+
}
|
|
32284
|
+
}
|
|
32285
|
+
if (!selected.ok && !planAuthorized) {
|
|
32286
|
+
if (selected.error.startsWith("no approved action")) {
|
|
32287
|
+
const proposal = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
|
|
32288
|
+
const pending = actionProposals.get(context.sessionID) ?? [];
|
|
32289
|
+
const existing = pending.find((candidate) => candidate.descriptor === descriptor);
|
|
32290
|
+
const open = existing ?? {
|
|
32291
|
+
descriptor,
|
|
32292
|
+
presented: proposal.presented,
|
|
32293
|
+
approvedText: proposal.approvedText,
|
|
32294
|
+
request: resolved.data.request
|
|
32295
|
+
};
|
|
32296
|
+
if (!existing) {
|
|
32297
|
+
pending.push(open);
|
|
32298
|
+
if (pending.length > 8)
|
|
32299
|
+
pending.shift();
|
|
32300
|
+
actionProposals.set(context.sessionID, pending);
|
|
32301
|
+
}
|
|
32302
|
+
return output2(failure("needs_input", proposal.presented, {
|
|
32303
|
+
outcome: "not_started",
|
|
32304
|
+
operation: resolved.data.request.operation,
|
|
32305
|
+
proposal: {
|
|
32306
|
+
presented: proposal.presented,
|
|
32307
|
+
approvedContent: proposal.approvedText,
|
|
32308
|
+
descriptorDigest: sha256(descriptor)
|
|
32309
|
+
},
|
|
32310
|
+
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."
|
|
32311
|
+
}));
|
|
32312
|
+
}
|
|
32313
|
+
return output2(selected);
|
|
32314
|
+
}
|
|
31633
32315
|
const result = await nativeExternalActionRunner(context.directory, context.sessionID, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, context.directory, step, {
|
|
31634
32316
|
host: "opencode",
|
|
31635
32317
|
actor: context.sessionID
|
|
@@ -31729,26 +32411,14 @@ var loadProvenance = (logger, pkgUrl) => {
|
|
|
31729
32411
|
};
|
|
31730
32412
|
|
|
31731
32413
|
// packages/workit-opencode/src/plugin.ts
|
|
31732
|
-
var root =
|
|
32414
|
+
var root = fileURLToPath3(new URL("../assets/", import.meta.url));
|
|
31733
32415
|
var skillsPath = path25.join(root, "skills");
|
|
31734
32416
|
var logger = createLogger({
|
|
31735
32417
|
appLog: () => {
|
|
31736
32418
|
return;
|
|
31737
32419
|
}
|
|
31738
32420
|
});
|
|
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)));
|
|
32421
|
+
var sourceMarker = markSourcesLoaded(pluginSourceFiles);
|
|
31752
32422
|
var staleSourcesWarned = false;
|
|
31753
32423
|
var warnStaleSources = () => {
|
|
31754
32424
|
if (staleSourcesWarned)
|
|
@@ -32062,11 +32732,17 @@ var plugin = async ({ client, directory }) => {
|
|
|
32062
32732
|
warnStaleSources();
|
|
32063
32733
|
if (input.tool === "bash") {
|
|
32064
32734
|
const command = output?.args?.command;
|
|
32065
|
-
const route = typeof command === "string" ?
|
|
32735
|
+
const route = typeof command === "string" ? shouldDenyShellRoute(directory, command) : null;
|
|
32066
32736
|
if (route)
|
|
32067
32737
|
throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
|
|
32068
32738
|
return;
|
|
32069
32739
|
}
|
|
32740
|
+
if (input.tool === "question") {
|
|
32741
|
+
const issue = workitBindingQuestionIssue(output?.args?.questions);
|
|
32742
|
+
if (issue)
|
|
32743
|
+
throw new Error(issue);
|
|
32744
|
+
return;
|
|
32745
|
+
}
|
|
32070
32746
|
if (input.tool === "task") {
|
|
32071
32747
|
const listed = new TaskStore(directory).listTasks();
|
|
32072
32748
|
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 +32762,7 @@ var plugin = async ({ client, directory }) => {
|
|
|
32086
32762
|
config: async (config) => {
|
|
32087
32763
|
const mutable = config;
|
|
32088
32764
|
const paths = [...mutable.skills?.paths ?? []];
|
|
32089
|
-
if (
|
|
32765
|
+
if (existsSync13(skillsPath) && !paths.includes(skillsPath))
|
|
32090
32766
|
paths.push(skillsPath);
|
|
32091
32767
|
mutable.skills ??= {};
|
|
32092
32768
|
mutable.skills.paths = paths;
|