@brainervirus/workit-pi 1.0.12 → 1.1.0
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 +19 -4
- package/dist/workit.js +2404 -2015
- package/package.json +1 -1
package/dist/workit.js
CHANGED
|
@@ -7576,7 +7576,13 @@ var refSchema = discriminatedUnion("kind", [
|
|
|
7576
7576
|
id
|
|
7577
7577
|
}).strict(),
|
|
7578
7578
|
object({ kind: literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
|
|
7579
|
-
object({ kind: literal("external"), url: text.url() }).strict()
|
|
7579
|
+
object({ kind: literal("external"), url: text.url() }).strict(),
|
|
7580
|
+
object({
|
|
7581
|
+
kind: literal("standing"),
|
|
7582
|
+
workspace: nonEmpty,
|
|
7583
|
+
class: nonEmpty,
|
|
7584
|
+
configDigest: nullableDigest
|
|
7585
|
+
}).strict()
|
|
7580
7586
|
]);
|
|
7581
7587
|
var rewriteUnknown = (value, map) => {
|
|
7582
7588
|
if (Array.isArray(value)) {
|
|
@@ -7881,7 +7887,8 @@ var decisionSchema = object({
|
|
|
7881
7887
|
approvedContent: text,
|
|
7882
7888
|
displayed: text.optional(),
|
|
7883
7889
|
contentRefs: array(refSchema),
|
|
7884
|
-
statedChoice: object({ ref: text, text }).strict().optional()
|
|
7890
|
+
statedChoice: object({ ref: text, text }).strict().optional(),
|
|
7891
|
+
standing: object({ workspace: nonEmpty, class: nonEmpty }).strict().optional()
|
|
7885
7892
|
}).strict(),
|
|
7886
7893
|
digest,
|
|
7887
7894
|
response: _enum(["approved", "rejected", "stated"]),
|
|
@@ -9978,2136 +9985,2383 @@ function assertProductWriteAllowed(input) {
|
|
|
9978
9985
|
}
|
|
9979
9986
|
|
|
9980
9987
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
9988
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
9989
|
+
import * as fs7 from "node:fs";
|
|
9990
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9991
|
+
import path10 from "node:path";
|
|
9992
|
+
|
|
9993
|
+
// packages/workit-core/src/core/authority.ts
|
|
9981
9994
|
import { createHash as createHash4 } from "node:crypto";
|
|
9982
9995
|
import * as fs6 from "node:fs";
|
|
9983
|
-
import
|
|
9984
|
-
import path6 from "node:path";
|
|
9996
|
+
import path9 from "node:path";
|
|
9985
9997
|
|
|
9986
|
-
// packages/workit-core/src/core/
|
|
9998
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
9987
9999
|
import { createHash as createHash3 } from "node:crypto";
|
|
9988
|
-
import
|
|
10000
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
10001
|
+
|
|
10002
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
10003
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
10004
|
+
import path6 from "node:path";
|
|
10005
|
+
|
|
10006
|
+
// packages/workit-core/src/core/config.ts
|
|
10007
|
+
import {
|
|
10008
|
+
copyFileSync,
|
|
10009
|
+
cpSync,
|
|
10010
|
+
existsSync as existsSync3,
|
|
10011
|
+
mkdirSync as mkdirSync2,
|
|
10012
|
+
readFileSync as readFileSync2,
|
|
10013
|
+
readdirSync as readdirSync2,
|
|
10014
|
+
writeFileSync as writeFileSync2
|
|
10015
|
+
} from "node:fs";
|
|
10016
|
+
import os from "node:os";
|
|
9989
10017
|
import path5 from "node:path";
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
10018
|
+
|
|
10019
|
+
// packages/workit-core/src/core/boundary.ts
|
|
10020
|
+
var EVENT = {
|
|
10021
|
+
initialization: "initialization",
|
|
10022
|
+
provenance: "provenance",
|
|
10023
|
+
assets: "assets",
|
|
10024
|
+
configurationSource: "configuration_source",
|
|
10025
|
+
hooks: "hooks",
|
|
10026
|
+
mcpConnection: "mcp_connection",
|
|
10027
|
+
migration: "migration",
|
|
10028
|
+
installSteps: "install_steps",
|
|
10029
|
+
uncaughtFailure: "uncaught_failure",
|
|
10030
|
+
toolsFailed: "tools_failed",
|
|
10031
|
+
doctor: "doctor"
|
|
9995
10032
|
};
|
|
9996
|
-
var
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10033
|
+
var errorDetail = (err) => {
|
|
10034
|
+
if (err instanceof Error) {
|
|
10035
|
+
return { error_name: err.name, error: err.message };
|
|
10036
|
+
}
|
|
10037
|
+
return { error_name: "unknown", error: String(err) };
|
|
10038
|
+
};
|
|
10039
|
+
|
|
10040
|
+
// packages/workit-core/src/core/config.ts
|
|
10041
|
+
var diagnosticLogger;
|
|
10042
|
+
var PRESETS = {
|
|
10043
|
+
gitflow: {
|
|
10044
|
+
allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
|
|
10045
|
+
protected: ["main", "develop", "master", "prod", "production"]
|
|
10046
|
+
},
|
|
10047
|
+
"github-flow": { allowed: ["*"], protected: ["main"] },
|
|
10048
|
+
"trunk-based": { allowed: ["*"], protected: ["main"] },
|
|
10049
|
+
custom: { allowed: [], protected: [] }
|
|
10050
|
+
};
|
|
10051
|
+
var mergePreset = (preset, input = {}, current = {
|
|
10052
|
+
branchPolicy: { preset, allowed: [], protected: [] }
|
|
10053
|
+
}) => {
|
|
10054
|
+
const defs = PRESETS[preset];
|
|
10055
|
+
return {
|
|
10056
|
+
preset,
|
|
10057
|
+
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
10058
|
+
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
10059
|
+
};
|
|
10060
|
+
};
|
|
10061
|
+
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workit");
|
|
10062
|
+
var migratedDir = null;
|
|
10063
|
+
var migrationFailed = false;
|
|
10064
|
+
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
10065
|
+
if (migratedDir === dir)
|
|
10066
|
+
return dir;
|
|
10067
|
+
if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
|
|
10068
|
+
migratedDir = dir;
|
|
10069
|
+
return dir;
|
|
10070
|
+
}
|
|
10071
|
+
const legacy = path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
10072
|
+
if (!existsSync3(legacy)) {
|
|
10073
|
+
migratedDir = dir;
|
|
10074
|
+
return dir;
|
|
10075
|
+
}
|
|
10076
|
+
if (!migrationFailed && existsSync3(dir)) {
|
|
10077
|
+
migratedDir = dir;
|
|
10078
|
+
return dir;
|
|
10079
|
+
}
|
|
10080
|
+
migrationFailed = false;
|
|
10081
|
+
mkdirSync2(dir, { recursive: true });
|
|
10082
|
+
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
10083
|
+
for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
|
|
10084
|
+
const src = path5.join(legacy, entry.name);
|
|
10085
|
+
const dest = path5.join(dir, entry.name);
|
|
10086
|
+
if (existsSync3(dest))
|
|
10017
10087
|
continue;
|
|
10088
|
+
try {
|
|
10089
|
+
if (entry.isDirectory())
|
|
10090
|
+
cpSync(src, dest, { recursive: true });
|
|
10091
|
+
else if (entry.isFile())
|
|
10092
|
+
copyFileSync(src, dest);
|
|
10093
|
+
} catch (err) {
|
|
10094
|
+
migrationFailed = true;
|
|
10095
|
+
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
10096
|
+
console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
|
|
10018
10097
|
}
|
|
10019
|
-
return null;
|
|
10020
10098
|
}
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
return normalized;
|
|
10099
|
+
if (!migrationFailed)
|
|
10100
|
+
migratedDir = dir;
|
|
10101
|
+
return dir;
|
|
10025
10102
|
};
|
|
10026
|
-
var
|
|
10027
|
-
var
|
|
10028
|
-
|
|
10103
|
+
var configDir = () => ensureConfigDir();
|
|
10104
|
+
var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
10105
|
+
var DEFAULTS = {
|
|
10106
|
+
locale: "en",
|
|
10107
|
+
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
10108
|
+
timezone: "America/Santiago",
|
|
10109
|
+
branchPolicy: {
|
|
10110
|
+
preset: "gitflow",
|
|
10111
|
+
allowed: [...PRESETS.gitflow.allowed],
|
|
10112
|
+
protected: [...PRESETS.gitflow.protected]
|
|
10113
|
+
},
|
|
10114
|
+
commitPolicy: { preset: "conventional" }
|
|
10115
|
+
};
|
|
10116
|
+
var readSafe = (p) => {
|
|
10029
10117
|
try {
|
|
10030
|
-
|
|
10118
|
+
return readFileSync2(p, "utf8");
|
|
10031
10119
|
} catch {
|
|
10032
|
-
return [];
|
|
10033
|
-
}
|
|
10034
|
-
if (typeof value !== "object" || value === null || !("steps" in value))
|
|
10035
|
-
return [];
|
|
10036
|
-
const steps = value.steps;
|
|
10037
|
-
if (!Array.isArray(steps))
|
|
10038
10120
|
return null;
|
|
10039
|
-
|
|
10040
|
-
return normalized;
|
|
10121
|
+
}
|
|
10041
10122
|
};
|
|
10042
|
-
var
|
|
10043
|
-
|
|
10044
|
-
|
|
10123
|
+
var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10124
|
+
var parseConfigResult = (raw, file) => {
|
|
10125
|
+
if (raw === null)
|
|
10126
|
+
return { status: "missing", path: file };
|
|
10127
|
+
let parsed;
|
|
10128
|
+
try {
|
|
10129
|
+
parsed = JSON.parse(raw);
|
|
10130
|
+
} catch {
|
|
10131
|
+
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
10132
|
+
}
|
|
10133
|
+
if (!isConfigObject(parsed)) {
|
|
10134
|
+
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
10135
|
+
}
|
|
10136
|
+
const input = parsed;
|
|
10137
|
+
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
10138
|
+
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
10139
|
+
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
10140
|
+
return {
|
|
10141
|
+
status: "valid",
|
|
10142
|
+
path: file,
|
|
10143
|
+
config: {
|
|
10144
|
+
locale,
|
|
10145
|
+
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
10146
|
+
timezone: input.timezone ?? DEFAULTS.timezone,
|
|
10147
|
+
branchPolicy: mergePreset(preset, {
|
|
10148
|
+
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
10149
|
+
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
10150
|
+
}, DEFAULTS),
|
|
10151
|
+
commitPolicy: {
|
|
10152
|
+
preset: commitPreset,
|
|
10153
|
+
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
};
|
|
10045
10157
|
};
|
|
10046
|
-
var
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
return false;
|
|
10050
|
-
const provenance = value;
|
|
10051
|
-
return provenance.kind === "host_observed" && provenance.host === caller.host && provenance.session?.kind === "host" && provenance.session.host === caller.host && provenance.session.handle === caller.actor && provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host);
|
|
10158
|
+
var readConfigTyped = (dir) => {
|
|
10159
|
+
const file = path5.join(dir ?? configDir(), "config.json");
|
|
10160
|
+
return parseConfigResult(readSafe(file), file);
|
|
10052
10161
|
};
|
|
10053
|
-
var
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
return failure("permission_denied", "native decision observation was not attested");
|
|
10059
|
-
return success(null, null, trustedAuthority("decision", result.data, {
|
|
10060
|
-
taskId: input.expected.taskId,
|
|
10061
|
-
workspaceId: input.expected.workspaceId,
|
|
10062
|
-
purpose: input.expected.purpose,
|
|
10063
|
-
response: input.expected.response,
|
|
10064
|
-
binding: input.expected.binding,
|
|
10065
|
-
bindingBytes: input.expected.bindingBytes,
|
|
10066
|
-
digest: input.expected.digest,
|
|
10067
|
-
requirementIds: [...input.expected.requirementIds]
|
|
10068
|
-
}, binding, input.caller));
|
|
10162
|
+
var readConfig = () => {
|
|
10163
|
+
const result = readConfigTyped();
|
|
10164
|
+
if (result.status === "malformed")
|
|
10165
|
+
throw new Error(result.error);
|
|
10166
|
+
return result.config ?? DEFAULTS;
|
|
10069
10167
|
};
|
|
10070
|
-
var
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
const
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
response: input.expected.decision.response,
|
|
10086
|
-
binding: input.expected.decision.binding,
|
|
10087
|
-
digest: input.expected.decision.digest,
|
|
10088
|
-
requirementIds: [...input.expected.decision.requirementIds]
|
|
10089
|
-
}, binding, input.caller));
|
|
10168
|
+
var resolveBranchPolicy = (config, workspace) => {
|
|
10169
|
+
const wp = workspace?.branchPolicy ?? {};
|
|
10170
|
+
const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
|
|
10171
|
+
const merged = mergePreset(preset, {
|
|
10172
|
+
allowed: wp.allowed,
|
|
10173
|
+
protectedNames: wp.protected
|
|
10174
|
+
}, config);
|
|
10175
|
+
const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
|
|
10176
|
+
return {
|
|
10177
|
+
preset,
|
|
10178
|
+
allowed,
|
|
10179
|
+
protected: new Set(merged.protected.map((p) => p.toLowerCase())),
|
|
10180
|
+
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
10181
|
+
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
10182
|
+
};
|
|
10090
10183
|
};
|
|
10091
|
-
var
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
response: input.expected.decision.response,
|
|
10109
|
-
binding: input.expected.decision.binding,
|
|
10110
|
-
digest: input.expected.decision.digest,
|
|
10111
|
-
requirementIds: [...input.expected.decision.requirementIds]
|
|
10112
|
-
}, binding, input.caller));
|
|
10184
|
+
var COMMIT_PRESETS = [
|
|
10185
|
+
"conventional",
|
|
10186
|
+
"gitmoji",
|
|
10187
|
+
"ticket-prefix",
|
|
10188
|
+
"freeform",
|
|
10189
|
+
"custom",
|
|
10190
|
+
"auto"
|
|
10191
|
+
];
|
|
10192
|
+
var resolveCommitPolicy = (config, workspace) => {
|
|
10193
|
+
const wp = workspace?.commitPolicy ?? {};
|
|
10194
|
+
if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
|
|
10195
|
+
return {
|
|
10196
|
+
preset: wp.preset,
|
|
10197
|
+
...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
|
|
10198
|
+
};
|
|
10199
|
+
}
|
|
10200
|
+
return config.commitPolicy ?? { preset: "conventional" };
|
|
10113
10201
|
};
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10202
|
+
|
|
10203
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
10204
|
+
var readWorkspacesResult = (dir = configDir()) => {
|
|
10205
|
+
const file = path6.join(dir, "workspaces.json");
|
|
10206
|
+
let raw;
|
|
10207
|
+
try {
|
|
10208
|
+
raw = readFileSync3(file, "utf8");
|
|
10209
|
+
} catch {
|
|
10210
|
+
return { status: "missing", path: file, entries: [] };
|
|
10211
|
+
}
|
|
10212
|
+
let parsed;
|
|
10213
|
+
try {
|
|
10214
|
+
parsed = JSON.parse(raw);
|
|
10215
|
+
} catch {
|
|
10216
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
|
|
10217
|
+
}
|
|
10218
|
+
if (!isConfigObject(parsed)) {
|
|
10219
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
10220
|
+
}
|
|
10221
|
+
const list = parsed.workspaces;
|
|
10222
|
+
return {
|
|
10223
|
+
status: "valid",
|
|
10224
|
+
path: file,
|
|
10225
|
+
entries: Array.isArray(list) ? list : []
|
|
10226
|
+
};
|
|
10122
10227
|
};
|
|
10123
|
-
var
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10228
|
+
var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
|
|
10229
|
+
var globToRegExp = (glob) => {
|
|
10230
|
+
let out = "";
|
|
10231
|
+
for (let i = 0;i < glob.length; i++) {
|
|
10232
|
+
const c = glob[i];
|
|
10233
|
+
if (c === "*") {
|
|
10234
|
+
if (glob[i + 1] === "*") {
|
|
10235
|
+
if (glob[i + 2] === "/") {
|
|
10236
|
+
if (out === "")
|
|
10237
|
+
out += "/?";
|
|
10238
|
+
out += "(?:[^/]+/)*";
|
|
10239
|
+
i += 2;
|
|
10240
|
+
} else {
|
|
10241
|
+
if (i + 2 >= glob.length) {
|
|
10242
|
+
if (out.endsWith("/")) {
|
|
10243
|
+
out = out.slice(0, -1);
|
|
10244
|
+
out += "(?:/.*)?";
|
|
10245
|
+
} else {
|
|
10246
|
+
out += ".*";
|
|
10247
|
+
}
|
|
10248
|
+
} else {
|
|
10249
|
+
out += ".*";
|
|
10250
|
+
}
|
|
10251
|
+
i++;
|
|
10252
|
+
}
|
|
10253
|
+
} else {
|
|
10254
|
+
out += "[^/]*";
|
|
10255
|
+
}
|
|
10256
|
+
} else {
|
|
10257
|
+
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10260
|
+
return new RegExp(`^${out}$`);
|
|
10127
10261
|
};
|
|
10128
|
-
var
|
|
10129
|
-
var
|
|
10130
|
-
|
|
10131
|
-
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
10132
|
-
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
10133
|
-
return [];
|
|
10134
|
-
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
10135
|
-
}
|
|
10136
|
-
var digestBytes2 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
|
|
10137
|
-
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
10138
|
-
let root;
|
|
10262
|
+
var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
|
|
10263
|
+
var realpathOf = (p) => {
|
|
10264
|
+
const real = realpathSync2.native ?? realpathSync2;
|
|
10139
10265
|
try {
|
|
10140
|
-
|
|
10266
|
+
return real(p);
|
|
10141
10267
|
} catch {
|
|
10142
|
-
return
|
|
10268
|
+
return p;
|
|
10143
10269
|
}
|
|
10144
|
-
|
|
10145
|
-
|
|
10270
|
+
};
|
|
10271
|
+
var canonicalGlob = (glob) => {
|
|
10272
|
+
const m = /[*?[\]{]/.exec(glob);
|
|
10273
|
+
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
10274
|
+
const rest = m ? glob.slice(m.index) : "";
|
|
10275
|
+
if (!prefix)
|
|
10276
|
+
return glob;
|
|
10277
|
+
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
10278
|
+
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
10279
|
+
};
|
|
10280
|
+
var resolveWorkspaceFrom = (cwd, dir) => {
|
|
10281
|
+
const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
|
|
10282
|
+
for (const entry of loadWorkspacesFrom(dir)) {
|
|
10283
|
+
if (!entry || typeof entry !== "object")
|
|
10146
10284
|
continue;
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
const relative = path5.relative(root, target);
|
|
10158
|
-
let current = root;
|
|
10159
|
-
for (const segment of relative.split(path5.sep)) {
|
|
10160
|
-
if (!segment)
|
|
10161
|
-
continue;
|
|
10162
|
-
current = path5.join(current, segment);
|
|
10163
|
-
const stat = fs5.lstatSync(current);
|
|
10164
|
-
const real = fs5.realpathSync(current);
|
|
10165
|
-
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
10166
|
-
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
10167
|
-
fields: [
|
|
10168
|
-
{
|
|
10169
|
-
path: "contentRefs.path",
|
|
10170
|
-
reason: `${reference.path} resolves outside the checkout`
|
|
10171
|
-
}
|
|
10172
|
-
]
|
|
10173
|
-
});
|
|
10174
|
-
if (stat.isSymbolicLink())
|
|
10175
|
-
return failure("invalid_input", "document reference uses a symlink");
|
|
10176
|
-
if (current === target && !stat.isFile())
|
|
10177
|
-
return failure("invalid_input", "document reference is not a regular file");
|
|
10178
|
-
if (current !== target && !stat.isDirectory())
|
|
10179
|
-
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
10180
|
-
}
|
|
10181
|
-
if (digestBytes2(fs5.readFileSync(target)) !== reference.digest)
|
|
10182
|
-
return failure("permission_denied", "approved document bytes have changed");
|
|
10183
|
-
} catch {
|
|
10184
|
-
return failure("invalid_input", "approved document is unavailable");
|
|
10285
|
+
const ws = entry;
|
|
10286
|
+
if (typeof ws.glob !== "string" || !ws.glob)
|
|
10287
|
+
continue;
|
|
10288
|
+
const glob = ws.glob.replaceAll("\\", "/");
|
|
10289
|
+
const canonical = canonicalGlob(glob);
|
|
10290
|
+
for (const target of targets) {
|
|
10291
|
+
if (matchWorkspace(glob, target))
|
|
10292
|
+
return ws;
|
|
10293
|
+
if (canonical !== glob && matchWorkspace(canonical, target))
|
|
10294
|
+
return ws;
|
|
10185
10295
|
}
|
|
10186
10296
|
}
|
|
10187
|
-
return
|
|
10297
|
+
return null;
|
|
10188
10298
|
};
|
|
10189
|
-
var
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
if (decision.response !== "approved")
|
|
10215
|
-
return failure("permission_denied", "decision was rejected");
|
|
10216
|
-
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
10217
|
-
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
10218
|
-
if (decision.revoked)
|
|
10219
|
-
return failure("permission_denied", "decision is revoked");
|
|
10220
|
-
if (decision.digest !== decisionDigest(decision))
|
|
10221
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
10222
|
-
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
10223
|
-
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
10224
|
-
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
10225
|
-
return failure("permission_denied", "action binding does not match the approved decision");
|
|
10226
|
-
const content = verifyContentRefs(store, decision.binding);
|
|
10227
|
-
if (!content.ok)
|
|
10228
|
-
return content;
|
|
10229
|
-
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
10230
|
-
if (requestedSteps === null)
|
|
10231
|
-
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
10232
|
-
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
10233
|
-
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
10234
|
-
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
10235
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
10236
|
-
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
10237
|
-
return failure("permission_denied", "bounded action scope changed");
|
|
10238
|
-
return success(null, null, { entry, workflow });
|
|
10299
|
+
var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
|
|
10300
|
+
|
|
10301
|
+
// packages/workit-core/src/core/branch.ts
|
|
10302
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
10303
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10304
|
+
import path8 from "node:path";
|
|
10305
|
+
|
|
10306
|
+
// packages/workit-core/src/core/git.ts
|
|
10307
|
+
import { execFileSync } from "node:child_process";
|
|
10308
|
+
var run = (cwd, args) => {
|
|
10309
|
+
try {
|
|
10310
|
+
const stdout = execFileSync("git", args, {
|
|
10311
|
+
cwd,
|
|
10312
|
+
encoding: "utf8",
|
|
10313
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10314
|
+
});
|
|
10315
|
+
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
10316
|
+
} catch (error) {
|
|
10317
|
+
const e = error;
|
|
10318
|
+
return {
|
|
10319
|
+
stdout: (e.stdout ?? "").trimEnd(),
|
|
10320
|
+
stderr: (e.stderr ?? "").trimEnd(),
|
|
10321
|
+
exitCode: e.status ?? 1
|
|
10322
|
+
};
|
|
10323
|
+
}
|
|
10239
10324
|
};
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
const
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
return failure("permission_denied", "native reservation authority is not verified");
|
|
10250
|
-
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
10251
|
-
const workspace = input.store.readWorkspace();
|
|
10252
|
-
if (!workspace.ok)
|
|
10253
|
-
return workspace;
|
|
10254
|
-
if (!workspace.data)
|
|
10255
|
-
return failure("not_found", "workspace not found");
|
|
10256
|
-
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10257
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
10258
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
10259
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
10260
|
-
});
|
|
10261
|
-
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
10262
|
-
if (!valid.ok)
|
|
10263
|
-
return valid;
|
|
10264
|
-
const { entry, workflow } = valid.data;
|
|
10265
|
-
const existing = entry.data.consumption;
|
|
10266
|
-
if (existing?.state === "uncertain")
|
|
10267
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
10268
|
-
operation: "reserve_action",
|
|
10269
|
-
outcome: "unknown"
|
|
10270
|
-
});
|
|
10271
|
-
if (existing?.state === "consumed")
|
|
10272
|
-
return failure("permission_denied", "action approval is consumed");
|
|
10273
|
-
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
10274
|
-
return failure("permission_denied", "action approval is already reserved");
|
|
10275
|
-
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
10276
|
-
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
10277
|
-
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
10278
|
-
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
10279
|
-
return failure("permission_denied", "action step was already completed");
|
|
10280
|
-
const nextDecision = {
|
|
10281
|
-
...entry.data,
|
|
10282
|
-
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
10283
|
-
};
|
|
10284
|
-
const actionProgress = workflow.steps.length ? [
|
|
10285
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
10286
|
-
{
|
|
10287
|
-
decisionId: input.decisionId,
|
|
10288
|
-
steps: [...workflow.steps],
|
|
10289
|
-
completedSteps: [...workflow.completed]
|
|
10290
|
-
}
|
|
10291
|
-
] : current.actionProgress ?? [];
|
|
10292
|
-
return success(mutation.revision, null, {
|
|
10293
|
-
...current,
|
|
10294
|
-
actionProgress,
|
|
10295
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
10296
|
-
});
|
|
10297
|
-
}, input.native?.now);
|
|
10298
|
-
if (!changed.ok)
|
|
10299
|
-
return changed;
|
|
10300
|
-
const workspace = input.store.readWorkspace();
|
|
10301
|
-
if (!workspace.ok || !workspace.data)
|
|
10302
|
-
return failure("recovery_required", "workspace disappeared after reservation");
|
|
10303
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
10304
|
-
if (!entry)
|
|
10305
|
-
return failure("recovery_required", "reserved decision disappeared");
|
|
10306
|
-
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
10307
|
-
steps: [],
|
|
10308
|
-
completedSteps: []
|
|
10309
|
-
};
|
|
10310
|
-
return success(changed.data.revision, workspace.data.revision, {
|
|
10311
|
-
taskId: input.taskId,
|
|
10312
|
-
decisionId: input.decisionId,
|
|
10313
|
-
actionRef: input.actionRef,
|
|
10314
|
-
taskRevision: changed.data.revision,
|
|
10315
|
-
workspaceRevision: workspace.data.revision,
|
|
10316
|
-
completedSteps: [...workflow.completedSteps],
|
|
10317
|
-
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
10318
|
-
});
|
|
10319
|
-
}
|
|
10320
|
-
function settleAction(input) {
|
|
10321
|
-
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
10322
|
-
return failure("invalid_input", "action settlement preconditions are required");
|
|
10323
|
-
const authority = takeAuthority(input.authority, {
|
|
10324
|
-
owner: input.authorityOwner,
|
|
10325
|
-
store: input.store,
|
|
10326
|
-
root: input.store.root
|
|
10327
|
-
}, input.authorityCaller);
|
|
10328
|
-
if (!authority)
|
|
10329
|
-
return failure("permission_denied", "native settlement authority is not verified");
|
|
10330
|
-
if (!refSchema.safeParse(input.actionRef).success)
|
|
10331
|
-
return failure("invalid_input", "action reference is invalid");
|
|
10332
|
-
if (!authorityMatches(authority, {
|
|
10333
|
-
taskId: input.taskId,
|
|
10334
|
-
workspaceId: authority.workspaceId,
|
|
10335
|
-
decisionId: input.decisionId,
|
|
10336
|
-
actionRef: input.actionRef,
|
|
10337
|
-
taskRevision: input.taskRevision,
|
|
10338
|
-
workspaceRevision: input.workspaceRevision,
|
|
10339
|
-
outcome: input.outcome
|
|
10340
|
-
}))
|
|
10341
|
-
return failure("permission_denied", "native settlement authority is not verified");
|
|
10342
|
-
let outcomeResult = null;
|
|
10343
|
-
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
10344
|
-
const workspace = input.store.readWorkspace();
|
|
10345
|
-
if (!workspace.ok)
|
|
10346
|
-
return workspace;
|
|
10347
|
-
if (!workspace.data)
|
|
10348
|
-
return failure("not_found", "workspace not found");
|
|
10349
|
-
if (!authorityMatches(authority, {
|
|
10350
|
-
taskId: input.taskId,
|
|
10351
|
-
workspaceId: workspace.data.id,
|
|
10352
|
-
decisionId: input.decisionId,
|
|
10353
|
-
actionRef: input.actionRef,
|
|
10354
|
-
taskRevision: input.taskRevision,
|
|
10355
|
-
workspaceRevision: input.workspaceRevision,
|
|
10356
|
-
outcome: input.outcome
|
|
10357
|
-
}))
|
|
10358
|
-
return failure("permission_denied", "native settlement authority does not match context");
|
|
10359
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
10360
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
10361
|
-
expectedWorkspaceRevision: input.workspaceRevision,
|
|
10362
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
10363
|
-
});
|
|
10364
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
10365
|
-
if (!entry)
|
|
10366
|
-
return failure("not_found", "decision not found");
|
|
10367
|
-
if (!authorityDecisionMatches(authority, entry.data))
|
|
10368
|
-
return failure("permission_denied", "native settlement authority does not match decision");
|
|
10369
|
-
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
10370
|
-
return failure("permission_denied", "native settlement caller does not match approval");
|
|
10371
|
-
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
10372
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
10373
|
-
if (entry.data.consumption?.state === "uncertain")
|
|
10374
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
10375
|
-
operation: "settle_action",
|
|
10376
|
-
outcome: "unknown"
|
|
10377
|
-
});
|
|
10378
|
-
if (entry.data.consumption?.state !== "reserved")
|
|
10379
|
-
return failure("permission_denied", "action is not reserved");
|
|
10380
|
-
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
10381
|
-
return failure("permission_denied", "settlement reference does not match reservation");
|
|
10382
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
10383
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
10384
|
-
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
10385
|
-
return failure("recovery_required", "bounded action progress is missing");
|
|
10386
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
10387
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
10388
|
-
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
10389
|
-
if (input.outcome === "unknown") {
|
|
10390
|
-
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
10391
|
-
operation: "settle_action",
|
|
10392
|
-
outcome: "unknown"
|
|
10393
|
-
});
|
|
10394
|
-
const uncertain = {
|
|
10395
|
-
...entry.data,
|
|
10396
|
-
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
10397
|
-
};
|
|
10398
|
-
return success(mutation.revision, null, {
|
|
10399
|
-
...current,
|
|
10400
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
10401
|
-
});
|
|
10402
|
-
}
|
|
10403
|
-
if (input.outcome === "not_started") {
|
|
10404
|
-
const released = { ...entry.data, consumption: null };
|
|
10405
|
-
return success(mutation.revision, null, {
|
|
10406
|
-
...current,
|
|
10407
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
|
|
10408
|
-
});
|
|
10409
|
-
}
|
|
10410
|
-
if (currentStep)
|
|
10411
|
-
workflow.completed.push(currentStep);
|
|
10412
|
-
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
10413
|
-
const settled = {
|
|
10414
|
-
...entry.data,
|
|
10415
|
-
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
10416
|
-
};
|
|
10417
|
-
const actionProgress = workflow.steps.length ? [
|
|
10418
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
10419
|
-
{
|
|
10420
|
-
decisionId: input.decisionId,
|
|
10421
|
-
steps: [...workflow.steps],
|
|
10422
|
-
completedSteps: [...workflow.completed]
|
|
10423
|
-
}
|
|
10424
|
-
] : current.actionProgress ?? [];
|
|
10425
|
-
const next = {
|
|
10426
|
-
...current,
|
|
10427
|
-
actionProgress,
|
|
10428
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
|
|
10429
|
-
};
|
|
10430
|
-
if (!complete) {
|
|
10431
|
-
outcomeResult = success(mutation.revision, input.workspaceRevision, {
|
|
10432
|
-
...entry,
|
|
10433
|
-
data: settled
|
|
10434
|
-
});
|
|
10435
|
-
}
|
|
10436
|
-
return success(mutation.revision, null, next);
|
|
10437
|
-
}, input.native?.now);
|
|
10438
|
-
if (!changed.ok)
|
|
10439
|
-
return changed;
|
|
10440
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
10441
|
-
if (!entry)
|
|
10442
|
-
return failure("recovery_required", "settled decision disappeared");
|
|
10443
|
-
if (outcomeResult)
|
|
10444
|
-
return outcomeResult;
|
|
10445
|
-
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
10446
|
-
}
|
|
10447
|
-
function reconcileAction(input) {
|
|
10448
|
-
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
10449
|
-
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
10450
|
-
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
10451
|
-
if (!authority || authority.kind !== "reconciliation")
|
|
10452
|
-
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
10453
|
-
if (authority.taskId !== input.taskId || authority.decisionId !== input.decisionId || !sameRef(authority.actionRef, input.actionRef) || authority.taskRevision !== input.taskRevision || authority.workspaceRevision !== input.workspaceRevision || authority.outcome !== input.outcome || authority.evidenceDigest !== input.evidenceDigest || authority.step !== input.step)
|
|
10454
|
-
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
10455
|
-
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
10456
|
-
const workspace = input.store.readWorkspace();
|
|
10457
|
-
if (!workspace.ok)
|
|
10458
|
-
return workspace;
|
|
10459
|
-
if (!workspace.data)
|
|
10460
|
-
return failure("not_found", "workspace not found");
|
|
10461
|
-
if (authority.workspaceId !== workspace.data.id)
|
|
10462
|
-
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
10463
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
10464
|
-
return failure("revision_conflict", "workspace revision does not match");
|
|
10465
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
10466
|
-
if (!entry || entry.data.purpose !== "action")
|
|
10467
|
-
return failure("not_found", "decision not found");
|
|
10468
|
-
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
10469
|
-
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
10470
|
-
if (entry.data.response !== "approved" || entry.data.revoked !== null || entry.data.digest !== decisionDigest(entry.data) || entry.data.binding.taskId !== current.id || entry.data.binding.workspaceId !== current.workspaceId || entry.data.binding.workspaceId !== workspace.data.id)
|
|
10471
|
-
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
10472
|
-
if (entry.data.consumption?.state !== "uncertain")
|
|
10473
|
-
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
10474
|
-
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
10475
|
-
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
10476
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
10477
|
-
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
10478
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
10479
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
10480
|
-
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
10481
|
-
if (input.outcome === "not_started") {
|
|
10482
|
-
return success(mutation.revision, null, {
|
|
10483
|
-
...current,
|
|
10484
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
10485
|
-
});
|
|
10486
|
-
}
|
|
10487
|
-
if (currentStep)
|
|
10488
|
-
workflow.completed.push(currentStep);
|
|
10489
|
-
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
10490
|
-
const nextDecision = {
|
|
10491
|
-
...entry.data,
|
|
10492
|
-
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
10493
|
-
};
|
|
10494
|
-
const actionProgress = workflow.steps.length ? [
|
|
10495
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
10496
|
-
{
|
|
10497
|
-
decisionId: input.decisionId,
|
|
10498
|
-
steps: workflow.steps,
|
|
10499
|
-
completedSteps: workflow.completed
|
|
10500
|
-
}
|
|
10501
|
-
] : current.actionProgress ?? [];
|
|
10502
|
-
return success(mutation.revision, null, {
|
|
10503
|
-
...current,
|
|
10504
|
-
actionProgress,
|
|
10505
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
10506
|
-
});
|
|
10507
|
-
}, input.native?.now);
|
|
10508
|
-
if (!changed.ok)
|
|
10509
|
-
return changed;
|
|
10510
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
10511
|
-
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
10512
|
-
}
|
|
10513
|
-
var bindingCovers = scopeCovers;
|
|
10514
|
-
var verifyDecisionContent = verifyContentRefs;
|
|
10515
|
-
|
|
10516
|
-
// packages/workit-core/src/core/task-evaluation.ts
|
|
10517
|
-
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
10518
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
10519
|
-
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
10520
|
-
var canonicalPath = (value) => {
|
|
10521
|
-
if (typeof value !== "string" || !value)
|
|
10522
|
-
return null;
|
|
10523
|
-
if (value.includes("\\") || value.split("/").includes(".."))
|
|
10524
|
-
return null;
|
|
10525
|
-
const normalized = path6.posix.normalize(value);
|
|
10526
|
-
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
10527
|
-
return null;
|
|
10528
|
-
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
10529
|
-
};
|
|
10530
|
-
var canonicalScope = (scope) => {
|
|
10531
|
-
const paths = scope.paths.length ? scope.paths : ["."];
|
|
10532
|
-
const normalizedPaths = paths.map(canonicalPath);
|
|
10533
|
-
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
10534
|
-
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
10535
|
-
return null;
|
|
10536
|
-
return {
|
|
10537
|
-
description: scope.description,
|
|
10538
|
-
paths: [...new Set(normalizedPaths)],
|
|
10539
|
-
exclusions: [...new Set(normalizedExclusions)]
|
|
10540
|
-
};
|
|
10541
|
-
};
|
|
10542
|
-
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
10543
|
-
var scopeMatches = (value, scope) => {
|
|
10544
|
-
const normalized = canonicalScope(scope);
|
|
10545
|
-
if (!normalized)
|
|
10546
|
-
return false;
|
|
10547
|
-
const paths = normalized.paths;
|
|
10548
|
-
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
10549
|
-
};
|
|
10550
|
-
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
10551
|
-
var compareCodeUnits2 = (left, right) => {
|
|
10552
|
-
const length = Math.min(left.length, right.length);
|
|
10553
|
-
for (let index = 0;index < length; index += 1) {
|
|
10554
|
-
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
10555
|
-
if (difference !== 0)
|
|
10556
|
-
return difference;
|
|
10557
|
-
}
|
|
10558
|
-
return left.length - right.length;
|
|
10559
|
-
};
|
|
10560
|
-
var gitPaths = (root) => {
|
|
10561
|
-
const result = spawnSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
10562
|
-
cwd: root,
|
|
10563
|
-
encoding: "buffer"
|
|
10564
|
-
});
|
|
10565
|
-
if (result.status !== 0) {
|
|
10566
|
-
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
10567
|
-
return {
|
|
10568
|
-
paths: [],
|
|
10569
|
-
uncertain: !/not a git repository/i.test(stderr),
|
|
10570
|
-
git: /not a git repository/i.test(stderr) ? false : true
|
|
10571
|
-
};
|
|
10572
|
-
}
|
|
10573
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
10574
|
-
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
10575
|
-
if (stagedDeleted.status !== 0) {
|
|
10576
|
-
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
10577
|
-
if (!/not a git repository/i.test(stderr))
|
|
10578
|
-
return { paths, uncertain: true, git: true };
|
|
10579
|
-
} else if (stagedDeleted.stdout) {
|
|
10580
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
10581
|
-
}
|
|
10582
|
-
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
10583
|
-
};
|
|
10584
|
-
var walk = (root, directory, output) => {
|
|
10585
|
-
let names;
|
|
10586
|
-
try {
|
|
10587
|
-
names = fs6.readdirSync(directory);
|
|
10588
|
-
} catch {
|
|
10589
|
-
return true;
|
|
10590
|
-
}
|
|
10591
|
-
let uncertain = false;
|
|
10592
|
-
for (const name of names) {
|
|
10593
|
-
if (name === ".git" || name === ".workit")
|
|
10594
|
-
continue;
|
|
10595
|
-
const target = path6.join(directory, name);
|
|
10596
|
-
let stat;
|
|
10597
|
-
try {
|
|
10598
|
-
stat = fs6.lstatSync(target);
|
|
10599
|
-
} catch {
|
|
10600
|
-
uncertain = true;
|
|
10601
|
-
continue;
|
|
10602
|
-
}
|
|
10603
|
-
const item = relative(root, target);
|
|
10604
|
-
output.add(item);
|
|
10605
|
-
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
10606
|
-
uncertain = walk(root, target, output) || uncertain;
|
|
10607
|
-
}
|
|
10608
|
-
return uncertain;
|
|
10609
|
-
};
|
|
10610
|
-
var scopeRoots = (root, scope) => {
|
|
10611
|
-
const roots = [];
|
|
10612
|
-
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
10613
|
-
const target = path6.resolve(root, item);
|
|
10614
|
-
if (!inside(root, target))
|
|
10615
|
-
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` }] });
|
|
10616
|
-
let ancestor = target;
|
|
10617
|
-
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
10618
|
-
ancestor = path6.dirname(ancestor);
|
|
10619
|
-
try {
|
|
10620
|
-
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
10621
|
-
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` }] });
|
|
10622
|
-
} catch {
|
|
10623
|
-
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
10624
|
-
}
|
|
10625
|
-
roots.push(target);
|
|
10626
|
-
}
|
|
10627
|
-
return success(null, null, roots);
|
|
10628
|
-
};
|
|
10629
|
-
function captureCandidate(root, scope, environment = []) {
|
|
10630
|
-
let checkout;
|
|
10631
|
-
try {
|
|
10632
|
-
checkout = fs6.realpathSync(root);
|
|
10633
|
-
} catch {
|
|
10634
|
-
return failure("invalid_input", "candidate root does not exist");
|
|
10635
|
-
}
|
|
10636
|
-
const normalizedScope = canonicalScope(scope);
|
|
10637
|
-
if (!normalizedScope)
|
|
10638
|
-
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" }] });
|
|
10639
|
-
const roots = scopeRoots(checkout, normalizedScope);
|
|
10640
|
-
if (!roots.ok)
|
|
10641
|
-
return roots;
|
|
10642
|
-
const git = gitPaths(checkout);
|
|
10643
|
-
const names = new Set;
|
|
10644
|
-
let uncertain = git.uncertain;
|
|
10645
|
-
for (const target of roots.data) {
|
|
10646
|
-
const item = relative(checkout, target);
|
|
10647
|
-
if (item === ".git" || item === ".workit")
|
|
10648
|
-
continue;
|
|
10649
|
-
names.add(item);
|
|
10650
|
-
if (!fs6.existsSync(target))
|
|
10651
|
-
continue;
|
|
10652
|
-
let stat;
|
|
10653
|
-
try {
|
|
10654
|
-
stat = fs6.lstatSync(target);
|
|
10655
|
-
} catch {
|
|
10656
|
-
uncertain = true;
|
|
10657
|
-
continue;
|
|
10658
|
-
}
|
|
10659
|
-
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
10660
|
-
uncertain = walk(checkout, target, names) || uncertain;
|
|
10661
|
-
}
|
|
10662
|
-
for (const item of git.paths)
|
|
10663
|
-
if (scopeMatches(item, normalizedScope))
|
|
10664
|
-
names.add(item);
|
|
10665
|
-
const files = [];
|
|
10666
|
-
let completeness = uncertain ? "uncertain" : "known";
|
|
10667
|
-
for (const item of [...names].sort()) {
|
|
10668
|
-
if (!scopeMatches(item, normalizedScope))
|
|
10669
|
-
continue;
|
|
10670
|
-
const target = path6.join(checkout, item);
|
|
10671
|
-
try {
|
|
10672
|
-
const stat = fs6.lstatSync(target);
|
|
10673
|
-
if (stat.isSymbolicLink()) {
|
|
10674
|
-
const link = fs6.readlinkSync(target);
|
|
10675
|
-
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
10676
|
-
} else if (stat.isFile()) {
|
|
10677
|
-
files.push({
|
|
10678
|
-
path: item,
|
|
10679
|
-
kind: "file",
|
|
10680
|
-
digest: digestBytes3(fs6.readFileSync(target)),
|
|
10681
|
-
executable: (stat.mode & 73) !== 0
|
|
10682
|
-
});
|
|
10683
|
-
} else if (!stat.isDirectory()) {
|
|
10684
|
-
completeness = "uncertain";
|
|
10685
|
-
} else if (git.git && git.paths.includes(item)) {
|
|
10686
|
-
completeness = "uncertain";
|
|
10687
|
-
}
|
|
10688
|
-
} catch (error) {
|
|
10689
|
-
if (error?.code === "ENOENT") {
|
|
10690
|
-
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
10691
|
-
} else
|
|
10692
|
-
completeness = "uncertain";
|
|
10693
|
-
}
|
|
10694
|
-
}
|
|
10695
|
-
const supplied = Array.isArray(environment) ? environment.map((value) => typeof value === "string" ? { name: value, value: process.env[value] ?? null } : { name: value.name, value: value.value ?? null }) : Object.keys(environment).map((name) => ({
|
|
10696
|
-
name,
|
|
10697
|
-
value: environment[name] ?? null
|
|
10698
|
-
}));
|
|
10699
|
-
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
10700
|
-
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
10701
|
-
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
10702
|
-
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
10703
|
-
if (values.some(({ value }) => value === null))
|
|
10704
|
-
completeness = "uncertain";
|
|
10705
|
-
const headResult = spawnSync("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
10706
|
-
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
10707
|
-
completeness = "uncertain";
|
|
10708
|
-
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
10709
|
-
const partial = {
|
|
10710
|
-
id: "0".repeat(64),
|
|
10711
|
-
scope: normalizedScope,
|
|
10712
|
-
completeness,
|
|
10713
|
-
files,
|
|
10714
|
-
environment: values,
|
|
10715
|
-
head
|
|
10325
|
+
var gitContext = (workspaceRoot, paths = []) => {
|
|
10326
|
+
const cwd = workspaceRoot;
|
|
10327
|
+
let failure;
|
|
10328
|
+
const runHere = (args) => {
|
|
10329
|
+
const result = run(cwd, args);
|
|
10330
|
+
if (!failure && result.exitCode !== 0) {
|
|
10331
|
+
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
10332
|
+
}
|
|
10333
|
+
return result.stdout;
|
|
10716
10334
|
};
|
|
10717
|
-
const
|
|
10718
|
-
const
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
var sessionFromRef = (value) => {
|
|
10734
|
-
if (typeof value !== "object" || value === null || value.kind !== "host")
|
|
10735
|
-
return null;
|
|
10736
|
-
return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10737
|
-
};
|
|
10738
|
-
var sameSession = (left, right) => {
|
|
10739
|
-
const other = sessionFromRef(right);
|
|
10740
|
-
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
10741
|
-
};
|
|
10742
|
-
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
10743
|
-
function evaluateEvidence(task, candidate) {
|
|
10744
|
-
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
10745
|
-
return task.evidence.map((entry) => {
|
|
10746
|
-
const evidence = entry.data;
|
|
10747
|
-
if (evidence.result === "failed")
|
|
10748
|
-
return {
|
|
10749
|
-
evidenceId: entry.id,
|
|
10750
|
-
status: "failed",
|
|
10751
|
-
reason: "failed evidence remains historical"
|
|
10752
|
-
};
|
|
10753
|
-
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
10754
|
-
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
10755
|
-
return {
|
|
10756
|
-
evidenceId: entry.id,
|
|
10757
|
-
status: "stale",
|
|
10758
|
-
reason: "check or review evidence lacks start and end candidates"
|
|
10759
|
-
};
|
|
10760
|
-
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
10761
|
-
return {
|
|
10762
|
-
evidenceId: entry.id,
|
|
10763
|
-
status: "stale",
|
|
10764
|
-
reason: "candidate changed during the check or review"
|
|
10765
|
-
};
|
|
10335
|
+
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
10336
|
+
const status_short = runHere(["status", "--porcelain"]);
|
|
10337
|
+
const staged = [];
|
|
10338
|
+
const unstaged = [];
|
|
10339
|
+
const untracked = [];
|
|
10340
|
+
for (const line of status_short.split(`
|
|
10341
|
+
`).filter(Boolean)) {
|
|
10342
|
+
const code = line.slice(0, 2);
|
|
10343
|
+
const file = line.slice(3);
|
|
10344
|
+
if (code === "??") {
|
|
10345
|
+
untracked.push(file);
|
|
10346
|
+
} else {
|
|
10347
|
+
if (code[0] !== " " && code[0] !== "?")
|
|
10348
|
+
staged.push(file);
|
|
10349
|
+
if (code[1] !== " " && code[1] !== "?")
|
|
10350
|
+
unstaged.push(file);
|
|
10766
10351
|
}
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
10775
|
-
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
10776
|
-
if (observed.id === current.id)
|
|
10777
|
-
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
10778
|
-
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
10779
|
-
const changed = changedPaths(observed, current);
|
|
10780
|
-
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
10781
|
-
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
10782
|
-
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
10783
|
-
return {
|
|
10784
|
-
evidenceId: entry.id,
|
|
10785
|
-
status: evidence.result,
|
|
10786
|
-
reason: "candidate changed outside evidence scope"
|
|
10787
|
-
};
|
|
10788
|
-
});
|
|
10789
|
-
}
|
|
10790
|
-
var scopeCovers2 = scopeCovers;
|
|
10791
|
-
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
10792
|
-
const pin = assignment.candidateId ?? null;
|
|
10793
|
-
if (pin === null)
|
|
10794
|
-
return true;
|
|
10795
|
-
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
10796
|
-
};
|
|
10797
|
-
var resolveBinding = (evidence, pin, currentCandidateId) => {
|
|
10798
|
-
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
10799
|
-
const fallback = bound ? pin ?? currentCandidateId : null;
|
|
10352
|
+
}
|
|
10353
|
+
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
10354
|
+
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
10355
|
+
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
10356
|
+
const stagedSet = new Set(staged);
|
|
10357
|
+
const unstagedSet = new Set(unstaged);
|
|
10358
|
+
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
10800
10359
|
return {
|
|
10801
|
-
|
|
10802
|
-
|
|
10360
|
+
workspace_root: cwd,
|
|
10361
|
+
branch,
|
|
10362
|
+
status_short,
|
|
10363
|
+
staged,
|
|
10364
|
+
unstaged,
|
|
10365
|
+
untracked,
|
|
10366
|
+
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
10367
|
+
`),
|
|
10368
|
+
partial_staged: partial_staged.length > 0,
|
|
10369
|
+
partial_staged_files: partial_staged,
|
|
10370
|
+
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
10803
10371
|
};
|
|
10804
10372
|
};
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
if (
|
|
10816
|
-
return
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
}
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
if (
|
|
10839
|
-
return
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
decisionIds: [],
|
|
10869
|
-
reason: "fresh applicable evidence passed"
|
|
10870
|
-
};
|
|
10871
|
-
if (requirement.dimension === "testing")
|
|
10872
|
-
return {
|
|
10873
|
-
requirementId: requirement.id,
|
|
10874
|
-
status: "unsatisfied",
|
|
10875
|
-
evidenceIds: [],
|
|
10876
|
-
decisionIds: [],
|
|
10877
|
-
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
10878
|
-
};
|
|
10879
|
-
}
|
|
10880
|
-
if (requirement.dimension === "delegation") {
|
|
10881
|
-
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
10882
|
-
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
10883
|
-
if (reconciled)
|
|
10884
|
-
return {
|
|
10885
|
-
requirementId: requirement.id,
|
|
10886
|
-
status: "satisfied",
|
|
10887
|
-
evidenceIds: [],
|
|
10888
|
-
decisionIds: [],
|
|
10889
|
-
reason: "bounded helper completed and its result is recorded"
|
|
10890
|
-
};
|
|
10891
|
-
}
|
|
10892
|
-
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
10893
|
-
if (decisions.length)
|
|
10894
|
-
return {
|
|
10895
|
-
requirementId: requirement.id,
|
|
10896
|
-
status: "satisfied",
|
|
10897
|
-
evidenceIds: [],
|
|
10898
|
-
decisionIds: decisions.map((decision) => decision.id),
|
|
10899
|
-
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
10900
|
-
};
|
|
10901
|
-
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
10902
|
-
if (limitations.length)
|
|
10903
|
-
return {
|
|
10904
|
-
requirementId: requirement.id,
|
|
10905
|
-
status: "accepted_limitation",
|
|
10906
|
-
evidenceIds: [],
|
|
10907
|
-
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
10908
|
-
reason: "an applicable approved limitation permits the missing evidence"
|
|
10909
|
-
};
|
|
10910
|
-
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
10911
|
-
if (!unavailable && related.length > 0 && !passed.length)
|
|
10912
|
-
return {
|
|
10913
|
-
requirementId: requirement.id,
|
|
10914
|
-
status: "unsatisfied",
|
|
10915
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
10916
|
-
decisionIds: [],
|
|
10917
|
-
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
10918
|
-
};
|
|
10373
|
+
|
|
10374
|
+
// packages/workit-core/src/core/vcs-config.ts
|
|
10375
|
+
import fs5 from "node:fs";
|
|
10376
|
+
import path7 from "node:path";
|
|
10377
|
+
import { spawnSync } from "node:child_process";
|
|
10378
|
+
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
10379
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
|
|
10380
|
+
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
10381
|
+
var remoteProvider = (cwd) => {
|
|
10382
|
+
const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
10383
|
+
if (r.status !== 0)
|
|
10384
|
+
return null;
|
|
10385
|
+
const url = (r.stdout ?? "").trim();
|
|
10386
|
+
if (/github\.com[:/]/.test(url))
|
|
10387
|
+
return "github";
|
|
10388
|
+
if (/gitlab\.com[:/]/.test(url))
|
|
10389
|
+
return "gitlab";
|
|
10390
|
+
return null;
|
|
10391
|
+
};
|
|
10392
|
+
var readVcsConfig = () => {
|
|
10393
|
+
const file = vcsConfigPath();
|
|
10394
|
+
let raw;
|
|
10395
|
+
try {
|
|
10396
|
+
raw = fs5.readFileSync(file, "utf8");
|
|
10397
|
+
} catch {
|
|
10398
|
+
return { status: "missing", path: file, config: {} };
|
|
10399
|
+
}
|
|
10400
|
+
let parsed;
|
|
10401
|
+
try {
|
|
10402
|
+
parsed = JSON.parse(raw);
|
|
10403
|
+
} catch {
|
|
10404
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
10405
|
+
}
|
|
10406
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10407
|
+
return { status: "valid", path: file, config: parsed };
|
|
10408
|
+
}
|
|
10409
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
10410
|
+
};
|
|
10411
|
+
function vcsConfig(mode, cwd) {
|
|
10412
|
+
const ws = resolveWorkspace(vcsCwd(cwd));
|
|
10413
|
+
const wsVcs = ws?.vcs ?? {};
|
|
10414
|
+
const wsYt = ws?.youtrack ?? {};
|
|
10415
|
+
const wsIssues = ws?.issues ?? {};
|
|
10416
|
+
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
10417
|
+
const root = vcsCwd(cwd);
|
|
10418
|
+
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
10419
|
+
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
10420
|
+
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
10421
|
+
const wp = ws?.branchPolicy ?? {};
|
|
10422
|
+
const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
|
|
10423
|
+
const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
10424
|
+
const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
|
|
10425
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
10426
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
10427
|
+
let issuesProvider = null;
|
|
10428
|
+
let linkOnPr = null;
|
|
10429
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
10430
|
+
issuesProvider = "github";
|
|
10431
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
10432
|
+
}
|
|
10433
|
+
if (mode === "resolve") {
|
|
10434
|
+
if (cfgStatus === "malformed")
|
|
10435
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
10919
10436
|
return {
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10437
|
+
ok: true,
|
|
10438
|
+
workspace_name: ws?.name ?? null,
|
|
10439
|
+
provider,
|
|
10440
|
+
defaultTargetBranch: defaultTarget,
|
|
10441
|
+
link_issues: linkIssues,
|
|
10442
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
10443
|
+
issues_provider: issuesProvider,
|
|
10444
|
+
link_on_pr: linkOnPr
|
|
10925
10445
|
};
|
|
10926
|
-
}
|
|
10446
|
+
}
|
|
10447
|
+
if (cfgStatus === "malformed") {
|
|
10448
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
10449
|
+
}
|
|
10450
|
+
if (cfgStatus === "missing") {
|
|
10451
|
+
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
10452
|
+
}
|
|
10453
|
+
if (provider === null) {
|
|
10454
|
+
return {
|
|
10455
|
+
ok: false,
|
|
10456
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
10457
|
+
configPath: cfgPath
|
|
10458
|
+
};
|
|
10459
|
+
}
|
|
10460
|
+
const prov = cfg[provider] ?? {};
|
|
10461
|
+
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
10462
|
+
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path7.join(configDir(), `${provider}.token`));
|
|
10463
|
+
const tokenPath = path7.resolve(tokenFile);
|
|
10464
|
+
let tokenOk = false;
|
|
10465
|
+
if (fs5.existsSync(tokenPath)) {
|
|
10466
|
+
const token = fs5.readFileSync(tokenPath, "utf8").trim();
|
|
10467
|
+
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
10468
|
+
tokenOk = !placeholder;
|
|
10469
|
+
}
|
|
10470
|
+
const out = {
|
|
10471
|
+
ok: true,
|
|
10472
|
+
configPath: path7.resolve(cfgPath),
|
|
10473
|
+
provider,
|
|
10474
|
+
defaultTargetBranch: defaultTarget,
|
|
10475
|
+
pr: cfg.pr ?? {},
|
|
10476
|
+
tokenPath,
|
|
10477
|
+
tokenPresent: fs5.existsSync(tokenPath),
|
|
10478
|
+
tokenReady: tokenOk,
|
|
10479
|
+
workspace_name: ws?.name ?? null,
|
|
10480
|
+
link_issues: linkIssues,
|
|
10481
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
10482
|
+
issues_provider: issuesProvider,
|
|
10483
|
+
link_on_pr: linkOnPr
|
|
10484
|
+
};
|
|
10485
|
+
if (provider === "gitlab") {
|
|
10486
|
+
out.gitlab = {
|
|
10487
|
+
host: prov.host ?? "gitlab.com",
|
|
10488
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
10489
|
+
};
|
|
10490
|
+
} else if (provider === "github") {
|
|
10491
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
10492
|
+
}
|
|
10493
|
+
if (mode === "summary")
|
|
10494
|
+
delete out.tokenPath;
|
|
10495
|
+
return out;
|
|
10927
10496
|
}
|
|
10928
|
-
function
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
const
|
|
10934
|
-
const
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
}
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10497
|
+
function mergedPrStyle(limit = 6, cwd) {
|
|
10498
|
+
const cfg = vcsConfig("load", cwd);
|
|
10499
|
+
if (!cfg.ok || !cfg.tokenReady)
|
|
10500
|
+
return { ok: false, error: "vcs not configured" };
|
|
10501
|
+
const provider = cfg.provider;
|
|
10502
|
+
const token = fs5.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
10503
|
+
const examples = [];
|
|
10504
|
+
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
10505
|
+
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
10506
|
+
sections: desc.split(`
|
|
10507
|
+
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
10508
|
+
descriptionPreview: desc.slice(0, 600)
|
|
10509
|
+
});
|
|
10510
|
+
if (provider === "gitlab") {
|
|
10511
|
+
const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
10512
|
+
if (remote.status !== 0)
|
|
10513
|
+
return { ok: false, error: "no origin remote" };
|
|
10514
|
+
const url = (remote.stdout ?? "").trim();
|
|
10515
|
+
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
10516
|
+
if (!m)
|
|
10517
|
+
return { ok: false, error: "not a gitlab.com origin" };
|
|
10518
|
+
const project = m[1];
|
|
10519
|
+
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
10520
|
+
const run = (args) => spawnSync("glab", ["api", ...args], { cwd, encoding: "utf8", env });
|
|
10521
|
+
let r = run([
|
|
10522
|
+
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
|
|
10523
|
+
]);
|
|
10524
|
+
if (r.status !== 0) {
|
|
10525
|
+
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
10526
|
+
}
|
|
10527
|
+
if (r.status !== 0)
|
|
10528
|
+
return { ok: false, error: "could not list merge requests" };
|
|
10529
|
+
for (const mr of JSON.parse(r.stdout ?? "[]")) {
|
|
10530
|
+
const desc = String(mr.description ?? "").trim();
|
|
10531
|
+
examples.push({
|
|
10532
|
+
title: mr.title,
|
|
10533
|
+
url: mr.web_url,
|
|
10534
|
+
squash: mr.squash,
|
|
10535
|
+
...descInfo(desc, true)
|
|
10536
|
+
});
|
|
10537
|
+
}
|
|
10538
|
+
} else if (provider === "github") {
|
|
10539
|
+
const r = spawnSync("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
10540
|
+
if (r.status !== 0)
|
|
10541
|
+
return { ok: false, error: "could not list pull requests" };
|
|
10542
|
+
for (const pr of JSON.parse(r.stdout ?? "[]")) {
|
|
10543
|
+
const desc = String(pr.body ?? "").trim();
|
|
10544
|
+
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
10963
10545
|
}
|
|
10964
10546
|
}
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
return {
|
|
10977
|
-
requirementId: item.requirementId,
|
|
10978
|
-
ruleId: requirement?.ruleId ?? "unknown",
|
|
10979
|
-
reason: item.reason,
|
|
10980
|
-
satisfaction: requirement?.satisfaction ?? "",
|
|
10981
|
-
dependentAction: requirement?.dependentAction ?? null
|
|
10982
|
-
};
|
|
10983
|
-
})
|
|
10984
|
-
});
|
|
10985
|
-
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
10986
|
-
if (requestedOutcome === "verified" && accepted.length)
|
|
10987
|
-
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
10988
|
-
requirementIds: accepted.map((item) => item.requirementId)
|
|
10989
|
-
});
|
|
10990
|
-
const evidenceIds = [
|
|
10991
|
-
...new Set([
|
|
10992
|
-
...evaluations.flatMap((item) => item.evidenceIds),
|
|
10993
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
10994
|
-
])
|
|
10995
|
-
];
|
|
10996
|
-
const decisionIds = [
|
|
10997
|
-
...new Set([
|
|
10998
|
-
...evaluations.flatMap((item) => item.decisionIds),
|
|
10999
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
11000
|
-
])
|
|
11001
|
-
];
|
|
11002
|
-
return success(null, null, {
|
|
11003
|
-
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
11004
|
-
evidenceIds,
|
|
11005
|
-
decisionIds,
|
|
11006
|
-
requirementIds: evaluations.map((item) => item.requirementId)
|
|
11007
|
-
});
|
|
10547
|
+
return {
|
|
10548
|
+
ok: true,
|
|
10549
|
+
provider,
|
|
10550
|
+
count: examples.length,
|
|
10551
|
+
styleHints: [
|
|
10552
|
+
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
10553
|
+
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
10554
|
+
"Do not paste commit log or diff stat into the body"
|
|
10555
|
+
],
|
|
10556
|
+
examples
|
|
10557
|
+
};
|
|
11008
10558
|
}
|
|
11009
10559
|
|
|
11010
|
-
// packages/workit-core/src/core/
|
|
11011
|
-
var
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
"
|
|
11018
|
-
|
|
11019
|
-
"
|
|
10560
|
+
// packages/workit-core/src/core/branch.ts
|
|
10561
|
+
var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
10562
|
+
var resolveCommitPolicyFor = (workspaceRoot) => resolveCommitPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
10563
|
+
var policy = (root) => resolveBranchPolicyFor(root);
|
|
10564
|
+
var allowedBranch = (root, name) => policy(root).allowed.some((r) => r.test(name));
|
|
10565
|
+
var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
|
|
10566
|
+
var verifyPushIdentity = (root, provider, account) => {
|
|
10567
|
+
if (!account || typeof account !== "string")
|
|
10568
|
+
return { ok: false, error: "push identity requires a configured area account" };
|
|
10569
|
+
const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
|
|
10570
|
+
const bin = provider === "gitlab" ? "glab" : "gh";
|
|
10571
|
+
let login;
|
|
10572
|
+
try {
|
|
10573
|
+
login = execFileSync2(bin, [...probe], {
|
|
10574
|
+
cwd: root,
|
|
10575
|
+
encoding: "utf8",
|
|
10576
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10577
|
+
}).trim();
|
|
10578
|
+
} catch {
|
|
10579
|
+
return { ok: false, error: `${bin} identity could not be resolved` };
|
|
10580
|
+
}
|
|
10581
|
+
if (!login || login.toLowerCase() !== account.toLowerCase())
|
|
10582
|
+
return {
|
|
10583
|
+
ok: false,
|
|
10584
|
+
error: `push identity ${login || "(unknown)"} does not match area account ${account}`
|
|
10585
|
+
};
|
|
10586
|
+
return { ok: true };
|
|
11020
10587
|
};
|
|
11021
|
-
var
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
const
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
10588
|
+
var baseBranch = (cwd) => {
|
|
10589
|
+
const resolved = vcsConfig("resolve", cwd);
|
|
10590
|
+
if (resolved.ok === false)
|
|
10591
|
+
return { error: String(resolved.error) };
|
|
10592
|
+
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
10593
|
+
};
|
|
10594
|
+
var originBaseReady = (cwd, base) => {
|
|
10595
|
+
const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10596
|
+
try {
|
|
10597
|
+
try {
|
|
10598
|
+
run(["fetch", "origin", base, "--prune"]);
|
|
10599
|
+
} catch {
|
|
10600
|
+
run(["fetch", "origin", "--prune"]);
|
|
10601
|
+
}
|
|
10602
|
+
try {
|
|
10603
|
+
execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
10604
|
+
cwd,
|
|
10605
|
+
stdio: "pipe"
|
|
10606
|
+
});
|
|
10607
|
+
} catch {
|
|
10608
|
+
return {
|
|
10609
|
+
ok: false,
|
|
10610
|
+
error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`
|
|
10611
|
+
};
|
|
10612
|
+
}
|
|
10613
|
+
return { ok: true };
|
|
10614
|
+
} catch (error) {
|
|
11032
10615
|
return {
|
|
11033
|
-
|
|
11034
|
-
|
|
10616
|
+
ok: false,
|
|
10617
|
+
error: error instanceof Error ? error.message : "ensure-base-branch failed"
|
|
11035
10618
|
};
|
|
11036
|
-
|
|
11037
|
-
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
11038
|
-
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
10619
|
+
}
|
|
11039
10620
|
};
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
missing authority. Record the plan's commit list once through the plan-scoped
|
|
11085
|
-
action approval (git.commit with plan_steps and plan_branch) so each listed
|
|
11086
|
-
commit executes without a new question; an unlisted message needs a fresh
|
|
11087
|
-
approval. task.close {outcome,
|
|
11088
|
-
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
11089
|
-
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
11090
|
-
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
11091
|
-
without a bound candidate goes stale; findings close only as fixed with passing
|
|
11092
|
-
verification, dismissed with supporting evidence, or deferred under an approved
|
|
11093
|
-
limitation. Use only the
|
|
11094
|
-
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
11095
|
-
and state changes. Authority is bounded by the requested scope, current
|
|
11096
|
-
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
11097
|
-
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
11098
|
-
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
11099
|
-
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
11100
|
-
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
11101
|
-
forgotten items mid-task → load workit-steer before responding; about to open
|
|
11102
|
-
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
11103
|
-
workit-green-run; small-looking change before close or merge → load
|
|
11104
|
-
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
11105
|
-
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
11106
|
-
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
11107
|
-
act from memory of it.
|
|
11108
|
-
`.trim();
|
|
11109
|
-
|
|
11110
|
-
// packages/workit-core/src/core/task-context.ts
|
|
11111
|
-
function reconcileResume(view, observations = []) {
|
|
11112
|
-
if (observations.length > 0)
|
|
11113
|
-
return failure("permission_denied", "worker observations require native host verification");
|
|
11114
|
-
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
11115
|
-
if (!candidate.ok)
|
|
11116
|
-
return candidate;
|
|
11117
|
-
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
11118
|
-
const blockers = [...view.task.progress.blockers];
|
|
11119
|
-
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
11120
|
-
blockers.push({
|
|
11121
|
-
reason: "worker state requires reconciliation",
|
|
11122
|
-
dependentAction: "resume",
|
|
11123
|
-
refs: []
|
|
10621
|
+
var fastForwardBase = (cwd, base) => {
|
|
10622
|
+
const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10623
|
+
try {
|
|
10624
|
+
let hasLocalBase = true;
|
|
10625
|
+
try {
|
|
10626
|
+
execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
10627
|
+
cwd,
|
|
10628
|
+
stdio: "pipe"
|
|
10629
|
+
});
|
|
10630
|
+
} catch {
|
|
10631
|
+
hasLocalBase = false;
|
|
10632
|
+
}
|
|
10633
|
+
if (hasLocalBase) {
|
|
10634
|
+
run(["checkout", base]);
|
|
10635
|
+
try {
|
|
10636
|
+
run(["merge", "--ff-only", `origin/${base}`]);
|
|
10637
|
+
} catch {}
|
|
10638
|
+
} else {
|
|
10639
|
+
run(["checkout", "-b", base, "--track", `origin/${base}`]);
|
|
10640
|
+
}
|
|
10641
|
+
return { ok: true };
|
|
10642
|
+
} catch (error) {
|
|
10643
|
+
return {
|
|
10644
|
+
ok: false,
|
|
10645
|
+
error: error instanceof Error ? error.message : "ensure-base-branch failed"
|
|
10646
|
+
};
|
|
10647
|
+
}
|
|
10648
|
+
};
|
|
10649
|
+
var ensureBaseBranch = (cwd, base) => {
|
|
10650
|
+
const git = gitContext(cwd);
|
|
10651
|
+
if (!git.branch || git.branch === "unknown")
|
|
10652
|
+
return { ok: false, error: "not in a git repository" };
|
|
10653
|
+
const ready = originBaseReady(cwd, base);
|
|
10654
|
+
if (!ready.ok)
|
|
10655
|
+
return ready;
|
|
10656
|
+
return fastForwardBase(cwd, base);
|
|
10657
|
+
};
|
|
10658
|
+
var classifyBranchDirt = (workspaceRoot) => {
|
|
10659
|
+
let raw;
|
|
10660
|
+
try {
|
|
10661
|
+
raw = execFileSync2("git", ["status", "--porcelain=v1"], {
|
|
10662
|
+
cwd: path8.resolve(workspaceRoot),
|
|
10663
|
+
encoding: "utf8",
|
|
10664
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11124
10665
|
});
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
var
|
|
11148
|
-
|
|
10666
|
+
} catch {
|
|
10667
|
+
return "clean";
|
|
10668
|
+
}
|
|
10669
|
+
let dirty = false;
|
|
10670
|
+
for (const line of raw.split(`
|
|
10671
|
+
`)) {
|
|
10672
|
+
if (!line.trim())
|
|
10673
|
+
continue;
|
|
10674
|
+
dirty = true;
|
|
10675
|
+
const code = line.slice(0, 2);
|
|
10676
|
+
if (code === "??")
|
|
10677
|
+
continue;
|
|
10678
|
+
let entry = line.slice(3);
|
|
10679
|
+
const arrow = entry.indexOf(" -> ");
|
|
10680
|
+
if (arrow >= 0)
|
|
10681
|
+
entry = entry.slice(arrow + 4);
|
|
10682
|
+
const unquoted = entry.startsWith('"') && entry.endsWith('"') ? entry.slice(1, -1) : entry;
|
|
10683
|
+
if (unquoted !== "docs" && !unquoted.startsWith("docs/"))
|
|
10684
|
+
return "stash-required";
|
|
10685
|
+
}
|
|
10686
|
+
return dirty ? "carry" : "clean";
|
|
10687
|
+
};
|
|
10688
|
+
var branchSetup = ({
|
|
10689
|
+
action,
|
|
10690
|
+
sdd_dir,
|
|
10691
|
+
target_branch,
|
|
10692
|
+
stash,
|
|
10693
|
+
workspace_root,
|
|
10694
|
+
log
|
|
10695
|
+
}) => {
|
|
10696
|
+
const cwd = path8.resolve(workspace_root);
|
|
10697
|
+
const exec = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10698
|
+
const current = gitContext(cwd).branch;
|
|
10699
|
+
if (!current || current === "unknown")
|
|
10700
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
10701
|
+
const sdd = sdd_dir ?? "docs";
|
|
10702
|
+
const manifestPath = path8.isAbsolute(sdd) ? path8.join(sdd, "manifest.json") : path8.join(cwd, sdd, "manifest.json");
|
|
10703
|
+
const ensureManifestDir = () => {
|
|
10704
|
+
const dir = path8.dirname(manifestPath);
|
|
10705
|
+
if (!existsSync4(dir))
|
|
10706
|
+
mkdirSync3(dir, { recursive: true, mode: 493 });
|
|
10707
|
+
};
|
|
10708
|
+
const readManifest = () => {
|
|
10709
|
+
try {
|
|
10710
|
+
return JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
10711
|
+
} catch {
|
|
10712
|
+
return {};
|
|
10713
|
+
}
|
|
10714
|
+
};
|
|
10715
|
+
const writeManifest = (data) => {
|
|
10716
|
+
ensureManifestDir();
|
|
10717
|
+
writeFileSync3(manifestPath, JSON.stringify(data, null, 2) + `
|
|
10718
|
+
`, "utf8");
|
|
10719
|
+
};
|
|
10720
|
+
const journal = (message) => log?.(`branch-setup: ${message}`);
|
|
10721
|
+
if (action === "reapply_stash") {
|
|
10722
|
+
const manifest = readManifest();
|
|
10723
|
+
const ref = manifest.stash_ref;
|
|
10724
|
+
if (!ref)
|
|
10725
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
10726
|
+
journal(`pre-pop: ${String(ref)}`);
|
|
10727
|
+
try {
|
|
10728
|
+
exec(["stash", "pop", String(ref)]);
|
|
10729
|
+
} catch (error) {
|
|
10730
|
+
journal("pop: failed");
|
|
10731
|
+
return {
|
|
10732
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
10733
|
+
phase: "post_mutation"
|
|
10734
|
+
};
|
|
10735
|
+
}
|
|
10736
|
+
journal("pop: ok");
|
|
10737
|
+
delete manifest.stash_ref;
|
|
10738
|
+
delete manifest.stash_created_at;
|
|
10739
|
+
writeManifest(manifest);
|
|
10740
|
+
return { action: "reapply_stash", ok: true };
|
|
10741
|
+
}
|
|
10742
|
+
const target = target_branch ?? "";
|
|
10743
|
+
if (!target)
|
|
11149
10744
|
return {
|
|
11150
|
-
|
|
11151
|
-
|
|
10745
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
10746
|
+
phase: "preflight"
|
|
11152
10747
|
};
|
|
11153
|
-
if (
|
|
10748
|
+
if (isProtected(cwd, target))
|
|
10749
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
10750
|
+
if (!allowedBranch(cwd, target))
|
|
11154
10751
|
return {
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
id: ref.id
|
|
10752
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
10753
|
+
phase: "preflight"
|
|
11158
10754
|
};
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
const
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
10755
|
+
let base;
|
|
10756
|
+
let targetExists = true;
|
|
10757
|
+
try {
|
|
10758
|
+
exec(["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]);
|
|
10759
|
+
} catch {
|
|
10760
|
+
targetExists = false;
|
|
10761
|
+
}
|
|
10762
|
+
if (!targetExists) {
|
|
10763
|
+
const baseResolved = baseBranch(cwd);
|
|
10764
|
+
if ("error" in baseResolved)
|
|
10765
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
10766
|
+
base = baseResolved.base;
|
|
10767
|
+
}
|
|
10768
|
+
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
10769
|
+
let stash_ref;
|
|
10770
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
10771
|
+
let suffix = "";
|
|
10772
|
+
if (stash_ref) {
|
|
10773
|
+
journal(`pre-pop: ${stash_ref}`);
|
|
10774
|
+
try {
|
|
10775
|
+
exec(["stash", "pop", stash_ref]);
|
|
10776
|
+
stash_ref = undefined;
|
|
10777
|
+
journal("pop: ok");
|
|
10778
|
+
} catch {
|
|
10779
|
+
journal("pop: failed");
|
|
10780
|
+
suffix = " (changes preserved in stash)";
|
|
10781
|
+
}
|
|
10782
|
+
}
|
|
10783
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
11186
10784
|
};
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
if (
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
10785
|
+
if (current !== target) {
|
|
10786
|
+
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
10787
|
+
if (classifyBranchDirt(cwd) === "stash-required" && stash !== "yes") {
|
|
10788
|
+
return {
|
|
10789
|
+
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
|
|
10790
|
+
phase: "preflight"
|
|
10791
|
+
};
|
|
10792
|
+
}
|
|
10793
|
+
let validatedBase;
|
|
10794
|
+
if (!targetExists && base !== undefined) {
|
|
10795
|
+
const ready = originBaseReady(cwd, base);
|
|
10796
|
+
if (!ready.ok)
|
|
10797
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
10798
|
+
validatedBase = base;
|
|
10799
|
+
}
|
|
10800
|
+
if (dirty && stash === "yes") {
|
|
10801
|
+
try {
|
|
10802
|
+
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
10803
|
+
} catch (error) {
|
|
10804
|
+
return {
|
|
10805
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
10806
|
+
phase: "post_mutation"
|
|
10807
|
+
};
|
|
10808
|
+
}
|
|
10809
|
+
stash_ref = "stash@{0}";
|
|
10810
|
+
journal(`stash push: ${stash_ref}`);
|
|
10811
|
+
}
|
|
10812
|
+
try {
|
|
10813
|
+
exec(["checkout", target]);
|
|
10814
|
+
journal("post-checkout");
|
|
10815
|
+
} catch (error) {
|
|
10816
|
+
const message = error instanceof Error ? error.message : "checkout failed";
|
|
10817
|
+
if (/worktree/i.test(message)) {
|
|
10818
|
+
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
|
|
10819
|
+
}
|
|
10820
|
+
try {
|
|
10821
|
+
let effectiveBase = base;
|
|
10822
|
+
if (effectiveBase === undefined) {
|
|
10823
|
+
const lateResolved = baseBranch(cwd);
|
|
10824
|
+
if ("error" in lateResolved)
|
|
10825
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
10826
|
+
effectiveBase = lateResolved.base;
|
|
10827
|
+
}
|
|
10828
|
+
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
10829
|
+
if (!baseResult.ok)
|
|
10830
|
+
return failAfterStash(baseResult.error ?? "ensure-base-branch failed");
|
|
10831
|
+
exec(["checkout", "-b", target]);
|
|
10832
|
+
journal("post-create");
|
|
10833
|
+
} catch (createError) {
|
|
10834
|
+
return failAfterStash(createError instanceof Error ? createError.message : "branch create failed");
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
11196
10837
|
}
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
10838
|
+
try {
|
|
10839
|
+
const manifest = readManifest();
|
|
10840
|
+
manifest.branch = target;
|
|
10841
|
+
manifest.previous_branch = current;
|
|
10842
|
+
if (stash_ref) {
|
|
10843
|
+
manifest.stash_ref = stash_ref;
|
|
10844
|
+
manifest.stash_created_at = new Date().toISOString();
|
|
10845
|
+
}
|
|
10846
|
+
writeManifest(manifest);
|
|
10847
|
+
} catch (error) {
|
|
10848
|
+
const result = failAfterStash(error instanceof Error ? `manifest update failed: ${error.message}` : "manifest update failed");
|
|
10849
|
+
if (stash_ref === undefined && gitContext(cwd).branch !== current) {
|
|
10850
|
+
try {
|
|
10851
|
+
exec(["checkout", current]);
|
|
10852
|
+
} catch {
|
|
10853
|
+
try {
|
|
10854
|
+
exec(["checkout", "-m", current]);
|
|
10855
|
+
} catch {}
|
|
10856
|
+
}
|
|
10857
|
+
}
|
|
10858
|
+
return result;
|
|
11201
10859
|
}
|
|
11202
|
-
return
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
}
|
|
11210
|
-
// packages/workit-core/src/core/route-intent.ts
|
|
11211
|
-
var GUIDANCE = {
|
|
11212
|
-
"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)",
|
|
11213
|
-
"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"
|
|
11214
|
-
};
|
|
11215
|
-
var unbalancedQuotes = (segment) => {
|
|
11216
|
-
const text = segment.replace(/\\./gu, "");
|
|
11217
|
-
return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
|
|
10860
|
+
return {
|
|
10861
|
+
action: "setup",
|
|
10862
|
+
ok: true,
|
|
10863
|
+
branch: target,
|
|
10864
|
+
previous_branch: current,
|
|
10865
|
+
stash_ref: stash_ref ?? null,
|
|
10866
|
+
manifest: manifestPath
|
|
10867
|
+
};
|
|
11218
10868
|
};
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
10869
|
+
|
|
10870
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
10871
|
+
var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
|
|
10872
|
+
var OPERATION_CLASS = {
|
|
10873
|
+
"git.branch_setup": "branch",
|
|
10874
|
+
"git.commit": "commit",
|
|
10875
|
+
"git.push": "push",
|
|
10876
|
+
"hosting.pull_request": "pr",
|
|
10877
|
+
"hosting.merge": "merge"
|
|
10878
|
+
};
|
|
10879
|
+
var operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null;
|
|
10880
|
+
var digestOf = (configDirPath) => {
|
|
10881
|
+
try {
|
|
10882
|
+
const raw = readFileSync5(`${configDirPath}/workspaces.json`, "utf8");
|
|
10883
|
+
return createHash3("sha256").update(raw).digest("hex");
|
|
10884
|
+
} catch {
|
|
11223
10885
|
return null;
|
|
11224
|
-
|
|
11225
|
-
while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
|
|
11226
|
-
parts.shift();
|
|
11227
|
-
return parts.length > 0 ? parts : null;
|
|
10886
|
+
}
|
|
11228
10887
|
};
|
|
11229
|
-
var
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
const second = unquote(parts[1] ?? "");
|
|
11239
|
-
const third = unquote(parts[2] ?? "");
|
|
11240
|
-
if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
|
|
11241
|
-
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
11242
|
-
if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
|
|
11243
|
-
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
11244
|
-
if (head === "gh" && second === "pr" && third === "create")
|
|
11245
|
-
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
11246
|
-
if (head === "glab" && second === "mr" && third === "create")
|
|
11247
|
-
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
10888
|
+
var normalizeClasses = (value) => {
|
|
10889
|
+
if (value === true)
|
|
10890
|
+
return [...AUTO_CLASSES];
|
|
10891
|
+
if (!Array.isArray(value))
|
|
10892
|
+
return [];
|
|
10893
|
+
const seen = new Set;
|
|
10894
|
+
for (const item of value) {
|
|
10895
|
+
if (typeof item === "string" && AUTO_CLASSES.includes(item))
|
|
10896
|
+
seen.add(item);
|
|
11248
10897
|
}
|
|
11249
|
-
return
|
|
11250
|
-
}
|
|
11251
|
-
|
|
10898
|
+
return [...seen];
|
|
10899
|
+
};
|
|
10900
|
+
var resolveAutoApproval = (root, dir = configDir()) => {
|
|
10901
|
+
let match;
|
|
10902
|
+
try {
|
|
10903
|
+
match = resolveWorkspaceFrom(root, dir);
|
|
10904
|
+
} catch {
|
|
10905
|
+
return { status: "off" };
|
|
10906
|
+
}
|
|
10907
|
+
if (!match)
|
|
10908
|
+
return { status: "off" };
|
|
10909
|
+
const classes = normalizeClasses(match.autoApprove);
|
|
10910
|
+
if (classes.length === 0)
|
|
10911
|
+
return { status: "off" };
|
|
10912
|
+
return {
|
|
10913
|
+
status: "on",
|
|
10914
|
+
workspace: match.name,
|
|
10915
|
+
classes,
|
|
10916
|
+
account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
|
|
10917
|
+
configDigest: digestOf(dir)
|
|
10918
|
+
};
|
|
10919
|
+
};
|
|
10920
|
+
var autoApproves = (root, cls, dir) => {
|
|
10921
|
+
if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
|
|
10922
|
+
return false;
|
|
10923
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
10924
|
+
return resolved.status === "on" && resolved.classes.includes(cls);
|
|
10925
|
+
};
|
|
10926
|
+
var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
|
|
10927
|
+
var standingApprovalLive = (root, receipt, cls, dir) => {
|
|
10928
|
+
if (!isStandingReceipt(receipt))
|
|
10929
|
+
return false;
|
|
10930
|
+
if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
|
|
10931
|
+
return false;
|
|
10932
|
+
if (receipt.class !== cls)
|
|
10933
|
+
return false;
|
|
10934
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
10935
|
+
if (resolved.status !== "on")
|
|
10936
|
+
return false;
|
|
10937
|
+
if (resolved.workspace !== receipt.workspace)
|
|
10938
|
+
return false;
|
|
10939
|
+
return resolved.classes.includes(cls);
|
|
10940
|
+
};
|
|
10941
|
+
var standingReceiptFor = (root, workspace, cls, dir) => {
|
|
10942
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
10943
|
+
return {
|
|
10944
|
+
kind: "standing",
|
|
10945
|
+
workspace,
|
|
10946
|
+
class: cls,
|
|
10947
|
+
configDigest: resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null
|
|
10948
|
+
};
|
|
10949
|
+
};
|
|
10950
|
+
var verifyStandingApproval = (root, task, caller, binding) => {
|
|
10951
|
+
if (task.status !== "active")
|
|
10952
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
10953
|
+
const session = task.intent?.provenance?.session;
|
|
10954
|
+
if (!session || session.kind !== "host" || session.host !== caller.host || session.handle !== caller.actor)
|
|
10955
|
+
return failure("permission_denied", "standing approval requires the lead session");
|
|
10956
|
+
const standing = binding.standing;
|
|
10957
|
+
if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
|
|
10958
|
+
return failure("invalid_input", "standing approval needs a workspace and class");
|
|
10959
|
+
let operation;
|
|
11252
10960
|
try {
|
|
11253
|
-
|
|
11254
|
-
return false;
|
|
11255
|
-
const listed = new TaskStore(dir).listTasks();
|
|
11256
|
-
if (!listed.ok)
|
|
11257
|
-
return false;
|
|
11258
|
-
return listed.data.some((task) => task.status === "active" || task.status === "paused");
|
|
10961
|
+
operation = typeof binding.approvedContent === "string" ? JSON.parse(binding.approvedContent).operation : null;
|
|
11259
10962
|
} catch {
|
|
11260
|
-
|
|
10963
|
+
operation = null;
|
|
10964
|
+
}
|
|
10965
|
+
const cls = operationAutoClass(operation);
|
|
10966
|
+
if (!cls || cls !== standing.class)
|
|
10967
|
+
return failure("permission_denied", "standing approval does not cover this operation");
|
|
10968
|
+
const resolved = resolveAutoApproval(root);
|
|
10969
|
+
if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
|
|
10970
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
10971
|
+
if (cls === "push") {
|
|
10972
|
+
const workspace = resolveWorkspaceFrom(root, configDir());
|
|
10973
|
+
const provider = workspace?.vcs?.provider;
|
|
10974
|
+
const account = resolved.account;
|
|
10975
|
+
if (!provider || !account)
|
|
10976
|
+
return failure("permission_denied", "auto-push requires a workspace provider and area account");
|
|
10977
|
+
const identity = verifyPushIdentity(root, provider, account);
|
|
10978
|
+
if (!identity.ok)
|
|
10979
|
+
return failure("permission_denied", identity.error);
|
|
11261
10980
|
}
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
// packages/workit-core/src/core/git.ts
|
|
11274
|
-
import { execFileSync } from "node:child_process";
|
|
11275
|
-
var run = (cwd, args) => {
|
|
10981
|
+
return success(null, null, {
|
|
10982
|
+
kind: "host_observed",
|
|
10983
|
+
host: caller.host,
|
|
10984
|
+
session: { kind: "host", host: caller.host, handle: caller.actor },
|
|
10985
|
+
workerId: null,
|
|
10986
|
+
receipts: [standingReceiptFor(root, standing.workspace, cls)]
|
|
10987
|
+
});
|
|
10988
|
+
};
|
|
10989
|
+
var standingAutoApplies = (store, host, actor, operation) => {
|
|
10990
|
+
let descriptor;
|
|
11276
10991
|
try {
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
11281
|
-
});
|
|
11282
|
-
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
11283
|
-
} catch (error) {
|
|
11284
|
-
const e = error;
|
|
11285
|
-
return {
|
|
11286
|
-
stdout: (e.stdout ?? "").trimEnd(),
|
|
11287
|
-
stderr: (e.stderr ?? "").trimEnd(),
|
|
11288
|
-
exitCode: e.status ?? 1
|
|
11289
|
-
};
|
|
10992
|
+
descriptor = JSON.parse(operation);
|
|
10993
|
+
} catch {
|
|
10994
|
+
return null;
|
|
11290
10995
|
}
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
const
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
return
|
|
10996
|
+
const cls = operationAutoClass(descriptor.operation);
|
|
10997
|
+
if (!cls || !autoApproves(store.root, cls))
|
|
10998
|
+
return null;
|
|
10999
|
+
const listed = store.listTasks();
|
|
11000
|
+
const workspace = store.readWorkspace();
|
|
11001
|
+
if (!listed.ok || !workspace.ok || !workspace.data)
|
|
11002
|
+
return null;
|
|
11003
|
+
const matches = listed.data.filter((task) => task.status === "active" && task.workspaceId === workspace.data.id && task.intent?.provenance?.session?.kind === "host" && task.intent.provenance.session.host === host && task.intent.provenance.session.handle === actor);
|
|
11004
|
+
if (matches.length !== 1)
|
|
11005
|
+
return null;
|
|
11006
|
+
const resolved = resolveAutoApproval(store.root);
|
|
11007
|
+
if (resolved.status !== "on")
|
|
11008
|
+
return null;
|
|
11009
|
+
return {
|
|
11010
|
+
task: matches[0],
|
|
11011
|
+
workspaceId: workspace.data.id,
|
|
11012
|
+
workspaceRevision: workspace.data.revision,
|
|
11013
|
+
cls,
|
|
11014
|
+
workspaceName: resolved.workspace
|
|
11301
11015
|
};
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
const code = line.slice(0, 2);
|
|
11310
|
-
const file = line.slice(3);
|
|
11311
|
-
if (code === "??") {
|
|
11312
|
-
untracked.push(file);
|
|
11313
|
-
} else {
|
|
11314
|
-
if (code[0] !== " " && code[0] !== "?")
|
|
11315
|
-
staged.push(file);
|
|
11316
|
-
if (code[1] !== " " && code[1] !== "?")
|
|
11317
|
-
unstaged.push(file);
|
|
11318
|
-
}
|
|
11016
|
+
};
|
|
11017
|
+
var standingAutoBinding = (core, store, host, actor, operation) => {
|
|
11018
|
+
let descriptor;
|
|
11019
|
+
try {
|
|
11020
|
+
descriptor = JSON.parse(operation);
|
|
11021
|
+
} catch {
|
|
11022
|
+
return null;
|
|
11319
11023
|
}
|
|
11320
|
-
const
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
const
|
|
11324
|
-
|
|
11325
|
-
|
|
11024
|
+
const cls = operationAutoClass(descriptor.operation);
|
|
11025
|
+
if (!cls || !autoApproves(store.root, cls))
|
|
11026
|
+
return null;
|
|
11027
|
+
const applies = standingAutoApplies(store, host, actor, operation);
|
|
11028
|
+
if (!applies)
|
|
11029
|
+
return null;
|
|
11030
|
+
const { task } = applies;
|
|
11031
|
+
const resolved = resolveAutoApproval(store.root);
|
|
11032
|
+
if (resolved.status !== "on")
|
|
11033
|
+
return null;
|
|
11034
|
+
const presented = `Workit decision: action — auto-approved ${String(descriptor.operation)} ` + `under workspace rule "${resolved.workspace}" (no question asked).`;
|
|
11035
|
+
const recorded = core.observeStandingDecision({
|
|
11036
|
+
schemaVersion: 1,
|
|
11037
|
+
action: "record",
|
|
11038
|
+
taskId: task.id,
|
|
11039
|
+
expectedRevision: task.revision,
|
|
11040
|
+
purpose: "action",
|
|
11041
|
+
binding: {
|
|
11042
|
+
taskId: task.id,
|
|
11043
|
+
workspaceId: applies.workspaceId,
|
|
11044
|
+
scope: task.intent.data.scope,
|
|
11045
|
+
presented,
|
|
11046
|
+
approvedContent: operation,
|
|
11047
|
+
contentRefs: [],
|
|
11048
|
+
standing: { workspace: resolved.workspace, class: cls }
|
|
11049
|
+
},
|
|
11050
|
+
response: "approved",
|
|
11051
|
+
requirementIds: []
|
|
11052
|
+
});
|
|
11053
|
+
if (!recorded.ok)
|
|
11054
|
+
return null;
|
|
11055
|
+
const fresh = store.readTask(task.id);
|
|
11056
|
+
if (!fresh.ok)
|
|
11057
|
+
return null;
|
|
11326
11058
|
return {
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
untracked,
|
|
11333
|
-
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
11334
|
-
`),
|
|
11335
|
-
partial_staged: partial_staged.length > 0,
|
|
11336
|
-
partial_staged_files: partial_staged,
|
|
11337
|
-
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
11059
|
+
taskId: task.id,
|
|
11060
|
+
decisionId: recorded.data.id,
|
|
11061
|
+
binding: recorded.data.data.binding,
|
|
11062
|
+
expectedRevision: fresh.data.revision,
|
|
11063
|
+
expectedWorkspaceRevision: applies.workspaceRevision
|
|
11338
11064
|
};
|
|
11339
11065
|
};
|
|
11340
11066
|
|
|
11341
|
-
// packages/workit-core/src/core/
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
readFileSync as readFileSync4,
|
|
11348
|
-
readdirSync as readdirSync3,
|
|
11349
|
-
writeFileSync as writeFileSync2
|
|
11350
|
-
} from "node:fs";
|
|
11351
|
-
import os from "node:os";
|
|
11352
|
-
import path7 from "node:path";
|
|
11353
|
-
|
|
11354
|
-
// packages/workit-core/src/core/boundary.ts
|
|
11355
|
-
var EVENT = {
|
|
11356
|
-
initialization: "initialization",
|
|
11357
|
-
provenance: "provenance",
|
|
11358
|
-
assets: "assets",
|
|
11359
|
-
configurationSource: "configuration_source",
|
|
11360
|
-
hooks: "hooks",
|
|
11361
|
-
mcpConnection: "mcp_connection",
|
|
11362
|
-
migration: "migration",
|
|
11363
|
-
installSteps: "install_steps",
|
|
11364
|
-
uncaughtFailure: "uncaught_failure",
|
|
11365
|
-
toolsFailed: "tools_failed",
|
|
11366
|
-
doctor: "doctor"
|
|
11067
|
+
// packages/workit-core/src/core/authority.ts
|
|
11068
|
+
var verifiedAuthorities = new WeakMap;
|
|
11069
|
+
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
11070
|
+
const token = {};
|
|
11071
|
+
verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
|
|
11072
|
+
return token;
|
|
11367
11073
|
};
|
|
11368
|
-
var
|
|
11369
|
-
|
|
11370
|
-
|
|
11074
|
+
var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
|
|
11075
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11076
|
+
var normalizeChainSteps = (steps) => {
|
|
11077
|
+
if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
|
|
11078
|
+
return null;
|
|
11079
|
+
const normalized = [];
|
|
11080
|
+
for (const step of steps) {
|
|
11081
|
+
if (typeof step === "string") {
|
|
11082
|
+
if (!step.trim())
|
|
11083
|
+
return null;
|
|
11084
|
+
normalized.push({ kind: "commit", message: step });
|
|
11085
|
+
continue;
|
|
11086
|
+
}
|
|
11087
|
+
if (!isRecord(step))
|
|
11088
|
+
return null;
|
|
11089
|
+
if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
|
|
11090
|
+
normalized.push({ kind: "branch", target: step.branch });
|
|
11091
|
+
continue;
|
|
11092
|
+
}
|
|
11093
|
+
if (step.pr === true && Object.keys(step).length === 1) {
|
|
11094
|
+
normalized.push({ kind: "pr" });
|
|
11095
|
+
continue;
|
|
11096
|
+
}
|
|
11097
|
+
return null;
|
|
11371
11098
|
}
|
|
11372
|
-
|
|
11099
|
+
const keys = normalized.map(chainStepKey);
|
|
11100
|
+
if (new Set(keys).size !== keys.length)
|
|
11101
|
+
return null;
|
|
11102
|
+
return normalized;
|
|
11103
|
+
};
|
|
11104
|
+
var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
11105
|
+
var parseSteps = (content) => {
|
|
11106
|
+
let value;
|
|
11107
|
+
try {
|
|
11108
|
+
value = JSON.parse(content);
|
|
11109
|
+
} catch {
|
|
11110
|
+
return [];
|
|
11111
|
+
}
|
|
11112
|
+
if (typeof value !== "object" || value === null || !("steps" in value))
|
|
11113
|
+
return [];
|
|
11114
|
+
const steps = value.steps;
|
|
11115
|
+
if (!Array.isArray(steps))
|
|
11116
|
+
return null;
|
|
11117
|
+
const normalized = normalizeChainSteps(steps);
|
|
11118
|
+
return normalized;
|
|
11119
|
+
};
|
|
11120
|
+
var decisionMatches = (entry, purpose, binding) => {
|
|
11121
|
+
const decision = entry.data;
|
|
11122
|
+
return binding.taskId === decision.binding.taskId && binding.workspaceId === decision.binding.workspaceId && decision.purpose === purpose && decision.response === "approved" && decision.revoked === null && (purpose !== "action" || decision.consumption === null) && decision.digest === decisionDigest(decision) && scopeEqual(decision.binding.scope, binding.scope) && canonicalJson(decision.binding) === canonicalJson(binding);
|
|
11123
|
+
};
|
|
11124
|
+
var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
11125
|
+
var validProvenance = (value, caller) => {
|
|
11126
|
+
if (!provenanceSchema.safeParse(value).success)
|
|
11127
|
+
return false;
|
|
11128
|
+
const provenance = value;
|
|
11129
|
+
return provenance.kind === "host_observed" && provenance.host === caller.host && provenance.session?.kind === "host" && provenance.session.host === caller.host && provenance.session.handle === caller.actor && provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host);
|
|
11130
|
+
};
|
|
11131
|
+
var verifyNativeDecision = (verifier, input, binding) => {
|
|
11132
|
+
if (!verifier)
|
|
11133
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
11134
|
+
const result = verifier.verifyDecision(input);
|
|
11135
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
11136
|
+
return failure("permission_denied", "native decision observation was not attested");
|
|
11137
|
+
return success(null, null, trustedAuthority("decision", result.data, {
|
|
11138
|
+
taskId: input.expected.taskId,
|
|
11139
|
+
workspaceId: input.expected.workspaceId,
|
|
11140
|
+
purpose: input.expected.purpose,
|
|
11141
|
+
response: input.expected.response,
|
|
11142
|
+
binding: input.expected.binding,
|
|
11143
|
+
bindingBytes: input.expected.bindingBytes,
|
|
11144
|
+
digest: input.expected.digest,
|
|
11145
|
+
requirementIds: [...input.expected.requirementIds]
|
|
11146
|
+
}, binding, input.caller));
|
|
11147
|
+
};
|
|
11148
|
+
var verifyNativeAction = (verifier, input, binding) => {
|
|
11149
|
+
if (!verifier)
|
|
11150
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
11151
|
+
const result = verifier.verifyAction(input);
|
|
11152
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
11153
|
+
return failure("permission_denied", "native action observation was not attested");
|
|
11154
|
+
return success(null, null, trustedAuthority("action", result.data, {
|
|
11155
|
+
taskId: input.expected.taskId,
|
|
11156
|
+
workspaceId: input.expected.workspaceId,
|
|
11157
|
+
decisionId: input.expected.decisionId,
|
|
11158
|
+
actionRef: input.expected.actionRef,
|
|
11159
|
+
taskRevision: input.expected.taskRevision,
|
|
11160
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
11161
|
+
outcome: input.expected.outcome,
|
|
11162
|
+
purpose: input.expected.decision.purpose,
|
|
11163
|
+
response: input.expected.decision.response,
|
|
11164
|
+
binding: input.expected.decision.binding,
|
|
11165
|
+
digest: input.expected.decision.digest,
|
|
11166
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
11167
|
+
}, binding, input.caller));
|
|
11168
|
+
};
|
|
11169
|
+
var verifyNativeReconciliation = (verifier, input, binding) => {
|
|
11170
|
+
if (!verifier?.verifyReconciliation)
|
|
11171
|
+
return failure("permission_denied", "native reconciliation authority is unavailable");
|
|
11172
|
+
const result = verifier.verifyReconciliation(input);
|
|
11173
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
11174
|
+
return failure("permission_denied", "native reconciliation observation was not attested");
|
|
11175
|
+
return success(null, null, trustedAuthority("reconciliation", result.data, {
|
|
11176
|
+
taskId: input.expected.taskId,
|
|
11177
|
+
workspaceId: input.expected.workspaceId,
|
|
11178
|
+
decisionId: input.expected.decisionId,
|
|
11179
|
+
actionRef: input.expected.actionRef,
|
|
11180
|
+
taskRevision: input.expected.taskRevision,
|
|
11181
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
11182
|
+
outcome: input.expected.outcome,
|
|
11183
|
+
evidenceDigest: input.expected.evidenceDigest,
|
|
11184
|
+
step: input.expected.step,
|
|
11185
|
+
purpose: input.expected.decision.purpose,
|
|
11186
|
+
response: input.expected.decision.response,
|
|
11187
|
+
binding: input.expected.decision.binding,
|
|
11188
|
+
digest: input.expected.decision.digest,
|
|
11189
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
11190
|
+
}, binding, input.caller));
|
|
11373
11191
|
};
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
"github-flow": { allowed: ["*"], protected: ["main"] },
|
|
11383
|
-
"trunk-based": { allowed: ["*"], protected: ["main"] },
|
|
11384
|
-
custom: { allowed: [], protected: [] }
|
|
11192
|
+
var takeAuthority = (authority, binding, caller) => {
|
|
11193
|
+
const record = verifiedAuthorities.get(authority);
|
|
11194
|
+
verifiedAuthorities.delete(authority);
|
|
11195
|
+
if (!record)
|
|
11196
|
+
return null;
|
|
11197
|
+
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
11198
|
+
return null;
|
|
11199
|
+
return record;
|
|
11385
11200
|
};
|
|
11386
|
-
var
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
return {
|
|
11391
|
-
preset,
|
|
11392
|
-
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
11393
|
-
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
11394
|
-
};
|
|
11201
|
+
var retireNativeAuthority = (authority) => {
|
|
11202
|
+
const record = verifiedAuthorities.get(authority);
|
|
11203
|
+
verifiedAuthorities.delete(authority);
|
|
11204
|
+
return record?.provenance ?? null;
|
|
11395
11205
|
};
|
|
11396
|
-
var
|
|
11397
|
-
var
|
|
11398
|
-
var
|
|
11399
|
-
|
|
11400
|
-
if (
|
|
11401
|
-
return
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
if (!migrationFailed && existsSync4(dir)) {
|
|
11412
|
-
migratedDir = dir;
|
|
11413
|
-
return dir;
|
|
11206
|
+
var authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome;
|
|
11207
|
+
var authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds);
|
|
11208
|
+
var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
|
|
11209
|
+
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
11210
|
+
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
11211
|
+
return [];
|
|
11212
|
+
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
11213
|
+
}
|
|
11214
|
+
var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
11215
|
+
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
11216
|
+
let root;
|
|
11217
|
+
try {
|
|
11218
|
+
root = fs6.realpathSync(checkoutRoot);
|
|
11219
|
+
} catch {
|
|
11220
|
+
return failure("permission_denied", "checkout root is unavailable");
|
|
11414
11221
|
}
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
11418
|
-
for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
|
|
11419
|
-
const src = path7.join(legacy, entry.name);
|
|
11420
|
-
const dest = path7.join(dir, entry.name);
|
|
11421
|
-
if (existsSync4(dest))
|
|
11222
|
+
for (const reference of binding.contentRefs) {
|
|
11223
|
+
if (reference.kind !== "file")
|
|
11422
11224
|
continue;
|
|
11225
|
+
if (!reference.digest)
|
|
11226
|
+
return failure("invalid_input", "document references require a byte digest");
|
|
11227
|
+
const target = path9.resolve(root, reference.path);
|
|
11228
|
+
if (target !== root && !target.startsWith(`${root}${path9.sep}`))
|
|
11229
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11230
|
+
fields: [
|
|
11231
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
11232
|
+
]
|
|
11233
|
+
});
|
|
11423
11234
|
try {
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11235
|
+
const relative = path9.relative(root, target);
|
|
11236
|
+
let current = root;
|
|
11237
|
+
for (const segment of relative.split(path9.sep)) {
|
|
11238
|
+
if (!segment)
|
|
11239
|
+
continue;
|
|
11240
|
+
current = path9.join(current, segment);
|
|
11241
|
+
const stat = fs6.lstatSync(current);
|
|
11242
|
+
const real = fs6.realpathSync(current);
|
|
11243
|
+
if (real !== root && !real.startsWith(`${root}${path9.sep}`))
|
|
11244
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11245
|
+
fields: [
|
|
11246
|
+
{
|
|
11247
|
+
path: "contentRefs.path",
|
|
11248
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
11249
|
+
}
|
|
11250
|
+
]
|
|
11251
|
+
});
|
|
11252
|
+
if (stat.isSymbolicLink())
|
|
11253
|
+
return failure("invalid_input", "document reference uses a symlink");
|
|
11254
|
+
if (current === target && !stat.isFile())
|
|
11255
|
+
return failure("invalid_input", "document reference is not a regular file");
|
|
11256
|
+
if (current !== target && !stat.isDirectory())
|
|
11257
|
+
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
11258
|
+
}
|
|
11259
|
+
if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
|
|
11260
|
+
return failure("permission_denied", "approved document bytes have changed");
|
|
11261
|
+
} catch {
|
|
11262
|
+
return failure("invalid_input", "approved document is unavailable");
|
|
11263
|
+
}
|
|
11264
|
+
}
|
|
11265
|
+
return success(null, null, null);
|
|
11266
|
+
};
|
|
11267
|
+
var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
|
|
11268
|
+
var storedDecisionApplicable = (store, task, purpose, binding) => task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && verifyDecisionContentAtRoot(store.root, entry.data.binding).ok);
|
|
11269
|
+
var validateAction = (store, task, workspaceId, input, authority) => {
|
|
11270
|
+
if (task.status !== "active")
|
|
11271
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
11272
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
11273
|
+
return failure("invalid_input", "action reference is invalid");
|
|
11274
|
+
const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
11275
|
+
if (!entry)
|
|
11276
|
+
return failure("not_found", "decision not found");
|
|
11277
|
+
const decision = entry.data;
|
|
11278
|
+
if (decision.purpose !== "action")
|
|
11279
|
+
return failure("permission_denied", "decision is not an action approval");
|
|
11280
|
+
if (!authorityMatches(authority, {
|
|
11281
|
+
taskId: task.id,
|
|
11282
|
+
workspaceId,
|
|
11283
|
+
decisionId: input.decisionId,
|
|
11284
|
+
actionRef: input.actionRef,
|
|
11285
|
+
taskRevision: input.expectedRevision,
|
|
11286
|
+
workspaceRevision: input.expectedWorkspaceRevision,
|
|
11287
|
+
outcome: "reserve"
|
|
11288
|
+
}))
|
|
11289
|
+
return failure("permission_denied", "native reservation authority is not bound");
|
|
11290
|
+
if (!authorityDecisionMatches(authority, decision))
|
|
11291
|
+
return failure("permission_denied", "native reservation authority does not match decision");
|
|
11292
|
+
if (decision.response !== "approved")
|
|
11293
|
+
return failure("permission_denied", "decision was rejected");
|
|
11294
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
11295
|
+
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
11296
|
+
const receipts = entry.provenance.receipts;
|
|
11297
|
+
const standing = receipts.filter((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "standing");
|
|
11298
|
+
const questionBacked = receipts.some((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "host");
|
|
11299
|
+
if (!questionBacked && standing.length > 0) {
|
|
11300
|
+
let operation;
|
|
11301
|
+
try {
|
|
11302
|
+
operation = JSON.parse(decision.binding.approvedContent).operation;
|
|
11303
|
+
} catch {
|
|
11304
|
+
operation = null;
|
|
11305
|
+
}
|
|
11306
|
+
const cls = operationAutoClass(operation);
|
|
11307
|
+
if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
|
|
11308
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
11309
|
+
}
|
|
11310
|
+
if (decision.revoked)
|
|
11311
|
+
return failure("permission_denied", "decision is revoked");
|
|
11312
|
+
if (decision.digest !== decisionDigest(decision))
|
|
11313
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
11314
|
+
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
11315
|
+
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
11316
|
+
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
11317
|
+
return failure("permission_denied", "action binding does not match the approved decision");
|
|
11318
|
+
const content = verifyContentRefs(store, decision.binding);
|
|
11319
|
+
if (!content.ok)
|
|
11320
|
+
return content;
|
|
11321
|
+
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
11322
|
+
if (requestedSteps === null)
|
|
11323
|
+
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
11324
|
+
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
11325
|
+
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
11326
|
+
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
11327
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
11328
|
+
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
11329
|
+
return failure("permission_denied", "bounded action scope changed");
|
|
11330
|
+
return success(null, null, { entry, workflow });
|
|
11331
|
+
};
|
|
11332
|
+
function reserveAction(input) {
|
|
11333
|
+
if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
|
|
11334
|
+
return failure("invalid_input", "action reservation preconditions are required");
|
|
11335
|
+
const authority = takeAuthority(input.authority, {
|
|
11336
|
+
owner: input.authorityOwner,
|
|
11337
|
+
store: input.store,
|
|
11338
|
+
root: input.store.root
|
|
11339
|
+
}, input.authorityCaller);
|
|
11340
|
+
if (!authority)
|
|
11341
|
+
return failure("permission_denied", "native reservation authority is not verified");
|
|
11342
|
+
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
11343
|
+
const workspace = input.store.readWorkspace();
|
|
11344
|
+
if (!workspace.ok)
|
|
11345
|
+
return workspace;
|
|
11346
|
+
if (!workspace.data)
|
|
11347
|
+
return failure("not_found", "workspace not found");
|
|
11348
|
+
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
11349
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
11350
|
+
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
11351
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
11352
|
+
});
|
|
11353
|
+
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
11354
|
+
if (!valid.ok)
|
|
11355
|
+
return valid;
|
|
11356
|
+
const { entry, workflow } = valid.data;
|
|
11357
|
+
const existing = entry.data.consumption;
|
|
11358
|
+
if (existing?.state === "uncertain")
|
|
11359
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
11360
|
+
operation: "reserve_action",
|
|
11361
|
+
outcome: "unknown"
|
|
11362
|
+
});
|
|
11363
|
+
if (existing?.state === "consumed")
|
|
11364
|
+
return failure("permission_denied", "action approval is consumed");
|
|
11365
|
+
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
11366
|
+
return failure("permission_denied", "action approval is already reserved");
|
|
11367
|
+
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
11368
|
+
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
11369
|
+
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
11370
|
+
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
11371
|
+
return failure("permission_denied", "action step was already completed");
|
|
11372
|
+
const nextDecision = {
|
|
11373
|
+
...entry.data,
|
|
11374
|
+
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
11375
|
+
};
|
|
11376
|
+
const actionProgress = workflow.steps.length ? [
|
|
11377
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
11378
|
+
{
|
|
11379
|
+
decisionId: input.decisionId,
|
|
11380
|
+
steps: [...workflow.steps],
|
|
11381
|
+
completedSteps: [...workflow.completed]
|
|
11382
|
+
}
|
|
11383
|
+
] : current.actionProgress ?? [];
|
|
11384
|
+
return success(mutation.revision, null, {
|
|
11385
|
+
...current,
|
|
11386
|
+
actionProgress,
|
|
11387
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
11388
|
+
});
|
|
11389
|
+
}, input.native?.now);
|
|
11390
|
+
if (!changed.ok)
|
|
11391
|
+
return changed;
|
|
11392
|
+
const workspace = input.store.readWorkspace();
|
|
11393
|
+
if (!workspace.ok || !workspace.data)
|
|
11394
|
+
return failure("recovery_required", "workspace disappeared after reservation");
|
|
11395
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
11396
|
+
if (!entry)
|
|
11397
|
+
return failure("recovery_required", "reserved decision disappeared");
|
|
11398
|
+
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
11399
|
+
steps: [],
|
|
11400
|
+
completedSteps: []
|
|
11401
|
+
};
|
|
11402
|
+
return success(changed.data.revision, workspace.data.revision, {
|
|
11403
|
+
taskId: input.taskId,
|
|
11404
|
+
decisionId: input.decisionId,
|
|
11405
|
+
actionRef: input.actionRef,
|
|
11406
|
+
taskRevision: changed.data.revision,
|
|
11407
|
+
workspaceRevision: workspace.data.revision,
|
|
11408
|
+
completedSteps: [...workflow.completedSteps],
|
|
11409
|
+
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
11410
|
+
});
|
|
11411
|
+
}
|
|
11412
|
+
function settleAction(input) {
|
|
11413
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
11414
|
+
return failure("invalid_input", "action settlement preconditions are required");
|
|
11415
|
+
const authority = takeAuthority(input.authority, {
|
|
11416
|
+
owner: input.authorityOwner,
|
|
11417
|
+
store: input.store,
|
|
11418
|
+
root: input.store.root
|
|
11419
|
+
}, input.authorityCaller);
|
|
11420
|
+
if (!authority)
|
|
11421
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
11422
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
11423
|
+
return failure("invalid_input", "action reference is invalid");
|
|
11424
|
+
if (!authorityMatches(authority, {
|
|
11425
|
+
taskId: input.taskId,
|
|
11426
|
+
workspaceId: authority.workspaceId,
|
|
11427
|
+
decisionId: input.decisionId,
|
|
11428
|
+
actionRef: input.actionRef,
|
|
11429
|
+
taskRevision: input.taskRevision,
|
|
11430
|
+
workspaceRevision: input.workspaceRevision,
|
|
11431
|
+
outcome: input.outcome
|
|
11432
|
+
}))
|
|
11433
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
11434
|
+
let outcomeResult = null;
|
|
11435
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
11436
|
+
const workspace = input.store.readWorkspace();
|
|
11437
|
+
if (!workspace.ok)
|
|
11438
|
+
return workspace;
|
|
11439
|
+
if (!workspace.data)
|
|
11440
|
+
return failure("not_found", "workspace not found");
|
|
11441
|
+
if (!authorityMatches(authority, {
|
|
11442
|
+
taskId: input.taskId,
|
|
11443
|
+
workspaceId: workspace.data.id,
|
|
11444
|
+
decisionId: input.decisionId,
|
|
11445
|
+
actionRef: input.actionRef,
|
|
11446
|
+
taskRevision: input.taskRevision,
|
|
11447
|
+
workspaceRevision: input.workspaceRevision,
|
|
11448
|
+
outcome: input.outcome
|
|
11449
|
+
}))
|
|
11450
|
+
return failure("permission_denied", "native settlement authority does not match context");
|
|
11451
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
11452
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
11453
|
+
expectedWorkspaceRevision: input.workspaceRevision,
|
|
11454
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
11455
|
+
});
|
|
11456
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
11457
|
+
if (!entry)
|
|
11458
|
+
return failure("not_found", "decision not found");
|
|
11459
|
+
if (!authorityDecisionMatches(authority, entry.data))
|
|
11460
|
+
return failure("permission_denied", "native settlement authority does not match decision");
|
|
11461
|
+
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
11462
|
+
return failure("permission_denied", "native settlement caller does not match approval");
|
|
11463
|
+
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
11464
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
11465
|
+
if (entry.data.consumption?.state === "uncertain")
|
|
11466
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
11467
|
+
operation: "settle_action",
|
|
11468
|
+
outcome: "unknown"
|
|
11469
|
+
});
|
|
11470
|
+
if (entry.data.consumption?.state !== "reserved")
|
|
11471
|
+
return failure("permission_denied", "action is not reserved");
|
|
11472
|
+
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
11473
|
+
return failure("permission_denied", "settlement reference does not match reservation");
|
|
11474
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
11475
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
11476
|
+
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
11477
|
+
return failure("recovery_required", "bounded action progress is missing");
|
|
11478
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
11479
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
11480
|
+
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
11481
|
+
if (input.outcome === "unknown") {
|
|
11482
|
+
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
11483
|
+
operation: "settle_action",
|
|
11484
|
+
outcome: "unknown"
|
|
11485
|
+
});
|
|
11486
|
+
const uncertain = {
|
|
11487
|
+
...entry.data,
|
|
11488
|
+
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
11489
|
+
};
|
|
11490
|
+
return success(mutation.revision, null, {
|
|
11491
|
+
...current,
|
|
11492
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
11493
|
+
});
|
|
11432
11494
|
}
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
}
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
return readFileSync4(p, "utf8");
|
|
11454
|
-
} catch {
|
|
11455
|
-
return null;
|
|
11456
|
-
}
|
|
11457
|
-
};
|
|
11458
|
-
var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11459
|
-
var parseConfigResult = (raw, file) => {
|
|
11460
|
-
if (raw === null)
|
|
11461
|
-
return { status: "missing", path: file };
|
|
11462
|
-
let parsed;
|
|
11463
|
-
try {
|
|
11464
|
-
parsed = JSON.parse(raw);
|
|
11465
|
-
} catch {
|
|
11466
|
-
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
11467
|
-
}
|
|
11468
|
-
if (!isConfigObject(parsed)) {
|
|
11469
|
-
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
11470
|
-
}
|
|
11471
|
-
const input = parsed;
|
|
11472
|
-
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
11473
|
-
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
11474
|
-
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
11475
|
-
return {
|
|
11476
|
-
status: "valid",
|
|
11477
|
-
path: file,
|
|
11478
|
-
config: {
|
|
11479
|
-
locale,
|
|
11480
|
-
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
11481
|
-
timezone: input.timezone ?? DEFAULTS.timezone,
|
|
11482
|
-
branchPolicy: mergePreset(preset, {
|
|
11483
|
-
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
11484
|
-
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
11485
|
-
}, DEFAULTS),
|
|
11486
|
-
commitPolicy: {
|
|
11487
|
-
preset: commitPreset,
|
|
11488
|
-
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
11495
|
+
if (input.outcome === "not_started") {
|
|
11496
|
+
const released = { ...entry.data, consumption: null };
|
|
11497
|
+
return success(mutation.revision, null, {
|
|
11498
|
+
...current,
|
|
11499
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
|
|
11500
|
+
});
|
|
11501
|
+
}
|
|
11502
|
+
if (currentStep)
|
|
11503
|
+
workflow.completed.push(currentStep);
|
|
11504
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
11505
|
+
const settled = {
|
|
11506
|
+
...entry.data,
|
|
11507
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
11508
|
+
};
|
|
11509
|
+
const actionProgress = workflow.steps.length ? [
|
|
11510
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
11511
|
+
{
|
|
11512
|
+
decisionId: input.decisionId,
|
|
11513
|
+
steps: [...workflow.steps],
|
|
11514
|
+
completedSteps: [...workflow.completed]
|
|
11489
11515
|
}
|
|
11516
|
+
] : current.actionProgress ?? [];
|
|
11517
|
+
const next = {
|
|
11518
|
+
...current,
|
|
11519
|
+
actionProgress,
|
|
11520
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
|
|
11521
|
+
};
|
|
11522
|
+
if (!complete) {
|
|
11523
|
+
outcomeResult = success(mutation.revision, input.workspaceRevision, {
|
|
11524
|
+
...entry,
|
|
11525
|
+
data: settled
|
|
11526
|
+
});
|
|
11490
11527
|
}
|
|
11491
|
-
|
|
11492
|
-
};
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11528
|
+
return success(mutation.revision, null, next);
|
|
11529
|
+
}, input.native?.now);
|
|
11530
|
+
if (!changed.ok)
|
|
11531
|
+
return changed;
|
|
11532
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
11533
|
+
if (!entry)
|
|
11534
|
+
return failure("recovery_required", "settled decision disappeared");
|
|
11535
|
+
if (outcomeResult)
|
|
11536
|
+
return outcomeResult;
|
|
11537
|
+
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
11538
|
+
}
|
|
11539
|
+
function reconcileAction(input) {
|
|
11540
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
11541
|
+
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
11542
|
+
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
11543
|
+
if (!authority || authority.kind !== "reconciliation")
|
|
11544
|
+
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
11545
|
+
if (authority.taskId !== input.taskId || authority.decisionId !== input.decisionId || !sameRef(authority.actionRef, input.actionRef) || authority.taskRevision !== input.taskRevision || authority.workspaceRevision !== input.workspaceRevision || authority.outcome !== input.outcome || authority.evidenceDigest !== input.evidenceDigest || authority.step !== input.step)
|
|
11546
|
+
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
11547
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
11548
|
+
const workspace = input.store.readWorkspace();
|
|
11549
|
+
if (!workspace.ok)
|
|
11550
|
+
return workspace;
|
|
11551
|
+
if (!workspace.data)
|
|
11552
|
+
return failure("not_found", "workspace not found");
|
|
11553
|
+
if (authority.workspaceId !== workspace.data.id)
|
|
11554
|
+
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
11555
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
11556
|
+
return failure("revision_conflict", "workspace revision does not match");
|
|
11557
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
11558
|
+
if (!entry || entry.data.purpose !== "action")
|
|
11559
|
+
return failure("not_found", "decision not found");
|
|
11560
|
+
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
11561
|
+
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
11562
|
+
if (entry.data.response !== "approved" || entry.data.revoked !== null || entry.data.digest !== decisionDigest(entry.data) || entry.data.binding.taskId !== current.id || entry.data.binding.workspaceId !== current.workspaceId || entry.data.binding.workspaceId !== workspace.data.id)
|
|
11563
|
+
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
11564
|
+
if (entry.data.consumption?.state !== "uncertain")
|
|
11565
|
+
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
11566
|
+
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
11567
|
+
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
11568
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
11569
|
+
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
11570
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
11571
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
11572
|
+
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
11573
|
+
if (input.outcome === "not_started") {
|
|
11574
|
+
return success(mutation.revision, null, {
|
|
11575
|
+
...current,
|
|
11576
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
11577
|
+
});
|
|
11578
|
+
}
|
|
11579
|
+
if (currentStep)
|
|
11580
|
+
workflow.completed.push(currentStep);
|
|
11581
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
11582
|
+
const nextDecision = {
|
|
11583
|
+
...entry.data,
|
|
11584
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
11585
|
+
};
|
|
11586
|
+
const actionProgress = workflow.steps.length ? [
|
|
11587
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
11588
|
+
{
|
|
11589
|
+
decisionId: input.decisionId,
|
|
11590
|
+
steps: workflow.steps,
|
|
11591
|
+
completedSteps: workflow.completed
|
|
11592
|
+
}
|
|
11593
|
+
] : current.actionProgress ?? [];
|
|
11594
|
+
return success(mutation.revision, null, {
|
|
11595
|
+
...current,
|
|
11596
|
+
actionProgress,
|
|
11597
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
11598
|
+
});
|
|
11599
|
+
}, input.native?.now);
|
|
11600
|
+
if (!changed.ok)
|
|
11601
|
+
return changed;
|
|
11602
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
11603
|
+
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
11604
|
+
}
|
|
11605
|
+
var bindingCovers = scopeCovers;
|
|
11606
|
+
var verifyDecisionContent = verifyContentRefs;
|
|
11607
|
+
|
|
11608
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
11609
|
+
var digestBytes3 = (value) => createHash5("sha256").update(value).digest("hex");
|
|
11610
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
|
|
11611
|
+
var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
|
|
11612
|
+
var canonicalPath = (value) => {
|
|
11613
|
+
if (typeof value !== "string" || !value)
|
|
11614
|
+
return null;
|
|
11615
|
+
if (value.includes("\\") || value.split("/").includes(".."))
|
|
11616
|
+
return null;
|
|
11617
|
+
const normalized = path10.posix.normalize(value);
|
|
11618
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
11619
|
+
return null;
|
|
11620
|
+
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
11502
11621
|
};
|
|
11503
|
-
var
|
|
11504
|
-
const
|
|
11505
|
-
const
|
|
11506
|
-
const
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
}, config);
|
|
11510
|
-
const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
|
|
11622
|
+
var canonicalScope = (scope) => {
|
|
11623
|
+
const paths = scope.paths.length ? scope.paths : ["."];
|
|
11624
|
+
const normalizedPaths = paths.map(canonicalPath);
|
|
11625
|
+
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
11626
|
+
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
11627
|
+
return null;
|
|
11511
11628
|
return {
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
11516
|
-
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
11629
|
+
description: scope.description,
|
|
11630
|
+
paths: [...new Set(normalizedPaths)],
|
|
11631
|
+
exclusions: [...new Set(normalizedExclusions)]
|
|
11517
11632
|
};
|
|
11518
11633
|
};
|
|
11519
|
-
var
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
"
|
|
11526
|
-
|
|
11527
|
-
var
|
|
11528
|
-
|
|
11529
|
-
|
|
11634
|
+
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
11635
|
+
var scopeMatches = (value, scope) => {
|
|
11636
|
+
const normalized = canonicalScope(scope);
|
|
11637
|
+
if (!normalized)
|
|
11638
|
+
return false;
|
|
11639
|
+
const paths = normalized.paths;
|
|
11640
|
+
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
11641
|
+
};
|
|
11642
|
+
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
11643
|
+
var compareCodeUnits2 = (left, right) => {
|
|
11644
|
+
const length = Math.min(left.length, right.length);
|
|
11645
|
+
for (let index = 0;index < length; index += 1) {
|
|
11646
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
11647
|
+
if (difference !== 0)
|
|
11648
|
+
return difference;
|
|
11649
|
+
}
|
|
11650
|
+
return left.length - right.length;
|
|
11651
|
+
};
|
|
11652
|
+
var gitPaths = (root) => {
|
|
11653
|
+
const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
11654
|
+
cwd: root,
|
|
11655
|
+
encoding: "buffer"
|
|
11656
|
+
});
|
|
11657
|
+
if (result.status !== 0) {
|
|
11658
|
+
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
11530
11659
|
return {
|
|
11531
|
-
|
|
11532
|
-
|
|
11660
|
+
paths: [],
|
|
11661
|
+
uncertain: !/not a git repository/i.test(stderr),
|
|
11662
|
+
git: /not a git repository/i.test(stderr) ? false : true
|
|
11533
11663
|
};
|
|
11534
11664
|
}
|
|
11535
|
-
|
|
11536
|
-
};
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
let raw;
|
|
11544
|
-
try {
|
|
11545
|
-
raw = readFileSync5(file, "utf8");
|
|
11546
|
-
} catch {
|
|
11547
|
-
return { status: "missing", path: file, entries: [] };
|
|
11665
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
|
|
11666
|
+
const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
11667
|
+
if (stagedDeleted.status !== 0) {
|
|
11668
|
+
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
11669
|
+
if (!/not a git repository/i.test(stderr))
|
|
11670
|
+
return { paths, uncertain: true, git: true };
|
|
11671
|
+
} else if (stagedDeleted.stdout) {
|
|
11672
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")));
|
|
11548
11673
|
}
|
|
11549
|
-
|
|
11674
|
+
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
11675
|
+
};
|
|
11676
|
+
var walk = (root, directory, output) => {
|
|
11677
|
+
let names;
|
|
11550
11678
|
try {
|
|
11551
|
-
|
|
11679
|
+
names = fs7.readdirSync(directory);
|
|
11552
11680
|
} catch {
|
|
11553
|
-
return
|
|
11681
|
+
return true;
|
|
11554
11682
|
}
|
|
11555
|
-
|
|
11556
|
-
|
|
11683
|
+
let uncertain = false;
|
|
11684
|
+
for (const name of names) {
|
|
11685
|
+
if (name === ".git" || name === ".workit")
|
|
11686
|
+
continue;
|
|
11687
|
+
const target = path10.join(directory, name);
|
|
11688
|
+
let stat;
|
|
11689
|
+
try {
|
|
11690
|
+
stat = fs7.lstatSync(target);
|
|
11691
|
+
} catch {
|
|
11692
|
+
uncertain = true;
|
|
11693
|
+
continue;
|
|
11694
|
+
}
|
|
11695
|
+
const item = relative(root, target);
|
|
11696
|
+
output.add(item);
|
|
11697
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
11698
|
+
uncertain = walk(root, target, output) || uncertain;
|
|
11557
11699
|
}
|
|
11558
|
-
|
|
11559
|
-
return {
|
|
11560
|
-
status: "valid",
|
|
11561
|
-
path: file,
|
|
11562
|
-
entries: Array.isArray(list) ? list : []
|
|
11563
|
-
};
|
|
11700
|
+
return uncertain;
|
|
11564
11701
|
};
|
|
11565
|
-
var
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
if (out.endsWith("/")) {
|
|
11580
|
-
out = out.slice(0, -1);
|
|
11581
|
-
out += "(?:/.*)?";
|
|
11582
|
-
} else {
|
|
11583
|
-
out += ".*";
|
|
11584
|
-
}
|
|
11585
|
-
} else {
|
|
11586
|
-
out += ".*";
|
|
11587
|
-
}
|
|
11588
|
-
i++;
|
|
11589
|
-
}
|
|
11590
|
-
} else {
|
|
11591
|
-
out += "[^/]*";
|
|
11592
|
-
}
|
|
11593
|
-
} else {
|
|
11594
|
-
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
11702
|
+
var scopeRoots = (root, scope) => {
|
|
11703
|
+
const roots = [];
|
|
11704
|
+
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
11705
|
+
const target = path10.resolve(root, item);
|
|
11706
|
+
if (!inside(root, target))
|
|
11707
|
+
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` }] });
|
|
11708
|
+
let ancestor = target;
|
|
11709
|
+
while (!fs7.existsSync(ancestor) && ancestor !== root)
|
|
11710
|
+
ancestor = path10.dirname(ancestor);
|
|
11711
|
+
try {
|
|
11712
|
+
if (!inside(root, fs7.realpathSync(ancestor)))
|
|
11713
|
+
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` }] });
|
|
11714
|
+
} catch {
|
|
11715
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
11595
11716
|
}
|
|
11717
|
+
roots.push(target);
|
|
11596
11718
|
}
|
|
11597
|
-
return
|
|
11719
|
+
return success(null, null, roots);
|
|
11598
11720
|
};
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
const real = realpathSync4.native ?? realpathSync4;
|
|
11721
|
+
function captureCandidate(root, scope, environment = []) {
|
|
11722
|
+
let checkout;
|
|
11602
11723
|
try {
|
|
11603
|
-
|
|
11724
|
+
checkout = fs7.realpathSync(root);
|
|
11604
11725
|
} catch {
|
|
11605
|
-
return
|
|
11726
|
+
return failure("invalid_input", "candidate root does not exist");
|
|
11606
11727
|
}
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
const
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
const
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
for (const entry of loadWorkspacesFrom(dir)) {
|
|
11620
|
-
if (!entry || typeof entry !== "object")
|
|
11728
|
+
const normalizedScope = canonicalScope(scope);
|
|
11729
|
+
if (!normalizedScope)
|
|
11730
|
+
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" }] });
|
|
11731
|
+
const roots = scopeRoots(checkout, normalizedScope);
|
|
11732
|
+
if (!roots.ok)
|
|
11733
|
+
return roots;
|
|
11734
|
+
const git = gitPaths(checkout);
|
|
11735
|
+
const names = new Set;
|
|
11736
|
+
let uncertain = git.uncertain;
|
|
11737
|
+
for (const target of roots.data) {
|
|
11738
|
+
const item = relative(checkout, target);
|
|
11739
|
+
if (item === ".git" || item === ".workit")
|
|
11621
11740
|
continue;
|
|
11622
|
-
|
|
11623
|
-
if (
|
|
11741
|
+
names.add(item);
|
|
11742
|
+
if (!fs7.existsSync(target))
|
|
11743
|
+
continue;
|
|
11744
|
+
let stat;
|
|
11745
|
+
try {
|
|
11746
|
+
stat = fs7.lstatSync(target);
|
|
11747
|
+
} catch {
|
|
11748
|
+
uncertain = true;
|
|
11624
11749
|
continue;
|
|
11625
|
-
const glob = ws.glob.replaceAll("\\", "/");
|
|
11626
|
-
const canonical = canonicalGlob(glob);
|
|
11627
|
-
for (const target of targets) {
|
|
11628
|
-
if (matchWorkspace(glob, target))
|
|
11629
|
-
return ws;
|
|
11630
|
-
if (canonical !== glob && matchWorkspace(canonical, target))
|
|
11631
|
-
return ws;
|
|
11632
11750
|
}
|
|
11751
|
+
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
11752
|
+
uncertain = walk(checkout, target, names) || uncertain;
|
|
11633
11753
|
}
|
|
11634
|
-
|
|
11754
|
+
for (const item of git.paths)
|
|
11755
|
+
if (scopeMatches(item, normalizedScope))
|
|
11756
|
+
names.add(item);
|
|
11757
|
+
const files = [];
|
|
11758
|
+
let completeness = uncertain ? "uncertain" : "known";
|
|
11759
|
+
for (const item of [...names].sort()) {
|
|
11760
|
+
if (!scopeMatches(item, normalizedScope))
|
|
11761
|
+
continue;
|
|
11762
|
+
const target = path10.join(checkout, item);
|
|
11763
|
+
try {
|
|
11764
|
+
const stat = fs7.lstatSync(target);
|
|
11765
|
+
if (stat.isSymbolicLink()) {
|
|
11766
|
+
const link = fs7.readlinkSync(target);
|
|
11767
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
11768
|
+
} else if (stat.isFile()) {
|
|
11769
|
+
files.push({
|
|
11770
|
+
path: item,
|
|
11771
|
+
kind: "file",
|
|
11772
|
+
digest: digestBytes3(fs7.readFileSync(target)),
|
|
11773
|
+
executable: (stat.mode & 73) !== 0
|
|
11774
|
+
});
|
|
11775
|
+
} else if (!stat.isDirectory()) {
|
|
11776
|
+
completeness = "uncertain";
|
|
11777
|
+
} else if (git.git && git.paths.includes(item)) {
|
|
11778
|
+
completeness = "uncertain";
|
|
11779
|
+
}
|
|
11780
|
+
} catch (error) {
|
|
11781
|
+
if (error?.code === "ENOENT") {
|
|
11782
|
+
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
11783
|
+
} else
|
|
11784
|
+
completeness = "uncertain";
|
|
11785
|
+
}
|
|
11786
|
+
}
|
|
11787
|
+
const supplied = Array.isArray(environment) ? environment.map((value) => typeof value === "string" ? { name: value, value: process.env[value] ?? null } : { name: value.name, value: value.value ?? null }) : Object.keys(environment).map((name) => ({
|
|
11788
|
+
name,
|
|
11789
|
+
value: environment[name] ?? null
|
|
11790
|
+
}));
|
|
11791
|
+
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
11792
|
+
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
11793
|
+
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
11794
|
+
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
11795
|
+
if (values.some(({ value }) => value === null))
|
|
11796
|
+
completeness = "uncertain";
|
|
11797
|
+
const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
11798
|
+
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
11799
|
+
completeness = "uncertain";
|
|
11800
|
+
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
11801
|
+
const partial = {
|
|
11802
|
+
id: "0".repeat(64),
|
|
11803
|
+
scope: normalizedScope,
|
|
11804
|
+
completeness,
|
|
11805
|
+
files,
|
|
11806
|
+
environment: values,
|
|
11807
|
+
head
|
|
11808
|
+
};
|
|
11809
|
+
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
11810
|
+
const parsed = candidateSchema.safeParse(candidate);
|
|
11811
|
+
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
11812
|
+
}
|
|
11813
|
+
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
11814
|
+
var changedPaths = (before, after) => {
|
|
11815
|
+
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
11816
|
+
const left = fileMap(before);
|
|
11817
|
+
const right = fileMap(after);
|
|
11818
|
+
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
11819
|
+
if (before.head !== after.head)
|
|
11820
|
+
changed.push(".");
|
|
11821
|
+
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
11822
|
+
changed.push(".");
|
|
11823
|
+
return changed;
|
|
11635
11824
|
};
|
|
11636
|
-
var
|
|
11637
|
-
|
|
11638
|
-
// packages/workit-core/src/core/vcs-config.ts
|
|
11639
|
-
import fs7 from "node:fs";
|
|
11640
|
-
import path9 from "node:path";
|
|
11641
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
11642
|
-
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
11643
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
|
|
11644
|
-
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
11645
|
-
var remoteProvider = (cwd) => {
|
|
11646
|
-
const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
11647
|
-
if (r.status !== 0)
|
|
11825
|
+
var sessionFromRef = (value) => {
|
|
11826
|
+
if (typeof value !== "object" || value === null || value.kind !== "host")
|
|
11648
11827
|
return null;
|
|
11649
|
-
|
|
11650
|
-
if (/github\.com[:/]/.test(url))
|
|
11651
|
-
return "github";
|
|
11652
|
-
if (/gitlab\.com[:/]/.test(url))
|
|
11653
|
-
return "gitlab";
|
|
11654
|
-
return null;
|
|
11828
|
+
return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
11655
11829
|
};
|
|
11656
|
-
var
|
|
11657
|
-
const
|
|
11658
|
-
|
|
11659
|
-
try {
|
|
11660
|
-
raw = fs7.readFileSync(file, "utf8");
|
|
11661
|
-
} catch {
|
|
11662
|
-
return { status: "missing", path: file, config: {} };
|
|
11663
|
-
}
|
|
11664
|
-
let parsed;
|
|
11665
|
-
try {
|
|
11666
|
-
parsed = JSON.parse(raw);
|
|
11667
|
-
} catch {
|
|
11668
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
11669
|
-
}
|
|
11670
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
11671
|
-
return { status: "valid", path: file, config: parsed };
|
|
11672
|
-
}
|
|
11673
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
11830
|
+
var sameSession = (left, right) => {
|
|
11831
|
+
const other = sessionFromRef(right);
|
|
11832
|
+
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
11674
11833
|
};
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
const
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
}
|
|
11717
|
-
if (provider === null) {
|
|
11834
|
+
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
11835
|
+
function evaluateEvidence(task, candidate) {
|
|
11836
|
+
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
11837
|
+
return task.evidence.map((entry) => {
|
|
11838
|
+
const evidence = entry.data;
|
|
11839
|
+
if (evidence.result === "failed")
|
|
11840
|
+
return {
|
|
11841
|
+
evidenceId: entry.id,
|
|
11842
|
+
status: "failed",
|
|
11843
|
+
reason: "failed evidence remains historical"
|
|
11844
|
+
};
|
|
11845
|
+
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
11846
|
+
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
11847
|
+
return {
|
|
11848
|
+
evidenceId: entry.id,
|
|
11849
|
+
status: "stale",
|
|
11850
|
+
reason: "check or review evidence lacks start and end candidates"
|
|
11851
|
+
};
|
|
11852
|
+
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
11853
|
+
return {
|
|
11854
|
+
evidenceId: entry.id,
|
|
11855
|
+
status: "stale",
|
|
11856
|
+
reason: "candidate changed during the check or review"
|
|
11857
|
+
};
|
|
11858
|
+
}
|
|
11859
|
+
if (!evidence.candidateId || !current)
|
|
11860
|
+
return {
|
|
11861
|
+
evidenceId: entry.id,
|
|
11862
|
+
status: evidence.result,
|
|
11863
|
+
reason: "evidence has no candidate binding"
|
|
11864
|
+
};
|
|
11865
|
+
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
11866
|
+
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
11867
|
+
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
11868
|
+
if (observed.id === current.id)
|
|
11869
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
11870
|
+
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
11871
|
+
const changed = changedPaths(observed, current);
|
|
11872
|
+
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
11873
|
+
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
11874
|
+
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
11718
11875
|
return {
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
};
|
|
11723
|
-
}
|
|
11724
|
-
const prov = cfg[provider] ?? {};
|
|
11725
|
-
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
11726
|
-
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
|
|
11727
|
-
const tokenPath = path9.resolve(tokenFile);
|
|
11728
|
-
let tokenOk = false;
|
|
11729
|
-
if (fs7.existsSync(tokenPath)) {
|
|
11730
|
-
const token = fs7.readFileSync(tokenPath, "utf8").trim();
|
|
11731
|
-
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
11732
|
-
tokenOk = !placeholder;
|
|
11733
|
-
}
|
|
11734
|
-
const out = {
|
|
11735
|
-
ok: true,
|
|
11736
|
-
configPath: path9.resolve(cfgPath),
|
|
11737
|
-
provider,
|
|
11738
|
-
defaultTargetBranch: defaultTarget,
|
|
11739
|
-
pr: cfg.pr ?? {},
|
|
11740
|
-
tokenPath,
|
|
11741
|
-
tokenPresent: fs7.existsSync(tokenPath),
|
|
11742
|
-
tokenReady: tokenOk,
|
|
11743
|
-
workspace_name: ws?.name ?? null,
|
|
11744
|
-
link_issues: linkIssues,
|
|
11745
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
11746
|
-
issues_provider: issuesProvider,
|
|
11747
|
-
link_on_pr: linkOnPr
|
|
11748
|
-
};
|
|
11749
|
-
if (provider === "gitlab") {
|
|
11750
|
-
out.gitlab = {
|
|
11751
|
-
host: prov.host ?? "gitlab.com",
|
|
11752
|
-
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
11876
|
+
evidenceId: entry.id,
|
|
11877
|
+
status: evidence.result,
|
|
11878
|
+
reason: "candidate changed outside evidence scope"
|
|
11753
11879
|
};
|
|
11754
|
-
} else if (provider === "github") {
|
|
11755
|
-
out.github = { host: prov.host ?? "github.com" };
|
|
11756
|
-
}
|
|
11757
|
-
if (mode === "summary")
|
|
11758
|
-
delete out.tokenPath;
|
|
11759
|
-
return out;
|
|
11760
|
-
}
|
|
11761
|
-
function mergedPrStyle(limit = 6, cwd) {
|
|
11762
|
-
const cfg = vcsConfig("load", cwd);
|
|
11763
|
-
if (!cfg.ok || !cfg.tokenReady)
|
|
11764
|
-
return { ok: false, error: "vcs not configured" };
|
|
11765
|
-
const provider = cfg.provider;
|
|
11766
|
-
const token = fs7.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
11767
|
-
const examples = [];
|
|
11768
|
-
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
11769
|
-
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
11770
|
-
sections: desc.split(`
|
|
11771
|
-
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
11772
|
-
descriptionPreview: desc.slice(0, 600)
|
|
11773
11880
|
});
|
|
11774
|
-
if (provider === "gitlab") {
|
|
11775
|
-
const remote = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
11776
|
-
if (remote.status !== 0)
|
|
11777
|
-
return { ok: false, error: "no origin remote" };
|
|
11778
|
-
const url = (remote.stdout ?? "").trim();
|
|
11779
|
-
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
11780
|
-
if (!m)
|
|
11781
|
-
return { ok: false, error: "not a gitlab.com origin" };
|
|
11782
|
-
const project = m[1];
|
|
11783
|
-
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
11784
|
-
const run = (args) => spawnSync2("glab", ["api", ...args], { cwd, encoding: "utf8", env });
|
|
11785
|
-
let r = run([
|
|
11786
|
-
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
|
|
11787
|
-
]);
|
|
11788
|
-
if (r.status !== 0) {
|
|
11789
|
-
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
11790
|
-
}
|
|
11791
|
-
if (r.status !== 0)
|
|
11792
|
-
return { ok: false, error: "could not list merge requests" };
|
|
11793
|
-
for (const mr of JSON.parse(r.stdout ?? "[]")) {
|
|
11794
|
-
const desc = String(mr.description ?? "").trim();
|
|
11795
|
-
examples.push({
|
|
11796
|
-
title: mr.title,
|
|
11797
|
-
url: mr.web_url,
|
|
11798
|
-
squash: mr.squash,
|
|
11799
|
-
...descInfo(desc, true)
|
|
11800
|
-
});
|
|
11801
|
-
}
|
|
11802
|
-
} else if (provider === "github") {
|
|
11803
|
-
const r = spawnSync2("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
11804
|
-
if (r.status !== 0)
|
|
11805
|
-
return { ok: false, error: "could not list pull requests" };
|
|
11806
|
-
for (const pr of JSON.parse(r.stdout ?? "[]")) {
|
|
11807
|
-
const desc = String(pr.body ?? "").trim();
|
|
11808
|
-
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
11809
|
-
}
|
|
11810
|
-
}
|
|
11811
|
-
return {
|
|
11812
|
-
ok: true,
|
|
11813
|
-
provider,
|
|
11814
|
-
count: examples.length,
|
|
11815
|
-
styleHints: [
|
|
11816
|
-
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
11817
|
-
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
11818
|
-
"Do not paste commit log or diff stat into the body"
|
|
11819
|
-
],
|
|
11820
|
-
examples
|
|
11821
|
-
};
|
|
11822
11881
|
}
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
var
|
|
11831
|
-
const
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11882
|
+
var scopeCovers2 = scopeCovers;
|
|
11883
|
+
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
11884
|
+
const pin = assignment.candidateId ?? null;
|
|
11885
|
+
if (pin === null)
|
|
11886
|
+
return true;
|
|
11887
|
+
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
11888
|
+
};
|
|
11889
|
+
var resolveBinding = (evidence, pin, currentCandidateId) => {
|
|
11890
|
+
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
11891
|
+
const fallback = bound ? pin ?? currentCandidateId : null;
|
|
11892
|
+
return {
|
|
11893
|
+
beforeCandidateId: evidence.beforeCandidateId ?? fallback,
|
|
11894
|
+
candidateId: evidence.candidateId ?? fallback
|
|
11895
|
+
};
|
|
11835
11896
|
};
|
|
11836
|
-
var
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11897
|
+
var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
|
|
11898
|
+
var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
|
|
11899
|
+
var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
|
|
11900
|
+
var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
|
|
11901
|
+
if (kind !== "passed")
|
|
11902
|
+
return false;
|
|
11903
|
+
if (dimension === "testing" || dimension === "verification")
|
|
11904
|
+
return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
|
|
11905
|
+
if (dimension === "review")
|
|
11906
|
+
return evidenceKind === "review";
|
|
11907
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
11908
|
+
return evidenceKind === "investigation";
|
|
11909
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
11910
|
+
return evidenceKind === "artifact";
|
|
11911
|
+
return false;
|
|
11912
|
+
};
|
|
11913
|
+
var expectedKinds = (dimension, ruleId) => {
|
|
11914
|
+
if (dimension === "testing")
|
|
11915
|
+
return "check";
|
|
11916
|
+
if (dimension === "verification")
|
|
11917
|
+
return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
|
|
11918
|
+
if (dimension === "review")
|
|
11919
|
+
return "review";
|
|
11920
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
11921
|
+
return "investigation";
|
|
11922
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
11923
|
+
return "artifact";
|
|
11924
|
+
if (dimension === "decisions")
|
|
11925
|
+
return "decision";
|
|
11926
|
+
return "worker report";
|
|
11927
|
+
};
|
|
11928
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
|
|
11929
|
+
const evidence = evaluateEvidence(task, candidate);
|
|
11930
|
+
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
11931
|
+
return task.policy.requirements.map((requirement) => ({
|
|
11932
|
+
requirementId: requirement.id,
|
|
11933
|
+
status: "unsatisfied",
|
|
11934
|
+
evidenceIds: [],
|
|
11935
|
+
decisionIds: [],
|
|
11936
|
+
reason: "stored policy version is unsupported; reassessment is required"
|
|
11937
|
+
}));
|
|
11938
|
+
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
11939
|
+
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
11940
|
+
const passed = related.filter(({ entry, evaluation }) => {
|
|
11941
|
+
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
11942
|
+
return false;
|
|
11943
|
+
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
11944
|
+
return false;
|
|
11945
|
+
if (requirement.dimension !== "review")
|
|
11946
|
+
return true;
|
|
11947
|
+
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
11948
|
+
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
11949
|
+
const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
|
|
11950
|
+
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
11951
|
+
});
|
|
11952
|
+
if (passed.length) {
|
|
11953
|
+
const currentId = candidate?.id ?? null;
|
|
11954
|
+
const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
|
|
11955
|
+
if (withRed.length)
|
|
11956
|
+
return {
|
|
11957
|
+
requirementId: requirement.id,
|
|
11958
|
+
status: "satisfied",
|
|
11959
|
+
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
11960
|
+
decisionIds: [],
|
|
11961
|
+
reason: "fresh applicable evidence passed"
|
|
11962
|
+
};
|
|
11963
|
+
if (requirement.dimension === "testing")
|
|
11964
|
+
return {
|
|
11965
|
+
requirementId: requirement.id,
|
|
11966
|
+
status: "unsatisfied",
|
|
11967
|
+
evidenceIds: [],
|
|
11968
|
+
decisionIds: [],
|
|
11969
|
+
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
11970
|
+
};
|
|
11843
11971
|
}
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11972
|
+
if (requirement.dimension === "delegation") {
|
|
11973
|
+
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
11974
|
+
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
11975
|
+
if (reconciled)
|
|
11976
|
+
return {
|
|
11977
|
+
requirementId: requirement.id,
|
|
11978
|
+
status: "satisfied",
|
|
11979
|
+
evidenceIds: [],
|
|
11980
|
+
decisionIds: [],
|
|
11981
|
+
reason: "bounded helper completed and its result is recorded"
|
|
11982
|
+
};
|
|
11983
|
+
}
|
|
11984
|
+
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
11985
|
+
if (decisions.length)
|
|
11850
11986
|
return {
|
|
11851
|
-
|
|
11852
|
-
|
|
11987
|
+
requirementId: requirement.id,
|
|
11988
|
+
status: "satisfied",
|
|
11989
|
+
evidenceIds: [],
|
|
11990
|
+
decisionIds: decisions.map((decision) => decision.id),
|
|
11991
|
+
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
11992
|
+
};
|
|
11993
|
+
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
11994
|
+
if (limitations.length)
|
|
11995
|
+
return {
|
|
11996
|
+
requirementId: requirement.id,
|
|
11997
|
+
status: "accepted_limitation",
|
|
11998
|
+
evidenceIds: [],
|
|
11999
|
+
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
12000
|
+
reason: "an applicable approved limitation permits the missing evidence"
|
|
12001
|
+
};
|
|
12002
|
+
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
12003
|
+
if (!unavailable && related.length > 0 && !passed.length)
|
|
12004
|
+
return {
|
|
12005
|
+
requirementId: requirement.id,
|
|
12006
|
+
status: "unsatisfied",
|
|
12007
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
12008
|
+
decisionIds: [],
|
|
12009
|
+
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
11853
12010
|
};
|
|
11854
|
-
}
|
|
11855
|
-
return { ok: true };
|
|
11856
|
-
} catch (error) {
|
|
11857
12011
|
return {
|
|
11858
|
-
|
|
11859
|
-
|
|
12012
|
+
requirementId: requirement.id,
|
|
12013
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
12014
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
12015
|
+
decisionIds: [],
|
|
12016
|
+
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
11860
12017
|
};
|
|
12018
|
+
});
|
|
12019
|
+
}
|
|
12020
|
+
function evaluateClosure(requestedOutcome, view) {
|
|
12021
|
+
if (requestedOutcome !== "stopped" && !view.task.policy)
|
|
12022
|
+
return failure("requirements_unsatisfied", "task has not been assessed");
|
|
12023
|
+
if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
|
|
12024
|
+
return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
|
|
12025
|
+
const evaluations = view.requirements;
|
|
12026
|
+
const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
|
|
12027
|
+
if (requestedOutcome !== "stopped" && openFindings.length)
|
|
12028
|
+
return failure("requirements_unsatisfied", "open findings must be resolved before closure");
|
|
12029
|
+
if (requestedOutcome !== "stopped") {
|
|
12030
|
+
const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
|
|
12031
|
+
for (const entry of view.task.findings) {
|
|
12032
|
+
if (entry.data.disposition === "fixed") {
|
|
12033
|
+
const verified = entry.data.resolution?.evidenceIds.some((id) => {
|
|
12034
|
+
const evidence = view.task.evidence.find((item) => item.id === id);
|
|
12035
|
+
if (!evidence)
|
|
12036
|
+
return false;
|
|
12037
|
+
return findingVerificationPasses(entry.data.candidateId, {
|
|
12038
|
+
kind: evidence.data.kind,
|
|
12039
|
+
candidateId: evidence.data.candidateId,
|
|
12040
|
+
status: evidenceById.get(id) ?? "missing"
|
|
12041
|
+
});
|
|
12042
|
+
});
|
|
12043
|
+
if (!verified)
|
|
12044
|
+
return failure("requirements_unsatisfied", "fixed findings require current verification");
|
|
12045
|
+
}
|
|
12046
|
+
if (entry.data.disposition === "deferred") {
|
|
12047
|
+
const valid = entry.data.resolution?.decisionIds.some((id) => {
|
|
12048
|
+
const decisionEntry = view.task.decisions.find((item) => item.id === id);
|
|
12049
|
+
const decision = decisionEntry?.data;
|
|
12050
|
+
return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
|
|
12051
|
+
});
|
|
12052
|
+
if (!valid)
|
|
12053
|
+
return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
12054
|
+
}
|
|
12055
|
+
}
|
|
11861
12056
|
}
|
|
12057
|
+
const blocking = evaluations.filter((item) => {
|
|
12058
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
12059
|
+
return false;
|
|
12060
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
12061
|
+
return !requirement || requirement.before === "close";
|
|
12062
|
+
});
|
|
12063
|
+
if (requestedOutcome !== "stopped" && blocking.length)
|
|
12064
|
+
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
12065
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
12066
|
+
requirements: blocking.map((item) => {
|
|
12067
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
12068
|
+
return {
|
|
12069
|
+
requirementId: item.requirementId,
|
|
12070
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
12071
|
+
reason: item.reason,
|
|
12072
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
12073
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
12074
|
+
};
|
|
12075
|
+
})
|
|
12076
|
+
});
|
|
12077
|
+
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
12078
|
+
if (requestedOutcome === "verified" && accepted.length)
|
|
12079
|
+
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
12080
|
+
requirementIds: accepted.map((item) => item.requirementId)
|
|
12081
|
+
});
|
|
12082
|
+
const evidenceIds = [
|
|
12083
|
+
...new Set([
|
|
12084
|
+
...evaluations.flatMap((item) => item.evidenceIds),
|
|
12085
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
12086
|
+
])
|
|
12087
|
+
];
|
|
12088
|
+
const decisionIds = [
|
|
12089
|
+
...new Set([
|
|
12090
|
+
...evaluations.flatMap((item) => item.decisionIds),
|
|
12091
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
12092
|
+
])
|
|
12093
|
+
];
|
|
12094
|
+
return success(null, null, {
|
|
12095
|
+
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
12096
|
+
evidenceIds,
|
|
12097
|
+
decisionIds,
|
|
12098
|
+
requirementIds: evaluations.map((item) => item.requirementId)
|
|
12099
|
+
});
|
|
12100
|
+
}
|
|
12101
|
+
|
|
12102
|
+
// packages/workit-core/src/core/methods.ts
|
|
12103
|
+
var METHODS = {
|
|
12104
|
+
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
12105
|
+
"workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
|
|
12106
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
12107
|
+
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
12108
|
+
"workit-implement": { dimensions: ["delegation"] },
|
|
12109
|
+
"workit-debug": {},
|
|
12110
|
+
"workit-handoff": {},
|
|
12111
|
+
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
11862
12112
|
};
|
|
11863
|
-
var
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
}
|
|
11875
|
-
if (hasLocalBase) {
|
|
11876
|
-
run(["checkout", base]);
|
|
11877
|
-
try {
|
|
11878
|
-
run(["merge", "--ff-only", `origin/${base}`]);
|
|
11879
|
-
} catch {}
|
|
11880
|
-
} else {
|
|
11881
|
-
run(["checkout", "-b", base, "--track", `origin/${base}`]);
|
|
11882
|
-
}
|
|
11883
|
-
return { ok: true };
|
|
11884
|
-
} catch (error) {
|
|
12113
|
+
var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
12114
|
+
var assuranceFor = (id, definition, capabilities) => {
|
|
12115
|
+
const keys = new Set([
|
|
12116
|
+
id,
|
|
12117
|
+
...definition.dimensions ?? [],
|
|
12118
|
+
...definition.ruleIds ?? []
|
|
12119
|
+
]);
|
|
12120
|
+
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
12121
|
+
if (id === "workit-review" && matches.length === 0)
|
|
12122
|
+
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
12123
|
+
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
11885
12124
|
return {
|
|
11886
|
-
|
|
11887
|
-
|
|
12125
|
+
assurance: "unavailable",
|
|
12126
|
+
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
11888
12127
|
};
|
|
11889
|
-
|
|
11890
|
-
};
|
|
11891
|
-
|
|
11892
|
-
const git = gitContext(cwd);
|
|
11893
|
-
if (!git.branch || git.branch === "unknown")
|
|
11894
|
-
return { ok: false, error: "not in a git repository" };
|
|
11895
|
-
const ready = originBaseReady(cwd, base);
|
|
11896
|
-
if (!ready.ok)
|
|
11897
|
-
return ready;
|
|
11898
|
-
return fastForwardBase(cwd, base);
|
|
12128
|
+
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
12129
|
+
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
12130
|
+
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
11899
12131
|
};
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
12132
|
+
function selectMethods(policy, capabilities) {
|
|
12133
|
+
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
12134
|
+
const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
|
|
12135
|
+
if (requirements.length === 0)
|
|
12136
|
+
return [];
|
|
12137
|
+
const capability = assuranceFor(id, definition, capabilities);
|
|
12138
|
+
const reasons = [
|
|
12139
|
+
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
12140
|
+
...capability.reasons
|
|
12141
|
+
];
|
|
12142
|
+
if (capability.assurance === "unavailable")
|
|
12143
|
+
reasons.unshift("required capability is unavailable");
|
|
12144
|
+
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
12145
|
+
});
|
|
12146
|
+
}
|
|
12147
|
+
var invariantBootstrap = () => `
|
|
12148
|
+
Workit keeps one accountable lead and one shared task state. Inspect current task
|
|
12149
|
+
state before acting. An empty task.list means no session yet, not permission to
|
|
12150
|
+
skip Workit; for user-requested product, debug, or behavior work with no active
|
|
12151
|
+
or paused task, run task.start then policy.assess before other product mutations.
|
|
12152
|
+
Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
|
|
12153
|
+
to the current records; explicit values are still concurrency-checked, so never
|
|
12154
|
+
copy revisions between calls. Happy path: task.list, task.start {intent} with no
|
|
12155
|
+
revisions, policy.assess {assessment: {facts, signals, consequences,
|
|
12156
|
+
verification}} where facts are inferred or observed with file refs,
|
|
12157
|
+
writer.acquire {taskId} before product writes, evidence.record {evidence} where
|
|
12158
|
+
check and review kinds auto-bind the current tree and need no digests. Record
|
|
12159
|
+
self-review requirements with kind review and reviewContext matching your own
|
|
12160
|
+
host session; the lead may also record checks. Fresh-context review requires a
|
|
12161
|
+
session distinct from the task creator and every other evidence recorder. Record
|
|
12162
|
+
verification evidence last: later evidence or commits can reopen fixed findings
|
|
12163
|
+
for re-verification, so verify, resolve, then close with nothing in between.
|
|
12164
|
+
decision.record {binding with taskId/workspaceId from inspect plus presented and
|
|
12165
|
+
approvedContent} only through a native approval question asked receipt-shaped:
|
|
12166
|
+
header Workit decision: <purpose>, the same label repeated in the question
|
|
12167
|
+
text (some host UIs do not render headers), exactly two options approved and
|
|
12168
|
+
rejected; ask once, never re-ask to mint a receipt. Before asking, present the
|
|
12169
|
+
item in the conversation (durable artifacts: complete digest plus exact path;
|
|
12170
|
+
inline plans: the content) and keep the question to one short scoped sentence;
|
|
12171
|
+
never ask to approve something unseen. An answer with custom text is steering,
|
|
12172
|
+
not approval: adjust, re-present, then ask a new scoped question. After the user
|
|
12173
|
+
approves a spec or plan, execute the remaining tasks continuously with one
|
|
12174
|
+
atomic commit per task and do not ask whether to continue; stop only for a new
|
|
12175
|
+
product decision, a failed safety or verification gate, a conflicting edit, or
|
|
12176
|
+
missing authority. Record the plan's commit list once through the plan-scoped
|
|
12177
|
+
action approval (git.commit with plan_steps and plan_branch) so each listed
|
|
12178
|
+
commit executes without a new question; an unlisted message needs a fresh
|
|
12179
|
+
approval. task.close {outcome,
|
|
12180
|
+
summary, decisionIds}. A completed native subagent run stops its bound worker
|
|
12181
|
+
by itself; if a worker strands in cancelling with its run verifiably over,
|
|
12182
|
+
repeat worker.cancel {reason} to confirm the stop. Check and review evidence
|
|
12183
|
+
without a bound candidate goes stale; findings close only as fixed with passing
|
|
12184
|
+
verification, dismissed with supporting evidence, or deferred under an approved
|
|
12185
|
+
limitation. Use only the
|
|
12186
|
+
shared operations for task, policy, evidence, finding, decision, worker, writer,
|
|
12187
|
+
and state changes. Authority is bounded by the requested scope, current
|
|
12188
|
+
revision, caller/session provenance, and observed capabilities. Never claim host
|
|
12189
|
+
enforcement or evidence that the host cannot provide. Shell-executed writes are
|
|
12190
|
+
unattested agent-guided work even when a writer is held. Preserve unresolved
|
|
12191
|
+
requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
|
|
12192
|
+
always load on demand. Model-invoked: new instructions, interruptions, or
|
|
12193
|
+
forgotten items mid-task → load workit-steer before responding; about to open
|
|
12194
|
+
a PR or fresh from implementing → load workit-deslop; CI red → load
|
|
12195
|
+
workit-green-run; small-looking change before close or merge → load
|
|
12196
|
+
workit-blast-radius; ambiguous, consequential, or disputed proposal before
|
|
12197
|
+
recommending → load workit-challenge; spec or plan needs a flow diagram →
|
|
12198
|
+
load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
|
|
12199
|
+
act from memory of it.
|
|
12200
|
+
`.trim();
|
|
12201
|
+
|
|
12202
|
+
// packages/workit-core/src/core/task-context.ts
|
|
12203
|
+
function reconcileResume(view, observations = []) {
|
|
12204
|
+
if (observations.length > 0)
|
|
12205
|
+
return failure("permission_denied", "worker observations require native host verification");
|
|
12206
|
+
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
12207
|
+
if (!candidate.ok)
|
|
12208
|
+
return candidate;
|
|
12209
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
12210
|
+
const blockers = [...view.task.progress.blockers];
|
|
12211
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
12212
|
+
blockers.push({
|
|
12213
|
+
reason: "worker state requires reconciliation",
|
|
12214
|
+
dependentAction: "resume",
|
|
12215
|
+
refs: []
|
|
11907
12216
|
});
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
}
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
var
|
|
11931
|
-
|
|
11932
|
-
sdd_dir,
|
|
11933
|
-
target_branch,
|
|
11934
|
-
stash,
|
|
11935
|
-
workspace_root,
|
|
11936
|
-
log
|
|
11937
|
-
}) => {
|
|
11938
|
-
const cwd = path10.resolve(workspace_root);
|
|
11939
|
-
const exec = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
11940
|
-
const current = gitContext(cwd).branch;
|
|
11941
|
-
if (!current || current === "unknown")
|
|
11942
|
-
return { error: "not in a git repository", phase: "preflight" };
|
|
11943
|
-
const sdd = sdd_dir ?? "docs";
|
|
11944
|
-
const manifestPath = path10.isAbsolute(sdd) ? path10.join(sdd, "manifest.json") : path10.join(cwd, sdd, "manifest.json");
|
|
11945
|
-
const ensureManifestDir = () => {
|
|
11946
|
-
const dir = path10.dirname(manifestPath);
|
|
11947
|
-
if (!existsSync5(dir))
|
|
11948
|
-
mkdirSync3(dir, { recursive: true, mode: 493 });
|
|
11949
|
-
};
|
|
11950
|
-
const readManifest = () => {
|
|
11951
|
-
try {
|
|
11952
|
-
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
11953
|
-
} catch {
|
|
11954
|
-
return {};
|
|
11955
|
-
}
|
|
11956
|
-
};
|
|
11957
|
-
const writeManifest = (data) => {
|
|
11958
|
-
ensureManifestDir();
|
|
11959
|
-
writeFileSync3(manifestPath, JSON.stringify(data, null, 2) + `
|
|
11960
|
-
`, "utf8");
|
|
11961
|
-
};
|
|
11962
|
-
const journal = (message) => log?.(`branch-setup: ${message}`);
|
|
11963
|
-
if (action === "reapply_stash") {
|
|
11964
|
-
const manifest = readManifest();
|
|
11965
|
-
const ref = manifest.stash_ref;
|
|
11966
|
-
if (!ref)
|
|
11967
|
-
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
11968
|
-
journal(`pre-pop: ${String(ref)}`);
|
|
11969
|
-
try {
|
|
11970
|
-
exec(["stash", "pop", String(ref)]);
|
|
11971
|
-
} catch (error) {
|
|
11972
|
-
journal("pop: failed");
|
|
11973
|
-
return {
|
|
11974
|
-
error: error instanceof Error ? error.message : "stash pop failed",
|
|
11975
|
-
phase: "post_mutation"
|
|
11976
|
-
};
|
|
11977
|
-
}
|
|
11978
|
-
journal("pop: ok");
|
|
11979
|
-
delete manifest.stash_ref;
|
|
11980
|
-
delete manifest.stash_created_at;
|
|
11981
|
-
writeManifest(manifest);
|
|
11982
|
-
return { action: "reapply_stash", ok: true };
|
|
11983
|
-
}
|
|
11984
|
-
const target = target_branch ?? "";
|
|
11985
|
-
if (!target)
|
|
12217
|
+
return success(null, null, {
|
|
12218
|
+
candidate: candidate.data,
|
|
12219
|
+
staleEvidenceIds,
|
|
12220
|
+
workerUpdates: [],
|
|
12221
|
+
blockers,
|
|
12222
|
+
reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
|
|
12223
|
+
});
|
|
12224
|
+
}
|
|
12225
|
+
var COMPACT_MAX_BYTES = 4096;
|
|
12226
|
+
var COMPACT_MAX_ITEMS = 8;
|
|
12227
|
+
var COMPACT_TEXT_BYTES = 128;
|
|
12228
|
+
var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
|
|
12229
|
+
var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
|
|
12230
|
+
var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
|
|
12231
|
+
var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
|
|
12232
|
+
const separator = match.search(/[:=]/u);
|
|
12233
|
+
if (separator >= 0)
|
|
12234
|
+
return `${match.slice(0, separator)}=<redacted>`;
|
|
12235
|
+
const space = match.search(/\s/u);
|
|
12236
|
+
return `${match.slice(0, space)} <redacted>`;
|
|
12237
|
+
}).slice(0, limit);
|
|
12238
|
+
var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
12239
|
+
var compactReference = (ref) => {
|
|
12240
|
+
if (ref.kind === "file")
|
|
11986
12241
|
return {
|
|
11987
|
-
|
|
11988
|
-
|
|
12242
|
+
kind: "file",
|
|
12243
|
+
path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
|
|
11989
12244
|
};
|
|
11990
|
-
if (
|
|
11991
|
-
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
11992
|
-
if (!allowedBranch(cwd, target))
|
|
12245
|
+
if (ref.kind === "record")
|
|
11993
12246
|
return {
|
|
11994
|
-
|
|
11995
|
-
|
|
12247
|
+
kind: "record",
|
|
12248
|
+
collection: ref.collection,
|
|
12249
|
+
id: ref.id
|
|
11996
12250
|
};
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12251
|
+
return { kind: ref.kind };
|
|
12252
|
+
};
|
|
12253
|
+
function compactTaskContext(view) {
|
|
12254
|
+
const decisions = view.task.decisions.slice().sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
|
|
12255
|
+
id: entry.id,
|
|
12256
|
+
purpose: entry.data.purpose,
|
|
12257
|
+
status: entry.data.response,
|
|
12258
|
+
digest: entry.data.digest,
|
|
12259
|
+
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
12260
|
+
}));
|
|
12261
|
+
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
12262
|
+
id: method.id,
|
|
12263
|
+
assurance: method.assurance,
|
|
12264
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
12265
|
+
})) : [];
|
|
12266
|
+
const gaps = Array.from(new Set([
|
|
12267
|
+
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
12268
|
+
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
12269
|
+
...view.task.progress.blockers.map((entry) => entry.reason)
|
|
12270
|
+
])).sort().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
|
|
12271
|
+
const context = {
|
|
12272
|
+
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
12273
|
+
status: view.task.status,
|
|
12274
|
+
decisions,
|
|
12275
|
+
methods,
|
|
12276
|
+
gaps,
|
|
12277
|
+
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
12278
|
+
};
|
|
12279
|
+
let encoded = JSON.stringify(context);
|
|
12280
|
+
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
12281
|
+
if (context.gaps.length)
|
|
12282
|
+
context.gaps.pop();
|
|
12283
|
+
else if (context.decisions.length)
|
|
12284
|
+
context.decisions.pop();
|
|
12285
|
+
else
|
|
12286
|
+
context.methods.pop();
|
|
12287
|
+
encoded = JSON.stringify(context);
|
|
12003
12288
|
}
|
|
12004
|
-
if (
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
base = baseResolved.base;
|
|
12289
|
+
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
12290
|
+
context.objective = compactText(context.objective, 64) ?? "";
|
|
12291
|
+
context.nextAction = compactText(context.nextAction, 64);
|
|
12292
|
+
encoded = JSON.stringify(context);
|
|
12009
12293
|
}
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
}
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
}
|
|
12062
|
-
try {
|
|
12063
|
-
let effectiveBase = base;
|
|
12064
|
-
if (effectiveBase === undefined) {
|
|
12065
|
-
const lateResolved = baseBranch(cwd);
|
|
12066
|
-
if ("error" in lateResolved)
|
|
12067
|
-
return failAfterStash(lateResolved.error, "preflight");
|
|
12068
|
-
effectiveBase = lateResolved.base;
|
|
12069
|
-
}
|
|
12070
|
-
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
12071
|
-
if (!baseResult.ok)
|
|
12072
|
-
return failAfterStash(baseResult.error ?? "ensure-base-branch failed");
|
|
12073
|
-
exec(["checkout", "-b", target]);
|
|
12074
|
-
journal("post-create");
|
|
12075
|
-
} catch (createError) {
|
|
12076
|
-
return failAfterStash(createError instanceof Error ? createError.message : "branch create failed");
|
|
12077
|
-
}
|
|
12078
|
-
}
|
|
12294
|
+
return encoded;
|
|
12295
|
+
}
|
|
12296
|
+
var exportDigest = (bundle) => sha256({
|
|
12297
|
+
schemaVersion: bundle.schemaVersion,
|
|
12298
|
+
exportedAt: bundle.exportedAt,
|
|
12299
|
+
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
12300
|
+
task: bundle.task
|
|
12301
|
+
});
|
|
12302
|
+
// packages/workit-core/src/core/route-intent.ts
|
|
12303
|
+
var GUIDANCE = {
|
|
12304
|
+
"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)",
|
|
12305
|
+
"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",
|
|
12306
|
+
"hosting.merge": "use the Workit hosting.merge action (workit_external_action in hosts, or `workit action hosting.merge --payload '{...}' --confirm --json` in the CLI)"
|
|
12307
|
+
};
|
|
12308
|
+
var unbalancedQuotes = (segment) => {
|
|
12309
|
+
const text = segment.replace(/\\./gu, "");
|
|
12310
|
+
return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
|
|
12311
|
+
};
|
|
12312
|
+
var segmentTokens = (segment) => {
|
|
12313
|
+
if (segment.includes("`") || segment.includes("$("))
|
|
12314
|
+
return null;
|
|
12315
|
+
if (unbalancedQuotes(segment))
|
|
12316
|
+
return null;
|
|
12317
|
+
const parts = segment.trim().split(/\s+/u);
|
|
12318
|
+
while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
|
|
12319
|
+
parts.shift();
|
|
12320
|
+
return parts.length > 0 ? parts : null;
|
|
12321
|
+
};
|
|
12322
|
+
var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
|
|
12323
|
+
function shellRouteIntent(command) {
|
|
12324
|
+
if (typeof command !== "string" || command.trim() === "")
|
|
12325
|
+
return null;
|
|
12326
|
+
for (const raw of command.split(/&&|\|\||;|\n/u)) {
|
|
12327
|
+
const parts = segmentTokens(raw);
|
|
12328
|
+
if (!parts)
|
|
12329
|
+
continue;
|
|
12330
|
+
const head = unquote(parts[0]);
|
|
12331
|
+
const second = unquote(parts[1] ?? "");
|
|
12332
|
+
const third = unquote(parts[2] ?? "");
|
|
12333
|
+
if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
|
|
12334
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
12335
|
+
if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
|
|
12336
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
12337
|
+
if (head === "gh" && second === "pr" && third === "create")
|
|
12338
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
12339
|
+
if (head === "glab" && second === "mr" && third === "create")
|
|
12340
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
12341
|
+
if (head === "gh" && second === "pr" && third === "merge")
|
|
12342
|
+
return { route: "hosting.merge", guidance: GUIDANCE["hosting.merge"] };
|
|
12343
|
+
if (head === "glab" && second === "mr" && third === "merge")
|
|
12344
|
+
return { route: "hosting.merge", guidance: GUIDANCE["hosting.merge"] };
|
|
12079
12345
|
}
|
|
12346
|
+
return null;
|
|
12347
|
+
}
|
|
12348
|
+
function hasLiveWorkitTask(dir) {
|
|
12080
12349
|
try {
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
if (
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
} catch (error) {
|
|
12090
|
-
const result = failAfterStash(error instanceof Error ? `manifest update failed: ${error.message}` : "manifest update failed");
|
|
12091
|
-
if (stash_ref === undefined && gitContext(cwd).branch !== current) {
|
|
12092
|
-
try {
|
|
12093
|
-
exec(["checkout", current]);
|
|
12094
|
-
} catch {
|
|
12095
|
-
try {
|
|
12096
|
-
exec(["checkout", "-m", current]);
|
|
12097
|
-
} catch {}
|
|
12098
|
-
}
|
|
12099
|
-
}
|
|
12100
|
-
return result;
|
|
12350
|
+
if (typeof dir !== "string" || dir === "")
|
|
12351
|
+
return false;
|
|
12352
|
+
const listed = new TaskStore(dir).listTasks();
|
|
12353
|
+
if (!listed.ok)
|
|
12354
|
+
return false;
|
|
12355
|
+
return listed.data.some((task) => task.status === "active" || task.status === "paused");
|
|
12356
|
+
} catch {
|
|
12357
|
+
return false;
|
|
12101
12358
|
}
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
manifest: manifestPath
|
|
12109
|
-
};
|
|
12110
|
-
};
|
|
12359
|
+
}
|
|
12360
|
+
function shouldDenyShellRoute(dir, command) {
|
|
12361
|
+
if (!hasLiveWorkitTask(dir))
|
|
12362
|
+
return null;
|
|
12363
|
+
return shellRouteIntent(command);
|
|
12364
|
+
}
|
|
12111
12365
|
// packages/workit-core/src/core/external-action.ts
|
|
12112
12366
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
12113
12367
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
@@ -12142,6 +12396,13 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
12142
12396
|
babysit: boolean2().optional()
|
|
12143
12397
|
}).strict()
|
|
12144
12398
|
}).strict(),
|
|
12399
|
+
object({
|
|
12400
|
+
operation: literal("hosting.merge"),
|
|
12401
|
+
payload: object({
|
|
12402
|
+
target_branch: string2().min(1).optional(),
|
|
12403
|
+
source_branch: string2().min(1).optional()
|
|
12404
|
+
}).strict()
|
|
12405
|
+
}).strict(),
|
|
12145
12406
|
object({
|
|
12146
12407
|
operation: literal("youtrack.update"),
|
|
12147
12408
|
payload: object({
|
|
@@ -12215,7 +12476,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
12215
12476
|
payload,
|
|
12216
12477
|
...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 } : {}
|
|
12217
12478
|
});
|
|
12218
|
-
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).";
|
|
12479
|
+
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?}; hosting.merge {target_branch?,source_branch?}; 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).";
|
|
12219
12480
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
12220
12481
|
kind: "host",
|
|
12221
12482
|
host,
|
|
@@ -13741,7 +14002,10 @@ class WorkitCore {
|
|
|
13741
14002
|
observeDecision(request, observation) {
|
|
13742
14003
|
return this.recordDecision(request, observation, true);
|
|
13743
14004
|
}
|
|
13744
|
-
|
|
14005
|
+
observeStandingDecision(request) {
|
|
14006
|
+
return this.recordDecision(request, undefined, true, true);
|
|
14007
|
+
}
|
|
14008
|
+
recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
|
|
13745
14009
|
const root = this.contextRootError();
|
|
13746
14010
|
if (!root.ok)
|
|
13747
14011
|
return root;
|
|
@@ -13756,8 +14020,10 @@ class WorkitCore {
|
|
|
13756
14020
|
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
13757
14021
|
if (stated && !input.binding?.statedChoice)
|
|
13758
14022
|
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
13759
|
-
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
14023
|
+
if (nativeRequired && nativeObservation === undefined && !stated && !standing)
|
|
13760
14024
|
return failure("permission_denied", "native decision observation is required");
|
|
14025
|
+
if (standing && (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved"))
|
|
14026
|
+
return failure("invalid_input", "standing approvals need an approved action binding with a standing rule");
|
|
13761
14027
|
const task = this.store.readTask(input.taskId);
|
|
13762
14028
|
if (!task.ok)
|
|
13763
14029
|
return task;
|
|
@@ -13789,7 +14055,10 @@ class WorkitCore {
|
|
|
13789
14055
|
consumption: null
|
|
13790
14056
|
};
|
|
13791
14057
|
const data = { ...base, digest: decisionDigest(base) };
|
|
13792
|
-
const
|
|
14058
|
+
const standingApproval = standing && !stated ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding) : null;
|
|
14059
|
+
if (standingApproval && !standingApproval.ok)
|
|
14060
|
+
return standingApproval;
|
|
14061
|
+
const native = nativeRequired && !stated && !standingApproval ? verifyNativeDecision(this.context.nativeAuthority, {
|
|
13793
14062
|
observation: nativeObservation,
|
|
13794
14063
|
expected: {
|
|
13795
14064
|
taskId: task.data.id,
|
|
@@ -13820,7 +14089,7 @@ class WorkitCore {
|
|
|
13820
14089
|
const entry = {
|
|
13821
14090
|
id: newId(),
|
|
13822
14091
|
recordedAt: mutation.now,
|
|
13823
|
-
provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
14092
|
+
provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
13824
14093
|
data
|
|
13825
14094
|
};
|
|
13826
14095
|
return success(mutation.revision, null, {
|
|
@@ -15337,10 +15606,58 @@ function prCreate(env, cwd) {
|
|
|
15337
15606
|
output: (result.stdout ?? "").trim()
|
|
15338
15607
|
};
|
|
15339
15608
|
}
|
|
15609
|
+
function mergePr(cwd, opts) {
|
|
15610
|
+
const root = repoRoot(cwd);
|
|
15611
|
+
const cfg = vcsConfig("load", root);
|
|
15612
|
+
if (!cfg.ok)
|
|
15613
|
+
return { error: cfg.error ?? "vcs config missing" };
|
|
15614
|
+
if (!cfg.tokenReady)
|
|
15615
|
+
return { error: "VCS token not ready — run /wk-init and edit token file locally" };
|
|
15616
|
+
const provider = cfg.provider;
|
|
15617
|
+
if (provider !== "gitlab" && provider !== "github")
|
|
15618
|
+
return { error: `unsupported provider: ${provider}` };
|
|
15619
|
+
if (!hostingCliAvailable(provider))
|
|
15620
|
+
return { error: `workflow CLI missing (required for ${provider})` };
|
|
15621
|
+
const pr = cfg.pr ?? {};
|
|
15622
|
+
const token = fs8.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
15623
|
+
const squash = pr.squashOnMerge !== false;
|
|
15624
|
+
const removeBranch = pr.removeSourceBranch !== false;
|
|
15625
|
+
let cmd;
|
|
15626
|
+
let cmdEnv;
|
|
15627
|
+
if (provider === "gitlab") {
|
|
15628
|
+
cmd = ["glab", "mr", "merge", opts.source];
|
|
15629
|
+
cmd.push(squash ? "--squash" : "--squash=false");
|
|
15630
|
+
if (removeBranch)
|
|
15631
|
+
cmd.push("--remove-source-branch");
|
|
15632
|
+
cmd.push("--yes");
|
|
15633
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GITLAB_TOKEN: token };
|
|
15634
|
+
} else {
|
|
15635
|
+
cmd = ["gh", "pr", "merge", opts.source];
|
|
15636
|
+
cmd.push(squash ? "--squash" : "--merge");
|
|
15637
|
+
if (removeBranch)
|
|
15638
|
+
cmd.push("--delete-branch");
|
|
15639
|
+
cmd.push("--yes");
|
|
15640
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GH_TOKEN: token };
|
|
15641
|
+
}
|
|
15642
|
+
const result = spawnSync4(cmd[0], cmd.slice(1), { cwd: root, encoding: "utf8", env: cmdEnv });
|
|
15643
|
+
if (result.status !== 0)
|
|
15644
|
+
return {
|
|
15645
|
+
error: "merge failed",
|
|
15646
|
+
provider,
|
|
15647
|
+
stderr: (result.stderr ?? result.stdout ?? "").trim().slice(0, 800)
|
|
15648
|
+
};
|
|
15649
|
+
return {
|
|
15650
|
+
ok: true,
|
|
15651
|
+
provider,
|
|
15652
|
+
targetBranch: opts.target,
|
|
15653
|
+
sourceBranch: opts.source,
|
|
15654
|
+
output: (result.stdout ?? "").trim()
|
|
15655
|
+
};
|
|
15656
|
+
}
|
|
15340
15657
|
|
|
15341
15658
|
// packages/workit-core/src/core/repo-context.ts
|
|
15342
15659
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
15343
|
-
import { existsSync as existsSync7, readFileSync as
|
|
15660
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
|
|
15344
15661
|
import path13 from "node:path";
|
|
15345
15662
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
15346
15663
|
try {
|
|
@@ -15510,7 +15827,7 @@ var packageScripts = (cwd, keys) => {
|
|
|
15510
15827
|
if (!existsSync7(pkgPath))
|
|
15511
15828
|
return [];
|
|
15512
15829
|
try {
|
|
15513
|
-
const pkg = JSON.parse(
|
|
15830
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
15514
15831
|
return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
|
|
15515
15832
|
} catch {
|
|
15516
15833
|
return [];
|
|
@@ -15544,7 +15861,7 @@ var documentationFiles = (cwd) => {
|
|
|
15544
15861
|
var readTrimmed = (file, maxLines) => {
|
|
15545
15862
|
if (!existsSync7(file))
|
|
15546
15863
|
return "";
|
|
15547
|
-
const lines =
|
|
15864
|
+
const lines = readFileSync8(file, "utf8").split(`
|
|
15548
15865
|
`);
|
|
15549
15866
|
return lines.slice(0, maxLines).join(`
|
|
15550
15867
|
`);
|
|
@@ -15732,7 +16049,7 @@ function docsRefreshContext(root, range) {
|
|
|
15732
16049
|
const pkgPath = path13.join(cwd, "package.json");
|
|
15733
16050
|
if (existsSync7(pkgPath)) {
|
|
15734
16051
|
try {
|
|
15735
|
-
const pkg = JSON.parse(
|
|
16052
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
15736
16053
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
15737
16054
|
`;
|
|
15738
16055
|
} catch {}
|
|
@@ -16633,7 +16950,7 @@ import path16 from "node:path";
|
|
|
16633
16950
|
// packages/workit-core/src/core/changelog.ts
|
|
16634
16951
|
import fs11 from "node:fs";
|
|
16635
16952
|
import path15 from "node:path";
|
|
16636
|
-
import { createHash as
|
|
16953
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
16637
16954
|
var CATEGORIES = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
|
|
16638
16955
|
var CAT_RE = /^###\s+(?:Added|Changed|Deprecated|Removed|Fixed|Security)\s*$/i;
|
|
16639
16956
|
var UNRELEASED_RE = /^##\s+\[Unreleased\]\s*$/i;
|
|
@@ -16650,7 +16967,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16650
16967
|
## [Unreleased]
|
|
16651
16968
|
|
|
16652
16969
|
`;
|
|
16653
|
-
var digestBytes4 = (value) =>
|
|
16970
|
+
var digestBytes4 = (value) => createHash6("sha256").update(value).digest("hex");
|
|
16654
16971
|
var readChangelogSource = (target) => {
|
|
16655
16972
|
if (!fs11.existsSync(target)) {
|
|
16656
16973
|
const bytes = Buffer.from(SKELETON, "utf8");
|
|
@@ -17262,6 +17579,11 @@ var actionProposalQuestion = (request, descriptorPayload) => {
|
|
|
17262
17579
|
presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
|
|
17263
17580
|
approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
|
|
17264
17581
|
};
|
|
17582
|
+
case "hosting.merge":
|
|
17583
|
+
return {
|
|
17584
|
+
presented: `Workit decision: action — Merge \`${String(resolved.source_branch ?? "")}\` into \`${payloadTarget}\` (squash, delete branch)?`,
|
|
17585
|
+
approvedText: `Merge ${String(resolved.source_branch ?? "")} → ${payloadTarget}.`
|
|
17586
|
+
};
|
|
17265
17587
|
case "changelog.apply":
|
|
17266
17588
|
return {
|
|
17267
17589
|
presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
|
|
@@ -17292,7 +17614,7 @@ var actionProposalQuestion = (request, descriptorPayload) => {
|
|
|
17292
17614
|
var approvedResolvedExternalAction = (content) => {
|
|
17293
17615
|
try {
|
|
17294
17616
|
const value = JSON.parse(content);
|
|
17295
|
-
if (value.operation !== "hosting.pull_request" && value.operation !== "changelog.apply" && value.operation !== "youtrack.update" && value.operation !== "youtrack.time" && value.operation !== "youtrack.meeting")
|
|
17617
|
+
if (value.operation !== "hosting.pull_request" && value.operation !== "hosting.merge" && value.operation !== "changelog.apply" && value.operation !== "youtrack.update" && value.operation !== "youtrack.time" && value.operation !== "youtrack.meeting")
|
|
17296
17618
|
return failure("invalid_input", "approved action is not a reconciliable external action");
|
|
17297
17619
|
if (!value.payload || typeof value.payload !== "object")
|
|
17298
17620
|
return failure("invalid_input", "approved action payload is incomplete");
|
|
@@ -17305,6 +17627,8 @@ var approvedResolvedExternalAction = (content) => {
|
|
|
17305
17627
|
const target = resolved;
|
|
17306
17628
|
if (value.operation === "hosting.pull_request" && (typeof target.marker !== "string" || typeof target.source_branch !== "string" || typeof target.source_commit !== "string" || typeof target.remote !== "string" || typeof payload.target_branch !== "string"))
|
|
17307
17629
|
return failure("invalid_input", "approved hosting target is incomplete");
|
|
17630
|
+
if (value.operation === "hosting.merge" && (typeof target.marker !== "string" || typeof target.source_branch !== "string" || typeof target.source_commit !== "string" || typeof target.remote !== "string" || typeof payload.target_branch !== "string"))
|
|
17631
|
+
return failure("invalid_input", "approved hosting target is incomplete");
|
|
17308
17632
|
if (value.operation === "changelog.apply" && (typeof target.target !== "string" || typeof target.beforeDigest !== "string" || typeof target.beforeExists !== "boolean" || typeof target.afterDigest !== "string"))
|
|
17309
17633
|
return failure("invalid_input", "approved changelog target is incomplete");
|
|
17310
17634
|
if (value.operation !== "hosting.pull_request" && value.operation !== "changelog.apply" && (typeof target.marker !== "string" || typeof target.baseUrl !== "string" || typeof target.issueId !== "string" || !Array.isArray(target.steps)))
|
|
@@ -17596,6 +17920,11 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
17596
17920
|
}
|
|
17597
17921
|
case "git.push": {
|
|
17598
17922
|
const branch = request.payload.branch ?? gitValue(root, ["branch", "--show-current"]);
|
|
17923
|
+
if (branch && isProtectedBranch2(root, branch))
|
|
17924
|
+
return failure("invalid_input", `cannot push protected branch ${branch}`, {
|
|
17925
|
+
outcome: "not_started",
|
|
17926
|
+
fields: [{ path: "branch", reason: "protected branch" }]
|
|
17927
|
+
});
|
|
17599
17928
|
const commit = branch ? gitValue(root, ["rev-parse", branch]) : null;
|
|
17600
17929
|
const remote = pushRemote(root);
|
|
17601
17930
|
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
@@ -17693,6 +18022,11 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
17693
18022
|
case "hosting.pull_request": {
|
|
17694
18023
|
const target_branch = request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
17695
18024
|
const source_branch = gitValue(root, ["branch", "--show-current"]);
|
|
18025
|
+
if (source_branch && isProtectedBranch2(root, source_branch))
|
|
18026
|
+
return failure("invalid_input", `cannot open a PR from protected branch ${source_branch}`, {
|
|
18027
|
+
outcome: "not_started",
|
|
18028
|
+
fields: [{ path: "source_branch", reason: "protected branch" }]
|
|
18029
|
+
});
|
|
17696
18030
|
const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
|
|
17697
18031
|
const remote = pushRemote(root);
|
|
17698
18032
|
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
@@ -17724,6 +18058,44 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
17724
18058
|
}
|
|
17725
18059
|
});
|
|
17726
18060
|
}
|
|
18061
|
+
case "hosting.merge": {
|
|
18062
|
+
const target_branch = request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
18063
|
+
const source_branch = request.payload.source_branch ?? gitValue(root, ["branch", "--show-current"]);
|
|
18064
|
+
if (source_branch && isProtectedBranch2(root, source_branch))
|
|
18065
|
+
return failure("invalid_input", `cannot merge protected branch ${source_branch}`, {
|
|
18066
|
+
outcome: "not_started",
|
|
18067
|
+
fields: [{ path: "source_branch", reason: "protected branch" }]
|
|
18068
|
+
});
|
|
18069
|
+
const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
|
|
18070
|
+
const remote = pushRemote(root);
|
|
18071
|
+
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
18072
|
+
if (!source_branch || !source_commit || !identity || remoteProvider2(identity) !== null && remoteProvider2(identity) !== String(vcsConfig("load", root).provider).toLowerCase())
|
|
18073
|
+
return failure("capability_unavailable", "hosting target is not bound to the configured remote provider", { capability: "hosting.merge" });
|
|
18074
|
+
const vcs = vcsConfig("load", root);
|
|
18075
|
+
if (!vcs.ok || !vcs.tokenReady)
|
|
18076
|
+
return failure("capability_unavailable", "hosting credentials are unavailable", {
|
|
18077
|
+
capability: "hosting.merge",
|
|
18078
|
+
outcome: "not_started"
|
|
18079
|
+
});
|
|
18080
|
+
if (!hostingCliAvailable(String(vcs.provider)))
|
|
18081
|
+
return failure("capability_unavailable", "hosting CLI is unavailable", {
|
|
18082
|
+
capability: "hosting.merge",
|
|
18083
|
+
outcome: "not_started"
|
|
18084
|
+
});
|
|
18085
|
+
const normalized = {
|
|
18086
|
+
operation: request.operation,
|
|
18087
|
+
payload: { ...request.payload, target_branch, source_branch }
|
|
18088
|
+
};
|
|
18089
|
+
const marker = `<!-- workit-action:${sha256({ operation: request.operation, payload: normalized.payload, source_commit, remote: identity })} -->`;
|
|
18090
|
+
return success(null, null, {
|
|
18091
|
+
request: normalized,
|
|
18092
|
+
marker,
|
|
18093
|
+
descriptorPayload: {
|
|
18094
|
+
...normalized.payload,
|
|
18095
|
+
resolved: { source_branch, source_commit, remote: identity, target_branch, marker }
|
|
18096
|
+
}
|
|
18097
|
+
});
|
|
18098
|
+
}
|
|
17727
18099
|
case "changelog.apply": {
|
|
17728
18100
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
17729
18101
|
if (preview.error)
|
|
@@ -17899,6 +18271,17 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
17899
18271
|
...request.payload.babysit ?? true ? { next: prBabysitNext(String(result.output ?? "")) } : {}
|
|
17900
18272
|
});
|
|
17901
18273
|
}
|
|
18274
|
+
case "hosting.merge": {
|
|
18275
|
+
const target = String(request.payload.target_branch ?? "");
|
|
18276
|
+
const source = String(request.payload.source_branch ?? "");
|
|
18277
|
+
if (!target || !source)
|
|
18278
|
+
return failure("invalid_input", "hosting.merge requires target and source branches", {
|
|
18279
|
+
outcome: "not_started",
|
|
18280
|
+
fields: [{ path: "target_branch", reason: "required" }]
|
|
18281
|
+
});
|
|
18282
|
+
const result = mergePr(root, { target, source });
|
|
18283
|
+
return result.error || result.ok === false ? unknown2(request.operation) : success(null, null, result);
|
|
18284
|
+
}
|
|
17902
18285
|
case "changelog.apply": {
|
|
17903
18286
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
17904
18287
|
if (preview.error || approvedBeforeDigest !== undefined && preview.beforeDigest !== approvedBeforeDigest || approvedBeforeExists !== undefined && preview.beforeExists !== approvedBeforeExists)
|
|
@@ -18047,7 +18430,7 @@ var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedEx
|
|
|
18047
18430
|
const store = new TaskStore(root);
|
|
18048
18431
|
const selected = approvedExternalAction(store, "pi", actor, operation);
|
|
18049
18432
|
if (!selected.ok) {
|
|
18050
|
-
const plan = planCommitBinding(store, "pi", actor, operation) ?? chainStepBinding(store, "pi", actor, operation);
|
|
18433
|
+
const plan = planCommitBinding(store, "pi", actor, operation) ?? chainStepBinding(store, "pi", actor, operation) ?? standingAutoBinding(core, store, "pi", actor, operation);
|
|
18051
18434
|
if (plan) {
|
|
18052
18435
|
const actionRef = externalActionRef("pi", actor, operation);
|
|
18053
18436
|
return {
|
|
@@ -18189,6 +18572,7 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
18189
18572
|
"git.commit",
|
|
18190
18573
|
"git.push",
|
|
18191
18574
|
"hosting.pull_request",
|
|
18575
|
+
"hosting.merge",
|
|
18192
18576
|
"youtrack.update",
|
|
18193
18577
|
"youtrack.time",
|
|
18194
18578
|
"youtrack.meeting",
|
|
@@ -18218,7 +18602,7 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
18218
18602
|
return output(failure("permission_denied", "Pi project is not trusted for external actions"));
|
|
18219
18603
|
if (context.workerId !== null)
|
|
18220
18604
|
return output(failure("permission_denied", "supervised Pi children cannot run external actions"));
|
|
18221
|
-
if (!ctx.hasUI)
|
|
18605
|
+
if (!ctx.hasUI && !standingAutoApplies(new TaskStore(ctx.cwd), "pi", context.caller.actor, externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload)))
|
|
18222
18606
|
return output(failure("needs_input", "interactive confirmation UI is unavailable", {
|
|
18223
18607
|
capability: "interactive_decision"
|
|
18224
18608
|
}));
|
|
@@ -18234,6 +18618,7 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
18234
18618
|
"git.commit",
|
|
18235
18619
|
"git.push",
|
|
18236
18620
|
"hosting.pull_request",
|
|
18621
|
+
"hosting.merge",
|
|
18237
18622
|
"changelog.apply"
|
|
18238
18623
|
].includes(resolved.data.request.operation);
|
|
18239
18624
|
if (localOperation) {
|
|
@@ -18295,6 +18680,10 @@ var registerWorkitTools = (pi, options = {}) => {
|
|
|
18295
18680
|
const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
|
|
18296
18681
|
return output(result);
|
|
18297
18682
|
}
|
|
18683
|
+
if (standingAutoApplies(store, "pi", actor, descriptor)) {
|
|
18684
|
+
const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
|
|
18685
|
+
return output(result);
|
|
18686
|
+
}
|
|
18298
18687
|
const prior = externalActionState(store, "pi", actor, descriptor);
|
|
18299
18688
|
if (prior.ok && prior.data.entry.data.consumption !== null)
|
|
18300
18689
|
return output(failure("permission_denied", "external action was already settled"));
|