@brainervirus/workit-pi 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/worker.js +20 -4
- package/dist/workit.js +731 -253
- package/package.json +1 -1
- package/skills/workit-plan/SKILL.md +14 -2
package/dist/workit.js
CHANGED
|
@@ -3,8 +3,8 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
3
3
|
|
|
4
4
|
// packages/workit-core/src/core.ts
|
|
5
5
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6
|
-
import { existsSync as
|
|
7
|
-
import
|
|
6
|
+
import { existsSync as existsSync4, realpathSync as realpathSync5 } from "node:fs";
|
|
7
|
+
import path7 from "node:path";
|
|
8
8
|
|
|
9
9
|
// packages/workit-core/src/core/task-contract.ts
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -7796,6 +7796,7 @@ var decisionSchema = object({
|
|
|
7796
7796
|
scope: scopeSchema,
|
|
7797
7797
|
presented: text,
|
|
7798
7798
|
approvedContent: text,
|
|
7799
|
+
displayed: text.optional(),
|
|
7799
7800
|
contentRefs: array(refSchema)
|
|
7800
7801
|
}).strict(),
|
|
7801
7802
|
digest,
|
|
@@ -7808,6 +7809,27 @@ var decisionSchema = object({
|
|
|
7808
7809
|
actionRef: refSchema
|
|
7809
7810
|
}).strict().nullable()
|
|
7810
7811
|
}).strict();
|
|
7812
|
+
var BINDING_QUESTION_BUDGET = 300;
|
|
7813
|
+
var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
|
|
7814
|
+
var workitBindingQuestionIssue = (questions) => {
|
|
7815
|
+
if (!Array.isArray(questions) || questions.length !== 1)
|
|
7816
|
+
return null;
|
|
7817
|
+
const question = questions[0];
|
|
7818
|
+
if (!question || typeof question !== "object")
|
|
7819
|
+
return null;
|
|
7820
|
+
if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
|
|
7821
|
+
return null;
|
|
7822
|
+
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.`;
|
|
7823
|
+
if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
|
|
7824
|
+
return over("the question text", question.question);
|
|
7825
|
+
if (Array.isArray(question.options))
|
|
7826
|
+
for (const option of question.options) {
|
|
7827
|
+
const description = option.description;
|
|
7828
|
+
if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
|
|
7829
|
+
return over("an option description", description);
|
|
7830
|
+
}
|
|
7831
|
+
return null;
|
|
7832
|
+
};
|
|
7811
7833
|
var findingSchema = object({
|
|
7812
7834
|
claim: text,
|
|
7813
7835
|
consequence: text,
|
|
@@ -7874,6 +7896,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
|
|
|
7874
7896
|
path: ["decisionId"]
|
|
7875
7897
|
});
|
|
7876
7898
|
});
|
|
7899
|
+
var runtimeSchema = object({
|
|
7900
|
+
createdWith: text.nullable(),
|
|
7901
|
+
updatedWith: text
|
|
7902
|
+
}).strict();
|
|
7877
7903
|
var taskRecordSchema = object({
|
|
7878
7904
|
schemaVersion: literal(1),
|
|
7879
7905
|
id,
|
|
@@ -7887,6 +7913,8 @@ var taskRecordSchema = object({
|
|
|
7887
7913
|
status: _enum(["active", "paused", "closed"]),
|
|
7888
7914
|
closure: closureSchema.nullable(),
|
|
7889
7915
|
progress: progressSchema,
|
|
7916
|
+
pauseReason: text.nullable().optional(),
|
|
7917
|
+
runtime: runtimeSchema.optional(),
|
|
7890
7918
|
assessments: array(entrySchema(assessmentSchema)),
|
|
7891
7919
|
policy: policySchema.nullable(),
|
|
7892
7920
|
policyChanges: array(policyChangeSchema),
|
|
@@ -7902,6 +7930,7 @@ var workspaceRecordSchema = object({
|
|
|
7902
7930
|
id,
|
|
7903
7931
|
revision,
|
|
7904
7932
|
root: nonEmpty,
|
|
7933
|
+
runtime: runtimeSchema.optional(),
|
|
7905
7934
|
writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
|
|
7906
7935
|
}).strict();
|
|
7907
7936
|
var capabilitySchema = object({
|
|
@@ -7929,6 +7958,9 @@ var taskSummarySchema = object({
|
|
|
7929
7958
|
id,
|
|
7930
7959
|
revision,
|
|
7931
7960
|
workspaceRevision: revision,
|
|
7961
|
+
createdAt: utc,
|
|
7962
|
+
updatedAt: utc,
|
|
7963
|
+
runtime: runtimeSchema.nullable(),
|
|
7932
7964
|
objective: text,
|
|
7933
7965
|
status: _enum(["active", "paused", "closed"]),
|
|
7934
7966
|
closure: closureSchema.nullable(),
|
|
@@ -7999,7 +8031,7 @@ var taskOperations = {
|
|
|
7999
8031
|
expectedWorkspaceRevision: revision.optional(),
|
|
8000
8032
|
outcome: outcomeSchema,
|
|
8001
8033
|
summary: text,
|
|
8002
|
-
decisionIds: array(id)
|
|
8034
|
+
decisionIds: array(id).optional()
|
|
8003
8035
|
})
|
|
8004
8036
|
};
|
|
8005
8037
|
var policyOperations = {
|
|
@@ -8091,14 +8123,15 @@ var writerOperations = {
|
|
|
8091
8123
|
...taskId,
|
|
8092
8124
|
expectedRevision: revision.optional(),
|
|
8093
8125
|
expectedWorkspaceRevision: revision.optional(),
|
|
8094
|
-
workerId: nullableId.optional()
|
|
8126
|
+
workerId: nullableId.optional(),
|
|
8127
|
+
reason: text.optional()
|
|
8095
8128
|
}),
|
|
8096
8129
|
release: operation({
|
|
8097
8130
|
action: literal("release"),
|
|
8098
8131
|
...taskId,
|
|
8099
8132
|
expectedRevision: revision.optional(),
|
|
8100
8133
|
expectedWorkspaceRevision: revision.optional(),
|
|
8101
|
-
reason: text
|
|
8134
|
+
reason: text.optional()
|
|
8102
8135
|
})
|
|
8103
8136
|
};
|
|
8104
8137
|
var stateOperations = {
|
|
@@ -8305,7 +8338,26 @@ function requirementId(input) {
|
|
|
8305
8338
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
8306
8339
|
import * as fs4 from "node:fs";
|
|
8307
8340
|
import { hostname } from "node:os";
|
|
8308
|
-
import
|
|
8341
|
+
import path4 from "node:path";
|
|
8342
|
+
|
|
8343
|
+
// packages/workit-core/src/core/package-root.ts
|
|
8344
|
+
import { existsSync } from "node:fs";
|
|
8345
|
+
import path from "node:path";
|
|
8346
|
+
import { fileURLToPath } from "node:url";
|
|
8347
|
+
var packageRoot = () => {
|
|
8348
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
8349
|
+
while (true) {
|
|
8350
|
+
const parent = path.dirname(dir);
|
|
8351
|
+
if (existsSync(path.join(dir, "package.json")) || parent === dir)
|
|
8352
|
+
return dir;
|
|
8353
|
+
dir = parent;
|
|
8354
|
+
}
|
|
8355
|
+
};
|
|
8356
|
+
var assetRoot = () => {
|
|
8357
|
+
const root = packageRoot();
|
|
8358
|
+
const assets = path.join(root, "assets");
|
|
8359
|
+
return existsSync(assets) ? assets : root;
|
|
8360
|
+
};
|
|
8309
8361
|
|
|
8310
8362
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
8311
8363
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -8356,7 +8408,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
8356
8408
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
8357
8409
|
import { randomBytes } from "node:crypto";
|
|
8358
8410
|
import fsSync from "node:fs";
|
|
8359
|
-
import
|
|
8411
|
+
import path2 from "node:path";
|
|
8360
8412
|
|
|
8361
8413
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
8362
8414
|
import fs from "node:fs";
|
|
@@ -8446,13 +8498,13 @@ ${ownershipToken}
|
|
|
8446
8498
|
return { raw, ownershipToken };
|
|
8447
8499
|
}
|
|
8448
8500
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
8449
|
-
const resolved =
|
|
8450
|
-
const lexicalRelative =
|
|
8451
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
8452
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
8501
|
+
const resolved = path2.resolve(lockPath);
|
|
8502
|
+
const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
|
|
8503
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
|
|
8504
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
|
|
8453
8505
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
8454
8506
|
}
|
|
8455
|
-
return relative.split(
|
|
8507
|
+
return relative.split(path2.sep).join(path2.posix.sep);
|
|
8456
8508
|
}
|
|
8457
8509
|
function parseSidecarLockPayload(raw, parser) {
|
|
8458
8510
|
if (parser) {
|
|
@@ -8599,7 +8651,7 @@ function getFsSafeLockConfig() {
|
|
|
8599
8651
|
|
|
8600
8652
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
8601
8653
|
import fs3 from "node:fs";
|
|
8602
|
-
import
|
|
8654
|
+
import path3 from "node:path";
|
|
8603
8655
|
|
|
8604
8656
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
8605
8657
|
function sleepSync(ms) {
|
|
@@ -8690,26 +8742,26 @@ function canonicalLockParentSync(parent) {
|
|
|
8690
8742
|
return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
|
|
8691
8743
|
}
|
|
8692
8744
|
function normalizeTargetPath(targetPath) {
|
|
8693
|
-
const resolved =
|
|
8694
|
-
fs3.mkdirSync(
|
|
8745
|
+
const resolved = path3.resolve(targetPath);
|
|
8746
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
8695
8747
|
try {
|
|
8696
|
-
return
|
|
8748
|
+
return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
|
|
8697
8749
|
} catch {
|
|
8698
8750
|
return resolved;
|
|
8699
8751
|
}
|
|
8700
8752
|
}
|
|
8701
8753
|
function boundedLockPath(lockPath, lockRoot) {
|
|
8702
|
-
const resolved =
|
|
8754
|
+
const resolved = path3.resolve(lockPath);
|
|
8703
8755
|
if (!lockRoot)
|
|
8704
8756
|
return resolved;
|
|
8705
8757
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
8706
|
-
const parent =
|
|
8758
|
+
const parent = path3.dirname(resolved);
|
|
8707
8759
|
const parentReal = canonicalLockParentSync(parent);
|
|
8708
|
-
const parentRelative =
|
|
8709
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
8760
|
+
const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
|
|
8761
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
|
|
8710
8762
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
8711
8763
|
}
|
|
8712
|
-
return
|
|
8764
|
+
return path3.join(parentReal, path3.basename(resolved));
|
|
8713
8765
|
}
|
|
8714
8766
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
8715
8767
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -8922,6 +8974,33 @@ var metadataLockSchema = object({
|
|
|
8922
8974
|
nonce: string2().min(1)
|
|
8923
8975
|
}).strict();
|
|
8924
8976
|
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8977
|
+
var cachedRuntimeVersion = null;
|
|
8978
|
+
var runtimeVersion = () => {
|
|
8979
|
+
if (cachedRuntimeVersion === null) {
|
|
8980
|
+
try {
|
|
8981
|
+
const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
|
|
8982
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
8983
|
+
} catch {
|
|
8984
|
+
cachedRuntimeVersion = "0.0.0";
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
8987
|
+
return cachedRuntimeVersion;
|
|
8988
|
+
};
|
|
8989
|
+
var stampNew = (record) => ({
|
|
8990
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
8991
|
+
updatedWith: runtimeVersion()
|
|
8992
|
+
});
|
|
8993
|
+
var isNewerVersion = (candidate, current) => {
|
|
8994
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
8995
|
+
const [next, now] = [parts(candidate), parts(current)];
|
|
8996
|
+
for (let index = 0;index < 3; index += 1) {
|
|
8997
|
+
const left = next[index] ?? 0;
|
|
8998
|
+
const right = now[index] ?? 0;
|
|
8999
|
+
if (left !== right)
|
|
9000
|
+
return left > right;
|
|
9001
|
+
}
|
|
9002
|
+
return false;
|
|
9003
|
+
};
|
|
8925
9004
|
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
8926
9005
|
`;
|
|
8927
9006
|
var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
@@ -8948,12 +9027,12 @@ var sameMetadataLock = (left, right) => {
|
|
|
8948
9027
|
class TaskStore {
|
|
8949
9028
|
root;
|
|
8950
9029
|
constructor(root) {
|
|
8951
|
-
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) :
|
|
9030
|
+
this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
|
|
8952
9031
|
}
|
|
8953
9032
|
readTask(taskId) {
|
|
8954
9033
|
if (!validId(taskId))
|
|
8955
9034
|
return failure("invalid_input", "task ID is invalid", { taskId });
|
|
8956
|
-
const result = this.readRecord(
|
|
9035
|
+
const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
|
|
8957
9036
|
if (!result.exists)
|
|
8958
9037
|
return failure("not_found", "task not found", { taskId });
|
|
8959
9038
|
if (result.result.ok && result.result.data.id !== taskId)
|
|
@@ -8987,7 +9066,7 @@ class TaskStore {
|
|
|
8987
9066
|
});
|
|
8988
9067
|
const tasks = [];
|
|
8989
9068
|
for (const name of names.sort()) {
|
|
8990
|
-
const item = this.readRecord(
|
|
9069
|
+
const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
|
|
8991
9070
|
if (!item.exists)
|
|
8992
9071
|
continue;
|
|
8993
9072
|
if (!item.result.ok)
|
|
@@ -9021,7 +9100,7 @@ class TaskStore {
|
|
|
9021
9100
|
if (!current.ok)
|
|
9022
9101
|
return current;
|
|
9023
9102
|
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
9024
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
9103
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
9025
9104
|
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
9026
9105
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
9027
9106
|
});
|
|
@@ -9033,11 +9112,17 @@ class TaskStore {
|
|
|
9033
9112
|
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
9034
9113
|
if (current.data && !previousWorkspaceBytes)
|
|
9035
9114
|
return failure("storage_error", "workspace snapshot disappeared during creation");
|
|
9036
|
-
const workspace = current.data ? {
|
|
9115
|
+
const workspace = current.data ? {
|
|
9116
|
+
...current.data,
|
|
9117
|
+
revision: newRevision(),
|
|
9118
|
+
root: this.root,
|
|
9119
|
+
runtime: stampNew(current.data)
|
|
9120
|
+
} : {
|
|
9037
9121
|
schemaVersion: SCHEMA_VERSION,
|
|
9038
9122
|
id: newId(),
|
|
9039
9123
|
revision: newRevision(),
|
|
9040
9124
|
root: this.root,
|
|
9125
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
9041
9126
|
writer: null
|
|
9042
9127
|
};
|
|
9043
9128
|
const timestamp = value.now ?? now();
|
|
@@ -9059,6 +9144,7 @@ class TaskStore {
|
|
|
9059
9144
|
status: "active",
|
|
9060
9145
|
closure: null,
|
|
9061
9146
|
progress: { summary: "", nextAction: null, blockers: [] },
|
|
9147
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
9062
9148
|
assessments: [],
|
|
9063
9149
|
policy: null,
|
|
9064
9150
|
policyChanges: [],
|
|
@@ -9089,7 +9175,7 @@ class TaskStore {
|
|
|
9089
9175
|
if (!current.ok)
|
|
9090
9176
|
return current;
|
|
9091
9177
|
if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
|
|
9092
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
9178
|
+
return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
9093
9179
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
9094
9180
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
9095
9181
|
});
|
|
@@ -9097,11 +9183,17 @@ class TaskStore {
|
|
|
9097
9183
|
if (current.data && !previousWorkspaceBytes)
|
|
9098
9184
|
return failure("storage_error", "workspace snapshot disappeared during import");
|
|
9099
9185
|
const timestamp = input.now ?? now();
|
|
9100
|
-
const workspace = current.data ? {
|
|
9186
|
+
const workspace = current.data ? {
|
|
9187
|
+
...current.data,
|
|
9188
|
+
revision: newRevision(),
|
|
9189
|
+
root: this.root,
|
|
9190
|
+
runtime: stampNew(current.data)
|
|
9191
|
+
} : {
|
|
9101
9192
|
schemaVersion: SCHEMA_VERSION,
|
|
9102
9193
|
id: input.workspaceId ?? newId(),
|
|
9103
9194
|
revision: newRevision(),
|
|
9104
9195
|
root: this.root,
|
|
9196
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
9105
9197
|
writer: null
|
|
9106
9198
|
};
|
|
9107
9199
|
const task = {
|
|
@@ -9109,7 +9201,11 @@ class TaskStore {
|
|
|
9109
9201
|
workspaceId: workspace.id,
|
|
9110
9202
|
revision: newRevision(),
|
|
9111
9203
|
createdAt: timestamp,
|
|
9112
|
-
updatedAt: timestamp
|
|
9204
|
+
updatedAt: timestamp,
|
|
9205
|
+
runtime: {
|
|
9206
|
+
createdWith: input.task.runtime?.createdWith ?? null,
|
|
9207
|
+
updatedWith: runtimeVersion()
|
|
9208
|
+
}
|
|
9113
9209
|
};
|
|
9114
9210
|
const validTask = taskRecordSchema.safeParse(task);
|
|
9115
9211
|
if (!validTask.success)
|
|
@@ -9158,7 +9254,8 @@ class TaskStore {
|
|
|
9158
9254
|
workspaceId: current.data.workspaceId,
|
|
9159
9255
|
createdAt: current.data.createdAt,
|
|
9160
9256
|
revision: context.revision,
|
|
9161
|
-
updatedAt: context.now
|
|
9257
|
+
updatedAt: context.now,
|
|
9258
|
+
runtime: stampNew(current.data)
|
|
9162
9259
|
};
|
|
9163
9260
|
const valid = taskRecordSchema.safeParse(record);
|
|
9164
9261
|
if (!valid.success)
|
|
@@ -9192,7 +9289,8 @@ class TaskStore {
|
|
|
9192
9289
|
...changed.data,
|
|
9193
9290
|
id: current.data.id,
|
|
9194
9291
|
root: this.root,
|
|
9195
|
-
revision: context.revision
|
|
9292
|
+
revision: context.revision,
|
|
9293
|
+
runtime: stampNew(current.data)
|
|
9196
9294
|
};
|
|
9197
9295
|
const valid = workspaceRecordSchema.safeParse(record);
|
|
9198
9296
|
if (!valid.success)
|
|
@@ -9235,7 +9333,8 @@ class TaskStore {
|
|
|
9235
9333
|
...changedWorkspace.data,
|
|
9236
9334
|
id: workspace.data.id,
|
|
9237
9335
|
root: this.root,
|
|
9238
|
-
revision: workspaceContext.revision
|
|
9336
|
+
revision: workspaceContext.revision,
|
|
9337
|
+
runtime: stampNew(workspace.data)
|
|
9239
9338
|
});
|
|
9240
9339
|
if (!nextWorkspace.success)
|
|
9241
9340
|
return failure("invalid_input", "workspace mutation produced an invalid record");
|
|
@@ -9260,7 +9359,8 @@ class TaskStore {
|
|
|
9260
9359
|
workspaceId: task.data.workspaceId,
|
|
9261
9360
|
createdAt: task.data.createdAt,
|
|
9262
9361
|
revision: taskContext.revision,
|
|
9263
|
-
updatedAt: taskContext.now
|
|
9362
|
+
updatedAt: taskContext.now,
|
|
9363
|
+
runtime: stampNew(task.data)
|
|
9264
9364
|
});
|
|
9265
9365
|
if (!nextTask.success) {
|
|
9266
9366
|
this.markUncertain(nextWorkspace.data);
|
|
@@ -9287,7 +9387,7 @@ class TaskStore {
|
|
|
9287
9387
|
if (match)
|
|
9288
9388
|
candidates.push({
|
|
9289
9389
|
target: match[1],
|
|
9290
|
-
path:
|
|
9390
|
+
path: path4.join(this.recoveryDir, name),
|
|
9291
9391
|
digest: match[3]
|
|
9292
9392
|
});
|
|
9293
9393
|
}
|
|
@@ -9480,7 +9580,8 @@ class TaskStore {
|
|
|
9480
9580
|
const value = {
|
|
9481
9581
|
...workspace,
|
|
9482
9582
|
writer: { ...workspace.writer, state: "uncertain" },
|
|
9483
|
-
revision: newRevision()
|
|
9583
|
+
revision: newRevision(),
|
|
9584
|
+
runtime: stampNew(workspace)
|
|
9484
9585
|
};
|
|
9485
9586
|
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
9486
9587
|
}
|
|
@@ -9558,7 +9659,7 @@ class TaskStore {
|
|
|
9558
9659
|
}
|
|
9559
9660
|
fs4.renameSync(temporary, file);
|
|
9560
9661
|
temporary = undefined;
|
|
9561
|
-
this.fsyncDirectory(
|
|
9662
|
+
this.fsyncDirectory(path4.dirname(file));
|
|
9562
9663
|
return success(null, null, value);
|
|
9563
9664
|
} catch (error) {
|
|
9564
9665
|
return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
@@ -9572,9 +9673,9 @@ class TaskStore {
|
|
|
9572
9673
|
}
|
|
9573
9674
|
}
|
|
9574
9675
|
saveRecovery(file, bytes) {
|
|
9575
|
-
const target =
|
|
9576
|
-
const id = target === "task" ?
|
|
9577
|
-
const destination =
|
|
9676
|
+
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
9677
|
+
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
9678
|
+
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
9578
9679
|
if (fs4.existsSync(destination)) {
|
|
9579
9680
|
if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
|
|
9580
9681
|
return;
|
|
@@ -9652,7 +9753,7 @@ class TaskStore {
|
|
|
9652
9753
|
for (const name of fs4.readdirSync(this.tasksDir)) {
|
|
9653
9754
|
if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
|
|
9654
9755
|
return null;
|
|
9655
|
-
const parsed = this.parseBytes(fs4.readFileSync(
|
|
9756
|
+
const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
|
|
9656
9757
|
if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
|
|
9657
9758
|
return null;
|
|
9658
9759
|
ids.add(parsed.data.workspaceId);
|
|
@@ -9694,7 +9795,12 @@ class TaskStore {
|
|
|
9694
9795
|
if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
9695
9796
|
return failure("unsupported_version", "unsupported snapshot schema version");
|
|
9696
9797
|
const parsed = schema.safeParse(value);
|
|
9697
|
-
|
|
9798
|
+
if (parsed.success)
|
|
9799
|
+
return success(null, null, parsed.data);
|
|
9800
|
+
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
9801
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
9802
|
+
return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
9803
|
+
return failure("recovery_required", "snapshot does not satisfy its schema");
|
|
9698
9804
|
}
|
|
9699
9805
|
processStart(pid) {
|
|
9700
9806
|
try {
|
|
@@ -9704,43 +9810,98 @@ class TaskStore {
|
|
|
9704
9810
|
}
|
|
9705
9811
|
}
|
|
9706
9812
|
conflict(expected, actual) {
|
|
9707
|
-
return failure("revision_conflict", "snapshot revision does not match", {
|
|
9813
|
+
return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
9708
9814
|
expectedRevision: expected,
|
|
9709
9815
|
actualRevision: actual
|
|
9710
9816
|
});
|
|
9711
9817
|
}
|
|
9712
9818
|
taskPath(taskId) {
|
|
9713
|
-
return
|
|
9819
|
+
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
9714
9820
|
}
|
|
9715
9821
|
get workitDir() {
|
|
9716
|
-
return
|
|
9822
|
+
return path4.join(this.root, ".workit");
|
|
9717
9823
|
}
|
|
9718
9824
|
get tasksDir() {
|
|
9719
|
-
return
|
|
9825
|
+
return path4.join(this.workitDir, "tasks");
|
|
9720
9826
|
}
|
|
9721
9827
|
get recoveryDir() {
|
|
9722
|
-
return
|
|
9828
|
+
return path4.join(this.workitDir, "recovery");
|
|
9723
9829
|
}
|
|
9724
9830
|
get workspacePath() {
|
|
9725
|
-
return
|
|
9831
|
+
return path4.join(this.workitDir, "workspace.json");
|
|
9726
9832
|
}
|
|
9727
9833
|
get lockPath() {
|
|
9728
|
-
return
|
|
9834
|
+
return path4.join(this.workitDir, "metadata.lock");
|
|
9729
9835
|
}
|
|
9730
9836
|
get gitignorePath() {
|
|
9731
|
-
return
|
|
9837
|
+
return path4.join(this.workitDir, ".gitignore");
|
|
9732
9838
|
}
|
|
9733
9839
|
}
|
|
9840
|
+
// packages/workit-core/src/core/workers.ts
|
|
9841
|
+
var same = (left, right) => {
|
|
9842
|
+
try {
|
|
9843
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
9844
|
+
} catch {
|
|
9845
|
+
return false;
|
|
9846
|
+
}
|
|
9847
|
+
};
|
|
9848
|
+
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
9849
|
+
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
9850
|
+
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
9851
|
+
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
9852
|
+
const code = char.charCodeAt(0);
|
|
9853
|
+
return code < 32 || code === 127;
|
|
9854
|
+
}) && !value.split(/[\\/]/).includes("..");
|
|
9855
|
+
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
9856
|
+
var workerIdOf = (value) => value.workerId ?? null;
|
|
9857
|
+
function assertProductWriteAllowed(input) {
|
|
9858
|
+
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
9859
|
+
return failure("invalid_input", "invalid product write path");
|
|
9860
|
+
if (!input.store)
|
|
9861
|
+
return failure("permission_denied", "write authorization requires core state");
|
|
9862
|
+
const task = input.store.readTask(input.task.id);
|
|
9863
|
+
if (!task.ok)
|
|
9864
|
+
return task;
|
|
9865
|
+
const workspace = input.store.readWorkspace();
|
|
9866
|
+
if (!workspace.ok)
|
|
9867
|
+
return workspace;
|
|
9868
|
+
if (!workspace.data)
|
|
9869
|
+
return failure("not_found", "workspace not found");
|
|
9870
|
+
const workspaceRecord = workspace.data;
|
|
9871
|
+
if (task.data.status !== "active")
|
|
9872
|
+
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
9873
|
+
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
9874
|
+
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
9875
|
+
const writer = workspaceRecord.writer;
|
|
9876
|
+
if (!writer)
|
|
9877
|
+
return failure("permission_denied", "checkout has no writer owner");
|
|
9878
|
+
if (writer.state === "uncertain")
|
|
9879
|
+
return failure("recovery_required", "checkout writer requires recovery", {
|
|
9880
|
+
owner: writer.owner
|
|
9881
|
+
});
|
|
9882
|
+
const caller = callerValue(input.caller);
|
|
9883
|
+
const workerId = workerIdOf(input.caller);
|
|
9884
|
+
const owner = writer.owner;
|
|
9885
|
+
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
9886
|
+
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
9887
|
+
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
9888
|
+
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
9889
|
+
return failure("permission_denied", "only implementers can own product writes");
|
|
9890
|
+
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))))
|
|
9891
|
+
return failure("recovery_required", "worker session is not an active writer");
|
|
9892
|
+
return success(null, workspaceRecord.revision, owner);
|
|
9893
|
+
}
|
|
9894
|
+
|
|
9734
9895
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9735
9896
|
import { createHash as createHash4 } from "node:crypto";
|
|
9736
9897
|
import * as fs6 from "node:fs";
|
|
9737
9898
|
import { spawnSync } from "node:child_process";
|
|
9738
|
-
import
|
|
9899
|
+
import path6 from "node:path";
|
|
9739
9900
|
|
|
9740
9901
|
// packages/workit-core/src/core/authority.ts
|
|
9741
9902
|
import { createHash as createHash3 } from "node:crypto";
|
|
9742
9903
|
import * as fs5 from "node:fs";
|
|
9743
|
-
import
|
|
9904
|
+
import path5 from "node:path";
|
|
9744
9905
|
var verifiedAuthorities = new WeakMap;
|
|
9745
9906
|
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
9746
9907
|
const token = {};
|
|
@@ -9867,20 +10028,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
9867
10028
|
continue;
|
|
9868
10029
|
if (!reference.digest)
|
|
9869
10030
|
return failure("invalid_input", "document references require a byte digest");
|
|
9870
|
-
const target =
|
|
9871
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
9872
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
10031
|
+
const target = path5.resolve(root, reference.path);
|
|
10032
|
+
if (target !== root && !target.startsWith(`${root}${path5.sep}`))
|
|
10033
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
10034
|
+
fields: [
|
|
10035
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
10036
|
+
]
|
|
10037
|
+
});
|
|
9873
10038
|
try {
|
|
9874
|
-
const relative =
|
|
10039
|
+
const relative = path5.relative(root, target);
|
|
9875
10040
|
let current = root;
|
|
9876
|
-
for (const segment of relative.split(
|
|
10041
|
+
for (const segment of relative.split(path5.sep)) {
|
|
9877
10042
|
if (!segment)
|
|
9878
10043
|
continue;
|
|
9879
|
-
current =
|
|
10044
|
+
current = path5.join(current, segment);
|
|
9880
10045
|
const stat = fs5.lstatSync(current);
|
|
9881
10046
|
const real = fs5.realpathSync(current);
|
|
9882
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
9883
|
-
return failure("invalid_input", "document reference escapes checkout"
|
|
10047
|
+
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
10048
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
10049
|
+
fields: [
|
|
10050
|
+
{
|
|
10051
|
+
path: "contentRefs.path",
|
|
10052
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
10053
|
+
}
|
|
10054
|
+
]
|
|
10055
|
+
});
|
|
9884
10056
|
if (stat.isSymbolicLink())
|
|
9885
10057
|
return failure("invalid_input", "document reference uses a symlink");
|
|
9886
10058
|
if (current === target && !stat.isFile())
|
|
@@ -10227,14 +10399,14 @@ var verifyDecisionContent = verifyContentRefs;
|
|
|
10227
10399
|
|
|
10228
10400
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
10229
10401
|
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
10230
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
10231
|
-
var relative = (root, candidate) =>
|
|
10402
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
10403
|
+
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
10232
10404
|
var canonicalPath = (value) => {
|
|
10233
10405
|
if (typeof value !== "string" || !value)
|
|
10234
10406
|
return null;
|
|
10235
10407
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
10236
10408
|
return null;
|
|
10237
|
-
const normalized =
|
|
10409
|
+
const normalized = path6.posix.normalize(value);
|
|
10238
10410
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
10239
10411
|
return null;
|
|
10240
10412
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -10282,14 +10454,14 @@ var gitPaths = (root) => {
|
|
|
10282
10454
|
git: /not a git repository/i.test(stderr) ? false : true
|
|
10283
10455
|
};
|
|
10284
10456
|
}
|
|
10285
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10457
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
10286
10458
|
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
10287
10459
|
if (stagedDeleted.status !== 0) {
|
|
10288
10460
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
10289
10461
|
if (!/not a git repository/i.test(stderr))
|
|
10290
10462
|
return { paths, uncertain: true, git: true };
|
|
10291
10463
|
} else if (stagedDeleted.stdout) {
|
|
10292
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
10464
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
10293
10465
|
}
|
|
10294
10466
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
10295
10467
|
};
|
|
@@ -10304,7 +10476,7 @@ var walk = (root, directory, output) => {
|
|
|
10304
10476
|
for (const name of names) {
|
|
10305
10477
|
if (name === ".git" || name === ".workit")
|
|
10306
10478
|
continue;
|
|
10307
|
-
const target =
|
|
10479
|
+
const target = path6.join(directory, name);
|
|
10308
10480
|
let stat;
|
|
10309
10481
|
try {
|
|
10310
10482
|
stat = fs6.lstatSync(target);
|
|
@@ -10322,17 +10494,17 @@ var walk = (root, directory, output) => {
|
|
|
10322
10494
|
var scopeRoots = (root, scope) => {
|
|
10323
10495
|
const roots = [];
|
|
10324
10496
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
10325
|
-
const target =
|
|
10497
|
+
const target = path6.resolve(root, item);
|
|
10326
10498
|
if (!inside(root, target))
|
|
10327
|
-
return failure("invalid_input",
|
|
10499
|
+
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` }] });
|
|
10328
10500
|
let ancestor = target;
|
|
10329
10501
|
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
10330
|
-
ancestor =
|
|
10502
|
+
ancestor = path6.dirname(ancestor);
|
|
10331
10503
|
try {
|
|
10332
10504
|
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
10333
|
-
return failure("invalid_input",
|
|
10505
|
+
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` }] });
|
|
10334
10506
|
} catch {
|
|
10335
|
-
return failure("invalid_input",
|
|
10507
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
10336
10508
|
}
|
|
10337
10509
|
roots.push(target);
|
|
10338
10510
|
}
|
|
@@ -10347,7 +10519,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10347
10519
|
}
|
|
10348
10520
|
const normalizedScope = canonicalScope(scope);
|
|
10349
10521
|
if (!normalizedScope)
|
|
10350
|
-
return failure("invalid_input", "candidate scope is invalid");
|
|
10522
|
+
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" }] });
|
|
10351
10523
|
const roots = scopeRoots(checkout, normalizedScope);
|
|
10352
10524
|
if (!roots.ok)
|
|
10353
10525
|
return roots;
|
|
@@ -10379,7 +10551,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
10379
10551
|
for (const item of [...names].sort()) {
|
|
10380
10552
|
if (!scopeMatches(item, normalizedScope))
|
|
10381
10553
|
continue;
|
|
10382
|
-
const target =
|
|
10554
|
+
const target = path6.join(checkout, item);
|
|
10383
10555
|
try {
|
|
10384
10556
|
const stat = fs6.lstatSync(target);
|
|
10385
10557
|
if (stat.isSymbolicLink()) {
|
|
@@ -10536,7 +10708,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
10536
10708
|
return true;
|
|
10537
10709
|
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
10538
10710
|
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
10539
|
-
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
|
|
10711
|
+
const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
|
|
10540
10712
|
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
10541
10713
|
});
|
|
10542
10714
|
if (passed.length) {
|
|
@@ -10632,7 +10804,17 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10632
10804
|
const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
|
|
10633
10805
|
if (requestedOutcome !== "stopped" && blocking.length)
|
|
10634
10806
|
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
10635
|
-
requirementIds: blocking.map((item) => item.requirementId)
|
|
10807
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
10808
|
+
requirements: blocking.map((item) => {
|
|
10809
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
10810
|
+
return {
|
|
10811
|
+
requirementId: item.requirementId,
|
|
10812
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
10813
|
+
reason: item.reason,
|
|
10814
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
10815
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
10816
|
+
};
|
|
10817
|
+
})
|
|
10636
10818
|
});
|
|
10637
10819
|
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
10638
10820
|
if (requestedOutcome === "verified" && accepted.length)
|
|
@@ -10663,7 +10845,7 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10663
10845
|
var METHODS = {
|
|
10664
10846
|
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
10665
10847
|
"workit-behavioral-tdd": { dimensions: ["testing"] },
|
|
10666
|
-
"workit-review": { dimensions: ["review"] },
|
|
10848
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
|
|
10667
10849
|
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10668
10850
|
"workit-implement": { dimensions: ["delegation"] },
|
|
10669
10851
|
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
@@ -10725,7 +10907,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
|
10725
10907
|
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10726
10908
|
header Workit decision: <purpose>, the same label repeated in the question
|
|
10727
10909
|
text (some host UIs do not render headers), exactly two options approved and
|
|
10728
|
-
rejected; ask once, never re-ask to mint a receipt.
|
|
10910
|
+
rejected; ask once, never re-ask to mint a receipt. Before asking, present the
|
|
10911
|
+
item in the conversation (durable artifacts: complete digest plus exact path;
|
|
10912
|
+
inline plans: the content) and keep the question to one short scoped sentence;
|
|
10913
|
+
never ask to approve something unseen. An answer with custom text is steering,
|
|
10914
|
+
not approval: adjust, re-present, then ask a new scoped question. After the user
|
|
10915
|
+
approves a spec or plan, execute the remaining tasks continuously with one
|
|
10916
|
+
atomic commit per task and do not ask whether to continue; stop only for a new
|
|
10917
|
+
product decision, a failed safety or verification gate, a conflicting edit, or
|
|
10918
|
+
missing authority. Record the plan's commit list once through the plan-scoped
|
|
10919
|
+
action approval (git.commit with plan_steps and plan_branch) so each listed
|
|
10920
|
+
commit executes without a new question; an unlisted message needs a fresh
|
|
10921
|
+
approval. task.close {outcome,
|
|
10729
10922
|
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10730
10923
|
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10731
10924
|
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
@@ -10757,7 +10950,7 @@ function reconcileResume(view, observations = []) {
|
|
|
10757
10950
|
return candidate;
|
|
10758
10951
|
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
10759
10952
|
const blockers = [...view.task.progress.blockers];
|
|
10760
|
-
if (view.task.workers.some((entry) =>
|
|
10953
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
10761
10954
|
blockers.push({
|
|
10762
10955
|
reason: "worker state requires reconciliation",
|
|
10763
10956
|
dependentAction: "resume",
|
|
@@ -10890,6 +11083,7 @@ function shellRouteIntent(command) {
|
|
|
10890
11083
|
return null;
|
|
10891
11084
|
}
|
|
10892
11085
|
// packages/workit-core/src/core/external-action.ts
|
|
11086
|
+
import { execFileSync } from "node:child_process";
|
|
10893
11087
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
10894
11088
|
object({
|
|
10895
11089
|
operation: literal("git.branch_setup"),
|
|
@@ -10902,7 +11096,11 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
10902
11096
|
}).strict(),
|
|
10903
11097
|
object({
|
|
10904
11098
|
operation: literal("git.commit"),
|
|
10905
|
-
payload: object({
|
|
11099
|
+
payload: object({
|
|
11100
|
+
message: string2().min(1).optional(),
|
|
11101
|
+
plan_steps: array(string2().min(1)).optional(),
|
|
11102
|
+
plan_branch: string2().min(1).optional()
|
|
11103
|
+
}).strict()
|
|
10906
11104
|
}).strict(),
|
|
10907
11105
|
object({
|
|
10908
11106
|
operation: literal("git.push"),
|
|
@@ -10991,7 +11189,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
10991
11189
|
payload,
|
|
10992
11190
|
...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 } : {}
|
|
10993
11191
|
});
|
|
10994
|
-
var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch
|
|
11192
|
+
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).";
|
|
10995
11193
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
10996
11194
|
kind: "host",
|
|
10997
11195
|
host,
|
|
@@ -11042,6 +11240,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
|
|
|
11042
11240
|
return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
|
|
11043
11241
|
return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
|
|
11044
11242
|
};
|
|
11243
|
+
var planCommitDescriptor = (content) => {
|
|
11244
|
+
try {
|
|
11245
|
+
const value = JSON.parse(content);
|
|
11246
|
+
if (value.operation !== "git.commit" || !value.payload)
|
|
11247
|
+
return null;
|
|
11248
|
+
const steps = value.payload.plan_steps;
|
|
11249
|
+
const branch = value.payload.plan_branch;
|
|
11250
|
+
if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
|
|
11251
|
+
return null;
|
|
11252
|
+
return { steps, branch };
|
|
11253
|
+
} catch {
|
|
11254
|
+
return null;
|
|
11255
|
+
}
|
|
11256
|
+
};
|
|
11257
|
+
var approvedPlanCommit = (store, host, actor, message) => {
|
|
11258
|
+
if (typeof message !== "string" || !message)
|
|
11259
|
+
return failure("invalid_input", "commit message is required");
|
|
11260
|
+
const listed = store.listTasks();
|
|
11261
|
+
const workspace = store.readWorkspace();
|
|
11262
|
+
if (!listed.ok || !workspace.ok)
|
|
11263
|
+
return failure("storage_error", "external action state is unavailable");
|
|
11264
|
+
if (!workspace.data)
|
|
11265
|
+
return failure("not_found", "workspace not found");
|
|
11266
|
+
const matches = listed.data.flatMap((task) => {
|
|
11267
|
+
if (task.status !== "active" || task.workspaceId !== workspace.data.id)
|
|
11268
|
+
return [];
|
|
11269
|
+
if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
|
|
11270
|
+
return [];
|
|
11271
|
+
return task.decisions.flatMap((entry) => {
|
|
11272
|
+
const decision = entry.data;
|
|
11273
|
+
if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
|
|
11274
|
+
return [];
|
|
11275
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
|
|
11276
|
+
return [];
|
|
11277
|
+
const plan = planCommitDescriptor(decision.binding.approvedContent);
|
|
11278
|
+
if (!plan)
|
|
11279
|
+
return [];
|
|
11280
|
+
const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
|
|
11281
|
+
if (plan.steps[completed.length] !== message)
|
|
11282
|
+
return [];
|
|
11283
|
+
return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
|
|
11284
|
+
});
|
|
11285
|
+
});
|
|
11286
|
+
if (matches.length !== 1)
|
|
11287
|
+
return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
|
|
11288
|
+
return success(workspace.data.revision, null, matches[0]);
|
|
11289
|
+
};
|
|
11290
|
+
var commitMessageFromDescriptor = (operation) => {
|
|
11291
|
+
try {
|
|
11292
|
+
const descriptor = JSON.parse(operation);
|
|
11293
|
+
return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
|
|
11294
|
+
} catch {
|
|
11295
|
+
return;
|
|
11296
|
+
}
|
|
11297
|
+
};
|
|
11298
|
+
var planCommitBinding = (store, host, actor, operation) => {
|
|
11299
|
+
const message = commitMessageFromDescriptor(operation);
|
|
11300
|
+
if (!message)
|
|
11301
|
+
return null;
|
|
11302
|
+
const plan = approvedPlanCommit(store, host, actor, message);
|
|
11303
|
+
if (!plan.ok)
|
|
11304
|
+
return null;
|
|
11305
|
+
let branch;
|
|
11306
|
+
try {
|
|
11307
|
+
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
11308
|
+
cwd: store.root,
|
|
11309
|
+
encoding: "utf8",
|
|
11310
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11311
|
+
}).trim();
|
|
11312
|
+
} catch {
|
|
11313
|
+
return null;
|
|
11314
|
+
}
|
|
11315
|
+
if (!branch || branch !== plan.data.branch)
|
|
11316
|
+
return null;
|
|
11317
|
+
const workspace = store.readWorkspace();
|
|
11318
|
+
if (!workspace.ok || !workspace.data)
|
|
11319
|
+
return null;
|
|
11320
|
+
return {
|
|
11321
|
+
taskId: plan.data.task.id,
|
|
11322
|
+
decisionId: plan.data.entry.id,
|
|
11323
|
+
binding: plan.data.entry.data.binding,
|
|
11324
|
+
expectedRevision: plan.data.task.revision,
|
|
11325
|
+
expectedWorkspaceRevision: workspace.data.revision,
|
|
11326
|
+
step: message
|
|
11327
|
+
};
|
|
11328
|
+
};
|
|
11045
11329
|
var approvedExternalAction = (store, host, actor, operation) => {
|
|
11046
11330
|
const selected = externalActionState(store, host, actor, operation);
|
|
11047
11331
|
if (!selected.ok)
|
|
@@ -11050,6 +11334,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
|
|
|
11050
11334
|
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");
|
|
11051
11335
|
return selected;
|
|
11052
11336
|
};
|
|
11337
|
+
var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
|
|
11338
|
+
const prior = priorExternalAction(store, host, actor, operation, payload);
|
|
11339
|
+
if (!prior.ok)
|
|
11340
|
+
return success(null, null, null);
|
|
11341
|
+
let baseline;
|
|
11342
|
+
try {
|
|
11343
|
+
const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
|
|
11344
|
+
baseline = parsed.payload?.resolved;
|
|
11345
|
+
} catch {
|
|
11346
|
+
return success(null, null, null);
|
|
11347
|
+
}
|
|
11348
|
+
if (baseline === undefined || baseline === null)
|
|
11349
|
+
return success(null, null, null);
|
|
11350
|
+
if (canonicalJson(baseline) === canonicalJson(currentResolved))
|
|
11351
|
+
return success(null, null, null);
|
|
11352
|
+
return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
|
|
11353
|
+
};
|
|
11053
11354
|
var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
|
|
11054
11355
|
let steps = [];
|
|
11055
11356
|
try {
|
|
@@ -11327,7 +11628,7 @@ function resolveRequirements(input) {
|
|
|
11327
11628
|
].join(", ")}`,
|
|
11328
11629
|
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
11329
11630
|
before: "dependent_action",
|
|
11330
|
-
dependentAction:
|
|
11631
|
+
dependentAction: null,
|
|
11331
11632
|
acceptanceAllowed: false
|
|
11332
11633
|
}));
|
|
11333
11634
|
if (signals.productChoiceOpen.value === true)
|
|
@@ -11338,7 +11639,7 @@ function resolveRequirements(input) {
|
|
|
11338
11639
|
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
11339
11640
|
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
11340
11641
|
before: "dependent_action",
|
|
11341
|
-
dependentAction:
|
|
11642
|
+
dependentAction: null,
|
|
11342
11643
|
acceptanceAllowed: false
|
|
11343
11644
|
}));
|
|
11344
11645
|
if (signals.behaviorChange.value === true)
|
|
@@ -11496,59 +11797,6 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
11496
11797
|
return parsed.data;
|
|
11497
11798
|
}
|
|
11498
11799
|
|
|
11499
|
-
// packages/workit-core/src/core/workers.ts
|
|
11500
|
-
var same = (left, right) => {
|
|
11501
|
-
try {
|
|
11502
|
-
return canonicalJson(left) === canonicalJson(right);
|
|
11503
|
-
} catch {
|
|
11504
|
-
return false;
|
|
11505
|
-
}
|
|
11506
|
-
};
|
|
11507
|
-
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
11508
|
-
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
11509
|
-
const code = char.charCodeAt(0);
|
|
11510
|
-
return code < 32 || code === 127;
|
|
11511
|
-
}) && !value.split(/[\\/]/).includes("..");
|
|
11512
|
-
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
11513
|
-
var workerIdOf = (value) => value.workerId ?? null;
|
|
11514
|
-
function assertProductWriteAllowed(input) {
|
|
11515
|
-
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
11516
|
-
return failure("invalid_input", "invalid product write path");
|
|
11517
|
-
if (!input.store)
|
|
11518
|
-
return failure("permission_denied", "write authorization requires core state");
|
|
11519
|
-
const task = input.store.readTask(input.task.id);
|
|
11520
|
-
if (!task.ok)
|
|
11521
|
-
return task;
|
|
11522
|
-
const workspace = input.store.readWorkspace();
|
|
11523
|
-
if (!workspace.ok)
|
|
11524
|
-
return workspace;
|
|
11525
|
-
if (!workspace.data)
|
|
11526
|
-
return failure("not_found", "workspace not found");
|
|
11527
|
-
const workspaceRecord = workspace.data;
|
|
11528
|
-
if (task.data.status !== "active")
|
|
11529
|
-
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
11530
|
-
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
11531
|
-
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
11532
|
-
const writer = workspaceRecord.writer;
|
|
11533
|
-
if (!writer)
|
|
11534
|
-
return failure("permission_denied", "checkout has no writer owner");
|
|
11535
|
-
if (writer.state === "uncertain")
|
|
11536
|
-
return failure("recovery_required", "checkout writer requires recovery", {
|
|
11537
|
-
owner: writer.owner
|
|
11538
|
-
});
|
|
11539
|
-
const caller = callerValue(input.caller);
|
|
11540
|
-
const workerId = workerIdOf(input.caller);
|
|
11541
|
-
const owner = writer.owner;
|
|
11542
|
-
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
11543
|
-
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
11544
|
-
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
11545
|
-
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
11546
|
-
return failure("permission_denied", "only implementers can own product writes");
|
|
11547
|
-
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))))
|
|
11548
|
-
return failure("recovery_required", "worker session is not an active writer");
|
|
11549
|
-
return success(null, workspaceRecord.revision, owner);
|
|
11550
|
-
}
|
|
11551
|
-
|
|
11552
11800
|
// packages/workit-core/src/core/task-engine.ts
|
|
11553
11801
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
11554
11802
|
kind,
|
|
@@ -11679,15 +11927,20 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11679
11927
|
return failure("invalid_transition", "worker has not started");
|
|
11680
11928
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
11681
11929
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
11930
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
11682
11931
|
const nextEntry = {
|
|
11683
11932
|
...entry,
|
|
11684
11933
|
recordedAt: input.now ?? entry.recordedAt,
|
|
11685
11934
|
provenance: authority.provenance,
|
|
11686
|
-
data: {
|
|
11935
|
+
data: {
|
|
11936
|
+
...entry.data,
|
|
11937
|
+
state: terminalCancel ? "cancelling" : input.state,
|
|
11938
|
+
session: input.session
|
|
11939
|
+
}
|
|
11687
11940
|
};
|
|
11688
11941
|
const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
|
|
11689
11942
|
const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
|
|
11690
|
-
if (entry.data.state ===
|
|
11943
|
+
if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
|
|
11691
11944
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
11692
11945
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
11693
11946
|
taskId: task.data.id,
|
|
@@ -12057,7 +12310,7 @@ class WorkitCore {
|
|
|
12057
12310
|
activeWorkerBlocker(task, workspace) {
|
|
12058
12311
|
if (workspace.writer)
|
|
12059
12312
|
return failure("recovery_required", "writer ownership must be released first");
|
|
12060
|
-
if (task.workers.some((entry) =>
|
|
12313
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
12061
12314
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
12062
12315
|
return null;
|
|
12063
12316
|
}
|
|
@@ -12294,11 +12547,6 @@ class WorkitCore {
|
|
|
12294
12547
|
if (helper && !helper.ok)
|
|
12295
12548
|
return helper;
|
|
12296
12549
|
const evidence = input.evidence;
|
|
12297
|
-
if (helper?.ok) {
|
|
12298
|
-
const assignment = helper.data.data.assignment;
|
|
12299
|
-
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))
|
|
12300
|
-
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
12301
|
-
}
|
|
12302
12550
|
if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
|
|
12303
12551
|
return failure("invalid_input", "missing or skipped evidence requires a reason");
|
|
12304
12552
|
if (evidence.kind === "review") {
|
|
@@ -12315,6 +12563,11 @@ class WorkitCore {
|
|
|
12315
12563
|
evidence.beforeCandidateId ??= null;
|
|
12316
12564
|
evidence.candidateId ??= null;
|
|
12317
12565
|
}
|
|
12566
|
+
if (helper?.ok) {
|
|
12567
|
+
const assignment = helper.data.data.assignment;
|
|
12568
|
+
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))
|
|
12569
|
+
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
12570
|
+
}
|
|
12318
12571
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
12319
12572
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
12320
12573
|
if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
|
|
@@ -13210,6 +13463,9 @@ class WorkitCore {
|
|
|
13210
13463
|
id: task.id,
|
|
13211
13464
|
revision: task.revision,
|
|
13212
13465
|
workspaceRevision: workspace.data.revision,
|
|
13466
|
+
createdAt: task.createdAt,
|
|
13467
|
+
updatedAt: task.updatedAt,
|
|
13468
|
+
runtime: task.runtime ?? null,
|
|
13213
13469
|
objective: task.intent.data.objective,
|
|
13214
13470
|
status: task.status,
|
|
13215
13471
|
closure: task.closure,
|
|
@@ -13295,7 +13551,7 @@ class WorkitCore {
|
|
|
13295
13551
|
const base = reconcileResume(effectiveView);
|
|
13296
13552
|
if (!base.ok)
|
|
13297
13553
|
return base;
|
|
13298
|
-
const needsReconciliation = view.task.workers.some((entry) =>
|
|
13554
|
+
const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
|
|
13299
13555
|
const blockers = [...base.data.blockers];
|
|
13300
13556
|
if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
|
|
13301
13557
|
blockers.push({
|
|
@@ -13362,7 +13618,8 @@ class WorkitCore {
|
|
|
13362
13618
|
...current,
|
|
13363
13619
|
status,
|
|
13364
13620
|
candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
|
|
13365
|
-
progress:
|
|
13621
|
+
progress: current.progress,
|
|
13622
|
+
pauseReason: status === "paused" ? input.reason ?? null : null
|
|
13366
13623
|
})
|
|
13367
13624
|
});
|
|
13368
13625
|
if (!changed.ok)
|
|
@@ -13398,7 +13655,7 @@ class WorkitCore {
|
|
|
13398
13655
|
return workspace;
|
|
13399
13656
|
if (!workspace.data)
|
|
13400
13657
|
return failure("not_found", "workspace not found");
|
|
13401
|
-
if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state
|
|
13658
|
+
if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
13402
13659
|
return failure("recovery_required", "scope revision requires worker ownership reconciliation");
|
|
13403
13660
|
this.fillRevisions(input, task.data, workspace.data);
|
|
13404
13661
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
@@ -13516,15 +13773,15 @@ function resolveGitRevision(root, value) {
|
|
|
13516
13773
|
}
|
|
13517
13774
|
function resolveInside(root, candidate) {
|
|
13518
13775
|
const base = realpathSync5(root);
|
|
13519
|
-
const target =
|
|
13520
|
-
if (target !== base && !target.startsWith(base +
|
|
13776
|
+
const target = path7.resolve(base, candidate);
|
|
13777
|
+
if (target !== base && !target.startsWith(base + path7.sep)) {
|
|
13521
13778
|
throw new Error("path must stay inside repository root");
|
|
13522
13779
|
}
|
|
13523
13780
|
let ancestor = target;
|
|
13524
|
-
while (!
|
|
13525
|
-
ancestor =
|
|
13781
|
+
while (!existsSync4(ancestor))
|
|
13782
|
+
ancestor = path7.dirname(ancestor);
|
|
13526
13783
|
const canonicalAncestor = realpathSync5(ancestor);
|
|
13527
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
13784
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
|
|
13528
13785
|
throw new Error("path must stay inside repository root");
|
|
13529
13786
|
}
|
|
13530
13787
|
return target;
|
|
@@ -13567,6 +13824,13 @@ var piCapabilities = (ctx) => [
|
|
|
13567
13824
|
assurance: "agent_guided",
|
|
13568
13825
|
reason: "Pi extensions do not sandbox arbitrary shell commands.",
|
|
13569
13826
|
refs: [hostRef("tool_call")]
|
|
13827
|
+
},
|
|
13828
|
+
{
|
|
13829
|
+
name: "fresh-context-review",
|
|
13830
|
+
surface: "supervised_worker",
|
|
13831
|
+
assurance: "agent_guided",
|
|
13832
|
+
reason: "independent review runs as a supervised stock-Pi process; evidence evaluation enforces reviewer exclusivity",
|
|
13833
|
+
refs: [hostRef("worker")]
|
|
13570
13834
|
}
|
|
13571
13835
|
];
|
|
13572
13836
|
var piContext = (ctx) => ({
|
|
@@ -13634,15 +13898,15 @@ var WORKIT_SKILL_ALIASES = {
|
|
|
13634
13898
|
if (false) {}
|
|
13635
13899
|
|
|
13636
13900
|
// packages/workit-core/src/core/branch.ts
|
|
13637
|
-
import { existsSync as
|
|
13638
|
-
import { execFileSync as
|
|
13639
|
-
import
|
|
13901
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
13902
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
13903
|
+
import path11 from "node:path";
|
|
13640
13904
|
|
|
13641
13905
|
// packages/workit-core/src/core/git.ts
|
|
13642
|
-
import { execFileSync } from "node:child_process";
|
|
13906
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
13643
13907
|
var run2 = (cwd, args) => {
|
|
13644
13908
|
try {
|
|
13645
|
-
const stdout =
|
|
13909
|
+
const stdout = execFileSync2("git", args, {
|
|
13646
13910
|
cwd,
|
|
13647
13911
|
encoding: "utf8",
|
|
13648
13912
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -13710,14 +13974,14 @@ var gitContext = (workspaceRoot, paths = []) => {
|
|
|
13710
13974
|
import {
|
|
13711
13975
|
copyFileSync,
|
|
13712
13976
|
cpSync,
|
|
13713
|
-
existsSync as
|
|
13977
|
+
existsSync as existsSync5,
|
|
13714
13978
|
mkdirSync as mkdirSync2,
|
|
13715
13979
|
readFileSync as readFileSync4,
|
|
13716
13980
|
readdirSync as readdirSync3,
|
|
13717
13981
|
writeFileSync as writeFileSync2
|
|
13718
13982
|
} from "node:fs";
|
|
13719
13983
|
import os from "node:os";
|
|
13720
|
-
import
|
|
13984
|
+
import path8 from "node:path";
|
|
13721
13985
|
|
|
13722
13986
|
// packages/workit-core/src/core/boundary.ts
|
|
13723
13987
|
var EVENT = {
|
|
@@ -13761,7 +14025,7 @@ var mergePreset = (preset, input = {}, current = {
|
|
|
13761
14025
|
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
13762
14026
|
};
|
|
13763
14027
|
};
|
|
13764
|
-
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
|
|
14028
|
+
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workit");
|
|
13765
14029
|
var migratedDir = null;
|
|
13766
14030
|
var migrationFailed = false;
|
|
13767
14031
|
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
@@ -13771,12 +14035,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
13771
14035
|
migratedDir = dir;
|
|
13772
14036
|
return dir;
|
|
13773
14037
|
}
|
|
13774
|
-
const legacy =
|
|
13775
|
-
if (!
|
|
14038
|
+
const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
14039
|
+
if (!existsSync5(legacy)) {
|
|
13776
14040
|
migratedDir = dir;
|
|
13777
14041
|
return dir;
|
|
13778
14042
|
}
|
|
13779
|
-
if (!migrationFailed &&
|
|
14043
|
+
if (!migrationFailed && existsSync5(dir)) {
|
|
13780
14044
|
migratedDir = dir;
|
|
13781
14045
|
return dir;
|
|
13782
14046
|
}
|
|
@@ -13784,9 +14048,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
13784
14048
|
mkdirSync2(dir, { recursive: true });
|
|
13785
14049
|
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
13786
14050
|
for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
|
|
13787
|
-
const src =
|
|
13788
|
-
const dest =
|
|
13789
|
-
if (
|
|
14051
|
+
const src = path8.join(legacy, entry.name);
|
|
14052
|
+
const dest = path8.join(dir, entry.name);
|
|
14053
|
+
if (existsSync5(dest))
|
|
13790
14054
|
continue;
|
|
13791
14055
|
try {
|
|
13792
14056
|
if (entry.isDirectory())
|
|
@@ -13859,7 +14123,7 @@ var parseConfigResult = (raw, file) => {
|
|
|
13859
14123
|
};
|
|
13860
14124
|
};
|
|
13861
14125
|
var readConfigTyped = (dir) => {
|
|
13862
|
-
const file =
|
|
14126
|
+
const file = path8.join(dir ?? configDir(), "config.json");
|
|
13863
14127
|
return parseConfigResult(readSafe(file), file);
|
|
13864
14128
|
};
|
|
13865
14129
|
var readConfig = () => {
|
|
@@ -13905,9 +14169,9 @@ var resolveCommitPolicy = (config, workspace) => {
|
|
|
13905
14169
|
|
|
13906
14170
|
// packages/workit-core/src/core/workspaces.ts
|
|
13907
14171
|
import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
|
|
13908
|
-
import
|
|
14172
|
+
import path9 from "node:path";
|
|
13909
14173
|
var readWorkspacesResult = (dir = configDir()) => {
|
|
13910
|
-
const file =
|
|
14174
|
+
const file = path9.join(dir, "workspaces.json");
|
|
13911
14175
|
let raw;
|
|
13912
14176
|
try {
|
|
13913
14177
|
raw = readFileSync5(file, "utf8");
|
|
@@ -14005,10 +14269,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
|
|
|
14005
14269
|
|
|
14006
14270
|
// packages/workit-core/src/core/vcs-config.ts
|
|
14007
14271
|
import fs7 from "node:fs";
|
|
14008
|
-
import
|
|
14272
|
+
import path10 from "node:path";
|
|
14009
14273
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
14010
14274
|
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
14011
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ??
|
|
14275
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path10.join(configDir(), "vcs.json");
|
|
14012
14276
|
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
14013
14277
|
var remoteProvider = (cwd) => {
|
|
14014
14278
|
const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
@@ -14091,8 +14355,8 @@ function vcsConfig(mode, cwd) {
|
|
|
14091
14355
|
}
|
|
14092
14356
|
const prov = cfg[provider] ?? {};
|
|
14093
14357
|
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
14094
|
-
const tokenFile = String(wsTokenFile ?? prov.tokenFile ??
|
|
14095
|
-
const tokenPath =
|
|
14358
|
+
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
|
|
14359
|
+
const tokenPath = path10.resolve(tokenFile);
|
|
14096
14360
|
let tokenOk = false;
|
|
14097
14361
|
if (fs7.existsSync(tokenPath)) {
|
|
14098
14362
|
const token = fs7.readFileSync(tokenPath, "utf8").trim();
|
|
@@ -14101,7 +14365,7 @@ function vcsConfig(mode, cwd) {
|
|
|
14101
14365
|
}
|
|
14102
14366
|
const out = {
|
|
14103
14367
|
ok: true,
|
|
14104
|
-
configPath:
|
|
14368
|
+
configPath: path10.resolve(cfgPath),
|
|
14105
14369
|
provider,
|
|
14106
14370
|
defaultTargetBranch: defaultTarget,
|
|
14107
14371
|
pr: cfg.pr ?? {},
|
|
@@ -14202,7 +14466,7 @@ var baseBranch = (cwd) => {
|
|
|
14202
14466
|
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
14203
14467
|
};
|
|
14204
14468
|
var originBaseReady = (cwd, base) => {
|
|
14205
|
-
const run = (args) =>
|
|
14469
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
14206
14470
|
try {
|
|
14207
14471
|
try {
|
|
14208
14472
|
run(["fetch", "origin", base, "--prune"]);
|
|
@@ -14210,7 +14474,7 @@ var originBaseReady = (cwd, base) => {
|
|
|
14210
14474
|
run(["fetch", "origin", "--prune"]);
|
|
14211
14475
|
}
|
|
14212
14476
|
try {
|
|
14213
|
-
|
|
14477
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
14214
14478
|
cwd,
|
|
14215
14479
|
stdio: "pipe"
|
|
14216
14480
|
});
|
|
@@ -14229,11 +14493,11 @@ var originBaseReady = (cwd, base) => {
|
|
|
14229
14493
|
}
|
|
14230
14494
|
};
|
|
14231
14495
|
var fastForwardBase = (cwd, base) => {
|
|
14232
|
-
const run = (args) =>
|
|
14496
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
14233
14497
|
try {
|
|
14234
14498
|
let hasLocalBase = true;
|
|
14235
14499
|
try {
|
|
14236
|
-
|
|
14500
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
14237
14501
|
cwd,
|
|
14238
14502
|
stdio: "pipe"
|
|
14239
14503
|
});
|
|
@@ -14273,19 +14537,18 @@ var branchSetup = ({
|
|
|
14273
14537
|
workspace_root,
|
|
14274
14538
|
log
|
|
14275
14539
|
}) => {
|
|
14276
|
-
const cwd =
|
|
14277
|
-
const exec = (args) =>
|
|
14540
|
+
const cwd = path11.resolve(workspace_root);
|
|
14541
|
+
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
14278
14542
|
const current = gitContext(cwd).branch;
|
|
14279
14543
|
if (!current || current === "unknown")
|
|
14280
|
-
return { error: "not in a git repository" };
|
|
14544
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
14281
14545
|
const sdd = sdd_dir ?? "docs";
|
|
14282
|
-
const manifestPath =
|
|
14546
|
+
const manifestPath = path11.isAbsolute(sdd) ? path11.join(sdd, "manifest.json") : path11.join(cwd, sdd, "manifest.json");
|
|
14283
14547
|
const ensureManifestDir = () => {
|
|
14284
|
-
const dir =
|
|
14285
|
-
if (!
|
|
14548
|
+
const dir = path11.dirname(manifestPath);
|
|
14549
|
+
if (!existsSync6(dir))
|
|
14286
14550
|
mkdirSync3(dir, { recursive: true, mode: 493 });
|
|
14287
14551
|
};
|
|
14288
|
-
ensureManifestDir();
|
|
14289
14552
|
const readManifest = () => {
|
|
14290
14553
|
try {
|
|
14291
14554
|
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
@@ -14303,13 +14566,16 @@ var branchSetup = ({
|
|
|
14303
14566
|
const manifest = readManifest();
|
|
14304
14567
|
const ref = manifest.stash_ref;
|
|
14305
14568
|
if (!ref)
|
|
14306
|
-
return { error: "no stash_ref in manifest" };
|
|
14569
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
14307
14570
|
journal(`pre-pop: ${String(ref)}`);
|
|
14308
14571
|
try {
|
|
14309
14572
|
exec(["stash", "pop", String(ref)]);
|
|
14310
14573
|
} catch (error) {
|
|
14311
14574
|
journal("pop: failed");
|
|
14312
|
-
return {
|
|
14575
|
+
return {
|
|
14576
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
14577
|
+
phase: "post_mutation"
|
|
14578
|
+
};
|
|
14313
14579
|
}
|
|
14314
14580
|
journal("pop: ok");
|
|
14315
14581
|
delete manifest.stash_ref;
|
|
@@ -14319,11 +14585,17 @@ var branchSetup = ({
|
|
|
14319
14585
|
}
|
|
14320
14586
|
const target = target_branch ?? "";
|
|
14321
14587
|
if (!target)
|
|
14322
|
-
return {
|
|
14588
|
+
return {
|
|
14589
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
14590
|
+
phase: "preflight"
|
|
14591
|
+
};
|
|
14323
14592
|
if (isProtected(cwd, target))
|
|
14324
|
-
return { error: `protected branch ${target}
|
|
14593
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
14325
14594
|
if (!allowedBranch(cwd, target))
|
|
14326
|
-
return {
|
|
14595
|
+
return {
|
|
14596
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
14597
|
+
phase: "preflight"
|
|
14598
|
+
};
|
|
14327
14599
|
let base;
|
|
14328
14600
|
let targetExists = true;
|
|
14329
14601
|
try {
|
|
@@ -14334,12 +14606,12 @@ var branchSetup = ({
|
|
|
14334
14606
|
if (!targetExists) {
|
|
14335
14607
|
const baseResolved = baseBranch(cwd);
|
|
14336
14608
|
if ("error" in baseResolved)
|
|
14337
|
-
return { error: baseResolved.error };
|
|
14609
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
14338
14610
|
base = baseResolved.base;
|
|
14339
14611
|
}
|
|
14340
14612
|
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
14341
14613
|
let stash_ref;
|
|
14342
|
-
const failAfterStash = (message) => {
|
|
14614
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
14343
14615
|
let suffix = "";
|
|
14344
14616
|
if (stash_ref) {
|
|
14345
14617
|
journal(`pre-pop: ${stash_ref}`);
|
|
@@ -14352,27 +14624,31 @@ var branchSetup = ({
|
|
|
14352
14624
|
suffix = " (changes preserved in stash)";
|
|
14353
14625
|
}
|
|
14354
14626
|
}
|
|
14355
|
-
return { error: `${message}${suffix}
|
|
14627
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
14356
14628
|
};
|
|
14357
14629
|
if (current !== target) {
|
|
14358
14630
|
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
14359
14631
|
if (dirty && stash !== "yes") {
|
|
14360
14632
|
return {
|
|
14361
|
-
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
|
|
14633
|
+
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
|
|
14634
|
+
phase: "preflight"
|
|
14362
14635
|
};
|
|
14363
14636
|
}
|
|
14364
14637
|
let validatedBase;
|
|
14365
14638
|
if (!targetExists && base !== undefined) {
|
|
14366
14639
|
const ready = originBaseReady(cwd, base);
|
|
14367
14640
|
if (!ready.ok)
|
|
14368
|
-
return { error: ready.error ?? "ensure-base-branch failed" };
|
|
14641
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
14369
14642
|
validatedBase = base;
|
|
14370
14643
|
}
|
|
14371
14644
|
if (dirty) {
|
|
14372
14645
|
try {
|
|
14373
14646
|
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
14374
14647
|
} catch (error) {
|
|
14375
|
-
return {
|
|
14648
|
+
return {
|
|
14649
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
14650
|
+
phase: "post_mutation"
|
|
14651
|
+
};
|
|
14376
14652
|
}
|
|
14377
14653
|
stash_ref = "stash@{0}";
|
|
14378
14654
|
journal(`stash push: ${stash_ref}`);
|
|
@@ -14383,14 +14659,14 @@ var branchSetup = ({
|
|
|
14383
14659
|
} catch (error) {
|
|
14384
14660
|
const message = error instanceof Error ? error.message : "checkout failed";
|
|
14385
14661
|
if (/worktree/i.test(message)) {
|
|
14386
|
-
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)
|
|
14662
|
+
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
|
|
14387
14663
|
}
|
|
14388
14664
|
try {
|
|
14389
14665
|
let effectiveBase = base;
|
|
14390
14666
|
if (effectiveBase === undefined) {
|
|
14391
14667
|
const lateResolved = baseBranch(cwd);
|
|
14392
14668
|
if ("error" in lateResolved)
|
|
14393
|
-
return failAfterStash(lateResolved.error);
|
|
14669
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
14394
14670
|
effectiveBase = lateResolved.base;
|
|
14395
14671
|
}
|
|
14396
14672
|
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
@@ -14437,7 +14713,7 @@ var branchSetup = ({
|
|
|
14437
14713
|
|
|
14438
14714
|
// packages/workit-core/src/core/pr-create.ts
|
|
14439
14715
|
import fs8 from "node:fs";
|
|
14440
|
-
import
|
|
14716
|
+
import path12 from "node:path";
|
|
14441
14717
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
14442
14718
|
function parseGhRepo(remote) {
|
|
14443
14719
|
remote = (remote || "").trim().replace(/\/+$/, "");
|
|
@@ -14502,11 +14778,11 @@ ${line}` : line;
|
|
|
14502
14778
|
}
|
|
14503
14779
|
function whichOnPath(tool) {
|
|
14504
14780
|
const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
|
|
14505
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
14781
|
+
for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
|
|
14506
14782
|
if (!dir)
|
|
14507
14783
|
continue;
|
|
14508
14784
|
for (const name of names) {
|
|
14509
|
-
const candidate =
|
|
14785
|
+
const candidate = path12.join(dir, name);
|
|
14510
14786
|
try {
|
|
14511
14787
|
fs8.accessSync(candidate, fs8.constants.X_OK);
|
|
14512
14788
|
return candidate;
|
|
@@ -14609,7 +14885,7 @@ function prCreate(env, cwd) {
|
|
|
14609
14885
|
const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
|
|
14610
14886
|
let baseUrl = cfg.youtrack_base_url;
|
|
14611
14887
|
if (!baseUrl) {
|
|
14612
|
-
const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
14888
|
+
const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path12.join(path12.dirname(String(cfg.configPath)), "youtrack.json");
|
|
14613
14889
|
try {
|
|
14614
14890
|
const yt = JSON.parse(fs8.readFileSync(ytCfg, "utf8"));
|
|
14615
14891
|
if (yt && typeof yt === "object")
|
|
@@ -14716,8 +14992,8 @@ function prCreate(env, cwd) {
|
|
|
14716
14992
|
|
|
14717
14993
|
// packages/workit-core/src/core/repo-context.ts
|
|
14718
14994
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
14719
|
-
import { existsSync as
|
|
14720
|
-
import
|
|
14995
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
14996
|
+
import path13 from "node:path";
|
|
14721
14997
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
14722
14998
|
try {
|
|
14723
14999
|
gitRevisionParts(value);
|
|
@@ -14851,7 +15127,7 @@ var findPrTemplate = (cwd) => {
|
|
|
14851
15127
|
]) {
|
|
14852
15128
|
const parts = rel.split("/");
|
|
14853
15129
|
const base = parts[parts.length - 1];
|
|
14854
|
-
const dir = parts.length > 1 ?
|
|
15130
|
+
const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
|
|
14855
15131
|
const actual = probeCaseInsensitive(dir, base);
|
|
14856
15132
|
if (actual)
|
|
14857
15133
|
return [...parts.slice(0, -1), actual].join("/");
|
|
@@ -14882,8 +15158,8 @@ var diffStatForRange = (cwd, range) => {
|
|
|
14882
15158
|
return runGit(cwd, ["diff", "--stat", "--"]).stdout;
|
|
14883
15159
|
};
|
|
14884
15160
|
var packageScripts = (cwd, keys) => {
|
|
14885
|
-
const pkgPath =
|
|
14886
|
-
if (!
|
|
15161
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
15162
|
+
if (!existsSync7(pkgPath))
|
|
14887
15163
|
return [];
|
|
14888
15164
|
try {
|
|
14889
15165
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
@@ -14908,9 +15184,9 @@ var documentationFiles = (cwd) => {
|
|
|
14908
15184
|
if (entry.isDirectory()) {
|
|
14909
15185
|
if (excluded.has(entry.name))
|
|
14910
15186
|
continue;
|
|
14911
|
-
walk(
|
|
15187
|
+
walk(path13.join(dir, entry.name), depth + 1);
|
|
14912
15188
|
} else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
|
|
14913
|
-
matches.push("./" +
|
|
15189
|
+
matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
|
|
14914
15190
|
}
|
|
14915
15191
|
}
|
|
14916
15192
|
};
|
|
@@ -14918,7 +15194,7 @@ var documentationFiles = (cwd) => {
|
|
|
14918
15194
|
return matches.sort().slice(0, 200);
|
|
14919
15195
|
};
|
|
14920
15196
|
var readTrimmed = (file, maxLines) => {
|
|
14921
|
-
if (!
|
|
15197
|
+
if (!existsSync7(file))
|
|
14922
15198
|
return "";
|
|
14923
15199
|
const lines = readFileSync7(file, "utf8").split(`
|
|
14924
15200
|
`);
|
|
@@ -14926,7 +15202,7 @@ var readTrimmed = (file, maxLines) => {
|
|
|
14926
15202
|
`);
|
|
14927
15203
|
};
|
|
14928
15204
|
function prReadyContext(root, range) {
|
|
14929
|
-
const cwd =
|
|
15205
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
14930
15206
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
14931
15207
|
return invalidRange(cwd);
|
|
14932
15208
|
let stdout = "";
|
|
@@ -14984,7 +15260,7 @@ ${status.stderr}` : "");
|
|
|
14984
15260
|
stdout += `template_path: ${template}
|
|
14985
15261
|
|
|
14986
15262
|
`;
|
|
14987
|
-
stdout += readTrimmed(
|
|
15263
|
+
stdout += readTrimmed(path13.join(cwd, template), 220);
|
|
14988
15264
|
} else {
|
|
14989
15265
|
stdout += `template_path: none
|
|
14990
15266
|
|
|
@@ -14994,7 +15270,7 @@ ${status.stderr}` : "");
|
|
|
14994
15270
|
stdout += `
|
|
14995
15271
|
`;
|
|
14996
15272
|
stdout += printSection("Recent Validation Signals");
|
|
14997
|
-
if (
|
|
15273
|
+
if (existsSync7(path13.join(cwd, "package.json"))) {
|
|
14998
15274
|
stdout += `package.json detected. Common scripts:
|
|
14999
15275
|
`;
|
|
15000
15276
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -15003,7 +15279,7 @@ ${status.stderr}` : "");
|
|
|
15003
15279
|
`) + `
|
|
15004
15280
|
`;
|
|
15005
15281
|
}
|
|
15006
|
-
if (
|
|
15282
|
+
if (existsSync7(path13.join(cwd, "Cargo.toml")) || existsSync7(path13.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
15007
15283
|
stdout += `Rust project detected.
|
|
15008
15284
|
`;
|
|
15009
15285
|
}
|
|
@@ -15036,7 +15312,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
|
|
|
15036
15312
|
- Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
|
|
15037
15313
|
- If Unreleased already has duplicate category headings, normalize_only first.`;
|
|
15038
15314
|
function changelogContext(root, range) {
|
|
15039
|
-
const cwd =
|
|
15315
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
15040
15316
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
15041
15317
|
return invalidRange(cwd);
|
|
15042
15318
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -15054,8 +15330,8 @@ function changelogContext(root, range) {
|
|
|
15054
15330
|
stdout += CHANGELOG_RULES + `
|
|
15055
15331
|
`;
|
|
15056
15332
|
stdout += printSection("Existing CHANGELOG.md");
|
|
15057
|
-
const changelogPath =
|
|
15058
|
-
if (
|
|
15333
|
+
const changelogPath = path13.join(cwd, "CHANGELOG.md");
|
|
15334
|
+
if (existsSync7(changelogPath)) {
|
|
15059
15335
|
stdout += readTrimmed(changelogPath, 260) + `
|
|
15060
15336
|
`;
|
|
15061
15337
|
} else {
|
|
@@ -15074,7 +15350,7 @@ function changelogContext(root, range) {
|
|
|
15074
15350
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
15075
15351
|
}
|
|
15076
15352
|
function docsRefreshContext(root, range) {
|
|
15077
|
-
const cwd =
|
|
15353
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
15078
15354
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
15079
15355
|
return invalidRange(cwd);
|
|
15080
15356
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -15096,8 +15372,8 @@ function docsRefreshContext(root, range) {
|
|
|
15096
15372
|
`) + `
|
|
15097
15373
|
`;
|
|
15098
15374
|
stdout += printSection("README Preview");
|
|
15099
|
-
const readme =
|
|
15100
|
-
if (
|
|
15375
|
+
const readme = path13.join(cwd, "README.md");
|
|
15376
|
+
if (existsSync7(readme)) {
|
|
15101
15377
|
stdout += readTrimmed(readme, 220) + `
|
|
15102
15378
|
`;
|
|
15103
15379
|
} else {
|
|
@@ -15105,8 +15381,8 @@ function docsRefreshContext(root, range) {
|
|
|
15105
15381
|
`;
|
|
15106
15382
|
}
|
|
15107
15383
|
stdout += printSection("Package Scripts");
|
|
15108
|
-
const pkgPath =
|
|
15109
|
-
if (
|
|
15384
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
15385
|
+
if (existsSync7(pkgPath)) {
|
|
15110
15386
|
try {
|
|
15111
15387
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
15112
15388
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
@@ -15119,7 +15395,7 @@ function docsRefreshContext(root, range) {
|
|
|
15119
15395
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
15120
15396
|
}
|
|
15121
15397
|
function releaseNotesContext(root, rangeOrTag) {
|
|
15122
|
-
const cwd =
|
|
15398
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
15123
15399
|
if (!rangeOrTag) {
|
|
15124
15400
|
return { stdout: "", stderr: `ERROR: release tag or range required
|
|
15125
15401
|
`, exitCode: 1, cwd };
|
|
@@ -15157,7 +15433,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
15157
15433
|
`;
|
|
15158
15434
|
stdout += printSection("Existing Release Files");
|
|
15159
15435
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
15160
|
-
if (
|
|
15436
|
+
if (existsSync7(path13.join(cwd, rel)))
|
|
15161
15437
|
stdout += rel + `
|
|
15162
15438
|
`;
|
|
15163
15439
|
}
|
|
@@ -15348,25 +15624,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
|
|
|
15348
15624
|
import fs10 from "node:fs";
|
|
15349
15625
|
import path14 from "node:path";
|
|
15350
15626
|
|
|
15351
|
-
// packages/workit-core/src/core/package-root.ts
|
|
15352
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
15353
|
-
import path13 from "node:path";
|
|
15354
|
-
import { fileURLToPath } from "node:url";
|
|
15355
|
-
var packageRoot = () => {
|
|
15356
|
-
let dir = path13.dirname(fileURLToPath(import.meta.url));
|
|
15357
|
-
while (true) {
|
|
15358
|
-
const parent = path13.dirname(dir);
|
|
15359
|
-
if (existsSync7(path13.join(dir, "package.json")) || parent === dir)
|
|
15360
|
-
return dir;
|
|
15361
|
-
dir = parent;
|
|
15362
|
-
}
|
|
15363
|
-
};
|
|
15364
|
-
var assetRoot = () => {
|
|
15365
|
-
const root = packageRoot();
|
|
15366
|
-
const assets = path13.join(root, "assets");
|
|
15367
|
-
return existsSync7(assets) ? assets : root;
|
|
15368
|
-
};
|
|
15369
|
-
|
|
15370
15627
|
// packages/workit-core/src/core/templates.ts
|
|
15371
15628
|
var repoRoot3 = assetRoot();
|
|
15372
15629
|
|
|
@@ -16572,6 +16829,94 @@ var safePath = (root, value) => {
|
|
|
16572
16829
|
return null;
|
|
16573
16830
|
}
|
|
16574
16831
|
};
|
|
16832
|
+
var branchCreationBaseline = (root, target, base) => {
|
|
16833
|
+
const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
|
|
16834
|
+
if (exists)
|
|
16835
|
+
return { base_branch: base, target_exists: true, remote_base: null };
|
|
16836
|
+
const remote = run3(root, ["ls-remote", "origin", `refs/heads/${base}`]);
|
|
16837
|
+
const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
|
|
16838
|
+
return {
|
|
16839
|
+
base_branch: base,
|
|
16840
|
+
target_exists: false,
|
|
16841
|
+
remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
|
|
16842
|
+
};
|
|
16843
|
+
};
|
|
16844
|
+
var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
|
|
16845
|
+
var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
|
|
16846
|
+
var actionProposalQuestion = (request, descriptorPayload) => {
|
|
16847
|
+
const payload = descriptorPayload ?? {};
|
|
16848
|
+
const resolved = payload.resolved ?? {};
|
|
16849
|
+
const payloadTarget = String(payload.target_branch ?? "");
|
|
16850
|
+
if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
|
|
16851
|
+
const steps = payload.plan_steps;
|
|
16852
|
+
return {
|
|
16853
|
+
presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
|
|
16854
|
+
approvedText: `Approve the ${steps.length} listed plan commits.`
|
|
16855
|
+
};
|
|
16856
|
+
}
|
|
16857
|
+
switch (request.operation) {
|
|
16858
|
+
case "git.branch_setup": {
|
|
16859
|
+
if (request.payload.action === "reapply_stash")
|
|
16860
|
+
return {
|
|
16861
|
+
presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
|
|
16862
|
+
approvedText: "Reapply the recorded stash."
|
|
16863
|
+
};
|
|
16864
|
+
const target = String(payload.target_branch ?? "");
|
|
16865
|
+
if (resolved.target_exists === true)
|
|
16866
|
+
return {
|
|
16867
|
+
presented: `Workit decision: action — Switch to branch \`${target}\`?`,
|
|
16868
|
+
approvedText: `Switch to \`${target}\`.`
|
|
16869
|
+
};
|
|
16870
|
+
const base = String(resolved.base_branch ?? "the base branch");
|
|
16871
|
+
return {
|
|
16872
|
+
presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
|
|
16873
|
+
approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
|
|
16874
|
+
};
|
|
16875
|
+
}
|
|
16876
|
+
case "git.commit": {
|
|
16877
|
+
const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
|
|
16878
|
+
return {
|
|
16879
|
+
presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
|
|
16880
|
+
approvedText: `Commit as "${String(request.payload.message)}".`
|
|
16881
|
+
};
|
|
16882
|
+
}
|
|
16883
|
+
case "git.push":
|
|
16884
|
+
return {
|
|
16885
|
+
presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
|
|
16886
|
+
approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
|
|
16887
|
+
};
|
|
16888
|
+
case "hosting.pull_request":
|
|
16889
|
+
return {
|
|
16890
|
+
presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
|
|
16891
|
+
approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
|
|
16892
|
+
};
|
|
16893
|
+
case "changelog.apply":
|
|
16894
|
+
return {
|
|
16895
|
+
presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
|
|
16896
|
+
approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
|
|
16897
|
+
};
|
|
16898
|
+
case "youtrack.update":
|
|
16899
|
+
return {
|
|
16900
|
+
presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
|
|
16901
|
+
approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
|
|
16902
|
+
};
|
|
16903
|
+
case "youtrack.time":
|
|
16904
|
+
return {
|
|
16905
|
+
presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
|
|
16906
|
+
approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
|
|
16907
|
+
};
|
|
16908
|
+
case "youtrack.meeting":
|
|
16909
|
+
return {
|
|
16910
|
+
presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
|
|
16911
|
+
approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
|
|
16912
|
+
};
|
|
16913
|
+
default:
|
|
16914
|
+
return {
|
|
16915
|
+
presented: `Workit decision: action — Approve ${request.operation}?`,
|
|
16916
|
+
approvedText: `Approve ${request.operation}.`
|
|
16917
|
+
};
|
|
16918
|
+
}
|
|
16919
|
+
};
|
|
16575
16920
|
var approvedResolvedExternalAction = (content) => {
|
|
16576
16921
|
try {
|
|
16577
16922
|
const value = JSON.parse(content);
|
|
@@ -16835,24 +17180,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16835
17180
|
const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
|
|
16836
17181
|
if (!sdd_dir)
|
|
16837
17182
|
return failure("invalid_input", "sdd_dir must stay inside the workspace");
|
|
17183
|
+
const action = request.payload.action ?? "setup";
|
|
16838
17184
|
const policy = resolveBranchPolicyFor(root);
|
|
16839
|
-
const target_branch = request.payload.target_branch
|
|
17185
|
+
const target_branch = request.payload.target_branch;
|
|
17186
|
+
if (action !== "reapply_stash" && !target_branch)
|
|
17187
|
+
return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
|
|
17188
|
+
outcome: "not_started",
|
|
17189
|
+
fields: [{ path: "target_branch", reason: "required for action=setup" }]
|
|
17190
|
+
});
|
|
16840
17191
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
16841
17192
|
if (!head)
|
|
16842
17193
|
return failure("storage_error", "current Git commit could not be resolved");
|
|
17194
|
+
const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
|
|
17195
|
+
const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
|
|
16843
17196
|
const normalized = {
|
|
16844
17197
|
operation: request.operation,
|
|
16845
17198
|
payload: {
|
|
16846
17199
|
...request.payload,
|
|
16847
|
-
action
|
|
17200
|
+
action,
|
|
16848
17201
|
sdd_dir,
|
|
16849
17202
|
stash: request.payload.stash ?? "no",
|
|
16850
|
-
target_branch
|
|
17203
|
+
...target_branch ? { target_branch } : {}
|
|
16851
17204
|
}
|
|
16852
17205
|
};
|
|
16853
17206
|
return success(null, null, {
|
|
16854
17207
|
request: normalized,
|
|
16855
|
-
descriptorPayload: {
|
|
17208
|
+
descriptorPayload: {
|
|
17209
|
+
...normalized.payload,
|
|
17210
|
+
resolved: { head, dirty, ...creation }
|
|
17211
|
+
}
|
|
16856
17212
|
});
|
|
16857
17213
|
}
|
|
16858
17214
|
case "git.push": {
|
|
@@ -16866,7 +17222,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16866
17222
|
}) : failure("invalid_input", "git push requires a resolvable current branch");
|
|
16867
17223
|
}
|
|
16868
17224
|
case "git.commit": {
|
|
17225
|
+
const planSteps = request.payload.plan_steps;
|
|
17226
|
+
const planBranch = request.payload.plan_branch;
|
|
16869
17227
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
17228
|
+
if (!head)
|
|
17229
|
+
return failure("storage_error", "Git state could not be resolved");
|
|
17230
|
+
const branch = gitValue(root, ["branch", "--show-current"]);
|
|
17231
|
+
if (planSteps !== undefined || planBranch !== undefined) {
|
|
17232
|
+
if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
|
|
17233
|
+
return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
|
|
17234
|
+
outcome: "not_started",
|
|
17235
|
+
fields: [{ path: "plan_steps", reason: "unique strings" }]
|
|
17236
|
+
});
|
|
17237
|
+
if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
|
|
17238
|
+
return failure("invalid_input", "plan_branch must name the current working branch", {
|
|
17239
|
+
outcome: "not_started",
|
|
17240
|
+
fields: [{ path: "plan_branch", reason: "current branch" }]
|
|
17241
|
+
});
|
|
17242
|
+
if (isProtectedBranch2(root, planBranch))
|
|
17243
|
+
return failure("invalid_input", "plan_branch must not be a protected branch", {
|
|
17244
|
+
outcome: "not_started",
|
|
17245
|
+
fields: [{ path: "plan_branch", reason: "protected branch" }]
|
|
17246
|
+
});
|
|
17247
|
+
return success(null, null, {
|
|
17248
|
+
request,
|
|
17249
|
+
descriptorPayload: {
|
|
17250
|
+
plan_steps: planSteps,
|
|
17251
|
+
plan_branch: planBranch,
|
|
17252
|
+
resolved: { head, branch, steps: planSteps }
|
|
17253
|
+
}
|
|
17254
|
+
});
|
|
17255
|
+
}
|
|
16870
17256
|
const staged = gitRaw(root, [
|
|
16871
17257
|
"diff",
|
|
16872
17258
|
"--cached",
|
|
@@ -16877,7 +17263,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16877
17263
|
"HEAD"
|
|
16878
17264
|
]);
|
|
16879
17265
|
const paths = stagedPaths(root);
|
|
16880
|
-
|
|
17266
|
+
const message = request.payload.message;
|
|
17267
|
+
if (typeof message !== "string" || !message)
|
|
17268
|
+
return failure("invalid_input", "git.commit requires a commit message", {
|
|
17269
|
+
outcome: "not_started",
|
|
17270
|
+
fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
|
|
17271
|
+
});
|
|
17272
|
+
if (staged === null || paths === null)
|
|
16881
17273
|
return failure("storage_error", "Git state could not be resolved");
|
|
16882
17274
|
let policy;
|
|
16883
17275
|
try {
|
|
@@ -16892,13 +17284,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16892
17284
|
`).filter(Boolean));
|
|
16893
17285
|
flavor = detected.flavor ?? "conventional";
|
|
16894
17286
|
}
|
|
16895
|
-
if (!matchCommitFlavor(
|
|
17287
|
+
if (!matchCommitFlavor(message, flavor, policy.pattern))
|
|
16896
17288
|
return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
|
|
16897
17289
|
return success(null, null, {
|
|
16898
17290
|
request,
|
|
16899
17291
|
descriptorPayload: {
|
|
16900
17292
|
...request.payload,
|
|
16901
|
-
resolved: { head, staged: sha256(staged), paths }
|
|
17293
|
+
resolved: { head, branch, staged: sha256(staged), paths }
|
|
16902
17294
|
}
|
|
16903
17295
|
});
|
|
16904
17296
|
}
|
|
@@ -17068,10 +17460,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
17068
17460
|
sdd_dir: request.payload.sdd_dir ?? "docs",
|
|
17069
17461
|
workspace_root: root
|
|
17070
17462
|
});
|
|
17071
|
-
|
|
17463
|
+
if ("error" in result) {
|
|
17464
|
+
if (result.phase === "preflight")
|
|
17465
|
+
return failure("invalid_input", result.error, {
|
|
17466
|
+
outcome: "not_started",
|
|
17467
|
+
operation: request.operation
|
|
17468
|
+
});
|
|
17469
|
+
return unknown2(request.operation);
|
|
17470
|
+
}
|
|
17471
|
+
return success(null, null, result);
|
|
17072
17472
|
}
|
|
17073
17473
|
case "git.commit": {
|
|
17074
|
-
const
|
|
17474
|
+
const message = request.payload.message;
|
|
17475
|
+
if (typeof message !== "string" || !message)
|
|
17476
|
+
return failure("invalid_input", "git.commit requires a commit message", {
|
|
17477
|
+
outcome: "not_started",
|
|
17478
|
+
fields: [{ path: "message", reason: "required" }]
|
|
17479
|
+
});
|
|
17480
|
+
const result = run3(root, ["commit", "-m", message]);
|
|
17075
17481
|
return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
|
|
17076
17482
|
}
|
|
17077
17483
|
case "git.push": {
|
|
@@ -17244,8 +17650,34 @@ var nativeAuthority = (actor, reconciliationTokens = new WeakSet) => ({
|
|
|
17244
17650
|
var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
|
|
17245
17651
|
const store = new TaskStore(root);
|
|
17246
17652
|
const selected = approvedExternalAction(store, "pi", actor, operation);
|
|
17247
|
-
if (!selected.ok)
|
|
17653
|
+
if (!selected.ok) {
|
|
17654
|
+
const plan = planCommitBinding(store, "pi", actor, operation);
|
|
17655
|
+
if (plan) {
|
|
17656
|
+
const actionRef = externalActionRef("pi", actor, operation);
|
|
17657
|
+
return {
|
|
17658
|
+
taskId: plan.taskId,
|
|
17659
|
+
decisionId: plan.decisionId,
|
|
17660
|
+
actionRef,
|
|
17661
|
+
expectedRevision: plan.expectedRevision,
|
|
17662
|
+
expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
|
|
17663
|
+
binding: plan.binding,
|
|
17664
|
+
step: plan.step,
|
|
17665
|
+
refresh: () => {
|
|
17666
|
+
const task = store.readTask(plan.taskId);
|
|
17667
|
+
const freshWorkspace = store.readWorkspace();
|
|
17668
|
+
if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
|
|
17669
|
+
throw new Error("external action state changed");
|
|
17670
|
+
return {
|
|
17671
|
+
expectedRevision: task.data.revision,
|
|
17672
|
+
expectedWorkspaceRevision: freshWorkspace.data.revision
|
|
17673
|
+
};
|
|
17674
|
+
},
|
|
17675
|
+
reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
|
|
17676
|
+
settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
|
|
17677
|
+
};
|
|
17678
|
+
}
|
|
17248
17679
|
return selected;
|
|
17680
|
+
}
|
|
17249
17681
|
const actionRef = externalActionRef("pi", actor, operation);
|
|
17250
17682
|
return {
|
|
17251
17683
|
taskId: selected.data.task.id,
|
|
@@ -17311,6 +17743,21 @@ var executeFamily = async (family, toolCallId, input, ctx) => {
|
|
|
17311
17743
|
operation: "decision.record"
|
|
17312
17744
|
}));
|
|
17313
17745
|
const decision = parsed.data;
|
|
17746
|
+
const budgetIssue = workitBindingQuestionIssue([
|
|
17747
|
+
{
|
|
17748
|
+
header: `Workit decision: ${parsed.data.purpose}`,
|
|
17749
|
+
question: decision.binding.presented,
|
|
17750
|
+
options: [
|
|
17751
|
+
{
|
|
17752
|
+
label: "approved",
|
|
17753
|
+
description: decision.binding.displayed ?? decision.binding.approvedContent
|
|
17754
|
+
},
|
|
17755
|
+
{ label: "rejected", description: "Reject this decision" }
|
|
17756
|
+
]
|
|
17757
|
+
}
|
|
17758
|
+
]);
|
|
17759
|
+
if (budgetIssue)
|
|
17760
|
+
return output(failure("invalid_input", budgetIssue));
|
|
17314
17761
|
const approved = await ctx.ui.confirm("Workit decision", decision.binding.presented);
|
|
17315
17762
|
if (approved !== (decision.response === "approved"))
|
|
17316
17763
|
return output(failure("permission_denied", "Pi decision answer did not match the requested response"));
|
|
@@ -17383,6 +17830,22 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17383
17830
|
...context,
|
|
17384
17831
|
nativeAuthority: nativeAuthority(actor, reconciliationTokens)
|
|
17385
17832
|
});
|
|
17833
|
+
const localOperation = [
|
|
17834
|
+
"git.branch_setup",
|
|
17835
|
+
"git.commit",
|
|
17836
|
+
"git.push",
|
|
17837
|
+
"hosting.pull_request",
|
|
17838
|
+
"changelog.apply"
|
|
17839
|
+
].includes(resolved.data.request.operation);
|
|
17840
|
+
if (localOperation) {
|
|
17841
|
+
const writer = assertLocalExternalActionWriter(ctx.cwd, { host: "pi", actor });
|
|
17842
|
+
if (!writer.ok)
|
|
17843
|
+
return output(failure("needs_input", "writer ownership is required before this action", {
|
|
17844
|
+
outcome: "not_started",
|
|
17845
|
+
operation: resolved.data.request.operation,
|
|
17846
|
+
guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
|
|
17847
|
+
}));
|
|
17848
|
+
}
|
|
17386
17849
|
const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
|
|
17387
17850
|
const priorRequest = priorExternalAction(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload);
|
|
17388
17851
|
if (priorRequest.ok && priorRequest.data.entry.data.consumption?.state === "uncertain") {
|
|
@@ -17421,8 +17884,18 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17421
17884
|
}
|
|
17422
17885
|
if (priorRequest.ok && priorRequest.data.entry.data.consumption !== null)
|
|
17423
17886
|
return output(failure("permission_denied", "external action was already settled"));
|
|
17887
|
+
const drift = resolved.data.request.operation === "git.branch_setup" ? priorResolvedDrift(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload, resolved.data.descriptorPayload.resolved) : success(null, null, null);
|
|
17888
|
+
if (!drift.ok)
|
|
17889
|
+
return output(drift);
|
|
17424
17890
|
const existing = approvedExternalAction(store, "pi", actor, descriptor);
|
|
17425
17891
|
if (!existing.ok) {
|
|
17892
|
+
const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
|
|
17893
|
+
const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
|
|
17894
|
+
const plan = typeof commitMessage === "string" && commitMessage ? approvedPlanCommit(store, "pi", actor, commitMessage) : null;
|
|
17895
|
+
if (plan?.ok && plan.data.branch === currentBranch) {
|
|
17896
|
+
const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
|
|
17897
|
+
return output(result);
|
|
17898
|
+
}
|
|
17426
17899
|
const prior = externalActionState(store, "pi", actor, descriptor);
|
|
17427
17900
|
if (prior.ok && prior.data.entry.data.consumption !== null)
|
|
17428
17901
|
return output(failure("permission_denied", "external action was already settled"));
|
|
@@ -17434,7 +17907,8 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17434
17907
|
if (candidates.length !== 1)
|
|
17435
17908
|
return output(failure("permission_denied", "external action requires exactly one active Pi task"));
|
|
17436
17909
|
const task = candidates[0];
|
|
17437
|
-
const
|
|
17910
|
+
const question = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
|
|
17911
|
+
const approved = await ctx.ui.confirm(question.presented, question.approvedText);
|
|
17438
17912
|
if (!approved)
|
|
17439
17913
|
return output(failure("permission_denied", "external action was not approved"));
|
|
17440
17914
|
const currentWorkspace = store.readWorkspace();
|
|
@@ -17450,8 +17924,9 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17450
17924
|
taskId: task.id,
|
|
17451
17925
|
workspaceId: currentWorkspace.data.id,
|
|
17452
17926
|
scope: task.intent.data.scope,
|
|
17453
|
-
presented:
|
|
17927
|
+
presented: question.presented,
|
|
17454
17928
|
approvedContent: descriptor,
|
|
17929
|
+
displayed: question.approvedText,
|
|
17455
17930
|
contentRefs: []
|
|
17456
17931
|
},
|
|
17457
17932
|
response: "approved",
|
|
@@ -17460,6 +17935,9 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17460
17935
|
if (!decision.ok)
|
|
17461
17936
|
return output(decision);
|
|
17462
17937
|
}
|
|
17938
|
+
const planSteps = resolved.data.descriptorPayload.plan_steps;
|
|
17939
|
+
if (resolved.data.request.operation === "git.commit" && Array.isArray(planSteps))
|
|
17940
|
+
return output(success(null, null, { plan_commits: planSteps.length }));
|
|
17463
17941
|
const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
|
|
17464
17942
|
return output(result);
|
|
17465
17943
|
}
|