@brainervirus/workit-codex 1.0.11 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,8 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // packages/workit-codex/hooks/workit-hook.ts
6
- import { existsSync as existsSync4, realpathSync as realpathSync5, statSync } from "node:fs";
7
- import path7 from "node:path";
6
+ import { existsSync as existsSync5, realpathSync as realpathSync6, statSync } from "node:fs";
7
+ import path9 from "node:path";
8
8
 
9
9
  // packages/workit-core/src/core/task-contract.ts
10
10
  import { createHash, randomUUID } from "node:crypto";
@@ -7353,8 +7353,97 @@ var refSchema = discriminatedUnion("kind", [
7353
7353
  id
7354
7354
  }).strict(),
7355
7355
  object({ kind: literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
7356
- object({ kind: literal("external"), url: text.url() }).strict()
7356
+ object({ kind: literal("external"), url: text.url() }).strict(),
7357
+ object({
7358
+ kind: literal("standing"),
7359
+ workspace: nonEmpty,
7360
+ class: nonEmpty,
7361
+ configDigest: nullableDigest
7362
+ }).strict()
7357
7363
  ]);
7364
+ var rewriteUnknown = (value, map) => {
7365
+ if (Array.isArray(value)) {
7366
+ const out = [];
7367
+ for (const item of value) {
7368
+ const rewritten = rewriteUnknown(item, map);
7369
+ if (!rewritten.drop)
7370
+ out.push(rewritten.value);
7371
+ }
7372
+ return { drop: false, value: out };
7373
+ }
7374
+ if (typeof value === "object" && value !== null) {
7375
+ if (refSchema.safeParse(value).success) {
7376
+ const mapped = map(value);
7377
+ return mapped === null ? { drop: true } : { drop: false, value: mapped };
7378
+ }
7379
+ const out = {};
7380
+ for (const [key, item] of Object.entries(value)) {
7381
+ const rewritten = rewriteUnknown(item, map);
7382
+ out[key] = rewritten.drop ? null : rewritten.value;
7383
+ }
7384
+ return { drop: false, value: out };
7385
+ }
7386
+ return { drop: false, value };
7387
+ };
7388
+ var rewriteNode = (value, schema, map) => {
7389
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && refSchema.safeParse(value).success) {
7390
+ const mapped = map(value);
7391
+ return mapped === null ? { drop: true } : { drop: false, value: mapped };
7392
+ }
7393
+ let node = schema;
7394
+ while (node instanceof ZodNullable || node instanceof ZodOptional || node instanceof ZodDefault) {
7395
+ node = node.unwrap();
7396
+ }
7397
+ if (node === refSchema) {
7398
+ if (!refSchema.safeParse(value).success)
7399
+ return { drop: false, value };
7400
+ const mapped = map(value);
7401
+ return mapped === null ? { drop: true } : { drop: false, value: mapped };
7402
+ }
7403
+ if (node instanceof ZodArray) {
7404
+ if (!Array.isArray(value))
7405
+ return { drop: false, value };
7406
+ const out = [];
7407
+ for (const item of value) {
7408
+ const rewritten = rewriteNode(item, node.element, map);
7409
+ if (!rewritten.drop)
7410
+ out.push(rewritten.value);
7411
+ }
7412
+ return { drop: false, value: out };
7413
+ }
7414
+ if (node instanceof ZodObject) {
7415
+ if (typeof value !== "object" || value === null || Array.isArray(value))
7416
+ return { drop: false, value };
7417
+ const shape = node.shape;
7418
+ const record = value;
7419
+ const out = {};
7420
+ for (const [key, item] of Object.entries(record)) {
7421
+ if (!(key in shape)) {
7422
+ const scrubbed = rewriteUnknown(item, map);
7423
+ out[key] = scrubbed.drop ? null : scrubbed.value;
7424
+ continue;
7425
+ }
7426
+ const child = shape[key];
7427
+ const rewritten = rewriteNode(item, child, map);
7428
+ if (rewritten.drop) {
7429
+ if (child instanceof ZodOptional || child instanceof ZodDefault)
7430
+ continue;
7431
+ if (child instanceof ZodNullable) {
7432
+ out[key] = null;
7433
+ continue;
7434
+ }
7435
+ return { drop: true };
7436
+ }
7437
+ out[key] = rewritten.value;
7438
+ }
7439
+ return { drop: false, value: out };
7440
+ }
7441
+ return { drop: false, value };
7442
+ };
7443
+ var rewriteRecordRefs = (value, schema, map) => {
7444
+ const rewritten = rewriteNode(value, schema, map);
7445
+ return rewritten.drop ? null : rewritten.value;
7446
+ };
7358
7447
  var provenanceSchema = object({
7359
7448
  kind: _enum(["host_observed", "agent_reported", "imported"]),
7360
7449
  host: hostSchema,
@@ -7575,7 +7664,8 @@ var decisionSchema = object({
7575
7664
  approvedContent: text,
7576
7665
  displayed: text.optional(),
7577
7666
  contentRefs: array(refSchema),
7578
- statedChoice: object({ ref: text, text }).strict().optional()
7667
+ statedChoice: object({ ref: text, text }).strict().optional(),
7668
+ standing: object({ workspace: nonEmpty, class: nonEmpty }).strict().optional()
7579
7669
  }).strict(),
7580
7670
  digest,
7581
7671
  response: _enum(["approved", "rejected", "stated"]),
@@ -9005,11 +9095,11 @@ class TaskStore {
9005
9095
  return workspace;
9006
9096
  if (!workspace.data)
9007
9097
  return failure("not_found", "workspace not found");
9008
- const expectedTaskRevision = input.expectedTaskRevision ?? input.expectedRevision;
9009
- if (!expectedTaskRevision)
9098
+ const expected = input.expectedRevision;
9099
+ if (!expected)
9010
9100
  return failure("invalid_input", "task revision is required");
9011
- if (task.data.revision !== expectedTaskRevision)
9012
- return this.conflict(expectedTaskRevision, task.data.revision);
9101
+ if (task.data.revision !== expected)
9102
+ return this.conflict(expected, task.data.revision);
9013
9103
  if (workspace.data.revision !== input.expectedWorkspaceRevision)
9014
9104
  return this.conflict(input.expectedWorkspaceRevision, workspace.data.revision);
9015
9105
  const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
@@ -9543,6 +9633,7 @@ var same = (left, right) => {
9543
9633
  };
9544
9634
  var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9545
9635
  var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9636
+ var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
9546
9637
  var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9547
9638
  var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9548
9639
  const code = char.charCodeAt(0);
@@ -9589,21 +9680,398 @@ function assertProductWriteAllowed(input) {
9589
9680
  }
9590
9681
 
9591
9682
  // packages/workit-core/src/core/task-evaluation.ts
9592
- import { createHash as createHash4 } from "node:crypto";
9683
+ import { createHash as createHash5 } from "node:crypto";
9593
9684
  import * as fs6 from "node:fs";
9594
9685
  import { spawnSync } from "node:child_process";
9595
- import path6 from "node:path";
9686
+ import path8 from "node:path";
9596
9687
 
9597
9688
  // packages/workit-core/src/core/authority.ts
9598
- import { createHash as createHash3 } from "node:crypto";
9689
+ import { createHash as createHash4 } from "node:crypto";
9599
9690
  import * as fs5 from "node:fs";
9691
+ import path7 from "node:path";
9692
+
9693
+ // packages/workit-core/src/core/auto-approval.ts
9694
+ import { createHash as createHash3 } from "node:crypto";
9695
+ import { readFileSync as readFileSync4 } from "node:fs";
9696
+
9697
+ // packages/workit-core/src/core/workspaces.ts
9698
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
9699
+ import path6 from "node:path";
9700
+
9701
+ // packages/workit-core/src/core/config.ts
9702
+ import {
9703
+ copyFileSync,
9704
+ cpSync,
9705
+ existsSync as existsSync3,
9706
+ mkdirSync as mkdirSync2,
9707
+ readFileSync as readFileSync2,
9708
+ readdirSync as readdirSync2,
9709
+ writeFileSync as writeFileSync2
9710
+ } from "node:fs";
9711
+ import os from "node:os";
9600
9712
  import path5 from "node:path";
9713
+
9714
+ // packages/workit-core/src/core/boundary.ts
9715
+ var EVENT = {
9716
+ initialization: "initialization",
9717
+ provenance: "provenance",
9718
+ assets: "assets",
9719
+ configurationSource: "configuration_source",
9720
+ hooks: "hooks",
9721
+ mcpConnection: "mcp_connection",
9722
+ migration: "migration",
9723
+ installSteps: "install_steps",
9724
+ uncaughtFailure: "uncaught_failure",
9725
+ toolsFailed: "tools_failed",
9726
+ doctor: "doctor"
9727
+ };
9728
+ var errorDetail = (err) => {
9729
+ if (err instanceof Error) {
9730
+ return { error_name: err.name, error: err.message };
9731
+ }
9732
+ return { error_name: "unknown", error: String(err) };
9733
+ };
9734
+
9735
+ // packages/workit-core/src/core/config.ts
9736
+ var diagnosticLogger;
9737
+ var PRESETS = {
9738
+ gitflow: {
9739
+ allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
9740
+ protected: ["main", "develop", "master", "prod", "production"]
9741
+ },
9742
+ "github-flow": { allowed: ["*"], protected: ["main"] },
9743
+ "trunk-based": { allowed: ["*"], protected: ["main"] },
9744
+ custom: { allowed: [], protected: [] }
9745
+ };
9746
+ 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");
9747
+ var migratedDir = null;
9748
+ var migrationFailed = false;
9749
+ var ensureConfigDir = (dir = resolveConfigDir()) => {
9750
+ if (migratedDir === dir)
9751
+ return dir;
9752
+ if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
9753
+ migratedDir = dir;
9754
+ return dir;
9755
+ }
9756
+ const legacy = path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workflow-toolkit");
9757
+ if (!existsSync3(legacy)) {
9758
+ migratedDir = dir;
9759
+ return dir;
9760
+ }
9761
+ if (!migrationFailed && existsSync3(dir)) {
9762
+ migratedDir = dir;
9763
+ return dir;
9764
+ }
9765
+ migrationFailed = false;
9766
+ mkdirSync2(dir, { recursive: true });
9767
+ diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
9768
+ for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
9769
+ const src = path5.join(legacy, entry.name);
9770
+ const dest = path5.join(dir, entry.name);
9771
+ if (existsSync3(dest))
9772
+ continue;
9773
+ try {
9774
+ if (entry.isDirectory())
9775
+ cpSync(src, dest, { recursive: true });
9776
+ else if (entry.isFile())
9777
+ copyFileSync(src, dest);
9778
+ } catch (err) {
9779
+ migrationFailed = true;
9780
+ diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
9781
+ console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
9782
+ }
9783
+ }
9784
+ if (!migrationFailed)
9785
+ migratedDir = dir;
9786
+ return dir;
9787
+ };
9788
+ var configDir = () => ensureConfigDir();
9789
+ var DEFAULTS = {
9790
+ locale: "en",
9791
+ localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
9792
+ timezone: "America/Santiago",
9793
+ branchPolicy: {
9794
+ preset: "gitflow",
9795
+ allowed: [...PRESETS.gitflow.allowed],
9796
+ protected: [...PRESETS.gitflow.protected]
9797
+ },
9798
+ commitPolicy: { preset: "conventional" }
9799
+ };
9800
+ var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9801
+
9802
+ // packages/workit-core/src/core/workspaces.ts
9803
+ var readWorkspacesResult = (dir = configDir()) => {
9804
+ const file = path6.join(dir, "workspaces.json");
9805
+ let raw;
9806
+ try {
9807
+ raw = readFileSync3(file, "utf8");
9808
+ } catch {
9809
+ return { status: "missing", path: file, entries: [] };
9810
+ }
9811
+ let parsed;
9812
+ try {
9813
+ parsed = JSON.parse(raw);
9814
+ } catch {
9815
+ return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
9816
+ }
9817
+ if (!isConfigObject(parsed)) {
9818
+ return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
9819
+ }
9820
+ const list = parsed.workspaces;
9821
+ return {
9822
+ status: "valid",
9823
+ path: file,
9824
+ entries: Array.isArray(list) ? list : []
9825
+ };
9826
+ };
9827
+ var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
9828
+ var globToRegExp = (glob) => {
9829
+ let out = "";
9830
+ for (let i = 0;i < glob.length; i++) {
9831
+ const c = glob[i];
9832
+ if (c === "*") {
9833
+ if (glob[i + 1] === "*") {
9834
+ if (glob[i + 2] === "/") {
9835
+ if (out === "")
9836
+ out += "/?";
9837
+ out += "(?:[^/]+/)*";
9838
+ i += 2;
9839
+ } else {
9840
+ if (i + 2 >= glob.length) {
9841
+ if (out.endsWith("/")) {
9842
+ out = out.slice(0, -1);
9843
+ out += "(?:/.*)?";
9844
+ } else {
9845
+ out += ".*";
9846
+ }
9847
+ } else {
9848
+ out += ".*";
9849
+ }
9850
+ i++;
9851
+ }
9852
+ } else {
9853
+ out += "[^/]*";
9854
+ }
9855
+ } else {
9856
+ out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
9857
+ }
9858
+ }
9859
+ return new RegExp(`^${out}$`);
9860
+ };
9861
+ var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
9862
+ var realpathOf = (p) => {
9863
+ const real = realpathSync2.native ?? realpathSync2;
9864
+ try {
9865
+ return real(p);
9866
+ } catch {
9867
+ return p;
9868
+ }
9869
+ };
9870
+ var canonicalGlob = (glob) => {
9871
+ const m = /[*?[\]{]/.exec(glob);
9872
+ const prefix = m ? glob.slice(0, m.index) : glob;
9873
+ const rest = m ? glob.slice(m.index) : "";
9874
+ if (!prefix)
9875
+ return glob;
9876
+ const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
9877
+ return real + (prefix.endsWith("/") ? "/" : "") + rest;
9878
+ };
9879
+ var resolveWorkspaceFrom = (cwd, dir) => {
9880
+ const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
9881
+ for (const entry of loadWorkspacesFrom(dir)) {
9882
+ if (!entry || typeof entry !== "object")
9883
+ continue;
9884
+ const ws = entry;
9885
+ if (typeof ws.glob !== "string" || !ws.glob)
9886
+ continue;
9887
+ const glob = ws.glob.replaceAll("\\", "/");
9888
+ const canonical = canonicalGlob(glob);
9889
+ for (const target of targets) {
9890
+ if (matchWorkspace(glob, target))
9891
+ return ws;
9892
+ if (canonical !== glob && matchWorkspace(canonical, target))
9893
+ return ws;
9894
+ }
9895
+ }
9896
+ return null;
9897
+ };
9898
+
9899
+ // packages/workit-core/src/core/branch.ts
9900
+ import { execFileSync } from "node:child_process";
9901
+ var verifyPushIdentity = (root, provider, account) => {
9902
+ if (!account || typeof account !== "string")
9903
+ return { ok: false, error: "push identity requires a configured area account" };
9904
+ const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
9905
+ const bin = provider === "gitlab" ? "glab" : "gh";
9906
+ let login;
9907
+ try {
9908
+ login = execFileSync(bin, [...probe], {
9909
+ cwd: root,
9910
+ encoding: "utf8",
9911
+ stdio: ["pipe", "pipe", "pipe"]
9912
+ }).trim();
9913
+ } catch {
9914
+ return { ok: false, error: `${bin} identity could not be resolved` };
9915
+ }
9916
+ if (!login || login.toLowerCase() !== account.toLowerCase())
9917
+ return {
9918
+ ok: false,
9919
+ error: `push identity ${login || "(unknown)"} does not match area account ${account}`
9920
+ };
9921
+ return { ok: true };
9922
+ };
9923
+
9924
+ // packages/workit-core/src/core/auto-approval.ts
9925
+ var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
9926
+ var OPERATION_CLASS = {
9927
+ "git.branch_setup": "branch",
9928
+ "git.commit": "commit",
9929
+ "git.push": "push",
9930
+ "hosting.pull_request": "pr",
9931
+ "hosting.merge": "merge"
9932
+ };
9933
+ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null;
9934
+ var digestOf = (configDirPath) => {
9935
+ try {
9936
+ const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
9937
+ return createHash3("sha256").update(raw).digest("hex");
9938
+ } catch {
9939
+ return null;
9940
+ }
9941
+ };
9942
+ var normalizeClasses = (value) => {
9943
+ if (value === true)
9944
+ return [...AUTO_CLASSES];
9945
+ if (!Array.isArray(value))
9946
+ return [];
9947
+ const seen = new Set;
9948
+ for (const item of value) {
9949
+ if (typeof item === "string" && AUTO_CLASSES.includes(item))
9950
+ seen.add(item);
9951
+ }
9952
+ return [...seen];
9953
+ };
9954
+ var resolveAutoApproval = (root, dir = configDir()) => {
9955
+ let match;
9956
+ try {
9957
+ match = resolveWorkspaceFrom(root, dir);
9958
+ } catch {
9959
+ return { status: "off" };
9960
+ }
9961
+ if (!match)
9962
+ return { status: "off" };
9963
+ const classes = normalizeClasses(match.autoApprove);
9964
+ if (classes.length === 0)
9965
+ return { status: "off" };
9966
+ return {
9967
+ status: "on",
9968
+ workspace: match.name,
9969
+ classes,
9970
+ account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
9971
+ configDigest: digestOf(dir)
9972
+ };
9973
+ };
9974
+ var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
9975
+ var standingApprovalLive = (root, receipt, cls, dir) => {
9976
+ if (!isStandingReceipt(receipt))
9977
+ return false;
9978
+ if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
9979
+ return false;
9980
+ if (receipt.class !== cls)
9981
+ return false;
9982
+ const resolved = resolveAutoApproval(root, dir);
9983
+ if (resolved.status !== "on")
9984
+ return false;
9985
+ if (resolved.workspace !== receipt.workspace)
9986
+ return false;
9987
+ return resolved.classes.includes(cls);
9988
+ };
9989
+ var standingReceiptFor = (root, workspace, cls, dir) => {
9990
+ const resolved = resolveAutoApproval(root, dir);
9991
+ return {
9992
+ kind: "standing",
9993
+ workspace,
9994
+ class: cls,
9995
+ configDigest: resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null
9996
+ };
9997
+ };
9998
+ var verifyStandingApproval = (root, task, caller, binding) => {
9999
+ if (task.status !== "active")
10000
+ return failure("invalid_transition", "only active tasks can authorize actions");
10001
+ const session = task.intent?.provenance?.session;
10002
+ if (!session || session.kind !== "host" || session.host !== caller.host || session.handle !== caller.actor)
10003
+ return failure("permission_denied", "standing approval requires the lead session");
10004
+ const standing = binding.standing;
10005
+ if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
10006
+ return failure("invalid_input", "standing approval needs a workspace and class");
10007
+ let operation;
10008
+ try {
10009
+ operation = typeof binding.approvedContent === "string" ? JSON.parse(binding.approvedContent).operation : null;
10010
+ } catch {
10011
+ operation = null;
10012
+ }
10013
+ const cls = operationAutoClass(operation);
10014
+ if (!cls || cls !== standing.class)
10015
+ return failure("permission_denied", "standing approval does not cover this operation");
10016
+ const resolved = resolveAutoApproval(root);
10017
+ if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
10018
+ return failure("permission_denied", "standing auto-approval is not live for this operation");
10019
+ if (cls === "push") {
10020
+ const workspace = resolveWorkspaceFrom(root, configDir());
10021
+ const provider = workspace?.vcs?.provider;
10022
+ const account = resolved.account;
10023
+ if (!provider || !account)
10024
+ return failure("permission_denied", "auto-push requires a workspace provider and area account");
10025
+ const identity = verifyPushIdentity(root, provider, account);
10026
+ if (!identity.ok)
10027
+ return failure("permission_denied", identity.error);
10028
+ }
10029
+ return success(null, null, {
10030
+ kind: "host_observed",
10031
+ host: caller.host,
10032
+ session: { kind: "host", host: caller.host, handle: caller.actor },
10033
+ workerId: null,
10034
+ receipts: [standingReceiptFor(root, standing.workspace, cls)]
10035
+ });
10036
+ };
10037
+
10038
+ // packages/workit-core/src/core/authority.ts
9601
10039
  var verifiedAuthorities = new WeakMap;
9602
10040
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9603
10041
  const token = {};
9604
10042
  verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
9605
10043
  return token;
9606
10044
  };
10045
+ var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
10046
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10047
+ var normalizeChainSteps = (steps) => {
10048
+ if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
10049
+ return null;
10050
+ const normalized = [];
10051
+ for (const step of steps) {
10052
+ if (typeof step === "string") {
10053
+ if (!step.trim())
10054
+ return null;
10055
+ normalized.push({ kind: "commit", message: step });
10056
+ continue;
10057
+ }
10058
+ if (!isRecord(step))
10059
+ return null;
10060
+ if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
10061
+ normalized.push({ kind: "branch", target: step.branch });
10062
+ continue;
10063
+ }
10064
+ if (step.pr === true && Object.keys(step).length === 1) {
10065
+ normalized.push({ kind: "pr" });
10066
+ continue;
10067
+ }
10068
+ return null;
10069
+ }
10070
+ const keys = normalized.map(chainStepKey);
10071
+ if (new Set(keys).size !== keys.length)
10072
+ return null;
10073
+ return normalized;
10074
+ };
9607
10075
  var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
