@brainervirus/workit-pi 1.0.12 → 1.1.1

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.
Files changed (3) hide show
  1. package/dist/worker.js +19 -4
  2. package/dist/workit.js +2403 -2015
  3. 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 { spawnSync } from "node:child_process";
9984
- import path6 from "node:path";
9996
+ import path9 from "node:path";
9985
9997
 
9986
- // packages/workit-core/src/core/authority.ts
9998
+ // packages/workit-core/src/core/auto-approval.ts
9987
9999
  import { createHash as createHash3 } from "node:crypto";
9988
- import * as fs5 from "node:fs";
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
- var verifiedAuthorities = new WeakMap;
9991
- var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9992
- const token = {};
9993
- verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
9994
- return token;
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 chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
9997
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9998
- var normalizeChainSteps = (steps) => {
9999
- if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
10000
- return null;
10001
- const normalized = [];
10002
- for (const step of steps) {
10003
- if (typeof step === "string") {
10004
- if (!step.trim())
10005
- return null;
10006
- normalized.push({ kind: "commit", message: step });
10007
- continue;
10008
- }
10009
- if (!isRecord(step))
10010
- return null;
10011
- if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
10012
- normalized.push({ kind: "branch", target: step.branch });
10013
- continue;
10014
- }
10015
- if (step.pr === true && Object.keys(step).length === 1) {
10016
- normalized.push({ kind: "pr" });
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
- const keys = normalized.map(chainStepKey);
10022
- if (new Set(keys).size !== keys.length)
10023
- return null;
10024
- return normalized;
10099
+ if (!migrationFailed)
10100
+ migratedDir = dir;
10101
+ return dir;
10025
10102
  };
10026
- var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
10027
- var parseSteps = (content) => {
10028
- let value;
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
- value = JSON.parse(content);
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
- const normalized = normalizeChainSteps(steps);
10040
- return normalized;
10121
+ }
10041
10122
  };
