@brainervirus/workit-pi 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/worker.js +21 -5
- package/dist/workit.js +887 -348
- package/package.json +1 -1
- package/skills/workit-babysit/SKILL.md +3 -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,
|
|
@@ -7834,7 +7856,7 @@ var workerReportSchema = object({
|
|
|
7834
7856
|
}).strict();
|
|
7835
7857
|
var workerSchema = object({
|
|
7836
7858
|
assignment: assignmentSchema,
|
|
7837
|
-
state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
|
|
7859
|
+
state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
|
|
7838
7860
|
session: refSchema.nullable(),
|
|
7839
7861
|
report: workerReportSchema.nullable()
|
|
7840
7862
|
}).strict();
|
|
@@ -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");
|
|
9838
|
+
}
|
|
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;
|
|
9732
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);
|
|
9733
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)
|
|
@@ -10659,6 +10841,106 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
10659
10841
|
});
|
|
10660
10842
|
}
|
|
10661
10843
|
|
|
10844
|
+
// packages/workit-core/src/core/methods.ts
|
|
10845
|
+
var METHODS = {
|
|
10846
|
+
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
10847
|
+
"workit-behavioral-tdd": { dimensions: ["testing"] },
|
|
10848
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
|
|
10849
|
+
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10850
|
+
"workit-implement": { dimensions: ["delegation"] },
|
|
10851
|
+
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
10852
|
+
"workit-handoff": { ruleIds: ["durable-handoff"] },
|
|
10853
|
+
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
10854
|
+
};
|
|
10855
|
+
var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
10856
|
+
var assuranceFor = (id, definition, capabilities) => {
|
|
10857
|
+
const keys = new Set([
|
|
10858
|
+
id,
|
|
10859
|
+
...definition.dimensions ?? [],
|
|
10860
|
+
...definition.ruleIds ?? []
|
|
10861
|
+
]);
|
|
10862
|
+
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
10863
|
+
if (id === "workit-review" && matches.length === 0)
|
|
10864
|
+
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
10865
|
+
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
10866
|
+
return {
|
|
10867
|
+
assurance: "unavailable",
|
|
10868
|
+
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
10869
|
+
};
|
|
10870
|
+
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
10871
|
+
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
10872
|
+
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
10873
|
+
};
|
|
10874
|
+
function selectMethods(policy, capabilities) {
|
|
10875
|
+
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
10876
|
+
const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
|
|
10877
|
+
if (requirements.length === 0)
|
|
10878
|
+
return [];
|
|
10879
|
+
const capability = assuranceFor(id, definition, capabilities);
|
|
10880
|
+
const reasons = [
|
|
10881
|
+
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
10882
|
+
...capability.reasons
|
|
10883
|
+
];
|
|
10884
|
+
if (capability.assurance === "unavailable")
|
|
10885
|
+
reasons.unshift("required capability is unavailable");
|
|
10886
|
+
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
10887
|
+
});
|
|
10888
|
+
}
|
|
10889
|
+
var invariantBootstrap = () => `
|
|
10890
|
+
Workit keeps one accountable lead and one shared task state. Inspect current task
|
|
10891
|
+
state before acting. An empty task.list means no session yet, not permission to
|
|
10892
|
+
skip Workit; for user-requested product, debug, or behavior work with no active
|
|
10893
|
+
or paused task, run task.start then policy.assess before other product mutations.
|
|
10894
|
+
Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
|
|
10895
|
+
to the current records; explicit values are still concurrency-checked, so never
|
|
10896
|
+
copy revisions between calls. Happy path: task.list, task.start {intent} with no
|
|
10897
|
+
revisions, policy.assess {assessment: {facts, signals, consequences,
|
|
10898
|
+
verification}} where facts are inferred or observed with file refs,
|
|
10899
|
+
writer.acquire {taskId} before product writes, evidence.record {evidence} where
|
|
10900
|
+
check and review kinds auto-bind the current tree and need no digests. Record
|
|
10901
|
+
self-review requirements with kind review and reviewContext matching your own
|
|
10902
|
+
host session; the lead may also record checks. Fresh-context review requires a
|
|
10903
|
+
session distinct from the task creator and every other evidence recorder. Record
|
|
10904
|
+
verification evidence last: later evidence or commits can reopen fixed findings
|
|
10905
|
+
for re-verification, so verify, resolve, then close with nothing in between.
|
|
10906
|
+
decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
10907
|
+
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10908
|
+
header Workit decision: <purpose>, the same label repeated in the question
|
|
10909
|
+
text (some host UIs do not render headers), exactly two options approved and
|
|
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,
|
|
10922
|
+
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10923
|
+
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10924
|
+
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
10925
|
+
without a bound candidate goes stale; findings close only as fixed with passing
|
|
10926
|
+
verification, dismissed with supporting evidence, or deferred under an approved
|
|
10927
|
+
limitation. Use only the
|
|
10928
|
+
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
10929
|
+
and state changes. Authority is bounded by the requested scope, current
|
|
10930
|
+
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
10931
|
+
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
10932
|
+
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
10933
|
+
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
10934
|
+
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
10935
|
+
forgotten items mid-task → load workit-steer before responding; about to open
|
|
10936
|
+
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
10937
|
+
workit-green-run; small-looking change before close or merge → load
|
|
10938
|
+
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
10939
|
+
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
10940
|
+
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
10941
|
+
act from memory of it.
|
|
10942
|
+
`.trim();
|
|
10943
|
+
|
|
10662
10944
|
// packages/workit-core/src/core/task-context.ts
|
|
10663
10945
|
function reconcileResume(view, observations = []) {
|
|
10664
10946
|
if (observations.length > 0)
|
|
@@ -10668,7 +10950,7 @@ function reconcileResume(view, observations = []) {
|
|
|
10668
10950
|
return candidate;
|
|
10669
10951
|
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
10670
10952
|
const blockers = [...view.task.progress.blockers];
|
|
10671
|
-
if (view.task.workers.some((entry) =>
|
|
10953
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
10672
10954
|
blockers.push({
|
|
10673
10955
|
reason: "worker state requires reconciliation",
|
|
10674
10956
|
dependentAction: "resume",
|
|
@@ -10718,6 +11000,11 @@ function compactTaskContext(view) {
|
|
|
10718
11000
|
digest: entry.data.digest,
|
|
10719
11001
|
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
10720
11002
|
}));
|
|
11003
|
+
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
11004
|
+
id: method.id,
|
|
11005
|
+
assurance: method.assurance,
|
|
11006
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
11007
|
+
})) : [];
|
|
10721
11008
|
const gaps = Array.from(new Set([
|
|
10722
11009
|
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
10723
11010
|
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
@@ -10727,15 +11014,18 @@ function compactTaskContext(view) {
|
|
|
10727
11014
|
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
10728
11015
|
status: view.task.status,
|
|
10729
11016
|
decisions,
|
|
11017
|
+
methods,
|
|
10730
11018
|
gaps,
|
|
10731
11019
|
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
10732
11020
|
};
|
|
10733
11021
|
let encoded = JSON.stringify(context);
|
|
10734
|
-
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
|
|
11022
|
+
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
10735
11023
|
if (context.gaps.length)
|
|
10736
11024
|
context.gaps.pop();
|
|
10737
|
-
else
|
|
11025
|
+
else if (context.decisions.length)
|
|
10738
11026
|
context.decisions.pop();
|
|
11027
|
+
else
|
|
11028
|
+
context.methods.pop();
|
|
10739
11029
|
encoded = JSON.stringify(context);
|
|
10740
11030
|
}
|
|
10741
11031
|
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
@@ -10751,95 +11041,49 @@ var exportDigest = (bundle) => sha256({
|
|
|
10751
11041
|
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
10752
11042
|
task: bundle.task
|
|
10753
11043
|
});
|
|
10754
|
-
// packages/workit-core/src/core/
|
|
10755
|
-
var
|
|
10756
|
-
"
|
|
10757
|
-
"
|
|
10758
|
-
"workit-review": { dimensions: ["review"] },
|
|
10759
|
-
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
10760
|
-
"workit-implement": { dimensions: ["delegation"] },
|
|
10761
|
-
"workit-debug": { ruleIds: ["root-cause-investigation"] },
|
|
10762
|
-
"workit-handoff": { ruleIds: ["durable-handoff"] },
|
|
10763
|
-
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
11044
|
+
// packages/workit-core/src/core/route-intent.ts
|
|
11045
|
+
var GUIDANCE = {
|
|
11046
|
+
"git.branch_setup": "use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
|
|
11047
|
+
"hosting.pull_request": "use the Workit hosting.pull_request action (workit_external_action in hosts, or `workit action hosting.pull_request --payload '{...}' --confirm --json` in the CLI), then drive the created PR through workit-babysit"
|
|
10764
11048
|
};
|
|
10765
|
-
var
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
id,
|
|
10769
|
-
...definition.dimensions ?? [],
|
|
10770
|
-
...definition.ruleIds ?? []
|
|
10771
|
-
]);
|
|
10772
|
-
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
10773
|
-
if (id === "workit-review" && matches.length === 0)
|
|
10774
|
-
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
10775
|
-
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
10776
|
-
return {
|
|
10777
|
-
assurance: "unavailable",
|
|
10778
|
-
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
10779
|
-
};
|
|
10780
|
-
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
10781
|
-
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
10782
|
-
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
11049
|
+
var unbalancedQuotes = (segment) => {
|
|
11050
|
+
const text = segment.replace(/\\./gu, "");
|
|
11051
|
+
return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
|
|
10783
11052
|
};
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
11053
|
+
var segmentTokens = (segment) => {
|
|
11054
|
+
if (segment.includes("`") || segment.includes("$("))
|
|
11055
|
+
return null;
|
|
11056
|
+
if (unbalancedQuotes(segment))
|
|
11057
|
+
return null;
|
|
11058
|
+
const parts = segment.trim().split(/\s+/u);
|
|
11059
|
+
while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
|
|
11060
|
+
parts.shift();
|
|
11061
|
+
return parts.length > 0 ? parts : null;
|
|
11062
|
+
};
|
|
11063
|
+
var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
|
|
11064
|
+
function shellRouteIntent(command) {
|
|
11065
|
+
if (typeof command !== "string" || command.trim() === "")
|
|
11066
|
+
return null;
|
|
11067
|
+
for (const raw of command.split(/&&|\|\||;|\n/u)) {
|
|
11068
|
+
const parts = segmentTokens(raw);
|
|
11069
|
+
if (!parts)
|
|
11070
|
+
continue;
|
|
11071
|
+
const head = unquote(parts[0]);
|
|
11072
|
+
const second = unquote(parts[1] ?? "");
|
|
11073
|
+
const third = unquote(parts[2] ?? "");
|
|
11074
|
+
if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
|
|
11075
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
11076
|
+
if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
|
|
11077
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
11078
|
+
if (head === "gh" && second === "pr" && third === "create")
|
|
11079
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
11080
|
+
if (head === "glab" && second === "mr" && third === "create")
|
|
11081
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
11082
|
+
}
|
|
11083
|
+
return null;
|
|
10798
11084
|
}
|
|
10799
|
-
var invariantBootstrap = () => `
|
|
10800
|
-
Workit keeps one accountable lead and one shared task state. Inspect current task
|
|
10801
|
-
state before acting. An empty task.list means no session yet, not permission to
|
|
10802
|
-
skip Workit; for user-requested product, debug, or behavior work with no active
|
|
10803
|
-
or paused task, run task.start then policy.assess before other product mutations.
|
|
10804
|
-
Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
|
|
10805
|
-
to the current records; explicit values are still concurrency-checked, so never
|
|
10806
|
-
copy revisions between calls. Happy path: task.list, task.start {intent} with no
|
|
10807
|
-
revisions, policy.assess {assessment: {facts, signals, consequences,
|
|
10808
|
-
verification}} where facts are inferred or observed with file refs,
|
|
10809
|
-
writer.acquire {taskId} before product writes, evidence.record {evidence} where
|
|
10810
|
-
check and review kinds auto-bind the current tree and need no digests. Record
|
|
10811
|
-
self-review requirements with kind review and reviewContext matching your own
|
|
10812
|
-
host session; the lead may also record checks. Fresh-context review requires a
|
|
10813
|
-
session distinct from the task creator and every other evidence recorder. Record
|
|
10814
|
-
verification evidence last: later evidence or commits can reopen fixed findings
|
|
10815
|
-
for re-verification, so verify, resolve, then close with nothing in between.
|
|
10816
|
-
decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
10817
|
-
approvedContent} only through a native approval question asked receipt-shaped:
|
|
10818
|
-
header Workit decision: <purpose>, the same label repeated in the question
|
|
10819
|
-
text (some host UIs do not render headers), exactly two options approved and
|
|
10820
|
-
rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
|
|
10821
|
-
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
10822
|
-
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
10823
|
-
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
10824
|
-
without a bound candidate goes stale; findings close only as fixed with passing
|
|
10825
|
-
verification, dismissed with supporting evidence, or deferred under an approved
|
|
10826
|
-
limitation. Use only the
|
|
10827
|
-
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
10828
|
-
and state changes. Authority is bounded by the requested scope, current
|
|
10829
|
-
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
10830
|
-
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
10831
|
-
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
10832
|
-
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
10833
|
-
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
10834
|
-
forgotten items mid-task → load workit-steer before responding; about to open
|
|
10835
|
-
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
10836
|
-
workit-green-run; small-looking change before close or merge → load
|
|
10837
|
-
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
10838
|
-
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
10839
|
-
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
10840
|
-
act from memory of it.
|
|
10841
|
-
`.trim();
|
|
10842
11085
|
// packages/workit-core/src/core/external-action.ts
|
|
11086
|
+
import { execFileSync } from "node:child_process";
|
|
10843
11087
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
10844
11088
|
object({
|
|
10845
11089
|
operation: literal("git.branch_setup"),
|
|
@@ -10852,7 +11096,11 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
10852
11096
|
}).strict(),
|
|
10853
11097
|
object({
|
|
10854
11098
|
operation: literal("git.commit"),
|
|
10855
|
-
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()
|
|
10856
11104
|
}).strict(),
|
|
10857
11105
|
object({
|
|
10858
11106
|
operation: literal("git.push"),
|
|
@@ -10941,7 +11189,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
10941
11189
|
payload,
|
|
10942
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 } : {}
|
|
10943
11191
|
});
|
|
10944
|
-
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).";
|
|
10945
11193
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
10946
11194
|
kind: "host",
|
|
10947
11195
|
host,
|
|
@@ -10992,6 +11240,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
|
|
|
10992
11240
|
return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
|
|
10993
11241
|
return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
|
|
10994
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
|
+
};
|
|
10995
11329
|
var approvedExternalAction = (store, host, actor, operation) => {
|
|
10996
11330
|
const selected = externalActionState(store, host, actor, operation);
|
|
10997
11331
|
if (!selected.ok)
|
|
@@ -11000,6 +11334,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
|
|
|
11000
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");
|
|
11001
11335
|
return selected;
|
|
11002
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
|
+
};
|
|
11003
11354
|
var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
|
|
11004
11355
|
let steps = [];
|
|
11005
11356
|
try {
|
|
@@ -11277,7 +11628,7 @@ function resolveRequirements(input) {
|
|
|
11277
11628
|
].join(", ")}`,
|
|
11278
11629
|
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
11279
11630
|
before: "dependent_action",
|
|
11280
|
-
dependentAction:
|
|
11631
|
+
dependentAction: null,
|
|
11281
11632
|
acceptanceAllowed: false
|
|
11282
11633
|
}));
|
|
11283
11634
|
if (signals.productChoiceOpen.value === true)
|
|
@@ -11288,7 +11639,7 @@ function resolveRequirements(input) {
|
|
|
11288
11639
|
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
11289
11640
|
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
11290
11641
|
before: "dependent_action",
|
|
11291
|
-
dependentAction:
|
|
11642
|
+
dependentAction: null,
|
|
11292
11643
|
acceptanceAllowed: false
|
|
11293
11644
|
}));
|
|
11294
11645
|
if (signals.behaviorChange.value === true)
|
|
@@ -11446,59 +11797,6 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
11446
11797
|
return parsed.data;
|
|
11447
11798
|
}
|
|
11448
11799
|
|
|
11449
|
-
// packages/workit-core/src/core/workers.ts
|
|
11450
|
-
var same = (left, right) => {
|
|
11451
|
-
try {
|
|
11452
|
-
return canonicalJson(left) === canonicalJson(right);
|
|
11453
|
-
} catch {
|
|
11454
|
-
return false;
|
|
11455
|
-
}
|
|
11456
|
-
};
|
|
11457
|
-
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
11458
|
-
var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
|
|
11459
|
-
const code = char.charCodeAt(0);
|
|
11460
|
-
return code < 32 || code === 127;
|
|
11461
|
-
}) && !value.split(/[\\/]/).includes("..");
|
|
11462
|
-
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
11463
|
-
var workerIdOf = (value) => value.workerId ?? null;
|
|
11464
|
-
function assertProductWriteAllowed(input) {
|
|
11465
|
-
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
11466
|
-
return failure("invalid_input", "invalid product write path");
|
|
11467
|
-
if (!input.store)
|
|
11468
|
-
return failure("permission_denied", "write authorization requires core state");
|
|
11469
|
-
const task = input.store.readTask(input.task.id);
|
|
11470
|
-
if (!task.ok)
|
|
11471
|
-
return task;
|
|
11472
|
-
const workspace = input.store.readWorkspace();
|
|
11473
|
-
if (!workspace.ok)
|
|
11474
|
-
return workspace;
|
|
11475
|
-
if (!workspace.data)
|
|
11476
|
-
return failure("not_found", "workspace not found");
|
|
11477
|
-
const workspaceRecord = workspace.data;
|
|
11478
|
-
if (task.data.status !== "active")
|
|
11479
|
-
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
11480
|
-
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
11481
|
-
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
11482
|
-
const writer = workspaceRecord.writer;
|
|
11483
|
-
if (!writer)
|
|
11484
|
-
return failure("permission_denied", "checkout has no writer owner");
|
|
11485
|
-
if (writer.state === "uncertain")
|
|
11486
|
-
return failure("recovery_required", "checkout writer requires recovery", {
|
|
11487
|
-
owner: writer.owner
|
|
11488
|
-
});
|
|
11489
|
-
const caller = callerValue(input.caller);
|
|
11490
|
-
const workerId = workerIdOf(input.caller);
|
|
11491
|
-
const owner = writer.owner;
|
|
11492
|
-
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
11493
|
-
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
11494
|
-
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
11495
|
-
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
11496
|
-
return failure("permission_denied", "only implementers can own product writes");
|
|
11497
|
-
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))))
|
|
11498
|
-
return failure("recovery_required", "worker session is not an active writer");
|
|
11499
|
-
return success(null, workspaceRecord.revision, owner);
|
|
11500
|
-
}
|
|
11501
|
-
|
|
11502
11800
|
// packages/workit-core/src/core/task-engine.ts
|
|
11503
11801
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
11504
11802
|
kind,
|
|
@@ -11617,10 +11915,10 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11617
11915
|
const notStarted = input.dispatch === true;
|
|
11618
11916
|
if (notStarted && (input.state !== "stopped" || input.session !== null))
|
|
11619
11917
|
return failure("invalid_input", "a never-dispatched worker stops with no session");
|
|
11620
|
-
if (notStarted &&
|
|
11621
|
-
return failure("invalid_transition", "worker is no longer awaiting dispatch");
|
|
11622
|
-
if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
|
|
11918
|
+
if (notStarted && entry.data.session !== null)
|
|
11623
11919
|
return failure("invalid_transition", "worker already has an observed session");
|
|
11920
|
+
if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
|
|
11921
|
+
return failure("invalid_transition", "worker launch was not claimed");
|
|
11624
11922
|
if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
|
|
11625
11923
|
return failure("invalid_input", "running worker observations require a session");
|
|
11626
11924
|
if (entry.data.session && !sameValue(entry.data.session, input.session))
|
|
@@ -11629,15 +11927,20 @@ var applyWorkerLifecycle = (input) => {
|
|
|
11629
11927
|
return failure("invalid_transition", "worker has not started");
|
|
11630
11928
|
if (input.state === "running" && entry.data.state === "stopped")
|
|
11631
11929
|
return failure("invalid_transition", "stopped worker cannot run again");
|
|
11930
|
+
const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
|
|
11632
11931
|
const nextEntry = {
|
|
11633
11932
|
...entry,
|
|
11634
11933
|
recordedAt: input.now ?? entry.recordedAt,
|
|
11635
11934
|
provenance: authority.provenance,
|
|
11636
|
-
data: {
|
|
11935
|
+
data: {
|
|
11936
|
+
...entry.data,
|
|
11937
|
+
state: terminalCancel ? "cancelling" : input.state,
|
|
11938
|
+
session: input.session
|
|
11939
|
+
}
|
|
11637
11940
|
};
|
|
11638
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;
|
|
11639
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;
|
|
11640
|
-
if (entry.data.state ===
|
|
11943
|
+
if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
|
|
11641
11944
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
11642
11945
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
11643
11946
|
taskId: task.data.id,
|
|
@@ -12007,7 +12310,7 @@ class WorkitCore {
|
|
|
12007
12310
|
activeWorkerBlocker(task, workspace) {
|
|
12008
12311
|
if (workspace.writer)
|
|
12009
12312
|
return failure("recovery_required", "writer ownership must be released first");
|
|
12010
|
-
if (task.workers.some((entry) =>
|
|
12313
|
+
if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
12011
12314
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
12012
12315
|
return null;
|
|
12013
12316
|
}
|
|
@@ -12244,11 +12547,6 @@ class WorkitCore {
|
|
|
12244
12547
|
if (helper && !helper.ok)
|
|
12245
12548
|
return helper;
|
|
12246
12549
|
const evidence = input.evidence;
|
|
12247
|
-
if (helper?.ok) {
|
|
12248
|
-
const assignment = helper.data.data.assignment;
|
|
12249
|
-
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))
|
|
12250
|
-
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
12251
|
-
}
|
|
12252
12550
|
if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
|
|
12253
12551
|
return failure("invalid_input", "missing or skipped evidence requires a reason");
|
|
12254
12552
|
if (evidence.kind === "review") {
|
|
@@ -12265,6 +12563,11 @@ class WorkitCore {
|
|
|
12265
12563
|
evidence.beforeCandidateId ??= null;
|
|
12266
12564
|
evidence.candidateId ??= null;
|
|
12267
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
|
+
}
|
|
12268
12571
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
12269
12572
|
for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
|
|
12270
12573
|
if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
|
|
@@ -12771,7 +13074,12 @@ class WorkitCore {
|
|
|
12771
13074
|
return failure("invalid_transition", "worker is not awaiting dispatch");
|
|
12772
13075
|
return success(mutation.revision, null, {
|
|
12773
13076
|
...current,
|
|
12774
|
-
workers: current.workers.map((item) => item.id === input.workerId ? {
|
|
13077
|
+
workers: current.workers.map((item) => item.id === input.workerId ? {
|
|
13078
|
+
...item,
|
|
13079
|
+
recordedAt: mutation.now,
|
|
13080
|
+
provenance: attested.data,
|
|
13081
|
+
data: { ...item.data, state: "dispatching" }
|
|
13082
|
+
} : item)
|
|
12775
13083
|
});
|
|
12776
13084
|
}
|
|
12777
13085
|
});
|
|
@@ -13155,6 +13463,9 @@ class WorkitCore {
|
|
|
13155
13463
|
id: task.id,
|
|
13156
13464
|
revision: task.revision,
|
|
13157
13465
|
workspaceRevision: workspace.data.revision,
|
|
13466
|
+
createdAt: task.createdAt,
|
|
13467
|
+
updatedAt: task.updatedAt,
|
|
13468
|
+
runtime: task.runtime ?? null,
|
|
13158
13469
|
objective: task.intent.data.objective,
|
|
13159
13470
|
status: task.status,
|
|
13160
13471
|
closure: task.closure,
|
|
@@ -13240,7 +13551,7 @@ class WorkitCore {
|
|
|
13240
13551
|
const base = reconcileResume(effectiveView);
|
|
13241
13552
|
if (!base.ok)
|
|
13242
13553
|
return base;
|
|
13243
|
-
const needsReconciliation = view.task.workers.some((entry) =>
|
|
13554
|
+
const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
|
|
13244
13555
|
const blockers = [...base.data.blockers];
|
|
13245
13556
|
if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
|
|
13246
13557
|
blockers.push({
|
|
@@ -13307,7 +13618,8 @@ class WorkitCore {
|
|
|
13307
13618
|
...current,
|
|
13308
13619
|
status,
|
|
13309
13620
|
candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
|
|
13310
|
-
progress:
|
|
13621
|
+
progress: current.progress,
|
|
13622
|
+
pauseReason: status === "paused" ? input.reason ?? null : null
|
|
13311
13623
|
})
|
|
13312
13624
|
});
|
|
13313
13625
|
if (!changed.ok)
|
|
@@ -13343,7 +13655,7 @@ class WorkitCore {
|
|
|
13343
13655
|
return workspace;
|
|
13344
13656
|
if (!workspace.data)
|
|
13345
13657
|
return failure("not_found", "workspace not found");
|
|
13346
|
-
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)))
|
|
13347
13659
|
return failure("recovery_required", "scope revision requires worker ownership reconciliation");
|
|
13348
13660
|
this.fillRevisions(input, task.data, workspace.data);
|
|
13349
13661
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
@@ -13461,15 +13773,15 @@ function resolveGitRevision(root, value) {
|
|
|
13461
13773
|
}
|
|
13462
13774
|
function resolveInside(root, candidate) {
|
|
13463
13775
|
const base = realpathSync5(root);
|
|
13464
|
-
const target =
|
|
13465
|
-
if (target !== base && !target.startsWith(base +
|
|
13776
|
+
const target = path7.resolve(base, candidate);
|
|
13777
|
+
if (target !== base && !target.startsWith(base + path7.sep)) {
|
|
13466
13778
|
throw new Error("path must stay inside repository root");
|
|
13467
13779
|
}
|
|
13468
13780
|
let ancestor = target;
|
|
13469
|
-
while (!
|
|
13470
|
-
ancestor =
|
|
13781
|
+
while (!existsSync4(ancestor))
|
|
13782
|
+
ancestor = path7.dirname(ancestor);
|
|
13471
13783
|
const canonicalAncestor = realpathSync5(ancestor);
|
|
13472
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
13784
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
|
|
13473
13785
|
throw new Error("path must stay inside repository root");
|
|
13474
13786
|
}
|
|
13475
13787
|
return target;
|
|
@@ -13512,6 +13824,13 @@ var piCapabilities = (ctx) => [
|
|
|
13512
13824
|
assurance: "agent_guided",
|
|
13513
13825
|
reason: "Pi extensions do not sandbox arbitrary shell commands.",
|
|
13514
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")]
|
|
13515
13834
|
}
|
|
13516
13835
|
];
|
|
13517
13836
|
var piContext = (ctx) => ({
|
|
@@ -13534,14 +13853,12 @@ var workitContext = (ctx) => {
|
|
|
13534
13853
|
|
|
13535
13854
|
Native Pi session: ${session}. Project-local Workit state is unavailable until Pi trusts this project.`;
|
|
13536
13855
|
let taskContext = null;
|
|
13537
|
-
let methods = [];
|
|
13538
13856
|
try {
|
|
13539
13857
|
const store = new TaskStore(ctx.cwd);
|
|
13540
13858
|
const tasks = store.listTasks();
|
|
13541
13859
|
if (tasks.ok) {
|
|
13542
13860
|
const task = tasks.data.filter((entry) => entry.status !== "closed" && (entry.intent.provenance.session?.kind === "host" && entry.intent.provenance.session.host === "pi" && entry.intent.provenance.session.handle === session || entry.workers.some((worker) => worker.data.session?.kind === "host" && worker.data.session.host === "pi" && worker.data.session.handle === session))).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
|
|
13543
13861
|
if (task) {
|
|
13544
|
-
methods = task.policy ? selectMethods(task.policy, piCapabilities(ctx)) : [];
|
|
13545
13862
|
const core = new WorkitCore(store, piContext(ctx));
|
|
13546
13863
|
const view = core.task({
|
|
13547
13864
|
schemaVersion: 1,
|
|
@@ -13554,13 +13871,11 @@ Native Pi session: ${session}. Project-local Workit state is unavailable until P
|
|
|
13554
13871
|
}
|
|
13555
13872
|
}
|
|
13556
13873
|
} catch {}
|
|
13557
|
-
const methodText = methods.length ? `
|
|
13558
|
-
Selected methods: ${methods.map((method) => `${method.id} (${method.assurance})`).join(", ")}.` : "";
|
|
13559
13874
|
const taskText = taskContext ? `
|
|
13560
13875
|
Current task context: ${taskContext}` : "";
|
|
13561
13876
|
return `${invariantBootstrap()}
|
|
13562
13877
|
|
|
13563
|
-
Native Pi session: ${session}.${
|
|
13878
|
+
Native Pi session: ${session}.${taskText}`;
|
|
13564
13879
|
};
|
|
13565
13880
|
|
|
13566
13881
|
// packages/workit-core/src/core/skill-manifests.ts
|
|
@@ -13583,15 +13898,15 @@ var WORKIT_SKILL_ALIASES = {
|
|
|
13583
13898
|
if (false) {}
|
|
13584
13899
|
|
|
13585
13900
|
// packages/workit-core/src/core/branch.ts
|
|
13586
|
-
import { existsSync as
|
|
13587
|
-
import { execFileSync as
|
|
13588
|
-
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";
|
|
13589
13904
|
|
|
13590
13905
|
// packages/workit-core/src/core/git.ts
|
|
13591
|
-
import { execFileSync } from "node:child_process";
|
|
13906
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
13592
13907
|
var run2 = (cwd, args) => {
|
|
13593
13908
|
try {
|
|
13594
|
-
const stdout =
|
|
13909
|
+
const stdout = execFileSync2("git", args, {
|
|
13595
13910
|
cwd,
|
|
13596
13911
|
encoding: "utf8",
|
|
13597
13912
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -13659,14 +13974,14 @@ var gitContext = (workspaceRoot, paths = []) => {
|
|
|
13659
13974
|
import {
|
|
13660
13975
|
copyFileSync,
|
|
13661
13976
|
cpSync,
|
|
13662
|
-
existsSync as
|
|
13977
|
+
existsSync as existsSync5,
|
|
13663
13978
|
mkdirSync as mkdirSync2,
|
|
13664
13979
|
readFileSync as readFileSync4,
|
|
13665
13980
|
readdirSync as readdirSync3,
|
|
13666
13981
|
writeFileSync as writeFileSync2
|
|
13667
13982
|
} from "node:fs";
|
|
13668
13983
|
import os from "node:os";
|
|
13669
|
-
import
|
|
13984
|
+
import path8 from "node:path";
|
|
13670
13985
|
|
|
13671
13986
|
// packages/workit-core/src/core/boundary.ts
|
|
13672
13987
|
var EVENT = {
|
|
@@ -13710,7 +14025,7 @@ var mergePreset = (preset, input = {}, current = {
|
|
|
13710
14025
|
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
13711
14026
|
};
|
|
13712
14027
|
};
|
|
13713
|
-
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");
|
|
13714
14029
|
var migratedDir = null;
|
|
13715
14030
|
var migrationFailed = false;
|
|
13716
14031
|
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
@@ -13720,12 +14035,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
13720
14035
|
migratedDir = dir;
|
|
13721
14036
|
return dir;
|
|
13722
14037
|
}
|
|
13723
|
-
const legacy =
|
|
13724
|
-
if (!
|
|
14038
|
+
const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
14039
|
+
if (!existsSync5(legacy)) {
|
|
13725
14040
|
migratedDir = dir;
|
|
13726
14041
|
return dir;
|
|
13727
14042
|
}
|
|
13728
|
-
if (!migrationFailed &&
|
|
14043
|
+
if (!migrationFailed && existsSync5(dir)) {
|
|
13729
14044
|
migratedDir = dir;
|
|
13730
14045
|
return dir;
|
|
13731
14046
|
}
|
|
@@ -13733,9 +14048,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
13733
14048
|
mkdirSync2(dir, { recursive: true });
|
|
13734
14049
|
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
13735
14050
|
for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
|
|
13736
|
-
const src =
|
|
13737
|
-
const dest =
|
|
13738
|
-
if (
|
|
14051
|
+
const src = path8.join(legacy, entry.name);
|
|
14052
|
+
const dest = path8.join(dir, entry.name);
|
|
14053
|
+
if (existsSync5(dest))
|
|
13739
14054
|
continue;
|
|
13740
14055
|
try {
|
|
13741
14056
|
if (entry.isDirectory())
|
|
@@ -13808,7 +14123,7 @@ var parseConfigResult = (raw, file) => {
|
|
|
13808
14123
|
};
|
|
13809
14124
|
};
|
|
13810
14125
|
var readConfigTyped = (dir) => {
|
|
13811
|
-
const file =
|
|
14126
|
+
const file = path8.join(dir ?? configDir(), "config.json");
|
|
13812
14127
|
return parseConfigResult(readSafe(file), file);
|
|
13813
14128
|
};
|
|
13814
14129
|
var readConfig = () => {
|
|
@@ -13854,9 +14169,9 @@ var resolveCommitPolicy = (config, workspace) => {
|
|
|
13854
14169
|
|
|
13855
14170
|
// packages/workit-core/src/core/workspaces.ts
|
|
13856
14171
|
import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
|
|
13857
|
-
import
|
|
14172
|
+
import path9 from "node:path";
|
|
13858
14173
|
var readWorkspacesResult = (dir = configDir()) => {
|
|
13859
|
-
const file =
|
|
14174
|
+
const file = path9.join(dir, "workspaces.json");
|
|
13860
14175
|
let raw;
|
|
13861
14176
|
try {
|
|
13862
14177
|
raw = readFileSync5(file, "utf8");
|
|
@@ -13954,10 +14269,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
|
|
|
13954
14269
|
|
|
13955
14270
|
// packages/workit-core/src/core/vcs-config.ts
|
|
13956
14271
|
import fs7 from "node:fs";
|
|
13957
|
-
import
|
|
14272
|
+
import path10 from "node:path";
|
|
13958
14273
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
13959
14274
|
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
13960
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ??
|
|
14275
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path10.join(configDir(), "vcs.json");
|
|
13961
14276
|
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
13962
14277
|
var remoteProvider = (cwd) => {
|
|
13963
14278
|
const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
@@ -14040,8 +14355,8 @@ function vcsConfig(mode, cwd) {
|
|
|
14040
14355
|
}
|
|
14041
14356
|
const prov = cfg[provider] ?? {};
|
|
14042
14357
|
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
14043
|
-
const tokenFile = String(wsTokenFile ?? prov.tokenFile ??
|
|
14044
|
-
const tokenPath =
|
|
14358
|
+
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
|
|
14359
|
+
const tokenPath = path10.resolve(tokenFile);
|
|
14045
14360
|
let tokenOk = false;
|
|
14046
14361
|
if (fs7.existsSync(tokenPath)) {
|
|
14047
14362
|
const token = fs7.readFileSync(tokenPath, "utf8").trim();
|
|
@@ -14050,7 +14365,7 @@ function vcsConfig(mode, cwd) {
|
|
|
14050
14365
|
}
|
|
14051
14366
|
const out = {
|
|
14052
14367
|
ok: true,
|
|
14053
|
-
configPath:
|
|
14368
|
+
configPath: path10.resolve(cfgPath),
|
|
14054
14369
|
provider,
|
|
14055
14370
|
defaultTargetBranch: defaultTarget,
|
|
14056
14371
|
pr: cfg.pr ?? {},
|
|
@@ -14151,7 +14466,7 @@ var baseBranch = (cwd) => {
|
|
|
14151
14466
|
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
14152
14467
|
};
|
|
14153
14468
|
var originBaseReady = (cwd, base) => {
|
|
14154
|
-
const run = (args) =>
|
|
14469
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
14155
14470
|
try {
|
|
14156
14471
|
try {
|
|
14157
14472
|
run(["fetch", "origin", base, "--prune"]);
|
|
@@ -14159,7 +14474,7 @@ var originBaseReady = (cwd, base) => {
|
|
|
14159
14474
|
run(["fetch", "origin", "--prune"]);
|
|
14160
14475
|
}
|
|
14161
14476
|
try {
|
|
14162
|
-
|
|
14477
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
14163
14478
|
cwd,
|
|
14164
14479
|
stdio: "pipe"
|
|
14165
14480
|
});
|
|
@@ -14178,11 +14493,11 @@ var originBaseReady = (cwd, base) => {
|
|
|
14178
14493
|
}
|
|
14179
14494
|
};
|
|
14180
14495
|
var fastForwardBase = (cwd, base) => {
|
|
14181
|
-
const run = (args) =>
|
|
14496
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
14182
14497
|
try {
|
|
14183
14498
|
let hasLocalBase = true;
|
|
14184
14499
|
try {
|
|
14185
|
-
|
|
14500
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
14186
14501
|
cwd,
|
|
14187
14502
|
stdio: "pipe"
|
|
14188
14503
|
});
|
|
@@ -14222,19 +14537,18 @@ var branchSetup = ({
|
|
|
14222
14537
|
workspace_root,
|
|
14223
14538
|
log
|
|
14224
14539
|
}) => {
|
|
14225
|
-
const cwd =
|
|
14226
|
-
const exec = (args) =>
|
|
14540
|
+
const cwd = path11.resolve(workspace_root);
|
|
14541
|
+
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
14227
14542
|
const current = gitContext(cwd).branch;
|
|
14228
14543
|
if (!current || current === "unknown")
|
|
14229
|
-
return { error: "not in a git repository" };
|
|
14544
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
14230
14545
|
const sdd = sdd_dir ?? "docs";
|
|
14231
|
-
const manifestPath =
|
|
14546
|
+
const manifestPath = path11.isAbsolute(sdd) ? path11.join(sdd, "manifest.json") : path11.join(cwd, sdd, "manifest.json");
|
|
14232
14547
|
const ensureManifestDir = () => {
|
|
14233
|
-
const dir =
|
|
14234
|
-
if (!
|
|
14548
|
+
const dir = path11.dirname(manifestPath);
|
|
14549
|
+
if (!existsSync6(dir))
|
|
14235
14550
|
mkdirSync3(dir, { recursive: true, mode: 493 });
|
|
14236
14551
|
};
|
|
14237
|
-
ensureManifestDir();
|
|
14238
14552
|
const readManifest = () => {
|
|
14239
14553
|
try {
|
|
14240
14554
|
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
@@ -14252,13 +14566,16 @@ var branchSetup = ({
|
|
|
14252
14566
|
const manifest = readManifest();
|
|
14253
14567
|
const ref = manifest.stash_ref;
|
|
14254
14568
|
if (!ref)
|
|
14255
|
-
return { error: "no stash_ref in manifest" };
|
|
14569
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
14256
14570
|
journal(`pre-pop: ${String(ref)}`);
|
|
14257
14571
|
try {
|
|
14258
14572
|
exec(["stash", "pop", String(ref)]);
|
|
14259
14573
|
} catch (error) {
|
|
14260
14574
|
journal("pop: failed");
|
|
14261
|
-
return {
|
|
14575
|
+
return {
|
|
14576
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
14577
|
+
phase: "post_mutation"
|
|
14578
|
+
};
|
|
14262
14579
|
}
|
|
14263
14580
|
journal("pop: ok");
|
|
14264
14581
|
delete manifest.stash_ref;
|
|
@@ -14268,11 +14585,17 @@ var branchSetup = ({
|
|
|
14268
14585
|
}
|
|
14269
14586
|
const target = target_branch ?? "";
|
|
14270
14587
|
if (!target)
|
|
14271
|
-
return {
|
|
14588
|
+
return {
|
|
14589
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
14590
|
+
phase: "preflight"
|
|
14591
|
+
};
|
|
14272
14592
|
if (isProtected(cwd, target))
|
|
14273
|
-
return { error: `protected branch ${target}
|
|
14593
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
14274
14594
|
if (!allowedBranch(cwd, target))
|
|
14275
|
-
return {
|
|
14595
|
+
return {
|
|
14596
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
14597
|
+
phase: "preflight"
|
|
14598
|
+
};
|
|
14276
14599
|
let base;
|
|
14277
14600
|
let targetExists = true;
|
|
14278
14601
|
try {
|
|
@@ -14283,12 +14606,12 @@ var branchSetup = ({
|
|
|
14283
14606
|
if (!targetExists) {
|
|
14284
14607
|
const baseResolved = baseBranch(cwd);
|
|
14285
14608
|
if ("error" in baseResolved)
|
|
14286
|
-
return { error: baseResolved.error };
|
|
14609
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
14287
14610
|
base = baseResolved.base;
|
|
14288
14611
|
}
|
|
14289
14612
|
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
14290
14613
|
let stash_ref;
|
|
14291
|
-
const failAfterStash = (message) => {
|
|
14614
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
14292
14615
|
let suffix = "";
|
|
14293
14616
|
if (stash_ref) {
|
|
14294
14617
|
journal(`pre-pop: ${stash_ref}`);
|
|
@@ -14301,27 +14624,31 @@ var branchSetup = ({
|
|
|
14301
14624
|
suffix = " (changes preserved in stash)";
|
|
14302
14625
|
}
|
|
14303
14626
|
}
|
|
14304
|
-
return { error: `${message}${suffix}
|
|
14627
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
14305
14628
|
};
|
|
14306
14629
|
if (current !== target) {
|
|
14307
14630
|
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
14308
14631
|
if (dirty && stash !== "yes") {
|
|
14309
14632
|
return {
|
|
14310
|
-
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"
|
|
14311
14635
|
};
|
|
14312
14636
|
}
|
|
14313
14637
|
let validatedBase;
|
|
14314
14638
|
if (!targetExists && base !== undefined) {
|
|
14315
14639
|
const ready = originBaseReady(cwd, base);
|
|
14316
14640
|
if (!ready.ok)
|
|
14317
|
-
return { error: ready.error ?? "ensure-base-branch failed" };
|
|
14641
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
14318
14642
|
validatedBase = base;
|
|
14319
14643
|
}
|
|
14320
14644
|
if (dirty) {
|
|
14321
14645
|
try {
|
|
14322
14646
|
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
14323
14647
|
} catch (error) {
|
|
14324
|
-
return {
|
|
14648
|
+
return {
|
|
14649
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
14650
|
+
phase: "post_mutation"
|
|
14651
|
+
};
|
|
14325
14652
|
}
|
|
14326
14653
|
stash_ref = "stash@{0}";
|
|
14327
14654
|
journal(`stash push: ${stash_ref}`);
|
|
@@ -14332,14 +14659,14 @@ var branchSetup = ({
|
|
|
14332
14659
|
} catch (error) {
|
|
14333
14660
|
const message = error instanceof Error ? error.message : "checkout failed";
|
|
14334
14661
|
if (/worktree/i.test(message)) {
|
|
14335
|
-
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");
|
|
14336
14663
|
}
|
|
14337
14664
|
try {
|
|
14338
14665
|
let effectiveBase = base;
|
|
14339
14666
|
if (effectiveBase === undefined) {
|
|
14340
14667
|
const lateResolved = baseBranch(cwd);
|
|
14341
14668
|
if ("error" in lateResolved)
|
|
14342
|
-
return failAfterStash(lateResolved.error);
|
|
14669
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
14343
14670
|
effectiveBase = lateResolved.base;
|
|
14344
14671
|
}
|
|
14345
14672
|
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
@@ -14386,7 +14713,7 @@ var branchSetup = ({
|
|
|
14386
14713
|
|
|
14387
14714
|
// packages/workit-core/src/core/pr-create.ts
|
|
14388
14715
|
import fs8 from "node:fs";
|
|
14389
|
-
import
|
|
14716
|
+
import path12 from "node:path";
|
|
14390
14717
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
14391
14718
|
function parseGhRepo(remote) {
|
|
14392
14719
|
remote = (remote || "").trim().replace(/\/+$/, "");
|
|
@@ -14451,11 +14778,11 @@ ${line}` : line;
|
|
|
14451
14778
|
}
|
|
14452
14779
|
function whichOnPath(tool) {
|
|
14453
14780
|
const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
|
|
14454
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
14781
|
+
for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
|
|
14455
14782
|
if (!dir)
|
|
14456
14783
|
continue;
|
|
14457
14784
|
for (const name of names) {
|
|
14458
|
-
const candidate =
|
|
14785
|
+
const candidate = path12.join(dir, name);
|
|
14459
14786
|
try {
|
|
14460
14787
|
fs8.accessSync(candidate, fs8.constants.X_OK);
|
|
14461
14788
|
return candidate;
|
|
@@ -14558,7 +14885,7 @@ function prCreate(env, cwd) {
|
|
|
14558
14885
|
const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
|
|
14559
14886
|
let baseUrl = cfg.youtrack_base_url;
|
|
14560
14887
|
if (!baseUrl) {
|
|
14561
|
-
const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
14888
|
+
const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path12.join(path12.dirname(String(cfg.configPath)), "youtrack.json");
|
|
14562
14889
|
try {
|
|
14563
14890
|
const yt = JSON.parse(fs8.readFileSync(ytCfg, "utf8"));
|
|
14564
14891
|
if (yt && typeof yt === "object")
|
|
@@ -14665,8 +14992,8 @@ function prCreate(env, cwd) {
|
|
|
14665
14992
|
|
|
14666
14993
|
// packages/workit-core/src/core/repo-context.ts
|
|
14667
14994
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
14668
|
-
import { existsSync as
|
|
14669
|
-
import
|
|
14995
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
14996
|
+
import path13 from "node:path";
|
|
14670
14997
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
14671
14998
|
try {
|
|
14672
14999
|
gitRevisionParts(value);
|
|
@@ -14800,7 +15127,7 @@ var findPrTemplate = (cwd) => {
|
|
|
14800
15127
|
]) {
|
|
14801
15128
|
const parts = rel.split("/");
|
|
14802
15129
|
const base = parts[parts.length - 1];
|
|
14803
|
-
const dir = parts.length > 1 ?
|
|
15130
|
+
const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
|
|
14804
15131
|
const actual = probeCaseInsensitive(dir, base);
|
|
14805
15132
|
if (actual)
|
|
14806
15133
|
return [...parts.slice(0, -1), actual].join("/");
|
|
@@ -14831,8 +15158,8 @@ var diffStatForRange = (cwd, range) => {
|
|
|
14831
15158
|
return runGit(cwd, ["diff", "--stat", "--"]).stdout;
|
|
14832
15159
|
};
|
|
14833
15160
|
var packageScripts = (cwd, keys) => {
|
|
14834
|
-
const pkgPath =
|
|
14835
|
-
if (!
|
|
15161
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
15162
|
+
if (!existsSync7(pkgPath))
|
|
14836
15163
|
return [];
|
|
14837
15164
|
try {
|
|
14838
15165
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
@@ -14857,9 +15184,9 @@ var documentationFiles = (cwd) => {
|
|
|
14857
15184
|
if (entry.isDirectory()) {
|
|
14858
15185
|
if (excluded.has(entry.name))
|
|
14859
15186
|
continue;
|
|
14860
|
-
walk(
|
|
15187
|
+
walk(path13.join(dir, entry.name), depth + 1);
|
|
14861
15188
|
} else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
|
|
14862
|
-
matches.push("./" +
|
|
15189
|
+
matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
|
|
14863
15190
|
}
|
|
14864
15191
|
}
|
|
14865
15192
|
};
|
|
@@ -14867,7 +15194,7 @@ var documentationFiles = (cwd) => {
|
|
|
14867
15194
|
return matches.sort().slice(0, 200);
|
|
14868
15195
|
};
|
|
14869
15196
|
var readTrimmed = (file, maxLines) => {
|
|
14870
|
-
if (!
|
|
15197
|
+
if (!existsSync7(file))
|
|
14871
15198
|
return "";
|
|
14872
15199
|
const lines = readFileSync7(file, "utf8").split(`
|
|
14873
15200
|
`);
|
|
@@ -14875,7 +15202,7 @@ var readTrimmed = (file, maxLines) => {
|
|
|
14875
15202
|
`);
|
|
14876
15203
|
};
|
|
14877
15204
|
function prReadyContext(root, range) {
|
|
14878
|
-
const cwd =
|
|
15205
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
14879
15206
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
14880
15207
|
return invalidRange(cwd);
|
|
14881
15208
|
let stdout = "";
|
|
@@ -14933,7 +15260,7 @@ ${status.stderr}` : "");
|
|
|
14933
15260
|
stdout += `template_path: ${template}
|
|
14934
15261
|
|
|
14935
15262
|
`;
|
|
14936
|
-
stdout += readTrimmed(
|
|
15263
|
+
stdout += readTrimmed(path13.join(cwd, template), 220);
|
|
14937
15264
|
} else {
|
|
14938
15265
|
stdout += `template_path: none
|
|
14939
15266
|
|
|
@@ -14943,7 +15270,7 @@ ${status.stderr}` : "");
|
|
|
14943
15270
|
stdout += `
|
|
14944
15271
|
`;
|
|
14945
15272
|
stdout += printSection("Recent Validation Signals");
|
|
14946
|
-
if (
|
|
15273
|
+
if (existsSync7(path13.join(cwd, "package.json"))) {
|
|
14947
15274
|
stdout += `package.json detected. Common scripts:
|
|
14948
15275
|
`;
|
|
14949
15276
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -14952,7 +15279,7 @@ ${status.stderr}` : "");
|
|
|
14952
15279
|
`) + `
|
|
14953
15280
|
`;
|
|
14954
15281
|
}
|
|
14955
|
-
if (
|
|
15282
|
+
if (existsSync7(path13.join(cwd, "Cargo.toml")) || existsSync7(path13.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
14956
15283
|
stdout += `Rust project detected.
|
|
14957
15284
|
`;
|
|
14958
15285
|
}
|
|
@@ -14985,7 +15312,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
|
|
|
14985
15312
|
- Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
|
|
14986
15313
|
- If Unreleased already has duplicate category headings, normalize_only first.`;
|
|
14987
15314
|
function changelogContext(root, range) {
|
|
14988
|
-
const cwd =
|
|
15315
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
14989
15316
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
14990
15317
|
return invalidRange(cwd);
|
|
14991
15318
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -15003,8 +15330,8 @@ function changelogContext(root, range) {
|
|
|
15003
15330
|
stdout += CHANGELOG_RULES + `
|
|
15004
15331
|
`;
|
|
15005
15332
|
stdout += printSection("Existing CHANGELOG.md");
|
|
15006
|
-
const changelogPath =
|
|
15007
|
-
if (
|
|
15333
|
+
const changelogPath = path13.join(cwd, "CHANGELOG.md");
|
|
15334
|
+
if (existsSync7(changelogPath)) {
|
|
15008
15335
|
stdout += readTrimmed(changelogPath, 260) + `
|
|
15009
15336
|
`;
|
|
15010
15337
|
} else {
|
|
@@ -15023,7 +15350,7 @@ function changelogContext(root, range) {
|
|
|
15023
15350
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
15024
15351
|
}
|
|
15025
15352
|
function docsRefreshContext(root, range) {
|
|
15026
|
-
const cwd =
|
|
15353
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
15027
15354
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
15028
15355
|
return invalidRange(cwd);
|
|
15029
15356
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -15045,8 +15372,8 @@ function docsRefreshContext(root, range) {
|
|
|
15045
15372
|
`) + `
|
|
15046
15373
|
`;
|
|
15047
15374
|
stdout += printSection("README Preview");
|
|
15048
|
-
const readme =
|
|
15049
|
-
if (
|
|
15375
|
+
const readme = path13.join(cwd, "README.md");
|
|
15376
|
+
if (existsSync7(readme)) {
|
|
15050
15377
|
stdout += readTrimmed(readme, 220) + `
|
|
15051
15378
|
`;
|
|
15052
15379
|
} else {
|
|
@@ -15054,8 +15381,8 @@ function docsRefreshContext(root, range) {
|
|
|
15054
15381
|
`;
|
|
15055
15382
|
}
|
|
15056
15383
|
stdout += printSection("Package Scripts");
|
|
15057
|
-
const pkgPath =
|
|
15058
|
-
if (
|
|
15384
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
15385
|
+
if (existsSync7(pkgPath)) {
|
|
15059
15386
|
try {
|
|
15060
15387
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
15061
15388
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
@@ -15068,7 +15395,7 @@ function docsRefreshContext(root, range) {
|
|
|
15068
15395
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
15069
15396
|
}
|
|
15070
15397
|
function releaseNotesContext(root, rangeOrTag) {
|
|
15071
|
-
const cwd =
|
|
15398
|
+
const cwd = path13.resolve(repoRoot2(root));
|
|
15072
15399
|
if (!rangeOrTag) {
|
|
15073
15400
|
return { stdout: "", stderr: `ERROR: release tag or range required
|
|
15074
15401
|
`, exitCode: 1, cwd };
|
|
@@ -15106,7 +15433,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
15106
15433
|
`;
|
|
15107
15434
|
stdout += printSection("Existing Release Files");
|
|
15108
15435
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
15109
|
-
if (
|
|
15436
|
+
if (existsSync7(path13.join(cwd, rel)))
|
|
15110
15437
|
stdout += rel + `
|
|
15111
15438
|
`;
|
|
15112
15439
|
}
|
|
@@ -15297,25 +15624,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
|
|
|
15297
15624
|
import fs10 from "node:fs";
|
|
15298
15625
|
import path14 from "node:path";
|
|
15299
15626
|
|
|
15300
|
-
// packages/workit-core/src/core/package-root.ts
|
|
15301
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
15302
|
-
import path13 from "node:path";
|
|
15303
|
-
import { fileURLToPath } from "node:url";
|
|
15304
|
-
var packageRoot = () => {
|
|
15305
|
-
let dir = path13.dirname(fileURLToPath(import.meta.url));
|
|
15306
|
-
while (true) {
|
|
15307
|
-
const parent = path13.dirname(dir);
|
|
15308
|
-
if (existsSync7(path13.join(dir, "package.json")) || parent === dir)
|
|
15309
|
-
return dir;
|
|
15310
|
-
dir = parent;
|
|
15311
|
-
}
|
|
15312
|
-
};
|
|
15313
|
-
var assetRoot = () => {
|
|
15314
|
-
const root = packageRoot();
|
|
15315
|
-
const assets = path13.join(root, "assets");
|
|
15316
|
-
return existsSync7(assets) ? assets : root;
|
|
15317
|
-
};
|
|
15318
|
-
|
|
15319
15627
|
// packages/workit-core/src/core/templates.ts
|
|
15320
15628
|
var repoRoot3 = assetRoot();
|
|
15321
15629
|
|
|
@@ -16521,6 +16829,94 @@ var safePath = (root, value) => {
|
|
|
16521
16829
|
return null;
|
|
16522
16830
|
}
|
|
16523
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
|
+
};
|
|
16524
16920
|
var approvedResolvedExternalAction = (content) => {
|
|
16525
16921
|
try {
|
|
16526
16922
|
const value = JSON.parse(content);
|
|
@@ -16784,24 +17180,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16784
17180
|
const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
|
|
16785
17181
|
if (!sdd_dir)
|
|
16786
17182
|
return failure("invalid_input", "sdd_dir must stay inside the workspace");
|
|
17183
|
+
const action = request.payload.action ?? "setup";
|
|
16787
17184
|
const policy = resolveBranchPolicyFor(root);
|
|
16788
|
-
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
|
+
});
|
|
16789
17191
|
const head = gitValue(root, ["rev-parse", "HEAD"]);
|
|
16790
17192
|
if (!head)
|
|
16791
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);
|
|
16792
17196
|
const normalized = {
|
|
16793
17197
|
operation: request.operation,
|
|
16794
17198
|
payload: {
|
|
16795
17199
|
...request.payload,
|
|
16796
|
-
action
|
|
17200
|
+
action,
|
|
16797
17201
|
sdd_dir,
|
|
16798
17202
|
stash: request.payload.stash ?? "no",
|
|
16799
|
-
target_branch
|
|
17203
|
+
...target_branch ? { target_branch } : {}
|
|
16800
17204
|
}
|
|
16801
17205
|
};
|
|
16802
17206
|
return success(null, null, {
|
|
16803
17207
|
request: normalized,
|
|
16804
|
-
descriptorPayload: {
|
|
17208
|
+
descriptorPayload: {
|
|
17209
|
+
...normalized.payload,
|
|
17210
|
+
resolved: { head, dirty, ...creation }
|
|
17211
|
+
}
|
|
16805
17212
|
});
|
|
16806
17213
|
}
|
|
16807
17214
|
case "git.push": {
|
|
@@ -16815,7 +17222,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16815
17222
|
}) : failure("invalid_input", "git push requires a resolvable current branch");
|
|
16816
17223
|
}
|
|
16817
17224
|
case "git.commit": {
|
|
17225
|
+
const planSteps = request.payload.plan_steps;
|
|
17226
|
+
const planBranch = request.payload.plan_branch;
|
|
16818
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
|
+
}
|
|
16819
17256
|
const staged = gitRaw(root, [
|
|
16820
17257
|
"diff",
|
|
16821
17258
|
"--cached",
|
|
@@ -16826,7 +17263,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16826
17263
|
"HEAD"
|
|
16827
17264
|
]);
|
|
16828
17265
|
const paths = stagedPaths(root);
|
|
16829
|
-
|
|
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)
|
|
16830
17273
|
return failure("storage_error", "Git state could not be resolved");
|
|
16831
17274
|
let policy;
|
|
16832
17275
|
try {
|
|
@@ -16841,13 +17284,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
16841
17284
|
`).filter(Boolean));
|
|
16842
17285
|
flavor = detected.flavor ?? "conventional";
|
|
16843
17286
|
}
|
|
16844
|
-
if (!matchCommitFlavor(
|
|
17287
|
+
if (!matchCommitFlavor(message, flavor, policy.pattern))
|
|
16845
17288
|
return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
|
|
16846
17289
|
return success(null, null, {
|
|
16847
17290
|
request,
|
|
16848
17291
|
descriptorPayload: {
|
|
16849
17292
|
...request.payload,
|
|
16850
|
-
resolved: { head, staged: sha256(staged), paths }
|
|
17293
|
+
resolved: { head, branch, staged: sha256(staged), paths }
|
|
16851
17294
|
}
|
|
16852
17295
|
});
|
|
16853
17296
|
}
|
|
@@ -17017,10 +17460,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
17017
17460
|
sdd_dir: request.payload.sdd_dir ?? "docs",
|
|
17018
17461
|
workspace_root: root
|
|
17019
17462
|
});
|
|
17020
|
-
|
|
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);
|
|
17021
17472
|
}
|
|
17022
17473
|
case "git.commit": {
|
|
17023
|
-
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]);
|
|
17024
17481
|
return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
|
|
17025
17482
|
}
|
|
17026
17483
|
case "git.push": {
|
|
@@ -17193,8 +17650,34 @@ var nativeAuthority = (actor, reconciliationTokens = new WeakSet) => ({
|
|
|
17193
17650
|
var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
|
|
17194
17651
|
const store = new TaskStore(root);
|
|
17195
17652
|
const selected = approvedExternalAction(store, "pi", actor, operation);
|
|
17196
|
-
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
|
+
}
|
|
17197
17679
|
return selected;
|
|
17680
|
+
}
|
|
17198
17681
|
const actionRef = externalActionRef("pi", actor, operation);
|
|
17199
17682
|
return {
|
|
17200
17683
|
taskId: selected.data.task.id,
|
|
@@ -17260,6 +17743,21 @@ var executeFamily = async (family, toolCallId, input, ctx) => {
|
|
|
17260
17743
|
operation: "decision.record"
|
|
17261
17744
|
}));
|
|
17262
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));
|
|
17263
17761
|
const approved = await ctx.ui.confirm("Workit decision", decision.binding.presented);
|
|
17264
17762
|
if (approved !== (decision.response === "approved"))
|
|
17265
17763
|
return output(failure("permission_denied", "Pi decision answer did not match the requested response"));
|
|
@@ -17332,6 +17830,22 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17332
17830
|
...context,
|
|
17333
17831
|
nativeAuthority: nativeAuthority(actor, reconciliationTokens)
|
|
17334
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
|
+
}
|
|
17335
17849
|
const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
|
|
17336
17850
|
const priorRequest = priorExternalAction(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload);
|
|
17337
17851
|
if (priorRequest.ok && priorRequest.data.entry.data.consumption?.state === "uncertain") {
|
|
@@ -17370,8 +17884,18 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17370
17884
|
}
|
|
17371
17885
|
if (priorRequest.ok && priorRequest.data.entry.data.consumption !== null)
|
|
17372
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);
|
|
17373
17890
|
const existing = approvedExternalAction(store, "pi", actor, descriptor);
|
|
17374
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
|
+
}
|
|
17375
17899
|
const prior = externalActionState(store, "pi", actor, descriptor);
|
|
17376
17900
|
if (prior.ok && prior.data.entry.data.consumption !== null)
|
|
17377
17901
|
return output(failure("permission_denied", "external action was already settled"));
|
|
@@ -17383,7 +17907,8 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17383
17907
|
if (candidates.length !== 1)
|
|
17384
17908
|
return output(failure("permission_denied", "external action requires exactly one active Pi task"));
|
|
17385
17909
|
const task = candidates[0];
|
|
17386
|
-
const
|
|
17910
|
+
const question = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
|
|
17911
|
+
const approved = await ctx.ui.confirm(question.presented, question.approvedText);
|
|
17387
17912
|
if (!approved)
|
|
17388
17913
|
return output(failure("permission_denied", "external action was not approved"));
|
|
17389
17914
|
const currentWorkspace = store.readWorkspace();
|
|
@@ -17399,8 +17924,9 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17399
17924
|
taskId: task.id,
|
|
17400
17925
|
workspaceId: currentWorkspace.data.id,
|
|
17401
17926
|
scope: task.intent.data.scope,
|
|
17402
|
-
presented:
|
|
17927
|
+
presented: question.presented,
|
|
17403
17928
|
approvedContent: descriptor,
|
|
17929
|
+
displayed: question.approvedText,
|
|
17404
17930
|
contentRefs: []
|
|
17405
17931
|
},
|
|
17406
17932
|
response: "approved",
|
|
@@ -17409,12 +17935,25 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
17409
17935
|
if (!decision.ok)
|
|
17410
17936
|
return output(decision);
|
|
17411
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 }));
|
|
17412
17941
|
const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
|
|
17413
17942
|
return output(result);
|
|
17414
17943
|
}
|
|
17415
17944
|
});
|
|
17416
17945
|
};
|
|
17417
17946
|
var enforceNativeWriter = (event, ctx) => {
|
|
17947
|
+
if (event.toolName === "bash") {
|
|
17948
|
+
const command = event.input?.command;
|
|
17949
|
+
const route = typeof command === "string" ? shellRouteIntent(command) : null;
|
|
17950
|
+
if (route)
|
|
17951
|
+
return {
|
|
17952
|
+
block: true,
|
|
17953
|
+
reason: `direct branch or PR creation bypasses the Workit route; ${route.guidance}`
|
|
17954
|
+
};
|
|
17955
|
+
return;
|
|
17956
|
+
}
|
|
17418
17957
|
if (event.toolName !== "write" && event.toolName !== "edit")
|
|
17419
17958
|
return;
|
|
17420
17959
|
if (!ctx.isProjectTrusted())
|