9608
10076
  var parseSteps = (content) => {
9609
10077
  let value;
@@ -9615,7 +10083,10 @@ var parseSteps = (content) => {
9615
10083
  if (typeof value !== "object" || value === null || !("steps" in value))
9616
10084
  return [];
9617
10085
  const steps = value.steps;
9618
- return Array.isArray(steps) && steps.every((step) => typeof step === "string") ? steps : null;
10086
+ if (!Array.isArray(steps))
10087
+ return null;
10088
+ const normalized = normalizeChainSteps(steps);
10089
+ return normalized;
9619
10090
  };
9620
10091
  var decisionMatches = (entry, purpose, binding) => {
9621
10092
  const decision = entry.data;
@@ -9711,7 +10182,7 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
9711
10182
  return [];
9712
10183
  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);
9713
10184
  }
9714
- var digestBytes2 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
10185
+ var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
9715
10186
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9716
10187
  let root;
9717
10188
  try {
@@ -9724,23 +10195,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9724
10195
  continue;
9725
10196
  if (!reference.digest)
9726
10197
  return failure("invalid_input", "document references require a byte digest");
9727
- const target = path5.resolve(root, reference.path);
9728
- if (target !== root && !target.startsWith(`${root}${path5.sep}`))
10198
+ const target = path7.resolve(root, reference.path);
10199
+ if (target !== root && !target.startsWith(`${root}${path7.sep}`))
9729
10200
  return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9730
10201
  fields: [
9731
10202
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
9732
10203
  ]
9733
10204
  });