10042
- var decisionMatches = (entry, purpose, binding) => {
10043
- const decision = entry.data;
10044
- 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);
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 sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
10047
- var validProvenance = (value, caller) => {
10048
- if (!provenanceSchema.safeParse(value).success)
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 verifyNativeDecision = (verifier, input, binding) => {
10054
- if (!verifier)
10055
- return failure("permission_denied", "native authority verifier is unavailable");
10056
- const result = verifier.verifyDecision(input);
10057
- if (!result.ok || !validProvenance(result.data, input.caller))
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 verifyNativeAction = (verifier, input, binding) => {
10071
- if (!verifier)
10072
- return failure("permission_denied", "native authority verifier is unavailable");
10073
- const result = verifier.verifyAction(input);
10074
- if (!result.ok || !validProvenance(result.data, input.caller))
10075
- return failure("permission_denied", "native action observation was not attested");
10076
- return success(null, null, trustedAuthority("action", result.data, {
10077
- taskId: input.expected.taskId,
10078
- workspaceId: input.expected.workspaceId,
10079
- decisionId: input.expected.decisionId,
10080
- actionRef: input.expected.actionRef,
10081
- taskRevision: input.expected.taskRevision,
10082
- workspaceRevision: input.expected.workspaceRevision,
10083
- outcome: input.expected.outcome,
10084
- purpose: input.expected.decision.purpose,
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 verifyNativeReconciliation = (verifier, input, binding) => {
10092
- if (!verifier?.verifyReconciliation)
10093
- return failure("permission_denied", "native reconciliation authority is unavailable");
10094
- const result = verifier.verifyReconciliation(input);
10095
- if (!result.ok || !validProvenance(result.data, input.caller))
10096
- return failure("permission_denied", "native reconciliation observation was not attested");
10097
- return success(null, null, trustedAuthority("reconciliation", result.data, {
10098
- taskId: input.expected.taskId,
10099
- workspaceId: input.expected.workspaceId,
10100
- decisionId: input.expected.decisionId,
10101
- actionRef: input.expected.actionRef,
10102
- taskRevision: input.expected.taskRevision,
10103
- workspaceRevision: input.expected.workspaceRevision,
10104
- outcome: input.expected.outcome,
10105
- evidenceDigest: input.expected.evidenceDigest,
10106
- step: input.expected.step,
10107
- purpose: input.expected.decision.purpose,
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
- var takeAuthority = (authority, binding, caller) => {
10115
- const record = verifiedAuthorities.get(authority);
10116
- verifiedAuthorities.delete(authority);
10117
- if (!record)
10118
- return null;
10119
- if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
10120
- return null;
10121
- return record;
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 retireNativeAuthority = (authority) => {
10124
- const record = verifiedAuthorities.get(authority);
10125
- verifiedAuthorities.delete(authority);
10126
- return record?.provenance ?? null;
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 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;
10129
- 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);
10130
- var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
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
- root = fs5.realpathSync(checkoutRoot);
10266
+ return real(p);
10141
10267
  } catch {
10142
- return failure("permission_denied", "checkout root is unavailable");
10268
+ return p;
10143
10269
  }
10144
- for (const reference of binding.contentRefs) {
10145
- if (reference.kind !== "file")
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
- if (!reference.digest)
10148
- return failure("invalid_input", "document references require a byte digest");
10149
- const target = path5.resolve(root, reference.path);
10150
- if (target !== root && !target.startsWith(`${root}${path5.sep}`))
10151
- return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
10152
- fields: [
10153
- { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
10154
- ]
10155
- });
10156
- try {
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 success(null, null, null);
10297
+ return null;
10188
10298
  };
10189
- var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
10190
- 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);
10191
- var validateAction = (store, task, workspaceId, input, authority) => {
10192
- if (task.status !== "active")
10193
- return failure("invalid_transition", "only active tasks can authorize actions");
10194
- if (!refSchema.safeParse(input.actionRef).success)
10195
- return failure("invalid_input", "action reference is invalid");
10196
- const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
10197
- if (!entry)
10198
- return failure("not_found", "decision not found");
10199
- const decision = entry.data;
10200
- if (decision.purpose !== "action")
10201
- return failure("permission_denied", "decision is not an action approval");
10202
- if (!authorityMatches(authority, {
10203
- taskId: task.id,
10204
- workspaceId,
10205
- decisionId: input.decisionId,
10206
- actionRef: input.actionRef,
10207
- taskRevision: input.expectedRevision,
10208
- workspaceRevision: input.expectedWorkspaceRevision,
10209
- outcome: "reserve"
10210
- }))
10211
- return failure("permission_denied", "native reservation authority is not bound");
10212
- if (!authorityDecisionMatches(authority, decision))
10213
- return failure("permission_denied", "native reservation authority does not match decision");
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
- function reserveAction(input) {
10241
- if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
10242
- return failure("invalid_input", "action reservation preconditions are required");
10243
- const authority = takeAuthority(input.authority, {
10244
- owner: input.authorityOwner,
10245
- store: input.store,
10246
- root: input.store.root
10247
- }, input.authorityCaller);
10248
- if (!authority)
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 candidate = { ...partial, id: candidateDigest(partial) };
10718
- const parsed = candidateSchema.safeParse(candidate);
10719
- return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
10720
- }
10721
- var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
10722
- var changedPaths = (before, after) => {
10723
- const paths = new Set([...before.files, ...after.files].map((file) => file.path));
10724
- const left = fileMap(before);
10725
- const right = fileMap(after);
10726
- const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
10727
- if (before.head !== after.head)
10728
- changed.push(".");
10729
- if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
10730
- changed.push(".");
10731
- return changed;
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
- if (!evidence.candidateId || !current)
10768
- return {
10769
- evidenceId: entry.id,
10770
- status: evidence.result,
10771
- reason: "evidence has no candidate binding"
10772
- };
10773
- const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
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
- beforeCandidateId: evidence.beforeCandidateId ?? fallback,
10802
- candidateId: evidence.candidateId ?? fallback
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
- var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
10806
- 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")));
10807
- 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 }));
10808
- var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
10809
- if (kind !== "passed")
10810
- return false;
10811
- if (dimension === "testing" || dimension === "verification")
10812
- return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
10813
- if (dimension === "review")
10814
- return evidenceKind === "review";
10815
- if (dimension === "investigation" || dimension === "challenge")
10816
- return evidenceKind === "investigation";
10817
- if (dimension === "artifacts" || dimension === "continuity")
10818
- return evidenceKind === "artifact";
10819
- return false;
10820
- };
10821
- var expectedKinds = (dimension, ruleId) => {
10822
- if (dimension === "testing")
10823
- return "check";
10824
- if (dimension === "verification")
10825
- return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
10826
- if (dimension === "review")
10827
- return "review";
10828
- if (dimension === "investigation" || dimension === "challenge")
10829
- return "investigation";
10830
- if (dimension === "artifacts" || dimension === "continuity")
10831
- return "artifact";
10832
- if (dimension === "decisions")
10833
- return "decision";
10834
- return "worker report";
10835
- };
10836
- function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
10837
- const evidence = evaluateEvidence(task, candidate);
10838
- if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
10839
- return task.policy.requirements.map((requirement) => ({
10840
- requirementId: requirement.id,
10841
- status: "unsatisfied",
10842
- evidenceIds: [],
10843
- decisionIds: [],
10844
- reason: "stored policy version is unsupported; reassessment is required"
10845
- }));
10846
- return (task.policy?.requirements ?? []).map((requirement) => {
10847
- const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
10848
- const passed = related.filter(({ entry, evaluation }) => {
10849
- if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
10850
- return false;
10851
- if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
10852
- return false;
10853
- if (requirement.dimension !== "review")
10854
- return true;
10855
- const reviewSession = sessionFromRef(entry.data.reviewContext);
10856
- const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10857
- 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));
10858
- return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10859
- });
10860
- if (passed.length) {
10861
- const currentId = candidate?.id ?? null;
10862
- 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;
10863
- if (withRed.length)
10864
- return {
10865
- requirementId: requirement.id,
10866
- status: "satisfied",
10867
- evidenceIds: withRed.map(({ entry }) => entry.id),
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
- requirementId: requirement.id,
10921
- status: unavailable ? "unavailable" : "unsatisfied",
10922
- evidenceIds: related.map(({ entry }) => entry.id),
10923
- decisionIds: [],
10924
- reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
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 evaluateClosure(requestedOutcome, view) {
10929
- if (requestedOutcome !== "stopped" && !view.task.policy)
10930
- return failure("requirements_unsatisfied", "task has not been assessed");
10931
- if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
10932
- return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
10933
- const evaluations = view.requirements;
10934
- const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
10935
- if (requestedOutcome !== "stopped" && openFindings.length)
10936
- return failure("requirements_unsatisfied", "open findings must be resolved before closure");
10937
- if (requestedOutcome !== "stopped") {
10938
- const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
10939
- for (const entry of view.task.findings) {
10940
- if (entry.data.disposition === "fixed") {
10941
- const verified = entry.data.resolution?.evidenceIds.some((id) => {
10942
- const evidence = view.task.evidence.find((item) => item.id === id);
10943
- if (!evidence)
10944
- return false;
10945
- return findingVerificationPasses(entry.data.candidateId, {
10946
- kind: evidence.data.kind,
10947
- candidateId: evidence.data.candidateId,
10948
- status: evidenceById.get(id) ?? "missing"
10949
- });
10950
- });
10951
- if (!verified)
10952
- return failure("requirements_unsatisfied", "fixed findings require current verification");
10953
- }
10954
- if (entry.data.disposition === "deferred") {
10955
- const valid = entry.data.resolution?.decisionIds.some((id) => {
10956
- const decisionEntry = view.task.decisions.find((item) => item.id === id);
10957
- const decision = decisionEntry?.data;
10958
- 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)));
10959
- });
10960
- if (!valid)
10961
- return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
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
- const blocking = evaluations.filter((item) => {
10966
- if (item.status !== "unsatisfied" && item.status !== "unavailable")
10967
- return false;
10968
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10969
- return !requirement || requirement.before === "close";
10970
- });
10971
- if (requestedOutcome !== "stopped" && blocking.length)
10972
- return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10973
- requirementIds: blocking.map((item) => item.requirementId),
10974
- requirements: blocking.map((item) => {
10975
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
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/methods.ts
11011
- var METHODS = {
11012
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
11013
- "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
11014
- "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
11015
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
11016
- "workit-implement": { dimensions: ["delegation"] },
11017
- "workit-debug": {},
11018
- "workit-handoff": {},
11019
- "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
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 methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
11022
- var assuranceFor = (id, definition, capabilities) => {
11023
- const keys = new Set([
11024
- id,
11025
- ...definition.dimensions ?? [],
11026
- ...definition.ruleIds ?? []
11027
- ]);
11028
- const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
11029
- if (id === "workit-review" && matches.length === 0)
11030
- return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
11031
- if (matches.some((capability) => capability.assurance === "unavailable"))
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
- assurance: "unavailable",
11034
- reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
10616
+ ok: false,
10617
+ error: error instanceof Error ? error.message : "ensure-base-branch failed"
11035
10618
  };
11036
- if (matches.some((capability) => capability.assurance === "enforced"))
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
- function selectMethods(policy, capabilities) {
11041
- return Object.entries(METHODS).flatMap(([id, definition]) => {
11042
- const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
11043
- if (requirements.length === 0)
11044
- return [];
11045
- const capability = assuranceFor(id, definition, capabilities);
11046
- const reasons = [
11047
- ...new Set(requirements.map((requirement) => requirement.reason)),
11048
- ...capability.reasons
11049
- ];
11050
- if (capability.assurance === "unavailable")
11051
- reasons.unshift("required capability is unavailable");
11052
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
11053
- });
11054
- }
11055
- var invariantBootstrap = () => `
11056
- Workit keeps one accountable lead and one shared task state. Inspect current task
11057
- state before acting. An empty task.list means no session yet, not permission to
11058
- skip Workit; for user-requested product, debug, or behavior work with no active
11059
- or paused task, run task.start then policy.assess before other product mutations.
11060
- Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
11061
- to the current records; explicit values are still concurrency-checked, so never
11062
- copy revisions between calls. Happy path: task.list, task.start {intent} with no
11063
- revisions, policy.assess {assessment: {facts, signals, consequences,
11064
- verification}} where facts are inferred or observed with file refs,
11065
- writer.acquire {taskId} before product writes, evidence.record {evidence} where
11066
- check and review kinds auto-bind the current tree and need no digests. Record
11067
- self-review requirements with kind review and reviewContext matching your own
11068
- host session; the lead may also record checks. Fresh-context review requires a
11069
- session distinct from the task creator and every other evidence recorder. Record
11070
- verification evidence last: later evidence or commits can reopen fixed findings
11071
- for re-verification, so verify, resolve, then close with nothing in between.
11072
- decision.record {binding with taskId/workspaceId from inspect plus presented and
11073
- approvedContent} only through a native approval question asked receipt-shaped:
11074
- header Workit decision: <purpose>, the same label repeated in the question
11075
- text (some host UIs do not render headers), exactly two options approved and
11076
- rejected; ask once, never re-ask to mint a receipt. Before asking, present the
11077
- item in the conversation (durable artifacts: complete digest plus exact path;
11078
- inline plans: the content) and keep the question to one short scoped sentence;
11079
- never ask to approve something unseen. An answer with custom text is steering,
11080
- not approval: adjust, re-present, then ask a new scoped question. After the user
11081
- approves a spec or plan, execute the remaining tasks continuously with one
11082
- atomic commit per task and do not ask whether to continue; stop only for a new
11083
- product decision, a failed safety or verification gate, a conflicting edit, or
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
- return success(null, null, {
11126
- candidate: candidate.data,
11127
- staleEvidenceIds,
11128
- workerUpdates: [],
11129
- blockers,
11130
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
11131
- });
11132
- }
11133
- var COMPACT_MAX_BYTES = 4096;
11134
- var COMPACT_MAX_ITEMS = 8;
11135
- var COMPACT_TEXT_BYTES = 128;
11136
- var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
11137
- var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
11138
- var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
11139
- var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
11140
- const separator = match.search(/[:=]/u);
11141
- if (separator >= 0)
11142
- return `${match.slice(0, separator)}=<redacted>`;
11143
- const space = match.search(/\s/u);
11144
- return `${match.slice(0, space)} <redacted>`;
11145
- }).slice(0, limit);
11146
- var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
11147
- var compactReference = (ref) => {
11148
- if (ref.kind === "file")
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
- kind: "file",
11151
- path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
10745
+ error: "target_branch (the working branch to create or switch to) is required for setup",
10746
+ phase: "preflight"
11152
10747
  };
11153
- if (ref.kind === "record")
10748
+ if (isProtected(cwd, target))
10749
+ return { error: `protected branch ${target}`, phase: "preflight" };
10750
+ if (!allowedBranch(cwd, target))
11154
10751
  return {
11155
- kind: "record",
11156
- collection: ref.collection,
11157
- id: ref.id
10752
+ error: `target branch ${target} is not allowed by the branch policy`,
10753
+ phase: "preflight"
11158
10754
  };
11159
- return { kind: ref.kind };
11160
- };
11161
- function compactTaskContext(view) {
11162
- 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) => ({
11163
- id: entry.id,
11164
- purpose: entry.data.purpose,
11165
- status: entry.data.response,
11166
- digest: entry.data.digest,
11167
- references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
11168
- }));
11169
- const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
11170
- id: method.id,
11171
- assurance: method.assurance,
11172
- reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
11173
- })) : [];
11174
- const gaps = Array.from(new Set([
11175
- ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
11176
- ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
11177
- ...view.task.progress.blockers.map((entry) => entry.reason)
11178
- ])).sort().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
11179
- const context = {
11180
- objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
11181
- status: view.task.status,
11182
- decisions,
11183
- methods,
11184
- gaps,
11185
- nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
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
- let encoded = JSON.stringify(context);
11188
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
11189
- if (context.gaps.length)
11190
- context.gaps.pop();
11191
- else if (context.decisions.length)
11192
- context.decisions.pop();
11193
- else
11194
- context.methods.pop();
11195
- encoded = JSON.stringify(context);
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
- if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
11198
- context.objective = compactText(context.objective, 64) ?? "";
11199
- context.nextAction = compactText(context.nextAction, 64);
11200
- encoded = JSON.stringify(context);
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 encoded;
11203
- }
11204
- var exportDigest = (bundle) => sha256({
11205
- schemaVersion: bundle.schemaVersion,
11206
- exportedAt: bundle.exportedAt,
11207
- sourceWorkspaceId: bundle.sourceWorkspaceId,
11208
- task: bundle.task
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
- var segmentTokens = (segment) => {
11220
- if (segment.includes("`") || segment.includes("$("))
11221
- return null;
11222
- if (unbalancedQuotes(segment))
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
- const parts = segment.trim().split(/\s+/u);
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 unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
11230
- function shellRouteIntent(command) {
11231
- if (typeof command !== "string" || command.trim() === "")
11232
- return null;
11233
- for (const raw of command.split(/&&|\|\||;|\n/u)) {
11234
- const parts = segmentTokens(raw);
11235
- if (!parts)
11236
- continue;
11237
- const head = unquote(parts[0]);
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 null;
11250
- }
11251
- function hasLiveWorkitTask(dir) {
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
- if (typeof dir !== "string" || dir === "")
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
- return false;
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
- function shouldDenyShellRoute(dir, command) {
11264
- if (!hasLiveWorkitTask(dir))
11265
- return null;
11266
- return shellRouteIntent(command);
11267
- }
11268
- // packages/workit-core/src/core/branch.ts
11269
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
11270
- import { execFileSync as execFileSync2 } from "node:child_process";
11271
- import path10 from "node:path";
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
- const stdout = execFileSync("git", args, {
11278
- cwd,
11279
- encoding: "utf8",
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
- var gitContext = (workspaceRoot, paths = []) => {
11293
- const cwd = workspaceRoot;
11294
- let failure;
11295
- const runHere = (args) => {
11296
- const result = run(cwd, args);
11297
- if (!failure && result.exitCode !== 0) {
11298
- failure = { stderr: result.stderr, exitCode: result.exitCode };
11299
- }
11300
- return result.stdout;
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
- const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
11303
- const status_short = runHere(["status", "--porcelain"]);
11304
- const staged = [];
11305
- const unstaged = [];
11306
- const untracked = [];
11307
- for (const line of status_short.split(`
11308
- `).filter(Boolean)) {
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 pathArgs = paths.length ? ["--", ...paths] : [];
11321
- const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
11322
- const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
11323
- const stagedSet = new Set(staged);
11324
- const unstagedSet = new Set(unstaged);
11325
- const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
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
- workspace_root: cwd,
11328
- branch,
11329
- status_short,
11330
- staged,
11331
- unstaged,
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/config.ts
11342
- import {
11343
- copyFileSync,
11344
- cpSync,
11345
- existsSync as existsSync4,
11346
- mkdirSync as mkdirSync2,
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 errorDetail = (err) => {
11369
- if (err instanceof Error) {
11370
- return { error_name: err.name, error: err.message };
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
- return { error_name: "unknown", error: String(err) };
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
- // packages/workit-core/src/core/config.ts
11376
- var diagnosticLogger;
11377
- var PRESETS = {
11378
- gitflow: {
11379
- allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
11380
- protected: ["main", "develop", "master", "prod", "production"]
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 mergePreset = (preset, input = {}, current = {
11387
- branchPolicy: { preset, allowed: [], protected: [] }
11388
- }) => {
11389
- const defs = PRESETS[preset];
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 resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
11397
- var migratedDir = null;
11398
- var migrationFailed = false;
11399
- var ensureConfigDir = (dir = resolveConfigDir()) => {
11400
- if (migratedDir === dir)
11401
- return dir;
11402
- if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
11403
- migratedDir = dir;
11404
- return dir;
11405
- }
11406
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
11407
- if (!existsSync4(legacy)) {
11408
- migratedDir = dir;
11409
- return dir;
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
- migrationFailed = false;
11416
- mkdirSync2(dir, { recursive: true });
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
- if (entry.isDirectory())
11425
- cpSync(src, dest, { recursive: true });
11426
- else if (entry.isFile())
11427
- copyFileSync(src, dest);
11428
- } catch (err) {
11429
- migrationFailed = true;
11430
- diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
11431
- console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
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
- if (!migrationFailed)
11435
- migratedDir = dir;
11436
- return dir;
11437
- };
11438
- var configDir = () => ensureConfigDir();
11439
- var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
11440
- var DEFAULTS = {
11441
- locale: "en",
11442
- localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
11443
- timezone: "America/Santiago",
11444
- branchPolicy: {
11445
- preset: "gitflow",
11446
- allowed: [...PRESETS.gitflow.allowed],
11447
- protected: [...PRESETS.gitflow.protected]
11448
- },
11449
- commitPolicy: { preset: "conventional" }
11450
- };
11451
- var readSafe = (p) => {
11452
- try {
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
- var readConfigTyped = (dir) => {
11494
- const file = path7.join(dir ?? configDir(), "config.json");
11495
- return parseConfigResult(readSafe(file), file);
11496
- };
11497
- var readConfig = () => {
11498
- const result = readConfigTyped();
11499
- if (result.status === "malformed")
11500
- throw new Error(result.error);
11501
- return result.config ?? DEFAULTS;
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 resolveBranchPolicy = (config, workspace) => {
11504
- const wp = workspace?.branchPolicy ?? {};
11505
- const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
11506
- const merged = mergePreset(preset, {
11507
- allowed: wp.allowed,
11508
- protectedNames: wp.protected
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
- preset,
11513
- allowed,
11514
- protected: new Set(merged.protected.map((p) => p.toLowerCase())),
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 COMMIT_PRESETS = [
11520
- "conventional",
11521
- "gitmoji",
11522
- "ticket-prefix",
11523
- "freeform",
11524
- "custom",
11525
- "auto"
11526
- ];
11527
- var resolveCommitPolicy = (config, workspace) => {
11528
- const wp = workspace?.commitPolicy ?? {};
11529
- if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
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
- preset: wp.preset,
11532
- ...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
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
- return config.commitPolicy ?? { preset: "conventional" };
11536
- };
11537
-
11538
- // packages/workit-core/src/core/workspaces.ts
11539
- import { readFileSync as readFileSync5, realpathSync as realpathSync4 } from "node:fs";
11540
- import path8 from "node:path";
11541
- var readWorkspacesResult = (dir = configDir()) => {
11542
- const file = path8.join(dir, "workspaces.json");
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
- let parsed;
11674
+ return { paths: [...new Set(paths)], uncertain: false, git: true };
11675
+ };
11676
+ var walk = (root, directory, output) => {
11677
+ let names;
11550
11678
  try {
11551
- parsed = JSON.parse(raw);
11679
+ names = fs7.readdirSync(directory);
11552
11680
  } catch {
11553
- return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
11681
+ return true;
11554
11682
  }
11555
- if (!isConfigObject(parsed)) {
11556
- return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
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
- const list = parsed.workspaces;
11559
- return {
11560
- status: "valid",
11561
- path: file,
11562
- entries: Array.isArray(list) ? list : []
11563
- };
11700
+ return uncertain;
11564
11701
  };
11565
- var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
11566
- var globToRegExp = (glob) => {
11567
- let out = "";
11568
- for (let i = 0;i < glob.length; i++) {
11569
- const c = glob[i];
11570
- if (c === "*") {
11571
- if (glob[i + 1] === "*") {
11572
- if (glob[i + 2] === "/") {
11573
- if (out === "")
11574
- out += "/?";
11575
- out += "(?:[^/]+/)*";
11576
- i += 2;
11577
- } else {
11578
- if (i + 2 >= glob.length) {
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 new RegExp(`^${out}$`);
11719
+ return success(null, null, roots);
11598
11720
  };
11599
- var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
11600
- var realpathOf = (p) => {
11601
- const real = realpathSync4.native ?? realpathSync4;
11721
+ function captureCandidate(root, scope, environment = []) {
11722
+ let checkout;
11602
11723
  try {
11603
- return real(p);
11724
+ checkout = fs7.realpathSync(root);
11604
11725
  } catch {
11605
- return p;
11726
+ return failure("invalid_input", "candidate root does not exist");
11606
11727
  }
11607
- };
11608
- var canonicalGlob = (glob) => {
11609
- const m = /[*?[\]{]/.exec(glob);
11610
- const prefix = m ? glob.slice(0, m.index) : glob;
11611
- const rest = m ? glob.slice(m.index) : "";
11612
- if (!prefix)
11613
- return glob;
11614
- const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
11615
- return real + (prefix.endsWith("/") ? "/" : "") + rest;
11616
- };
11617
- var resolveWorkspaceFrom = (cwd, dir) => {
11618
- const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
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
- const ws = entry;
11623
- if (typeof ws.glob !== "string" || !ws.glob)
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
- return null;
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 resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
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
- const url = (r.stdout ?? "").trim();
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 readVcsConfig = () => {
11657
- const file = vcsConfigPath();
11658
- let raw;
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
- function vcsConfig(mode, cwd) {
11676
- const ws = resolveWorkspace(vcsCwd(cwd));
11677
- const wsVcs = ws?.vcs ?? {};
11678
- const wsYt = ws?.youtrack ?? {};
11679
- const wsIssues = ws?.issues ?? {};
11680
- const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
11681
- const root = vcsCwd(cwd);
11682
- const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
11683
- const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
11684
- const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
11685
- const wp = ws?.branchPolicy ?? {};
11686
- const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
11687
- const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
11688
- const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
11689
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
11690
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
11691
- let issuesProvider = null;
11692
- let linkOnPr = null;
11693
- if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
11694
- issuesProvider = "github";
11695
- linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
11696
- }
11697
- if (mode === "resolve") {
11698
- if (cfgStatus === "malformed")
11699
- return { ok: false, error: cfgError, configPath: cfgPath };
11700
- return {
11701
- ok: true,
11702
- workspace_name: ws?.name ?? null,
11703
- provider,
11704
- defaultTargetBranch: defaultTarget,
11705
- link_issues: linkIssues,
11706
- youtrack_base_url: youtrackBaseUrl,
11707
- issues_provider: issuesProvider,
11708
- link_on_pr: linkOnPr
11709
- };
11710
- }
11711
- if (cfgStatus === "malformed") {
11712
- return { ok: false, error: cfgError, configPath: cfgPath };
11713
- }
11714
- if (cfgStatus === "missing") {
11715
- return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
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
- ok: false,
11720
- error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
11721
- configPath: cfgPath
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
- // packages/workit-core/src/core/branch.ts
11825
- var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
11826
- var resolveCommitPolicyFor = (workspaceRoot) => resolveCommitPolicy(readConfig(), resolveWorkspace(workspaceRoot));
11827
- var policy = (root) => resolveBranchPolicyFor(root);
11828
- var allowedBranch = (root, name) => policy(root).allowed.some((r) => r.test(name));
11829
- var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
11830
- var baseBranch = (cwd) => {
11831
- const resolved = vcsConfig("resolve", cwd);
11832
- if (resolved.ok === false)
11833
- return { error: String(resolved.error) };
11834
- return { base: String(resolved.defaultTargetBranch ?? "develop") };
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 originBaseReady = (cwd, base) => {
11837
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
11838
- try {
11839
- try {
11840
- run(["fetch", "origin", base, "--prune"]);
11841
- } catch {
11842
- run(["fetch", "origin", "--prune"]);
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
- try {
11845
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
11846
- cwd,
11847
- stdio: "pipe"
11848
- });
11849
- } catch {
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
- ok: false,
11852
- error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`
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
- ok: false,
11859
- error: error instanceof Error ? error.message : "ensure-base-branch failed"
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 fastForwardBase = (cwd, base) => {
11864
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
11865
- try {
11866
- let hasLocalBase = true;
11867
- try {
11868
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
11869
- cwd,
11870
- stdio: "pipe"
11871
- });
11872
- } catch {
11873
- hasLocalBase = false;
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
- ok: false,
11887
- error: error instanceof Error ? error.message : "ensure-base-branch failed"
12125
+ assurance: "unavailable",
12126
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
11888
12127
  };
11889
- }
11890
- };
11891
- var ensureBaseBranch = (cwd, base) => {
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
- var classifyBranchDirt = (workspaceRoot) => {
11901
- let raw;
11902
- try {
11903
- raw = execFileSync2("git", ["status", "--porcelain=v1"], {
11904
- cwd: path10.resolve(workspaceRoot),
11905
- encoding: "utf8",
11906
- stdio: ["pipe", "pipe", "pipe"]
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
- } catch {
11909
- return "clean";
11910
- }
11911
- let dirty = false;
11912
- for (const line of raw.split(`
11913
- `)) {
11914
- if (!line.trim())
11915
- continue;
11916
- dirty = true;
11917
- const code = line.slice(0, 2);
11918
- if (code === "??")
11919
- continue;
11920
- let entry = line.slice(3);
11921
- const arrow = entry.indexOf(" -> ");
11922
- if (arrow >= 0)
11923
- entry = entry.slice(arrow + 4);
11924
- const unquoted = entry.startsWith('"') && entry.endsWith('"') ? entry.slice(1, -1) : entry;
11925
- if (unquoted !== "docs" && !unquoted.startsWith("docs/"))
11926
- return "stash-required";
11927
- }
11928
- return dirty ? "carry" : "clean";
11929
- };
11930
- var branchSetup = ({
11931
- action,
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
- error: "target_branch (the working branch to create or switch to) is required for setup",
11988
- phase: "preflight"
12242
+ kind: "file",
12243
+ path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
11989
12244
  };
11990
- if (isProtected(cwd, target))
11991
- return { error: `protected branch ${target}`, phase: "preflight" };
11992
- if (!allowedBranch(cwd, target))
12245
+ if (ref.kind === "record")
11993
12246
  return {
11994
- error: `target branch ${target} is not allowed by the branch policy`,
11995
- phase: "preflight"
12247
+ kind: "record",
12248
+ collection: ref.collection,
12249
+ id: ref.id
11996
12250
  };
11997
- let base;
11998
- let targetExists = true;
11999
- try {
12000
- exec(["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]);
12001
- } catch {
12002
- targetExists = false;
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 (!targetExists) {
12005
- const baseResolved = baseBranch(cwd);
12006
- if ("error" in baseResolved)
12007
- return { error: baseResolved.error, phase: "preflight" };
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
- journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
12011
- let stash_ref;
12012
- const failAfterStash = (message, phase = "post_mutation") => {
12013
- let suffix = "";
12014
- if (stash_ref) {
12015
- journal(`pre-pop: ${stash_ref}`);
12016
- try {
12017
- exec(["stash", "pop", stash_ref]);
12018
- stash_ref = undefined;
12019
- journal("pop: ok");
12020
- } catch {
12021
- journal("pop: failed");
12022
- suffix = " (changes preserved in stash)";
12023
- }
12024
- }
12025
- return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
12026
- };
12027
- if (current !== target) {
12028
- const dirty = Boolean(gitContext(cwd).status_short.trim());
12029
- if (classifyBranchDirt(cwd) === "stash-required" && stash !== "yes") {
12030
- return {
12031
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
12032
- phase: "preflight"
12033
- };
12034
- }
12035
- let validatedBase;
12036
- if (!targetExists && base !== undefined) {
12037
- const ready = originBaseReady(cwd, base);
12038
- if (!ready.ok)
12039
- return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
12040
- validatedBase = base;
12041
- }
12042
- if (dirty && stash === "yes") {
12043
- try {
12044
- exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
12045
- } catch (error) {
12046
- return {
12047
- error: error instanceof Error ? error.message : "stash push failed",
12048
- phase: "post_mutation"
12049
- };
12050
- }
12051
- stash_ref = "stash@{0}";
12052
- journal(`stash push: ${stash_ref}`);
12053
- }
12054
- try {
12055
- exec(["checkout", target]);
12056
- journal("post-checkout");
12057
- } catch (error) {
12058
- const message = error instanceof Error ? error.message : "checkout failed";
12059
- if (/worktree/i.test(message)) {
12060
- return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
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
- const manifest = readManifest();
12082
- manifest.branch = target;
12083
- manifest.previous_branch = current;
12084
- if (stash_ref) {
12085
- manifest.stash_ref = stash_ref;
12086
- manifest.stash_created_at = new Date().toISOString();
12087
- }
12088
- writeManifest(manifest);
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
- return {
12103
- action: "setup",
12104
- ok: true,
12105
- branch: target,
12106
- previous_branch: current,
12107
- stash_ref: stash_ref ?? null,
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
- recordDecision(request, nativeObservation, nativeRequired = false) {
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 native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
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,57 @@ 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
+ cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GH_TOKEN: token };
15640
+ }
15641
+ const result = spawnSync4(cmd[0], cmd.slice(1), { cwd: root, encoding: "utf8", env: cmdEnv });
15642
+ if (result.status !== 0)
15643
+ return {
15644
+ error: "merge failed",
15645
+ provider,
15646
+ stderr: (result.stderr ?? result.stdout ?? "").trim().slice(0, 800)
15647
+ };
15648
+ return {
15649
+ ok: true,
15650
+ provider,
15651
+ targetBranch: opts.target,
15652
+ sourceBranch: opts.source,
15653
+ output: (result.stdout ?? "").trim()
15654
+ };
15655
+ }
15340
15656
 
15341
15657
  // packages/workit-core/src/core/repo-context.ts
15342
15658
  import { spawnSync as spawnSync5 } from "node:child_process";
15343
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
15659
+ import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
15344
15660
  import path13 from "node:path";
15345
15661
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
15346
15662
  try {
@@ -15510,7 +15826,7 @@ var packageScripts = (cwd, keys) => {
15510
15826
  if (!existsSync7(pkgPath))
15511
15827
  return [];
15512
15828
  try {
15513
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
15829
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
15514
15830
  return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
15515
15831
  } catch {
15516
15832
  return [];
@@ -15544,7 +15860,7 @@ var documentationFiles = (cwd) => {
15544
15860
  var readTrimmed = (file, maxLines) => {
15545
15861
  if (!existsSync7(file))
15546
15862
  return "";
15547
- const lines = readFileSync7(file, "utf8").split(`
15863
+ const lines = readFileSync8(file, "utf8").split(`
15548
15864
  `);
15549
15865
  return lines.slice(0, maxLines).join(`
15550
15866
  `);
@@ -15732,7 +16048,7 @@ function docsRefreshContext(root, range) {
15732
16048
  const pkgPath = path13.join(cwd, "package.json");
15733
16049
  if (existsSync7(pkgPath)) {
15734
16050
  try {
15735
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
16051
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
15736
16052
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
15737
16053
  `;
15738
16054
  } catch {}
@@ -16633,7 +16949,7 @@ import path16 from "node:path";
16633
16949
  // packages/workit-core/src/core/changelog.ts
16634
16950
  import fs11 from "node:fs";
16635
16951
  import path15 from "node:path";
16636
- import { createHash as createHash5 } from "node:crypto";
16952
+ import { createHash as createHash6 } from "node:crypto";
16637
16953
  var CATEGORIES = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
16638
16954
  var CAT_RE = /^###\s+(?:Added|Changed|Deprecated|Removed|Fixed|Security)\s*$/i;
16639
16955
  var UNRELEASED_RE = /^##\s+\[Unreleased\]\s*$/i;
@@ -16650,7 +16966,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16650
16966
  ## [Unreleased]
16651
16967
 
16652
16968
  `;
16653
- var digestBytes4 = (value) => createHash5("sha256").update(value).digest("hex");
16969
+ var digestBytes4 = (value) => createHash6("sha256").update(value).digest("hex");
16654
16970
  var readChangelogSource = (target) => {
16655
16971
  if (!fs11.existsSync(target)) {
16656
16972
  const bytes = Buffer.from(SKELETON, "utf8");
@@ -17262,6 +17578,11 @@ var actionProposalQuestion = (request, descriptorPayload) => {
17262
17578
  presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
17263
17579
  approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
17264
17580
  };
17581
+ case "hosting.merge":
17582
+ return {
17583
+ presented: `Workit decision: action — Merge \`${String(resolved.source_branch ?? "")}\` into \`${payloadTarget}\` (squash, delete branch)?`,
17584
+ approvedText: `Merge ${String(resolved.source_branch ?? "")} → ${payloadTarget}.`
17585
+ };
17265
17586
  case "changelog.apply":
17266
17587
  return {
17267
17588
  presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
@@ -17292,7 +17613,7 @@ var actionProposalQuestion = (request, descriptorPayload) => {
17292
17613
  var approvedResolvedExternalAction = (content) => {
17293
17614
  try {
17294
17615
  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")
17616
+ 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
17617
  return failure("invalid_input", "approved action is not a reconciliable external action");
17297
17618
  if (!value.payload || typeof value.payload !== "object")
17298
17619
  return failure("invalid_input", "approved action payload is incomplete");
@@ -17305,6 +17626,8 @@ var approvedResolvedExternalAction = (content) => {
17305
17626
  const target = resolved;
17306
17627
  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
17628
  return failure("invalid_input", "approved hosting target is incomplete");
17629
+ 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"))
17630
+ return failure("invalid_input", "approved hosting target is incomplete");
17308
17631
  if (value.operation === "changelog.apply" && (typeof target.target !== "string" || typeof target.beforeDigest !== "string" || typeof target.beforeExists !== "boolean" || typeof target.afterDigest !== "string"))
17309
17632
  return failure("invalid_input", "approved changelog target is incomplete");
17310
17633
  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 +17919,11 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
17596
17919
  }
17597
17920
  case "git.push": {
17598
17921
  const branch = request.payload.branch ?? gitValue(root, ["branch", "--show-current"]);
17922
+ if (branch && isProtectedBranch2(root, branch))
17923
+ return failure("invalid_input", `cannot push protected branch ${branch}`, {
17924
+ outcome: "not_started",
17925
+ fields: [{ path: "branch", reason: "protected branch" }]
17926
+ });
17599
17927
  const commit = branch ? gitValue(root, ["rev-parse", branch]) : null;
17600
17928
  const remote = pushRemote(root);
17601
17929
  const identity = remote ? credentialFreeRemote(remote) : null;
@@ -17693,6 +18021,11 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
17693
18021
  case "hosting.pull_request": {
17694
18022
  const target_branch = request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
17695
18023
  const source_branch = gitValue(root, ["branch", "--show-current"]);
18024
+ if (source_branch && isProtectedBranch2(root, source_branch))
18025
+ return failure("invalid_input", `cannot open a PR from protected branch ${source_branch}`, {
18026
+ outcome: "not_started",
18027
+ fields: [{ path: "source_branch", reason: "protected branch" }]
18028
+ });
17696
18029
  const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
17697
18030
  const remote = pushRemote(root);
17698
18031
  const identity = remote ? credentialFreeRemote(remote) : null;
@@ -17724,6 +18057,44 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
17724
18057
  }
17725
18058
  });
17726
18059
  }
18060
+ case "hosting.merge": {
18061
+ const target_branch = request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
18062
+ const source_branch = request.payload.source_branch ?? gitValue(root, ["branch", "--show-current"]);
18063
+ if (source_branch && isProtectedBranch2(root, source_branch))
18064
+ return failure("invalid_input", `cannot merge protected branch ${source_branch}`, {
18065
+ outcome: "not_started",
18066
+ fields: [{ path: "source_branch", reason: "protected branch" }]
18067
+ });
18068
+ const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
18069
+ const remote = pushRemote(root);
18070
+ const identity = remote ? credentialFreeRemote(remote) : null;
18071
+ if (!source_branch || !source_commit || !identity || remoteProvider2(identity) !== null && remoteProvider2(identity) !== String(vcsConfig("load", root).provider).toLowerCase())
18072
+ return failure("capability_unavailable", "hosting target is not bound to the configured remote provider", { capability: "hosting.merge" });
18073
+ const vcs = vcsConfig("load", root);
18074
+ if (!vcs.ok || !vcs.tokenReady)
18075
+ return failure("capability_unavailable", "hosting credentials are unavailable", {
18076
+ capability: "hosting.merge",
18077
+ outcome: "not_started"
18078
+ });
18079
+ if (!hostingCliAvailable(String(vcs.provider)))
18080
+ return failure("capability_unavailable", "hosting CLI is unavailable", {
18081
+ capability: "hosting.merge",
18082
+ outcome: "not_started"
18083
+ });
18084
+ const normalized = {
18085
+ operation: request.operation,
18086
+ payload: { ...request.payload, target_branch, source_branch }
18087
+ };
18088
+ const marker = `<!-- workit-action:${sha256({ operation: request.operation, payload: normalized.payload, source_commit, remote: identity })} -->`;
18089
+ return success(null, null, {
18090
+ request: normalized,
18091
+ marker,
18092
+ descriptorPayload: {
18093
+ ...normalized.payload,
18094
+ resolved: { source_branch, source_commit, remote: identity, target_branch, marker }
18095
+ }
18096
+ });
18097
+ }
17727
18098
  case "changelog.apply": {
17728
18099
  const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
17729
18100
  if (preview.error)
@@ -17899,6 +18270,17 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
17899
18270
  ...request.payload.babysit ?? true ? { next: prBabysitNext(String(result.output ?? "")) } : {}
17900
18271
  });
17901
18272
  }
18273
+ case "hosting.merge": {
18274
+ const target = String(request.payload.target_branch ?? "");
18275
+ const source = String(request.payload.source_branch ?? "");
18276
+ if (!target || !source)
18277
+ return failure("invalid_input", "hosting.merge requires target and source branches", {
18278
+ outcome: "not_started",
18279
+ fields: [{ path: "target_branch", reason: "required" }]
18280
+ });
18281
+ const result = mergePr(root, { target, source });
18282
+ return result.error || result.ok === false ? unknown2(request.operation) : success(null, null, result);
18283
+ }
17902
18284
  case "changelog.apply": {
17903
18285
  const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
17904
18286
  if (preview.error || approvedBeforeDigest !== undefined && preview.beforeDigest !== approvedBeforeDigest || approvedBeforeExists !== undefined && preview.beforeExists !== approvedBeforeExists)
@@ -18047,7 +18429,7 @@ var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedEx
18047
18429
  const store = new TaskStore(root);
18048
18430
  const selected = approvedExternalAction(store, "pi", actor, operation);
18049
18431
  if (!selected.ok) {
18050
- const plan = planCommitBinding(store, "pi", actor, operation) ?? chainStepBinding(store, "pi", actor, operation);
18432
+ const plan = planCommitBinding(store, "pi", actor, operation) ?? chainStepBinding(store, "pi", actor, operation) ?? standingAutoBinding(core, store, "pi", actor, operation);
18051
18433
  if (plan) {
18052
18434
  const actionRef = externalActionRef("pi", actor, operation);
18053
18435
  return {
@@ -18189,6 +18571,7 @@ var registerWorkitTools = (pi, options = {}) => {
18189
18571
  "git.commit",
18190
18572
  "git.push",
18191
18573
  "hosting.pull_request",
18574
+ "hosting.merge",
18192
18575
  "youtrack.update",
18193
18576
  "youtrack.time",
18194
18577
  "youtrack.meeting",
@@ -18218,7 +18601,7 @@ var registerWorkitTools = (pi, options = {}) => {
18218
18601
  return output(failure("permission_denied", "Pi project is not trusted for external actions"));
18219
18602
  if (context.workerId !== null)
18220
18603
  return output(failure("permission_denied", "supervised Pi children cannot run external actions"));
18221
- if (!ctx.hasUI)
18604
+ if (!ctx.hasUI && !standingAutoApplies(new TaskStore(ctx.cwd), "pi", context.caller.actor, externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload)))
18222
18605
  return output(failure("needs_input", "interactive confirmation UI is unavailable", {
18223
18606
  capability: "interactive_decision"
18224
18607
  }));
@@ -18234,6 +18617,7 @@ var registerWorkitTools = (pi, options = {}) => {
18234
18617
  "git.commit",
18235
18618
  "git.push",
18236
18619
  "hosting.pull_request",
18620
+ "hosting.merge",
18237
18621
  "changelog.apply"
18238
18622
  ].includes(resolved.data.request.operation);
18239
18623
  if (localOperation) {
@@ -18295,6 +18679,10 @@ var registerWorkitTools = (pi, options = {}) => {
18295
18679
  const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
18296
18680
  return output(result);
18297
18681
  }
18682
+ if (standingAutoApplies(store, "pi", actor, descriptor)) {
18683
+ const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
18684
+ return output(result);
18685
+ }
18298
18686
  const prior = externalActionState(store, "pi", actor, descriptor);
18299
18687
  if (prior.ok && prior.data.entry.data.consumption !== null)
18300
18688
  return output(failure("permission_denied", "external action was already settled"));