9734
10205
  try {
9735
- const relative = path5.relative(root, target);
10206
+ const relative = path7.relative(root, target);
9736
10207
  let current = root;
9737
- for (const segment of relative.split(path5.sep)) {
10208
+ for (const segment of relative.split(path7.sep)) {
9738
10209
  if (!segment)
9739
10210
  continue;
9740
- current = path5.join(current, segment);
10211
+ current = path7.join(current, segment);
9741
10212
  const stat = fs5.lstatSync(current);
9742
10213
  const real = fs5.realpathSync(current);
9743
- if (real !== root && !real.startsWith(`${root}${path5.sep}`))
10214
+ if (real !== root && !real.startsWith(`${root}${path7.sep}`))
9744
10215
  return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9745
10216
  fields: [
9746
10217
  {
@@ -9793,6 +10264,20 @@ var validateAction = (store, task, workspaceId, input, authority) => {
9793
10264
  return failure("permission_denied", "decision was rejected");
9794
10265
  if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
9795
10266
  return failure("permission_denied", "action approval lacks native receipt assurance");
10267
+ const receipts = entry.provenance.receipts;
10268
+ const standing = receipts.filter((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "standing");
10269
+ const questionBacked = receipts.some((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "host");
10270
+ if (!questionBacked && standing.length > 0) {
10271
+ let operation;
10272
+ try {
10273
+ operation = JSON.parse(decision.binding.approvedContent).operation;
10274
+ } catch {
10275
+ operation = null;
10276
+ }
10277
+ const cls = operationAutoClass(operation);
10278
+ if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
10279
+ return failure("permission_denied", "standing auto-approval is not live for this operation");
10280
+ }
9796
10281
  if (decision.revoked)
9797
10282
  return failure("permission_denied", "decision is revoked");
9798
10283
  if (decision.digest !== decisionDigest(decision))
@@ -9806,14 +10291,12 @@ var validateAction = (store, task, workspaceId, input, authority) => {
9806
10291
  return content;
9807
10292
  const requestedSteps = parseSteps(decision.binding.approvedContent);
9808
10293
  if (requestedSteps === null)
9809
- return failure("invalid_input", "bounded action steps must be a string array");
9810
- if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps))
10294
+ return failure("invalid_input", "bounded action steps must be a unique non-empty array");
10295
+ if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
9811
10296
  return failure("permission_denied", "bounded action steps do not match approved content");
9812
- if (requestedSteps.some((step) => !step || typeof step !== "string") || new Set(requestedSteps).size !== requestedSteps.length)
9813
- return failure("invalid_input", "bounded action steps must be unique and non-empty");
9814
10297
  const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
9815
- const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps, completed: [] };
9816
- if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps))
10298
+ const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
10299
+ if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
9817
10300
  return failure("permission_denied", "bounded action scope changed");
9818
10301
  return success(null, null, { entry, workflow });
9819
10302
  };
@@ -10094,15 +10577,15 @@ var bindingCovers = scopeCovers;
10094
10577
  var verifyDecisionContent = verifyContentRefs;
10095
10578
 
10096
10579
  // packages/workit-core/src/core/task-evaluation.ts
10097
- var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
10098
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10099
- var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
10580
+ var digestBytes3 = (value) => createHash5("sha256").update(value).digest("hex");
10581
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path8.sep}`);
10582
+ var relative = (root, candidate) => path8.relative(root, candidate).split(path8.sep).join("/") || ".";
10100
10583
  var canonicalPath = (value) => {
10101
10584
  if (typeof value !== "string" || !value)
10102
10585
  return null;
10103
10586
  if (value.includes("\\") || value.split("/").includes(".."))
10104
10587
  return null;
10105
- const normalized = path6.posix.normalize(value);
10588
+ const normalized = path8.posix.normalize(value);
10106
10589
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
10107
10590
  return null;
10108
10591
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10150,14 +10633,14 @@ var gitPaths = (root) => {
10150
10633
  git: /not a git repository/i.test(stderr) ? false : true
10151
10634
  };
10152
10635
  }
10153
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10636
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path8.sep).join("/")) : [];
10154
10637
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10155
10638
  if (stagedDeleted.status !== 0) {
10156
10639
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10157
10640
  if (!/not a git repository/i.test(stderr))
10158
10641
  return { paths, uncertain: true, git: true };
10159
10642
  } else if (stagedDeleted.stdout) {
10160
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10643
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path8.sep).join("/")));
10161
10644
  }
10162
10645
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10163
10646
  };
@@ -10172,7 +10655,7 @@ var walk = (root, directory, output) => {
10172
10655
  for (const name of names) {
10173
10656
  if (name === ".git" || name === ".workit")
10174
10657
  continue;
10175
- const target = path6.join(directory, name);
10658
+ const target = path8.join(directory, name);
10176
10659
  let stat;
10177
10660
  try {
10178
10661
  stat = fs6.lstatSync(target);
@@ -10190,12 +10673,12 @@ var walk = (root, directory, output) => {
10190
10673
  var scopeRoots = (root, scope) => {
10191
10674
  const roots = [];
10192
10675
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10193
- const target = path6.resolve(root, item);
10676
+ const target = path8.resolve(root, item);
10194
10677
  if (!inside(root, target))
10195
10678
  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` }] });
10196
10679
  let ancestor = target;
10197
10680
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10198
- ancestor = path6.dirname(ancestor);
10681
+ ancestor = path8.dirname(ancestor);
10199
10682
  try {
10200
10683
  if (!inside(root, fs6.realpathSync(ancestor)))
10201
10684
  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` }] });
@@ -10247,7 +10730,7 @@ function captureCandidate(root, scope, environment = []) {
10247
10730
  for (const item of [...names].sort()) {
10248
10731
  if (!scopeMatches(item, normalizedScope))
10249
10732
  continue;
10250
- const target = path6.join(checkout, item);
10733
+ const target = path8.join(checkout, item);
10251
10734
  try {
10252
10735
  const stat = fs6.lstatSync(target);
10253
10736
  if (stat.isSymbolicLink()) {
@@ -10368,13 +10851,28 @@ function evaluateEvidence(task, candidate) {
10368
10851
  });
10369
10852
  }
10370
10853
  var scopeCovers2 = scopeCovers;
10854
+ var checkPin = (assignment, beforeCandidateId, candidateId) => {
10855
+ const pin = assignment.candidateId ?? null;
10856
+ if (pin === null)
10857
+ return true;
10858
+ return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
10859
+ };
10860
+ var resolveBinding = (evidence, pin, currentCandidateId) => {
10861
+ const bound = evidence.kind === "check" || evidence.kind === "review";
10862
+ const fallback = bound ? pin ?? currentCandidateId : null;
10863
+ return {
10864
+ beforeCandidateId: evidence.beforeCandidateId ?? fallback,
10865
+ candidateId: evidence.candidateId ?? fallback
10866
+ };
10867
+ };
10868
+ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
10371
10869
  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")));
10372
10870
  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 }));
10373
- var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
10871
+ var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
10374
10872
  if (kind !== "passed")
10375
10873
  return false;
10376
10874
  if (dimension === "testing" || dimension === "verification")
10377
- return evidenceKind === "check";
10875
+ return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
10378
10876
  if (dimension === "review")
10379
10877
  return evidenceKind === "review";
10380
10878
  if (dimension === "investigation" || dimension === "challenge")
@@ -10383,6 +10881,21 @@ var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
10383
10881
  return evidenceKind === "artifact";
10384
10882
  return false;
10385
10883
  };
10884
+ var expectedKinds = (dimension, ruleId) => {
10885
+ if (dimension === "testing")
10886
+ return "check";
10887
+ if (dimension === "verification")
10888
+ return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
10889
+ if (dimension === "review")
10890
+ return "review";
10891
+ if (dimension === "investigation" || dimension === "challenge")
10892
+ return "investigation";
10893
+ if (dimension === "artifacts" || dimension === "continuity")
10894
+ return "artifact";
10895
+ if (dimension === "decisions")
10896
+ return "decision";
10897
+ return "worker report";
10898
+ };
10386
10899
  function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
10387
10900
  const evidence = evaluateEvidence(task, candidate);
10388
10901
  if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
@@ -10396,7 +10909,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10396
10909
  return (task.policy?.requirements ?? []).map((requirement) => {
10397
10910
  const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
10398
10911
  const passed = related.filter(({ entry, evaluation }) => {
10399
- if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension))
10912
+ if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
10400
10913
  return false;
10401
10914
  if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
10402
10915
  return false;
@@ -10404,11 +10917,12 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10404
10917
  return true;
10405
10918
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10406
10919
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10407
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10920
+ 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));
10408
10921
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10409
10922
  });
10410
10923
  if (passed.length) {
10411
- 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)) : passed;
10924
+ const currentId = candidate?.id ?? null;
10925
+ 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;
10412
10926
  if (withRed.length)
10413
10927
  return {
10414
10928
  requirementId: requirement.id,
@@ -10457,6 +10971,14 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10457
10971
  reason: "an applicable approved limitation permits the missing evidence"
10458
10972
  };
10459
10973
  const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
10974
+ if (!unavailable && related.length > 0 && !passed.length)
10975
+ return {
10976
+ requirementId: requirement.id,
10977
+ status: "unsatisfied",
10978
+ evidenceIds: related.map(({ entry }) => entry.id),
10979
+ decisionIds: [],
10980
+ reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
10981
+ };
10460
10982
  return {
10461
10983
  requirementId: requirement.id,
10462
10984
  status: unavailable ? "unavailable" : "unsatisfied",
@@ -10481,7 +11003,13 @@ function evaluateClosure(requestedOutcome, view) {
10481
11003
  if (entry.data.disposition === "fixed") {
10482
11004
  const verified = entry.data.resolution?.evidenceIds.some((id) => {
10483
11005
  const evidence = view.task.evidence.find((item) => item.id === id);
10484
- return evidenceById.get(id) === "passed" && (evidence?.data.kind === "check" || evidence?.data.kind === "review");
11006
+ if (!evidence)
11007
+ return false;
11008
+ return findingVerificationPasses(entry.data.candidateId, {
11009
+ kind: evidence.data.kind,
11010
+ candidateId: evidence.data.candidateId,
11011
+ status: evidenceById.get(id) ?? "missing"
11012
+ });
10485
11013
  });
10486
11014
  if (!verified)
10487
11015
  return failure("requirements_unsatisfied", "fixed findings require current verification");
@@ -10497,7 +11025,12 @@ function evaluateClosure(requestedOutcome, view) {
10497
11025
  }
10498
11026
  }
10499
11027
  }
10500
- const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
11028
+ const blocking = evaluations.filter((item) => {
11029
+ if (item.status !== "unsatisfied" && item.status !== "unavailable")
11030
+ return false;
11031
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
11032
+ return !requirement || requirement.before === "close";
11033
+ });
10501
11034
  if (requestedOutcome !== "stopped" && blocking.length)
10502
11035
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10503
11036
  requirementIds: blocking.map((item) => item.requirementId),
@@ -10540,15 +11073,15 @@ function evaluateClosure(requestedOutcome, view) {
10540
11073
  // packages/workit-core/src/core/methods.ts
10541
11074
  var METHODS = {
10542
11075
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
10543
- "workit-behavioral-tdd": { dimensions: ["testing"] },
10544
- "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
11076
+ "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
11077
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
10545
11078
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
10546
11079
  "workit-implement": { dimensions: ["delegation"] },
10547
- "workit-debug": { ruleIds: ["root-cause-investigation"] },
10548
- "workit-handoff": { ruleIds: ["durable-handoff"] },
11080
+ "workit-debug": {},
11081
+ "workit-handoff": {},
10549
11082
  "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
10550
11083
  };
10551
- var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
11084
+ var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
10552
11085
  var assuranceFor = (id, definition, capabilities) => {
10553
11086
  const keys = new Set([
10554
11087
  id,
@@ -10569,7 +11102,7 @@ var assuranceFor = (id, definition, capabilities) => {
10569
11102
  };
10570
11103
  function selectMethods(policy, capabilities) {
10571
11104
  return Object.entries(METHODS).flatMap(([id, definition]) => {
10572
- const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
11105
+ const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
10573
11106
  if (requirements.length === 0)
10574
11107
  return [];
10575
11108
  const capability = assuranceFor(id, definition, capabilities);
@@ -10740,7 +11273,8 @@ var exportDigest = (bundle) => sha256({
10740
11273
  // packages/workit-core/src/core/route-intent.ts
10741
11274
  var GUIDANCE = {
10742
11275
  "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)",
10743
- "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"
11276
+ "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",
11277
+ "hosting.merge": "use the Workit hosting.merge action (workit_external_action in hosts, or `workit action hosting.merge --payload '{...}' --confirm --json` in the CLI)"
10744
11278
  };
10745
11279
  var unbalancedQuotes = (segment) => {
10746
11280
  const text = segment.replace(/\\./gu, "");
@@ -10775,6 +11309,10 @@ function shellRouteIntent(command) {
10775
11309
  return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10776
11310
  if (head === "glab" && second === "mr" && third === "create")
10777
11311
  return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
11312
+ if (head === "gh" && second === "pr" && third === "merge")
11313
+ return { route: "hosting.merge", guidance: GUIDANCE["hosting.merge"] };
11314
+ if (head === "glab" && second === "mr" && third === "merge")
11315
+ return { route: "hosting.merge", guidance: GUIDANCE["hosting.merge"] };
10778
11316
  }
10779
11317
  return null;
10780
11318
  }
@@ -10828,6 +11366,13 @@ var externalActionSchema = discriminatedUnion("operation", [
10828
11366
  babysit: boolean2().optional()
10829
11367
  }).strict()
10830
11368
  }).strict(),
11369
+ object({
11370
+ operation: literal("hosting.merge"),
11371
+ payload: object({
11372
+ target_branch: string2().min(1).optional(),
11373
+ source_branch: string2().min(1).optional()
11374
+ }).strict()
11375
+ }).strict(),
10831
11376
  object({
10832
11377
  operation: literal("youtrack.update"),
10833
11378
  payload: object({
@@ -10888,7 +11433,7 @@ var externalActionSchema = discriminatedUnion("operation", [
10888
11433
  }).strict()
10889
11434
  ]);
10890
11435
  // packages/workit-core/src/core/task-engine.ts
10891
- import { realpathSync as realpathSync4 } from "node:fs";
11436
+ import { realpathSync as realpathSync5 } from "node:fs";
10892
11437
 
10893
11438
  // packages/workit-core/src/core/policy-resolver.ts
10894
11439
  var resolverInputSchema = object({
@@ -11332,6 +11877,8 @@ var applyWorkerLifecycle = (input) => {
11332
11877
  if (task.data.status === "paused" && (input.state === "running" || input.state === "cancelling"))
11333
11878
  return failure("invalid_transition", "paused task cannot run a worker");
11334
11879
  const notStarted = input.dispatch === true;
11880
+ if (notStarted && entry.data.state === "stopped" && input.session === null)
11881
+ return success(task.data.revision, workspace.data.revision, entry);
11335
11882
  if (notStarted && (input.state !== "stopped" || input.session !== null))
11336
11883
  return failure("invalid_input", "a never-dispatched worker stops with no session");
11337
11884
  if (notStarted && entry.data.session !== null)
@@ -11363,7 +11910,7 @@ var applyWorkerLifecycle = (input) => {
11363
11910
  return success(task.data.revision, workspace.data.revision, entry);
11364
11911
  const changed = input.store.mutateTaskAndWorkspace({
11365
11912
  taskId: task.data.id,
11366
- expectedTaskRevision: input.expectedRevision,
11913
+ expectedRevision: input.expectedRevision,
11367
11914
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
11368
11915
  now: input.now,
11369
11916
  workspace: (current, mutation) => success(mutation.revision, mutation.revision, {
@@ -11411,90 +11958,22 @@ var importedProvenance = (context) => ({
11411
11958
  receipts: []
11412
11959
  });
11413
11960
  var portableTask = (task) => {
11414
- const clone = structuredClone(task);
11415
- const portableProvenance = (value) => ({
11416
- ...value,
11417
- session: null,
11418
- receipts: []
11419
- });
11420
- const entries = [
11421
- clone.intent,
11422
- ...clone.assessments,
11423
- ...clone.evidence,
11424
- ...clone.decisions,
11425
- ...clone.findings,
11426
- ...clone.workers
11427
- ];
11428
- for (const entry of entries)
11429
- entry.provenance = portableProvenance(entry.provenance);
11430
- for (const worker of clone.workers)
11431
- worker.data.session = null;
11432
- clone.intent = {
11433
- ...clone.intent,
11434
- data: { ...clone.intent.data, authorityRefs: portableRefs(clone.intent.data.authorityRefs) }
11435
- };
11436
- clone.constraints = clone.constraints.flatMap((constraint) => {
11437
- const source = portableRef(constraint.source);
11438
- return source ? [
11439
- {
11440
- ...constraint,
11441
- source,
11442
- requires: constraint.requires.map((requirement) => ({
11443
- ...requirement,
11444
- refs: portableRefs(requirement.refs)
11445
- }))
11446
- }
11447
- ] : [];
11448
- });
11449
- clone.assessments = clone.assessments.map((entry) => ({
11450
- ...entry,
11451
- data: {
11452
- ...entry.data,
11453
- facts: entry.data.facts.map(portableFact),
11454
- signals: Object.fromEntries(Object.entries(entry.data.signals).map(([name, signal]) => [name, portableSignal(signal)])),
11455
- consequences: entry.data.consequences.map((consequence) => ({
11456
- ...consequence,
11457
- fact: portableFact(consequence.fact)
11458
- })),
11459
- verification: entry.data.verification.map((verification) => ({
11460
- ...verification,
11461
- availableChecks: portableRefs(verification.availableChecks)
11462
- }))
11463
- }
11464
- }));
11465
- clone.progress = {
11466
- ...clone.progress,
11467
- blockers: clone.progress.blockers.map((blocker) => ({
11468
- ...blocker,
11469
- refs: portableRefs(blocker.refs)
11470
- }))
11961
+ const stripped = rewriteRecordRefs(task, taskRecordSchema, portableRef);
11962
+ if (typeof stripped !== "object" || stripped === null || Array.isArray(stripped))
11963
+ throw new TypeError("portable task rewrite produced a non-record");
11964
+ const clone = structuredClone(stripped);
11965
+ const demote = (facts, signals, consequences) => {
11966
+ for (const fact of facts)
11967
+ Object.assign(fact, portableFact(fact));
11968
+ for (const name of Object.keys(signals)) {
11969
+ const signal = signals[name];
11970
+ Object.assign(signal, portableSignal(signal));
11971
+ }
11972
+ for (const consequence of consequences)
11973
+ Object.assign(consequence.fact, portableFact(consequence.fact));
11471
11974
  };
11472
- clone.evidence = clone.evidence.map((entry) => ({
11473
- ...entry,
11474
- data: {
11475
- ...entry.data,
11476
- refs: portableRefs(entry.data.refs),
11477
- reviewContext: portableRef(entry.data.reviewContext)
11478
- }
11479
- }));
11480
- clone.decisions = clone.decisions.map((entry) => ({
11481
- ...entry,
11482
- data: {
11483
- ...entry.data,
11484
- consumption: entry.data.consumption ? portableRef(entry.data.consumption.actionRef) ? {
11485
- ...entry.data.consumption,
11486
- actionRef: portableRef(entry.data.consumption.actionRef)
11487
- } : null : null,
11488
- binding: {
11489
- ...entry.data.binding,
11490
- contentRefs: portableRefs(entry.data.binding.contentRefs)
11491
- }
11492
- }
11493
- }));
11494
- clone.findings = clone.findings.map((entry) => ({
11495
- ...entry,
11496
- data: { ...entry.data, refs: portableRefs(entry.data.refs) }
11497
- }));
11975
+ for (const entry of clone.assessments)
11976
+ demote(entry.data.facts, entry.data.signals, entry.data.consequences);
11498
11977
  clone.candidates = [];
11499
11978
  return clone;
11500
11979
  };
@@ -11519,15 +11998,13 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
11519
11998
  allocate(entry.id);
11520
11999
  const fresh = (id) => allocate(id);
11521
12000
  const provenance = importedProvenance(context);
12001
+ const remap = (ref) => mapRef(ref, ids);
11522
12002
  const intent = {
11523
12003
  ...source.intent,
11524
12004
  id: fresh(source.intent.id),
11525
12005
  recordedAt: timestamp,
11526
12006
  provenance,
11527
- data: {
11528
- ...source.intent.data,
11529
- authorityRefs: source.intent.data.authorityRefs.map((ref) => mapRef(ref, ids))
11530
- }
12007
+ data: rewriteRecordRefs(source.intent.data, intentSchema, remap)
11531
12008
  };
11532
12009
  const assessments = source.assessments.map((entry) => {
11533
12010
  const data = entry.data;
@@ -11536,28 +12013,7 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
11536
12013
  id: fresh(entry.id),
11537
12014
  recordedAt: timestamp,
11538
12015
  provenance,
11539
- data: {
11540
- ...data,
11541
- facts: data.facts.map((fact) => ({
11542
- ...fact,
11543
- refs: fact.refs.map((ref) => mapRef(ref, ids))
11544
- })),
11545
- signals: Object.fromEntries(Object.entries(data.signals).map(([name, signal]) => [
11546
- name,
11547
- { ...signal, refs: signal.refs.map((ref) => mapRef(ref, ids)) }
11548
- ])),
11549
- consequences: data.consequences.map((consequence) => ({
11550
- ...consequence,
11551
- fact: {
11552
- ...consequence.fact,
11553
- refs: consequence.fact.refs.map((ref) => mapRef(ref, ids))
11554
- }
11555
- })),
11556
- verification: data.verification.map((verification) => ({
11557
- ...verification,
11558
- availableChecks: verification.availableChecks.map((ref) => mapRef(ref, ids))
11559
- }))
11560
- }
12016
+ data: rewriteRecordRefs(data, assessmentSchema, remap)
11561
12017
  };
11562
12018
  });
11563
12019
  const evidence = source.evidence.map((entry) => ({
@@ -11565,11 +12021,7 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
11565
12021
  id: fresh(entry.id),
11566
12022
  recordedAt: timestamp,
11567
12023
  provenance,
11568
- data: {
11569
- ...entry.data,
11570
- refs: entry.data.refs.map((ref) => mapRef(ref, ids)),
11571
- reviewContext: entry.data.reviewContext ? mapRef(entry.data.reviewContext, ids) : null
11572
- }
12024
+ data: rewriteRecordRefs(entry.data, evidenceSchema, remap)
11573
12025
  }));
11574
12026
  const decisions = source.decisions.map((entry) => ({
11575
12027
  ...entry,
@@ -11577,21 +12029,23 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
11577
12029
  recordedAt: timestamp,
11578
12030
  provenance
11579
12031
  }));
11580
- const findings = source.findings.map((entry) => ({
11581
- ...entry,
11582
- id: fresh(entry.id),
11583
- recordedAt: timestamp,
11584
- provenance,
11585
- data: {
11586
- ...entry.data,
11587
- refs: entry.data.refs.map((ref) => mapRef(ref, ids)),
11588
- resolution: entry.data.resolution ? {
11589
- ...entry.data.resolution,
11590
- evidenceIds: entry.data.resolution.evidenceIds.map((id) => ids.get(id) ?? id),
11591
- decisionIds: entry.data.resolution.decisionIds.map((id) => ids.get(id) ?? id)
11592
- } : null
11593
- }
11594
- }));
12032
+ const findings = source.findings.map((entry) => {
12033
+ const data = rewriteRecordRefs(entry.data, findingSchema, remap);
12034
+ return {
12035
+ ...entry,
12036
+ id: fresh(entry.id),
12037
+ recordedAt: timestamp,
12038
+ provenance,
12039
+ data: {
12040
+ ...data,
12041
+ resolution: data.resolution ? {
12042
+ ...data.resolution,
12043
+ evidenceIds: data.resolution.evidenceIds.map((id) => ids.get(id) ?? id),
12044
+ decisionIds: data.resolution.decisionIds.map((id) => ids.get(id) ?? id)
12045
+ } : null
12046
+ }
12047
+ };
12048
+ });
11595
12049
  const workers = source.workers.map((entry) => ({
11596
12050
  ...entry,
11597
12051
  id: fresh(entry.id),
@@ -11634,19 +12088,21 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
11634
12088
  closure: null,
11635
12089
  assessments,
11636
12090
  evidence,
11637
- decisions: decisions.map((entry) => ({
11638
- ...entry,
11639
- data: {
11640
- ...entry.data,
11641
- binding: {
11642
- ...entry.data.binding,
11643
- taskId: "",
11644
- workspaceId: destinationWorkspaceId,
11645
- contentRefs: entry.data.binding.contentRefs.map((ref) => mapRef(ref, ids))
11646
- },
11647
- consumption: null
11648
- }
11649
- })),
12091
+ decisions: decisions.map((entry) => {
12092
+ const data = rewriteRecordRefs(entry.data, decisionSchema, remap);
12093
+ return {
12094
+ ...entry,
12095
+ data: {
12096
+ ...data,
12097
+ binding: {
12098
+ ...data.binding,
12099
+ taskId: "",
12100
+ workspaceId: destinationWorkspaceId
12101
+ },
12102
+ consumption: null
12103
+ }
12104
+ };
12105
+ }),
11650
12106
  ...actionProgress ? { actionProgress } : {},
11651
12107
  findings,
11652
12108
  workers,
@@ -11684,7 +12140,7 @@ class WorkitCore {
11684
12140
  }
11685
12141
  contextRootError() {
11686
12142
  try {
11687
- if (realpathSync4(this.context.root) !== this.store.root)
12143
+ if (realpathSync5(this.context.root) !== this.store.root)
11688
12144
  return failure("invalid_input", "operation context root does not match the task store root");
11689
12145
  } catch {
11690
12146
  return failure("invalid_input", "operation context root cannot be resolved");
@@ -11729,7 +12185,7 @@ class WorkitCore {
11729
12185
  activeWorkerBlocker(task, workspace) {
11730
12186
  if (workspace.writer)
11731
12187
  return failure("recovery_required", "writer ownership must be released first");
11732
- if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
12188
+ if (task.workers.some((entry) => workerBlocksTransition("close", entry.data.state)))
11733
12189
  return failure("recovery_required", "worker state requires reconciliation");
11734
12190
  return null;
11735
12191
  }
@@ -11975,37 +12431,33 @@ class WorkitCore {
11975
12431
  const currentCandidate = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
11976
12432
  if (!currentCandidate.ok)
11977
12433
  return currentCandidate;
11978
- if (evidence.kind === "check" || evidence.kind === "review") {
11979
- evidence.beforeCandidateId ??= currentCandidate.data.id;
11980
- evidence.candidateId ??= currentCandidate.data.id;
11981
- } else {
11982
- evidence.beforeCandidateId ??= null;
11983
- evidence.candidateId ??= null;
11984
- }
12434
+ const pin = helper && helper.ok ? helper.data.data.assignment.candidateId : null;
12435
+ const bound = resolveBinding(evidence, pin, currentCandidate.data.id);
11985
12436
  if (helper?.ok) {
11986
12437
  const assignment = helper.data.data.assignment;
11987
- if (evidence.requirementIds.some((id) => !assignment.requirementIds.includes(id)) || assignment.candidateId !== null && evidence.candidateId !== assignment.candidateId && evidence.beforeCandidateId !== assignment.candidateId || !refsWithinScope(evidence.refs, assignment.scope) || !refsWithinScope(evidence.refs, task.data.intent.data.scope))
12438
+ if (evidence.requirementIds.some((id) => !assignment.requirementIds.includes(id)) || !checkPin(assignment, bound.beforeCandidateId, bound.candidateId) || !refsWithinScope(evidence.refs, assignment.scope) || !refsWithinScope(evidence.refs, task.data.intent.data.scope))
11988
12439
  return failure("permission_denied", "evidence is outside the worker assignment");
11989
12440
  }
11990
12441
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
11991
- for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
12442
+ for (const candidateId of [bound.beforeCandidateId, bound.candidateId]) {
11992
12443
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
11993
12444
  return failure("invalid_input", "evidence candidate is not a captured candidate");
11994
12445
  }
12446
+ const boundEvidence = { ...evidence, ...bound };
11995
12447
  const changed = this.store.mutateTask(task.data.id, input.expectedRevision, (current, mutation) => {
11996
12448
  const candidates = current.candidates.some((item) => item.id === currentCandidate.data.id) ? current.candidates : [...current.candidates, currentCandidate.data];
11997
12449
  const entry = {
11998
12450
  id: newId(),
11999
12451
  recordedAt: mutation.now,
12000
12452
  provenance: provenance(this.context, "agent_reported"),
12001
- data: evidence
12453
+ data: boundEvidence
12002
12454
  };
12003
12455
  return success(mutation.revision, null, {
12004
12456
  ...current,
12005
12457
  candidates,
12006
12458
  evidence: [...current.evidence, entry],
12007
12459
  findings: current.findings.map((finding) => {
12008
- if (finding.data.disposition === "open" || evidence.result === "skipped")
12460
+ if (finding.data.disposition === "open" || boundEvidence.result === "skipped")
12009
12461
  return finding;
12010
12462
  if (finding.data.disposition !== "fixed")
12011
12463
  return finding;
@@ -12014,8 +12466,12 @@ class WorkitCore {
12014
12466
  item.evidenceId,
12015
12467
  item.status
12016
12468
  ]));
12017
- const verified = withEntry.evidence.some((item) => statuses.get(item.id) === "passed" && (item.data.kind === "check" || item.data.kind === "review") && (finding.data.candidateId === null || item.data.candidateId === finding.data.candidateId));
12018
- const contradicted = evidence.result === "failed" && finding.data.refs.some((left) => evidence.refs.some((right) => JSON.stringify(left) === JSON.stringify(right)));
12469
+ const verified = withEntry.evidence.some((item) => findingVerificationPasses(finding.data.candidateId, {
12470
+ kind: item.data.kind,
12471
+ candidateId: item.data.candidateId,
12472
+ status: statuses.get(item.id) ?? "missing"
12473
+ }));
12474
+ const contradicted = boundEvidence.result === "failed" && finding.data.refs.some((left) => boundEvidence.refs.some((right) => sameValue(left, right)));
12019
12475
  return verified && !contradicted ? finding : { ...finding, data: { ...finding.data, disposition: "open", resolution: null } };
12020
12476
  })
12021
12477
  });
@@ -12030,7 +12486,10 @@ class WorkitCore {
12030
12486
  observeDecision(request, observation) {
12031
12487
  return this.recordDecision(request, observation, true);
12032
12488
  }
12033
- recordDecision(request, nativeObservation, nativeRequired = false) {
12489
+ observeStandingDecision(request) {
12490
+ return this.recordDecision(request, undefined, true, true);
12491
+ }
12492
+ recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
12034
12493
  const root = this.contextRootError();
12035
12494
  if (!root.ok)
12036
12495
  return root;
@@ -12045,8 +12504,10 @@ class WorkitCore {
12045
12504
  return failure("permission_denied", "stated choices cannot authorize mutating actions");
12046
12505
  if (stated && !input.binding?.statedChoice)
12047
12506
  return failure("invalid_input", "stated choices require binding.statedChoice");
12048
- if (nativeRequired && nativeObservation === undefined && !stated)
12507
+ if (nativeRequired && nativeObservation === undefined && !stated && !standing)
12049
12508
  return failure("permission_denied", "native decision observation is required");
12509
+ if (standing && (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved"))
12510
+ return failure("invalid_input", "standing approvals need an approved action binding with a standing rule");
12050
12511
  const task = this.store.readTask(input.taskId);
12051
12512
  if (!task.ok)
12052
12513
  return task;
@@ -12078,7 +12539,10 @@ class WorkitCore {
12078
12539
  consumption: null
12079
12540
  };
12080
12541
  const data = { ...base, digest: decisionDigest(base) };
12081
- const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
12542
+ const standingApproval = standing && !stated ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding) : null;
12543
+ if (standingApproval && !standingApproval.ok)
12544
+ return standingApproval;
12545
+ const native = nativeRequired && !stated && !standingApproval ? verifyNativeDecision(this.context.nativeAuthority, {
12082
12546
  observation: nativeObservation,
12083
12547
  expected: {
12084
12548
  taskId: task.data.id,
@@ -12109,7 +12573,7 @@ class WorkitCore {
12109
12573
  const entry = {
12110
12574
  id: newId(),
12111
12575
  recordedAt: mutation.now,
12112
- provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
12576
+ provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
12113
12577
  data
12114
12578
  };
12115
12579
  return success(mutation.revision, null, {
@@ -12173,7 +12637,7 @@ class WorkitCore {
12173
12637
  if (input.action === "record") {
12174
12638
  if (helper?.ok) {
12175
12639
  const assignment = helper.data.data.assignment;
12176
- if (!bindingCovers(assignment.scope, input.scope) || !bindingCovers(task.data.intent.data.scope, input.scope) || assignment.candidateId !== null && input.candidateId !== assignment.candidateId || !refsWithinScope(input.refs, assignment.scope) || !refsWithinScope(input.refs, task.data.intent.data.scope))
12640
+ if (!bindingCovers(assignment.scope, input.scope) || !bindingCovers(task.data.intent.data.scope, input.scope) || !checkPin(assignment, null, input.candidateId ?? null) || !refsWithinScope(input.refs, assignment.scope) || !refsWithinScope(input.refs, task.data.intent.data.scope))
12177
12641
  return failure("permission_denied", "finding is outside the worker assignment");
12178
12642
  }
12179
12643
  if (input.candidateId && !task.data.candidates.some((candidate) => candidate.id === input.candidateId))
@@ -12220,7 +12684,11 @@ class WorkitCore {
12220
12684
  const evaluations = evaluateEvidence(task.data, current.data);
12221
12685
  const verified = evidence.some((entry) => {
12222
12686
  const evaluation = evaluations.find((item) => item.evidenceId === entry.id);
12223
- return evaluation?.status === "passed" && (entry.data.kind === "check" || entry.data.kind === "review") && (finding.data.candidateId === null || entry.data.candidateId === finding.data.candidateId);
12687
+ return findingVerificationPasses(finding.data.candidateId, {
12688
+ kind: entry.data.kind,
12689
+ candidateId: entry.data.candidateId,
12690
+ status: evaluation?.status ?? "missing"
12691
+ });
12224
12692
  });
12225
12693
  if (!verified)
12226
12694
  return failure("permission_denied", "fixed findings require passing verification evidence");
@@ -12232,7 +12700,7 @@ class WorkitCore {
12232
12700
  if (entry.data.result === "missing" || entry.data.result === "skipped")
12233
12701
  return false;
12234
12702
  const candidateMatches = finding.data.candidateId === null ? entry.data.candidateId === null : entry.data.candidateId === finding.data.candidateId || entry.data.beforeCandidateId === finding.data.candidateId;
12235
- const referenceMatches = finding.data.refs.some((left) => entry.data.refs.some((right) => JSON.stringify(left) === JSON.stringify(right)));
12703
+ const referenceMatches = finding.data.refs.some((left) => entry.data.refs.some((right) => sameValue(left, right)));
12236
12704
  return candidateMatches && (referenceMatches || entry.data.claim === finding.data.claim);
12237
12705
  });
12238
12706
  if (!relevant)
@@ -12310,7 +12778,7 @@ class WorkitCore {
12310
12778
  const workerId = newId();
12311
12779
  const changed = this.store.mutateTaskAndWorkspace({
12312
12780
  taskId: task.data.id,
12313
- expectedTaskRevision: input.expectedRevision,
12781
+ expectedRevision: input.expectedRevision,
12314
12782
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
12315
12783
  now: trustedNow(this.context),
12316
12784
  workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
@@ -12348,7 +12816,7 @@ class WorkitCore {
12348
12816
  return failure("permission_denied", "worker report is outside the worker assignment");
12349
12817
  const changed = this.store.mutateTaskAndWorkspace({
12350
12818
  taskId: task.data.id,
12351
- expectedTaskRevision: input.expectedRevision,
12819
+ expectedRevision: input.expectedRevision,
12352
12820
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
12353
12821
  now: trustedNow(this.context),
12354
12822
  workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
@@ -12372,24 +12840,25 @@ class WorkitCore {
12372
12840
  return failure("invalid_transition", "closed task cannot cancel workers");
12373
12841
  if (entry.data.state === "unknown")
12374
12842
  return failure("recovery_required", "worker state requires recovery");
12375
- if (entry.data.state === "stopped")
12376
- return failure("invalid_transition", "stopped worker cannot be cancelled");
12377
- const settles = entry.data.report !== null || entry.data.state === "cancelling";
12843
+ if (entry.data.state === "stopped") {
12844
+ const stopped = task.data.workers.find((candidate) => candidate.id === input.workerId);
12845
+ return success(task.data.revision, workspace.data.revision, stopped);
12846
+ }
12378
12847
  const changed = this.store.mutateTaskAndWorkspace({
12379
12848
  taskId: task.data.id,
12380
- expectedTaskRevision: input.expectedRevision,
12849
+ expectedRevision: input.expectedRevision,
12381
12850
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
12382
12851
  now: trustedNow(this.context),
12383
- workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
12852
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, {
12853
+ ...current,
12854
+ writer: current.writer?.owner.workerId === input.workerId ? null : current.writer
12855
+ }),
12384
12856
  task: (current, mutation) => success(mutation.revision, null, {
12385
12857
  ...current,
12386
12858
  workers: current.workers.map((candidate) => candidate.id === input.workerId ? {
12387
12859
  ...candidate,
12388
12860
  recordedAt: mutation.now,
12389
- data: {
12390
- ...candidate.data,
12391
- state: settles ? "stopped" : "cancelling"
12392
- }
12861
+ data: { ...candidate.data, state: "stopped" }
12393
12862
  } : candidate)
12394
12863
  })
12395
12864
  });
@@ -12486,7 +12955,7 @@ class WorkitCore {
12486
12955
  return attested;
12487
12956
  const changed = this.store.mutateTaskAndWorkspace({
12488
12957
  taskId: task.data.id,
12489
- expectedTaskRevision: input.expectedRevision,
12958
+ expectedRevision: input.expectedRevision,
12490
12959
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
12491
12960
  now: trustedNow(this.context),
12492
12961
  workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
@@ -12652,6 +13121,12 @@ class WorkitCore {
12652
13121
  if (!sameSession2(helper.data.data.session, this.context))
12653
13122
  return failure("permission_denied", "worker session does not match the caller");
12654
13123
  }
13124
+ const gate = this.view(task.data);
13125
+ if (gate.ok) {
13126
+ const writeGates = (task.data.policy?.requirements ?? []).filter((requirement) => requirement.before === "write" && gate.data.requirements.some((evaluation) => evaluation.requirementId === requirement.id && (evaluation.status === "unsatisfied" || evaluation.status === "unavailable")));
13127
+ if (writeGates.length)
13128
+ return failure("requirements_unsatisfied", "writer ownership is gated by unsatisfied requirements; record their evidence or an approved limitation waiver first", { requirementIds: writeGates.map((requirement) => requirement.id) });
13129
+ }
12655
13130
  const owner = {
12656
13131
  taskId: task.data.id,
12657
13132
  workerId: helperId,
@@ -12663,7 +13138,7 @@ class WorkitCore {
12663
13138
  };
12664
13139
  const changed = this.store.mutateTaskAndWorkspace({
12665
13140
  taskId: task.data.id,
12666
- expectedTaskRevision: input.expectedRevision,
13141
+ expectedRevision: input.expectedRevision,
12667
13142
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
12668
13143
  now: trustedNow(this.context),
12669
13144
  workspace: (current, mutation) => success(mutation.revision, mutation.revision, {
@@ -12694,7 +13169,7 @@ class WorkitCore {
12694
13169
  }
12695
13170
  const changed = this.store.mutateTaskAndWorkspace({
12696
13171
  taskId: task.data.id,
12697
- expectedTaskRevision: input.expectedRevision,
13172
+ expectedRevision: input.expectedRevision,
12698
13173
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
12699
13174
  now: trustedNow(this.context),
12700
13175
  workspace: (currentWorkspace, mutation) => success(mutation.revision, mutation.revision, { ...currentWorkspace, writer: null }),
@@ -12926,7 +13401,7 @@ class WorkitCore {
12926
13401
  if (view.task.workspaceId !== view.workspace.id)
12927
13402
  return failure("invalid_input", "task and workspace bindings are invalid");
12928
13403
  try {
12929
- if (realpathSync4(view.workspace.root) !== this.store.root)
13404
+ if (realpathSync5(view.workspace.root) !== this.store.root)
12930
13405
  return failure("invalid_input", "task view root does not match the task store");
12931
13406
  } catch {
12932
13407
  return failure("invalid_input", "task view root cannot be resolved");
@@ -12943,7 +13418,15 @@ class WorkitCore {
12943
13418
  if (seen.has(observation.workerId))
12944
13419
  return failure("invalid_input", "worker observations must be unique");
12945
13420
  seen.add(observation.workerId);
12946
- if (observation.taskId !== view.task.id || observation.expectedRevision !== view.task.revision || observation.expectedWorkspaceRevision !== view.workspace.revision)
13421
+ const freshTask = this.store.readTask(view.task.id);
13422
+ const freshWorkspace = this.store.readWorkspace();
13423
+ if (!freshTask.ok)
13424
+ return freshTask;
13425
+ if (!freshWorkspace.ok)
13426
+ return freshWorkspace;
13427
+ if (!freshWorkspace.data)
13428
+ return failure("not_found", "workspace not found");
13429
+ if (observation.taskId !== view.task.id || observation.expectedRevision !== freshTask.data.revision || observation.expectedWorkspaceRevision !== freshWorkspace.data.revision)
12947
13430
  return failure("revision_conflict", "worker observation is stale");
12948
13431
  if (!view.task.workers.some((entry) => entry.id === observation.workerId))
12949
13432
  return failure("permission_denied", "worker observation is not assigned");
@@ -13029,19 +13512,26 @@ class WorkitCore {
13029
13512
  return failure("permission_denied", "imported resume requires native destination approval");
13030
13513
  resumeCandidate = current.data.candidate;
13031
13514
  }
13032
- const blocker = this.activeWorkerBlocker(task.data, workspace.data);
13515
+ const blocker = status === "paused" ? workspace.data.writer ? failure("recovery_required", "writer ownership must be released first") : null : this.activeWorkerBlocker(task.data, workspace.data);
13033
13516
  if (blocker)
13034
13517
  return blocker;
13518
+ let pauseCandidate = null;
13519
+ if (status === "paused") {
13520
+ const captured = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
13521
+ if (!captured.ok)
13522
+ return captured;
13523
+ pauseCandidate = captured.data;
13524
+ }
13035
13525
  const changed = this.store.mutateTaskAndWorkspace({
13036
13526
  taskId: task.data.id,
13037
- expectedTaskRevision: input.expectedRevision,
13527
+ expectedRevision: input.expectedRevision,
13038
13528
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
13039
13529
  now: trustedNow(this.context),
13040
13530
  workspace: (workspace, context) => success(context.revision, context.revision, workspace),
13041
13531
  task: (current, context) => success(context.revision, null, {
13042
13532
  ...current,
13043
13533
  status,
13044
- candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
13534
+ candidates: (resumeCandidate ?? pauseCandidate) && !current.candidates.some((candidate) => candidate.id === (resumeCandidate ?? pauseCandidate).id) ? [...current.candidates, resumeCandidate ?? pauseCandidate] : current.candidates,
13045
13535
  progress: current.progress,
13046
13536
  pauseReason: status === "paused" ? input.reason ?? null : null
13047
13537
  })
@@ -13079,12 +13569,13 @@ class WorkitCore {
13079
13569
  return workspace;
13080
13570
  if (!workspace.data)
13081
13571
  return failure("not_found", "workspace not found");
13082
- if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
13083
- return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13572
+ const revisionBlocker = this.activeWorkerBlocker(task.data, workspace.data);
13573
+ if (revisionBlocker)
13574
+ return revisionBlocker;
13084
13575
  this.fillRevisions(input, task.data, workspace.data);
13085
13576
  const changed = this.store.mutateTaskAndWorkspace({
13086
13577
  taskId: task.data.id,
13087
- expectedTaskRevision: input.expectedRevision,
13578
+ expectedRevision: input.expectedRevision,
13088
13579
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
13089
13580
  now: trustedNow(this.context),
13090
13581
  workspace: (workspace, context) => success(context.revision, context.revision, workspace),
@@ -13140,7 +13631,7 @@ class WorkitCore {
13140
13631
  return closure;
13141
13632
  const changed = this.store.mutateTaskAndWorkspace({
13142
13633
  taskId: task.data.id,
13143
- expectedTaskRevision: input.expectedRevision,
13634
+ expectedRevision: input.expectedRevision,
13144
13635
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
13145
13636
  now: trustedNow(this.context),
13146
13637
  workspace: (value, context) => success(context.revision, context.revision, value),
@@ -13300,7 +13791,7 @@ var parseCodexHookInput = (value) => {
13300
13791
  return { ok: false, error: "permission_mode is invalid" };
13301
13792
  if (!(value.transcript_path === null || nonEmpty2(value.transcript_path)))
13302
13793
  return { ok: false, error: "transcript_path must be a string or null" };
13303
- if (!nonEmpty2(value.cwd) || !path7.isAbsolute(value.cwd) || !existsSync4(value.cwd))
13794
+ if (!nonEmpty2(value.cwd) || !path9.isAbsolute(value.cwd) || !existsSync5(value.cwd))
13304
13795
  return { ok: false, error: "cwd must be an existing absolute path" };
13305
13796
  try {
13306
13797
  if (!statSync(value.cwd).isDirectory())
@@ -13326,7 +13817,7 @@ var parseCodexHookInput = (value) => {
13326
13817
  model: value.model,
13327
13818
  permission_mode: value.permission_mode,
13328
13819
  transcript_path: value.transcript_path,
13329
- cwd: realpathSync5(value.cwd),
13820
+ cwd: realpathSync6(value.cwd),
13330
13821
  ...event === "SessionStart" ? { source: value.source } : {},
13331
13822
  ...nonEmpty2(value.turn_id) ? { turn_id: value.turn_id } : {},
13332
13823
  ...nonEmpty2(value.tool_name) ? { tool_name: value.tool_name } : {},