@brainervirus/workit-mcp 1.0.12 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1701 -1517
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6623,7 +6623,7 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
6623
6623
  import path15 from "node:path";
6624
6624
 
6625
6625
  // packages/workit-mcp/src/server.ts
6626
- import { existsSync as existsSync7, readFileSync as readFileSync7, realpathSync as realpathSync7 } from "node:fs";
6626
+ import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync7 } from "node:fs";
6627
6627
  import path14 from "node:path";
6628
6628
 
6629
6629
  // node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/core.js
@@ -20050,7 +20050,13 @@ var refSchema = discriminatedUnion2("kind", [
20050
20050
  id
20051
20051
  }).strict(),
20052
20052
  object4({ kind: literal3("host"), host: hostSchema, handle: nonEmpty }).strict(),
20053
- object4({ kind: literal3("external"), url: text.url() }).strict()
20053
+ object4({ kind: literal3("external"), url: text.url() }).strict(),
20054
+ object4({
20055
+ kind: literal3("standing"),
20056
+ workspace: nonEmpty,
20057
+ class: nonEmpty,
20058
+ configDigest: nullableDigest
20059
+ }).strict()
20054
20060
  ]);
20055
20061
  var rewriteUnknown = (value, map) => {
20056
20062
  if (Array.isArray(value)) {
@@ -20355,7 +20361,8 @@ var decisionSchema = object4({
20355
20361
  approvedContent: text,
20356
20362
  displayed: text.optional(),
20357
20363
  contentRefs: array3(refSchema),
20358
- statedChoice: object4({ ref: text, text }).strict().optional()
20364
+ statedChoice: object4({ ref: text, text }).strict().optional(),
20365
+ standing: object4({ workspace: nonEmpty, class: nonEmpty }).strict().optional()
20359
20366
  }).strict(),
20360
20367
  digest,
20361
20368
  response: _enum2(["approved", "rejected", "stated"]),
@@ -22431,1622 +22438,1784 @@ function assertProductWriteAllowed(input) {
22431
22438
  }
22432
22439
 
22433
22440
  // packages/workit-core/src/core/task-evaluation.ts
22441
+ import { createHash as createHash5 } from "node:crypto";
22442
+ import * as fs7 from "node:fs";
22443
+ import { spawnSync as spawnSync2 } from "node:child_process";
22444
+ import path9 from "node:path";
22445
+
22446
+ // packages/workit-core/src/core/authority.ts
22434
22447
  import { createHash as createHash4 } from "node:crypto";
22435
22448
  import * as fs6 from "node:fs";
22436
- import { spawnSync } from "node:child_process";
22437
- import path6 from "node:path";
22449
+ import path8 from "node:path";
22438
22450
 
22439
- // packages/workit-core/src/core/authority.ts
22451
+ // packages/workit-core/src/core/auto-approval.ts
22440
22452
  import { createHash as createHash3 } from "node:crypto";
22441
- import * as fs5 from "node:fs";
22453
+ import { readFileSync as readFileSync4 } from "node:fs";
22454
+
22455
+ // packages/workit-core/src/core/workspaces.ts
22456
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
22457
+ import path6 from "node:path";
22458
+
22459
+ // packages/workit-core/src/core/config.ts
22460
+ import {
22461
+ copyFileSync,
22462
+ cpSync,
22463
+ existsSync as existsSync3,
22464
+ mkdirSync as mkdirSync2,
22465
+ readFileSync as readFileSync2,
22466
+ readdirSync as readdirSync2,
22467
+ writeFileSync as writeFileSync2
22468
+ } from "node:fs";
22469
+ import os from "node:os";
22442
22470
  import path5 from "node:path";
22443
- var verifiedAuthorities = new WeakMap;
22444
- var trustedAuthority = (kind, provenance, expected, binding, caller) => {
22445
- const token = {};
22446
- verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
22447
- return token;
22471
+
22472
+ // packages/workit-core/src/core/boundary.ts
22473
+ import { statSync } from "node:fs";
22474
+ var EVENT = {
22475
+ initialization: "initialization",
22476
+ provenance: "provenance",
22477
+ assets: "assets",
22478
+ configurationSource: "configuration_source",
22479
+ hooks: "hooks",
22480
+ mcpConnection: "mcp_connection",
22481
+ migration: "migration",
22482
+ installSteps: "install_steps",
22483
+ uncaughtFailure: "uncaught_failure",
22484
+ toolsFailed: "tools_failed",
22485
+ doctor: "doctor"
22448
22486
  };
22449
- var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
22450
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22451
- var normalizeChainSteps = (steps) => {
22452
- if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
22453
- return null;
22454
- const normalized = [];
22455
- for (const step of steps) {
22456
- if (typeof step === "string") {
22457
- if (!step.trim())
22458
- return null;
22459
- normalized.push({ kind: "commit", message: step });
22460
- continue;
22461
- }
22462
- if (!isRecord(step))
22463
- return null;
22464
- if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
22465
- normalized.push({ kind: "branch", target: step.branch });
22466
- continue;
22467
- }
22468
- if (step.pr === true && Object.keys(step).length === 1) {
22469
- normalized.push({ kind: "pr" });
22487
+ var errorDetail = (err) => {
22488
+ if (err instanceof Error) {
22489
+ return { error_name: err.name, error: err.message };
22490
+ }
22491
+ return { error_name: "unknown", error: String(err) };
22492
+ };
22493
+ var markSourcesLoaded = (files, loadedAt = Date.now()) => ({
22494
+ loadedAt,
22495
+ files: [...files]
22496
+ });
22497
+ var changedSourcesSinceLoad = (marker) => {
22498
+ const changed = [];
22499
+ for (const file of marker.files) {
22500
+ try {
22501
+ if (statSync(file).mtimeMs > marker.loadedAt)
22502
+ changed.push(file);
22503
+ } catch {}
22504
+ }
22505
+ return changed;
22506
+ };
22507
+
22508
+ // packages/workit-core/src/core/config.ts
22509
+ var diagnosticLogger;
22510
+ var PRESETS = {
22511
+ gitflow: {
22512
+ allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
22513
+ protected: ["main", "develop", "master", "prod", "production"]
22514
+ },
22515
+ "github-flow": { allowed: ["*"], protected: ["main"] },
22516
+ "trunk-based": { allowed: ["*"], protected: ["main"] },
22517
+ custom: { allowed: [], protected: [] }
22518
+ };
22519
+ var mergePreset = (preset, input = {}, current = {
22520
+ branchPolicy: { preset, allowed: [], protected: [] }
22521
+ }) => {
22522
+ const defs = PRESETS[preset];
22523
+ return {
22524
+ preset,
22525
+ allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
22526
+ protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
22527
+ };
22528
+ };
22529
+ 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");
22530
+ var migratedDir = null;
22531
+ var migrationFailed = false;
22532
+ var ensureConfigDir = (dir = resolveConfigDir()) => {
22533
+ if (migratedDir === dir)
22534
+ return dir;
22535
+ if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
22536
+ migratedDir = dir;
22537
+ return dir;
22538
+ }
22539
+ const legacy = path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workflow-toolkit");
22540
+ if (!existsSync3(legacy)) {
22541
+ migratedDir = dir;
22542
+ return dir;
22543
+ }
22544
+ if (!migrationFailed && existsSync3(dir)) {
22545
+ migratedDir = dir;
22546
+ return dir;
22547
+ }
22548
+ migrationFailed = false;
22549
+ mkdirSync2(dir, { recursive: true });
22550
+ diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
22551
+ for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
22552
+ const src = path5.join(legacy, entry.name);
22553
+ const dest = path5.join(dir, entry.name);
22554
+ if (existsSync3(dest))
22470
22555
  continue;
22556
+ try {
22557
+ if (entry.isDirectory())
22558
+ cpSync(src, dest, { recursive: true });
22559
+ else if (entry.isFile())
22560
+ copyFileSync(src, dest);
22561
+ } catch (err) {
22562
+ migrationFailed = true;
22563
+ diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
22564
+ console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
22471
22565
  }
22472
- return null;
22473
22566
  }
22474
- const keys = normalized.map(chainStepKey);
22475
- if (new Set(keys).size !== keys.length)
22567
+ if (!migrationFailed)
22568
+ migratedDir = dir;
22569
+ return dir;
22570
+ };
22571
+ var configDir = () => ensureConfigDir();
22572
+ var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
22573
+ var DEFAULTS = {
22574
+ locale: "en",
22575
+ localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
22576
+ timezone: "America/Santiago",
22577
+ branchPolicy: {
22578
+ preset: "gitflow",
22579
+ allowed: [...PRESETS.gitflow.allowed],
22580
+ protected: [...PRESETS.gitflow.protected]
22581
+ },
22582
+ commitPolicy: { preset: "conventional" }
22583
+ };
22584
+ var readSafe = (p) => {
22585
+ try {
22586
+ return readFileSync2(p, "utf8");
22587
+ } catch {
22476
22588
  return null;
22477
- return normalized;
22589
+ }
22478
22590
  };
22479
- var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
22480
- var parseSteps = (content) => {
22481
- let value;
22591
+ var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
22592
+ var parseConfigResult = (raw, file) => {
22593
+ if (raw === null)
22594
+ return { status: "missing", path: file };
22595
+ let parsed;
22482
22596
  try {
22483
- value = JSON.parse(content);
22597
+ parsed = JSON.parse(raw);
22484
22598
  } catch {
22485
- return [];
22599
+ return { status: "malformed", path: file, error: `${file} is not valid JSON` };
22486
22600
  }
22487
- if (typeof value !== "object" || value === null || !("steps" in value))
22488
- return [];
22489
- const steps = value.steps;
22490
- if (!Array.isArray(steps))
22491
- return null;
22492
- const normalized = normalizeChainSteps(steps);
22493
- return normalized;
22601
+ if (!isConfigObject(parsed)) {
22602
+ return { status: "malformed", path: file, error: `${file} is not a JSON object` };
22603
+ }
22604
+ const input = parsed;
22605
+ const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
22606
+ const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
22607
+ const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
22608
+ return {
22609
+ status: "valid",
22610
+ path: file,
22611
+ config: {
22612
+ locale,
22613
+ localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
22614
+ timezone: input.timezone ?? DEFAULTS.timezone,
22615
+ branchPolicy: mergePreset(preset, {
22616
+ allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
22617
+ protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
22618
+ }, DEFAULTS),
22619
+ commitPolicy: {
22620
+ preset: commitPreset,
22621
+ ...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
22622
+ }
22623
+ }
22624
+ };
22494
22625
  };
22495
- var decisionMatches = (entry, purpose, binding) => {
22496
- const decision = entry.data;
22497
- 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);
22626
+ var readConfigTyped = (dir) => {
22627
+ const file = path5.join(dir ?? configDir(), "config.json");
22628
+ return parseConfigResult(readSafe(file), file);
22498
22629
  };
22499
- var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
22500
- var validProvenance = (value, caller) => {
22501
- if (!provenanceSchema.safeParse(value).success)
22502
- return false;
22503
- const provenance = value;
22504
- 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);
22630
+ var readConfig = () => {
22631
+ const result = readConfigTyped();
22632
+ if (result.status === "malformed")
22633
+ throw new Error(result.error);
22634
+ return result.config ?? DEFAULTS;
22505
22635
  };
22506
- var verifyNativeDecision = (verifier, input, binding) => {
22507
- if (!verifier)
22508
- return failure("permission_denied", "native authority verifier is unavailable");
22509
- const result = verifier.verifyDecision(input);
22510
- if (!result.ok || !validProvenance(result.data, input.caller))
22511
- return failure("permission_denied", "native decision observation was not attested");
22512
- return success(null, null, trustedAuthority("decision", result.data, {
22513
- taskId: input.expected.taskId,
22514
- workspaceId: input.expected.workspaceId,
22515
- purpose: input.expected.purpose,
22516
- response: input.expected.response,
22517
- binding: input.expected.binding,
22518
- bindingBytes: input.expected.bindingBytes,
22519
- digest: input.expected.digest,
22520
- requirementIds: [...input.expected.requirementIds]
22521
- }, binding, input.caller));
22636
+ var resolveBranchPolicy = (config, workspace) => {
22637
+ const wp = workspace?.branchPolicy ?? {};
22638
+ const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
22639
+ const merged = mergePreset(preset, {
22640
+ allowed: wp.allowed,
22641
+ protectedNames: wp.protected
22642
+ }, config);
22643
+ const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
22644
+ return {
22645
+ preset,
22646
+ allowed,
22647
+ protected: new Set(merged.protected.map((p) => p.toLowerCase())),
22648
+ integration: wp.integration === "merge" ? "merge" : "pr",
22649
+ defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
22650
+ };
22522
22651
  };
22523
- var verifyNativeAction = (verifier, input, binding) => {
22524
- if (!verifier)
22525
- return failure("permission_denied", "native authority verifier is unavailable");
22526
- const result = verifier.verifyAction(input);
22527
- if (!result.ok || !validProvenance(result.data, input.caller))
22528
- return failure("permission_denied", "native action observation was not attested");
22529
- return success(null, null, trustedAuthority("action", result.data, {
22530
- taskId: input.expected.taskId,
22531
- workspaceId: input.expected.workspaceId,
22532
- decisionId: input.expected.decisionId,
22533
- actionRef: input.expected.actionRef,
22534
- taskRevision: input.expected.taskRevision,
22535
- workspaceRevision: input.expected.workspaceRevision,
22536
- outcome: input.expected.outcome,
22537
- purpose: input.expected.decision.purpose,
22538
- response: input.expected.decision.response,
22539
- binding: input.expected.decision.binding,
22540
- digest: input.expected.decision.digest,
22541
- requirementIds: [...input.expected.decision.requirementIds]
22542
- }, binding, input.caller));
22543
- };
22544
- var verifyNativeReconciliation = (verifier, input, binding) => {
22545
- if (!verifier?.verifyReconciliation)
22546
- return failure("permission_denied", "native reconciliation authority is unavailable");
22547
- const result = verifier.verifyReconciliation(input);
22548
- if (!result.ok || !validProvenance(result.data, input.caller))
22549
- return failure("permission_denied", "native reconciliation observation was not attested");
22550
- return success(null, null, trustedAuthority("reconciliation", result.data, {
22551
- taskId: input.expected.taskId,
22552
- workspaceId: input.expected.workspaceId,
22553
- decisionId: input.expected.decisionId,
22554
- actionRef: input.expected.actionRef,
22555
- taskRevision: input.expected.taskRevision,
22556
- workspaceRevision: input.expected.workspaceRevision,
22557
- outcome: input.expected.outcome,
22558
- evidenceDigest: input.expected.evidenceDigest,
22559
- step: input.expected.step,
22560
- purpose: input.expected.decision.purpose,
22561
- response: input.expected.decision.response,
22562
- binding: input.expected.decision.binding,
22563
- digest: input.expected.decision.digest,
22564
- requirementIds: [...input.expected.decision.requirementIds]
22565
- }, binding, input.caller));
22566
- };
22567
- var takeAuthority = (authority, binding, caller) => {
22568
- const record = verifiedAuthorities.get(authority);
22569
- verifiedAuthorities.delete(authority);
22570
- if (!record)
22571
- return null;
22572
- if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
22573
- return null;
22574
- return record;
22652
+ var COMMIT_PRESETS = [
22653
+ "conventional",
22654
+ "gitmoji",
22655
+ "ticket-prefix",
22656
+ "freeform",
22657
+ "custom",
22658
+ "auto"
22659
+ ];
22660
+
22661
+ // packages/workit-core/src/core/workspaces.ts
22662
+ var readWorkspacesResult = (dir = configDir()) => {
22663
+ const file = path6.join(dir, "workspaces.json");
22664
+ let raw;
22665
+ try {
22666
+ raw = readFileSync3(file, "utf8");
22667
+ } catch {
22668
+ return { status: "missing", path: file, entries: [] };
22669
+ }
22670
+ let parsed;
22671
+ try {
22672
+ parsed = JSON.parse(raw);
22673
+ } catch {
22674
+ return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
22675
+ }
22676
+ if (!isConfigObject(parsed)) {
22677
+ return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
22678
+ }
22679
+ const list = parsed.workspaces;
22680
+ return {
22681
+ status: "valid",
22682
+ path: file,
22683
+ entries: Array.isArray(list) ? list : []
22684
+ };
22575
22685
  };
22576
- var retireNativeAuthority = (authority) => {
22577
- const record = verifiedAuthorities.get(authority);
22578
- verifiedAuthorities.delete(authority);
22579
- return record?.provenance ?? null;
22686
+ var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
22687
+ var globToRegExp = (glob) => {
22688
+ let out = "";
22689
+ for (let i = 0;i < glob.length; i++) {
22690
+ const c = glob[i];
22691
+ if (c === "*") {
22692
+ if (glob[i + 1] === "*") {
22693
+ if (glob[i + 2] === "/") {
22694
+ if (out === "")
22695
+ out += "/?";
22696
+ out += "(?:[^/]+/)*";
22697
+ i += 2;
22698
+ } else {
22699
+ if (i + 2 >= glob.length) {
22700
+ if (out.endsWith("/")) {
22701
+ out = out.slice(0, -1);
22702
+ out += "(?:/.*)?";
22703
+ } else {
22704
+ out += ".*";
22705
+ }
22706
+ } else {
22707
+ out += ".*";
22708
+ }
22709
+ i++;
22710
+ }
22711
+ } else {
22712
+ out += "[^/]*";
22713
+ }
22714
+ } else {
22715
+ out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
22716
+ }
22717
+ }
22718
+ return new RegExp(`^${out}$`);
22580
22719
  };
22581
- 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;
22582
- 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);
22583
- var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
22584
- function applicableDecision(task, purpose, binding, checkoutRoot) {
22585
- if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
22586
- return [];
22587
- 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);
22588
- }
22589
- var digestBytes2 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
22590
- var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
22591
- let root;
22720
+ var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
22721
+ var realpathOf = (p) => {
22722
+ const real = realpathSync2.native ?? realpathSync2;
22592
22723
  try {
22593
- root = fs5.realpathSync(checkoutRoot);
22724
+ return real(p);
22594
22725
  } catch {
22595
- return failure("permission_denied", "checkout root is unavailable");
22726
+ return p;
22596
22727
  }
22597
- for (const reference of binding.contentRefs) {
22598
- if (reference.kind !== "file")
22728
+ };
22729
+ var canonicalGlob = (glob) => {
22730
+ const m = /[*?[\]{]/.exec(glob);
22731
+ const prefix = m ? glob.slice(0, m.index) : glob;
22732
+ const rest = m ? glob.slice(m.index) : "";
22733
+ if (!prefix)
22734
+ return glob;
22735
+ const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
22736
+ return real + (prefix.endsWith("/") ? "/" : "") + rest;
22737
+ };
22738
+ var resolveWorkspaceFrom = (cwd, dir) => {
22739
+ const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
22740
+ for (const entry of loadWorkspacesFrom(dir)) {
22741
+ if (!entry || typeof entry !== "object")
22599
22742
  continue;
22600
- if (!reference.digest)
22601
- return failure("invalid_input", "document references require a byte digest");
22602
- const target = path5.resolve(root, reference.path);
22603
- if (target !== root && !target.startsWith(`${root}${path5.sep}`))
22604
- return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
22605
- fields: [
22606
- { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
22607
- ]
22608
- });
22609
- try {
22610
- const relative = path5.relative(root, target);
22611
- let current = root;
22612
- for (const segment of relative.split(path5.sep)) {
22613
- if (!segment)
22614
- continue;
22615
- current = path5.join(current, segment);
22616
- const stat = fs5.lstatSync(current);
22617
- const real = fs5.realpathSync(current);
22618
- if (real !== root && !real.startsWith(`${root}${path5.sep}`))
22619
- return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
22620
- fields: [
22621
- {
22622
- path: "contentRefs.path",
22623
- reason: `${reference.path} resolves outside the checkout`
22624
- }
22625
- ]
22626
- });
22627
- if (stat.isSymbolicLink())
22628
- return failure("invalid_input", "document reference uses a symlink");
22629
- if (current === target && !stat.isFile())
22630
- return failure("invalid_input", "document reference is not a regular file");
22631
- if (current !== target && !stat.isDirectory())
22632
- return failure("invalid_input", "document reference ancestor is not a directory");
22633
- }
22634
- if (digestBytes2(fs5.readFileSync(target)) !== reference.digest)
22635
- return failure("permission_denied", "approved document bytes have changed");
22636
- } catch {
22637
- return failure("invalid_input", "approved document is unavailable");
22743
+ const ws = entry;
22744
+ if (typeof ws.glob !== "string" || !ws.glob)
22745
+ continue;
22746
+ const glob = ws.glob.replaceAll("\\", "/");
22747
+ const canonical = canonicalGlob(glob);
22748
+ for (const target of targets) {
22749
+ if (matchWorkspace(glob, target))
22750
+ return ws;
22751
+ if (canonical !== glob && matchWorkspace(canonical, target))
22752
+ return ws;
22638
22753
  }
22639
22754
  }
22640
- return success(null, null, null);
22755
+ return null;
22641
22756
  };
22642
- var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
22643
- 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);
22644
- var validateAction = (store, task, workspaceId, input, authority) => {
22645
- if (task.status !== "active")
22646
- return failure("invalid_transition", "only active tasks can authorize actions");
22647
- if (!refSchema.safeParse(input.actionRef).success)
22648
- return failure("invalid_input", "action reference is invalid");
22649
- const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
22650
- if (!entry)
22651
- return failure("not_found", "decision not found");
22652
- const decision = entry.data;
22653
- if (decision.purpose !== "action")
22654
- return failure("permission_denied", "decision is not an action approval");
22655
- if (!authorityMatches(authority, {
22656
- taskId: task.id,
22657
- workspaceId,
22658
- decisionId: input.decisionId,
22659
- actionRef: input.actionRef,
22660
- taskRevision: input.expectedRevision,
22661
- workspaceRevision: input.expectedWorkspaceRevision,
22662
- outcome: "reserve"
22663
- }))
22664
- return failure("permission_denied", "native reservation authority is not bound");
22665
- if (!authorityDecisionMatches(authority, decision))
22666
- return failure("permission_denied", "native reservation authority does not match decision");
22667
- if (decision.response !== "approved")
22668
- return failure("permission_denied", "decision was rejected");
22669
- if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
22670
- return failure("permission_denied", "action approval lacks native receipt assurance");
22671
- if (decision.revoked)
22672
- return failure("permission_denied", "decision is revoked");
22673
- if (decision.digest !== decisionDigest(decision))
22674
- return failure("permission_denied", "decision binding is invalid");
22675
- if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
22676
- return failure("permission_denied", "decision task or workspace binding is invalid");
22677
- if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
22678
- return failure("permission_denied", "action binding does not match the approved decision");
22679
- const content = verifyContentRefs(store, decision.binding);
22680
- if (!content.ok)
22681
- return content;
22682
- const requestedSteps = parseSteps(decision.binding.approvedContent);
22683
- if (requestedSteps === null)
22684
- return failure("invalid_input", "bounded action steps must be a unique non-empty array");
22685
- if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
22686
- return failure("permission_denied", "bounded action steps do not match approved content");
22687
- const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
22688
- const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
22689
- if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
22690
- return failure("permission_denied", "bounded action scope changed");
22691
- return success(null, null, { entry, workflow });
22757
+ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
22758
+
22759
+ // packages/workit-core/src/core/branch.ts
22760
+ import { execFileSync as execFileSync2 } from "node:child_process";
22761
+
22762
+ // packages/workit-core/src/core/git.ts
22763
+ import { execFileSync } from "node:child_process";
22764
+ var run = (cwd, args) => {
22765
+ try {
22766
+ const stdout = execFileSync("git", args, {
22767
+ cwd,
22768
+ encoding: "utf8",
22769
+ stdio: ["pipe", "pipe", "pipe"]
22770
+ });
22771
+ return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
22772
+ } catch (error) {
22773
+ const e = error;
22774
+ return {
22775
+ stdout: (e.stdout ?? "").trimEnd(),
22776
+ stderr: (e.stderr ?? "").trimEnd(),
22777
+ exitCode: e.status ?? 1
22778
+ };
22779
+ }
22692
22780
  };
22693
- function reserveAction(input) {
22694
- if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
22695
- return failure("invalid_input", "action reservation preconditions are required");
22696
- const authority = takeAuthority(input.authority, {
22697
- owner: input.authorityOwner,
22698
- store: input.store,
22699
- root: input.store.root
22700
- }, input.authorityCaller);
22701
- if (!authority)
22702
- return failure("permission_denied", "native reservation authority is not verified");
22703
- const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
22704
- const workspace = input.store.readWorkspace();
22705
- if (!workspace.ok)
22706
- return workspace;
22707
- if (!workspace.data)
22708
- return failure("not_found", "workspace not found");
22709
- if (workspace.data.revision !== input.expectedWorkspaceRevision)
22710
- return failure("revision_conflict", "workspace revision does not match", {
22711
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
22712
- actualWorkspaceRevision: workspace.data.revision
22713
- });
22714
- const valid = validateAction(input.store, current, workspace.data.id, input, authority);
22715
- if (!valid.ok)
22716
- return valid;
22717
- const { entry, workflow } = valid.data;
22718
- const existing = entry.data.consumption;
22719
- if (existing?.state === "uncertain")
22720
- return failure("external_outcome_unknown", "previous action outcome is unknown", {
22721
- operation: "reserve_action",
22722
- outcome: "unknown"
22723
- });
22724
- if (existing?.state === "consumed")
22725
- return failure("permission_denied", "action approval is consumed");
22726
- if (existing?.state === "reserved" && workflow.completed.length === 0)
22727
- return failure("permission_denied", "action approval is already reserved");
22728
- const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
22729
- if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
22730
- return failure("permission_denied", "action step is outside the approved remaining workflow");
22731
- if (requestedStep && workflow.completed.includes(requestedStep))
22732
- return failure("permission_denied", "action step was already completed");
22733
- const nextDecision = {
22734
- ...entry.data,
22735
- consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
22736
- };
22737
- const actionProgress = workflow.steps.length ? [
22738
- ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
22739
- {
22740
- decisionId: input.decisionId,
22741
- steps: [...workflow.steps],
22742
- completedSteps: [...workflow.completed]
22743
- }
22744
- ] : current.actionProgress ?? [];
22745
- return success(mutation.revision, null, {
22746
- ...current,
22747
- actionProgress,
22748
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
22749
- });
22750
- }, input.native?.now);
22751
- if (!changed.ok)
22752
- return changed;
22753
- const workspace = input.store.readWorkspace();
22754
- if (!workspace.ok || !workspace.data)
22755
- return failure("recovery_required", "workspace disappeared after reservation");
22756
- const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
22757
- if (!entry)
22758
- return failure("recovery_required", "reserved decision disappeared");
22759
- const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
22760
- steps: [],
22761
- completedSteps: []
22762
- };
22763
- return success(changed.data.revision, workspace.data.revision, {
22764
- taskId: input.taskId,
22765
- decisionId: input.decisionId,
22766
- actionRef: input.actionRef,
22767
- taskRevision: changed.data.revision,
22768
- workspaceRevision: workspace.data.revision,
22769
- completedSteps: [...workflow.completedSteps],
22770
- remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
22771
- });
22772
- }
22773
- function settleAction(input) {
22774
- if (!input.store || !input.taskRevision || !input.workspaceRevision)
22775
- return failure("invalid_input", "action settlement preconditions are required");
22776
- const authority = takeAuthority(input.authority, {
22777
- owner: input.authorityOwner,
22778
- store: input.store,
22779
- root: input.store.root
22780
- }, input.authorityCaller);
22781
- if (!authority)
22782
- return failure("permission_denied", "native settlement authority is not verified");
22783
- if (!refSchema.safeParse(input.actionRef).success)
22784
- return failure("invalid_input", "action reference is invalid");
22785
- if (!authorityMatches(authority, {
22786
- taskId: input.taskId,
22787
- workspaceId: authority.workspaceId,
22788
- decisionId: input.decisionId,
22789
- actionRef: input.actionRef,
22790
- taskRevision: input.taskRevision,
22791
- workspaceRevision: input.workspaceRevision,
22792
- outcome: input.outcome
22793
- }))
22794
- return failure("permission_denied", "native settlement authority is not verified");
22795
- let outcomeResult = null;
22796
- const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
22797
- const workspace = input.store.readWorkspace();
22798
- if (!workspace.ok)
22799
- return workspace;
22800
- if (!workspace.data)
22801
- return failure("not_found", "workspace not found");
22802
- if (!authorityMatches(authority, {
22803
- taskId: input.taskId,
22804
- workspaceId: workspace.data.id,
22805
- decisionId: input.decisionId,
22806
- actionRef: input.actionRef,
22807
- taskRevision: input.taskRevision,
22808
- workspaceRevision: input.workspaceRevision,
22809
- outcome: input.outcome
22810
- }))
22811
- return failure("permission_denied", "native settlement authority does not match context");
22812
- if (workspace.data.revision !== input.workspaceRevision)
22813
- return failure("revision_conflict", "workspace revision does not match", {
22814
- expectedWorkspaceRevision: input.workspaceRevision,
22815
- actualWorkspaceRevision: workspace.data.revision
22816
- });
22817
- const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
22818
- if (!entry)
22819
- return failure("not_found", "decision not found");
22820
- if (!authorityDecisionMatches(authority, entry.data))
22821
- return failure("permission_denied", "native settlement authority does not match decision");
22822
- if (!provenanceMatches(entry.provenance, authority.provenance))
22823
- return failure("permission_denied", "native settlement caller does not match approval");
22824
- if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
22825
- return failure("permission_denied", "decision binding is invalid");
22826
- if (entry.data.consumption?.state === "uncertain")
22827
- return failure("external_outcome_unknown", "previous action outcome is unknown", {
22828
- operation: "settle_action",
22829
- outcome: "unknown"
22830
- });
22831
- if (entry.data.consumption?.state !== "reserved")
22832
- return failure("permission_denied", "action is not reserved");
22833
- if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
22834
- return failure("permission_denied", "settlement reference does not match reservation");
22835
- const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
22836
- const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
22837
- if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
22838
- return failure("recovery_required", "bounded action progress is missing");
22839
- const currentStep = input.step ?? workflow.steps[workflow.completed.length];
22840
- if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
22841
- return failure("permission_denied", "settlement step is outside the approved workflow");
22842
- if (input.outcome === "unknown") {
22843
- outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
22844
- operation: "settle_action",
22845
- outcome: "unknown"
22846
- });
22847
- const uncertain = {
22848
- ...entry.data,
22849
- consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
22850
- };
22851
- return success(mutation.revision, null, {
22852
- ...current,
22853
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
22854
- });
22781
+ var gitContext = (workspaceRoot, paths = []) => {
22782
+ const cwd = workspaceRoot;
22783
+ let failure;
22784
+ const runHere = (args) => {
22785
+ const result = run(cwd, args);
22786
+ if (!failure && result.exitCode !== 0) {
22787
+ failure = { stderr: result.stderr, exitCode: result.exitCode };
22855
22788
  }
22856
- if (input.outcome === "not_started") {
22857
- const released = { ...entry.data, consumption: null };
22858
- return success(mutation.revision, null, {
22859
- ...current,
22860
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
22861
- });
22789
+ return result.stdout;
22790
+ };
22791
+ const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
22792
+ const status_short = runHere(["status", "--porcelain"]);
22793
+ const staged = [];
22794
+ const unstaged = [];
22795
+ const untracked = [];
22796
+ for (const line of status_short.split(`
22797
+ `).filter(Boolean)) {
22798
+ const code = line.slice(0, 2);
22799
+ const file = line.slice(3);
22800
+ if (code === "??") {
22801
+ untracked.push(file);
22802
+ } else {
22803
+ if (code[0] !== " " && code[0] !== "?")
22804
+ staged.push(file);
22805
+ if (code[1] !== " " && code[1] !== "?")
22806
+ unstaged.push(file);
22862
22807
  }
22863
- if (currentStep)
22864
- workflow.completed.push(currentStep);
22865
- const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
22866
- const settled = {
22867
- ...entry.data,
22868
- consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
22808
+ }
22809
+ const pathArgs = paths.length ? ["--", ...paths] : [];
22810
+ const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
22811
+ const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
22812
+ const stagedSet = new Set(staged);
22813
+ const unstagedSet = new Set(unstaged);
22814
+ const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
22815
+ return {
22816
+ workspace_root: cwd,
22817
+ branch,
22818
+ status_short,
22819
+ staged,
22820
+ unstaged,
22821
+ untracked,
22822
+ diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
22823
+ `),
22824
+ partial_staged: partial_staged.length > 0,
22825
+ partial_staged_files: partial_staged,
22826
+ ...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
22827
+ };
22828
+ };
22829
+
22830
+ // packages/workit-core/src/core/vcs-config.ts
22831
+ import fs5 from "node:fs";
22832
+ import path7 from "node:path";
22833
+ import { spawnSync } from "node:child_process";
22834
+ var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
22835
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
22836
+ var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
22837
+ var remoteProvider = (cwd) => {
22838
+ const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
22839
+ if (r.status !== 0)
22840
+ return null;
22841
+ const url = (r.stdout ?? "").trim();
22842
+ if (/github\.com[:/]/.test(url))
22843
+ return "github";
22844
+ if (/gitlab\.com[:/]/.test(url))
22845
+ return "gitlab";
22846
+ return null;
22847
+ };
22848
+ var readVcsConfig = () => {
22849
+ const file = vcsConfigPath();
22850
+ let raw;
22851
+ try {
22852
+ raw = fs5.readFileSync(file, "utf8");
22853
+ } catch {
22854
+ return { status: "missing", path: file, config: {} };
22855
+ }
22856
+ let parsed;
22857
+ try {
22858
+ parsed = JSON.parse(raw);
22859
+ } catch {
22860
+ return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
22861
+ }
22862
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
22863
+ return { status: "valid", path: file, config: parsed };
22864
+ }
22865
+ return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
22866
+ };
22867
+ function vcsConfig(mode, cwd) {
22868
+ const ws = resolveWorkspace(vcsCwd(cwd));
22869
+ const wsVcs = ws?.vcs ?? {};
22870
+ const wsYt = ws?.youtrack ?? {};
22871
+ const wsIssues = ws?.issues ?? {};
22872
+ const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
22873
+ const root = vcsCwd(cwd);
22874
+ const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
22875
+ const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
22876
+ const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
22877
+ const wp = ws?.branchPolicy ?? {};
22878
+ const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
22879
+ const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
22880
+ const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
22881
+ const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
22882
+ const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
22883
+ let issuesProvider = null;
22884
+ let linkOnPr = null;
22885
+ if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
22886
+ issuesProvider = "github";
22887
+ linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
22888
+ }
22889
+ if (mode === "resolve") {
22890
+ if (cfgStatus === "malformed")
22891
+ return { ok: false, error: cfgError, configPath: cfgPath };
22892
+ return {
22893
+ ok: true,
22894
+ workspace_name: ws?.name ?? null,
22895
+ provider,
22896
+ defaultTargetBranch: defaultTarget,
22897
+ link_issues: linkIssues,
22898
+ youtrack_base_url: youtrackBaseUrl,
22899
+ issues_provider: issuesProvider,
22900
+ link_on_pr: linkOnPr
22869
22901
  };
22870
- const actionProgress = workflow.steps.length ? [
22871
- ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
22872
- {
22873
- decisionId: input.decisionId,
22874
- steps: [...workflow.steps],
22875
- completedSteps: [...workflow.completed]
22876
- }
22877
- ] : current.actionProgress ?? [];
22878
- const next = {
22879
- ...current,
22880
- actionProgress,
22881
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
22902
+ }
22903
+ if (cfgStatus === "malformed") {
22904
+ return { ok: false, error: cfgError, configPath: cfgPath };
22905
+ }
22906
+ if (cfgStatus === "missing") {
22907
+ return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
22908
+ }
22909
+ if (provider === null) {
22910
+ return {
22911
+ ok: false,
22912
+ error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
22913
+ configPath: cfgPath
22882
22914
  };
22883
- if (!complete) {
22884
- outcomeResult = success(mutation.revision, input.workspaceRevision, {
22885
- ...entry,
22886
- data: settled
22915
+ }
22916
+ const prov = cfg[provider] ?? {};
22917
+ const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
22918
+ const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path7.join(configDir(), `${provider}.token`));
22919
+ const tokenPath = path7.resolve(tokenFile);
22920
+ let tokenOk = false;
22921
+ if (fs5.existsSync(tokenPath)) {
22922
+ const token = fs5.readFileSync(tokenPath, "utf8").trim();
22923
+ const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
22924
+ tokenOk = !placeholder;
22925
+ }
22926
+ const out = {
22927
+ ok: true,
22928
+ configPath: path7.resolve(cfgPath),
22929
+ provider,
22930
+ defaultTargetBranch: defaultTarget,
22931
+ pr: cfg.pr ?? {},
22932
+ tokenPath,
22933
+ tokenPresent: fs5.existsSync(tokenPath),
22934
+ tokenReady: tokenOk,
22935
+ workspace_name: ws?.name ?? null,
22936
+ link_issues: linkIssues,
22937
+ youtrack_base_url: youtrackBaseUrl,
22938
+ issues_provider: issuesProvider,
22939
+ link_on_pr: linkOnPr
22940
+ };
22941
+ if (provider === "gitlab") {
22942
+ out.gitlab = {
22943
+ host: prov.host ?? "gitlab.com",
22944
+ apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
22945
+ };
22946
+ } else if (provider === "github") {
22947
+ out.github = { host: prov.host ?? "github.com" };
22948
+ }
22949
+ if (mode === "summary")
22950
+ delete out.tokenPath;
22951
+ return out;
22952
+ }
22953
+ function mergedPrStyle(limit = 6, cwd) {
22954
+ const cfg = vcsConfig("load", cwd);
22955
+ if (!cfg.ok || !cfg.tokenReady)
22956
+ return { ok: false, error: "vcs not configured" };
22957
+ const provider = cfg.provider;
22958
+ const token = fs5.readFileSync(cfg.tokenPath, "utf8").trim();
22959
+ const examples = [];
22960
+ const descInfo = (desc, caseInsensitiveNotes = false) => ({
22961
+ hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
22962
+ sections: desc.split(`
22963
+ `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
22964
+ descriptionPreview: desc.slice(0, 600)
22965
+ });
22966
+ if (provider === "gitlab") {
22967
+ const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
22968
+ if (remote.status !== 0)
22969
+ return { ok: false, error: "no origin remote" };
22970
+ const url = (remote.stdout ?? "").trim();
22971
+ const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
22972
+ if (!m)
22973
+ return { ok: false, error: "not a gitlab.com origin" };
22974
+ const project = m[1];
22975
+ const env = { ...process.env, GITLAB_TOKEN: token };
22976
+ const run = (args) => spawnSync("glab", ["api", ...args], { cwd, encoding: "utf8", env });
22977
+ let r = run([
22978
+ `projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
22979
+ ]);
22980
+ if (r.status !== 0) {
22981
+ r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
22982
+ }
22983
+ if (r.status !== 0)
22984
+ return { ok: false, error: "could not list merge requests" };
22985
+ for (const mr of JSON.parse(r.stdout ?? "[]")) {
22986
+ const desc = String(mr.description ?? "").trim();
22987
+ examples.push({
22988
+ title: mr.title,
22989
+ url: mr.web_url,
22990
+ squash: mr.squash,
22991
+ ...descInfo(desc, true)
22887
22992
  });
22888
22993
  }
22889
- return success(mutation.revision, null, next);
22890
- }, input.native?.now);
22891
- if (!changed.ok)
22892
- return changed;
22893
- const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
22894
- if (!entry)
22895
- return failure("recovery_required", "settled decision disappeared");
22896
- if (outcomeResult)
22897
- return outcomeResult;
22898
- return success(changed.data.revision, input.workspaceRevision, entry);
22899
- }
22900
- function reconcileAction(input) {
22901
- if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
22902
- return failure("invalid_input", "action reconciliation preconditions are required");
22903
- const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
22904
- if (!authority || authority.kind !== "reconciliation")
22905
- return failure("permission_denied", "native reconciliation authority is not verified");
22906
- 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)
22907
- return failure("permission_denied", "native reconciliation authority is not bound");
22908
- const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
22909
- const workspace = input.store.readWorkspace();
22910
- if (!workspace.ok)
22911
- return workspace;
22912
- if (!workspace.data)
22913
- return failure("not_found", "workspace not found");
22914
- if (authority.workspaceId !== workspace.data.id)
22915
- return failure("permission_denied", "native reconciliation workspace does not match");
22916
- if (workspace.data.revision !== input.workspaceRevision)
22917
- return failure("revision_conflict", "workspace revision does not match");
22918
- const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
22919
- if (!entry || entry.data.purpose !== "action")
22920
- return failure("not_found", "decision not found");
22921
- if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
22922
- return failure("permission_denied", "native reconciliation caller does not match approval");
22923
- 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)
22924
- return failure("permission_denied", "reconciliation decision binding is invalid");
22925
- if (entry.data.consumption?.state !== "uncertain")
22926
- return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
22927
- if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
22928
- return failure("permission_denied", "reconciliation reference does not match reservation");
22929
- const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
22930
- const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
22931
- const currentStep = input.step ?? workflow.steps[workflow.completed.length];
22932
- if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
22933
- return failure("permission_denied", "reconciliation step is outside the approved workflow");
22934
- if (input.outcome === "not_started") {
22935
- return success(mutation.revision, null, {
22936
- ...current,
22937
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
22938
- });
22994
+ } else if (provider === "github") {
22995
+ const r = spawnSync("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
22996
+ if (r.status !== 0)
22997
+ return { ok: false, error: "could not list pull requests" };
22998
+ for (const pr of JSON.parse(r.stdout ?? "[]")) {
22999
+ const desc = String(pr.body ?? "").trim();
23000
+ examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
22939
23001
  }
22940
- if (currentStep)
22941
- workflow.completed.push(currentStep);
22942
- const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
22943
- const nextDecision = {
22944
- ...entry.data,
22945
- consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
22946
- };
22947
- const actionProgress = workflow.steps.length ? [
22948
- ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
22949
- {
22950
- decisionId: input.decisionId,
22951
- steps: workflow.steps,
22952
- completedSteps: workflow.completed
22953
- }
22954
- ] : current.actionProgress ?? [];
22955
- return success(mutation.revision, null, {
22956
- ...current,
22957
- actionProgress,
22958
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
22959
- });
22960
- }, input.native?.now);
22961
- if (!changed.ok)
22962
- return changed;
22963
- const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
22964
- return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
22965
- }
22966
- var bindingCovers = scopeCovers;
22967
- var verifyDecisionContent = verifyContentRefs;
22968
-
22969
- // packages/workit-core/src/core/task-evaluation.ts
22970
- var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
22971
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
22972
- var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
22973
- var canonicalPath = (value) => {
22974
- if (typeof value !== "string" || !value)
22975
- return null;
22976
- if (value.includes("\\") || value.split("/").includes(".."))
22977
- return null;
22978
- const normalized = path6.posix.normalize(value);
22979
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
22980
- return null;
22981
- return normalized === "." ? "." : normalized.replace(/\/$/, "");
22982
- };
22983
- var canonicalScope = (scope) => {
22984
- const paths = scope.paths.length ? scope.paths : ["."];
22985
- const normalizedPaths = paths.map(canonicalPath);
22986
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
22987
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
22988
- return null;
23002
+ }
22989
23003
  return {
22990
- description: scope.description,
22991
- paths: [...new Set(normalizedPaths)],
22992
- exclusions: [...new Set(normalizedExclusions)]
23004
+ ok: true,
23005
+ provider,
23006
+ count: examples.length,
23007
+ styleHints: [
23008
+ "Prefer ## Summary bullets + ## Validation or ## Test plan only",
23009
+ "Do not add ## Notes with branch names, commit counts, or diff stats",
23010
+ "Do not paste commit log or diff stat into the body"
23011
+ ],
23012
+ examples
22993
23013
  };
22994
- };
22995
- var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
22996
- var scopeMatches = (value, scope) => {
22997
- const normalized = canonicalScope(scope);
22998
- if (!normalized)
22999
- return false;
23000
- const paths = normalized.paths;
23001
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
23002
- };
23003
- var pathRelevant = (value, scope) => scopeMatches(value, scope);
23004
- var compareCodeUnits2 = (left, right) => {
23005
- const length = Math.min(left.length, right.length);
23006
- for (let index = 0;index < length; index += 1) {
23007
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
23008
- if (difference !== 0)
23009
- return difference;
23014
+ }
23015
+
23016
+ // packages/workit-core/src/core/branch.ts
23017
+ var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
23018
+ var verifyPushIdentity = (root, provider, account) => {
23019
+ if (!account || typeof account !== "string")
23020
+ return { ok: false, error: "push identity requires a configured area account" };
23021
+ const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
23022
+ const bin = provider === "gitlab" ? "glab" : "gh";
23023
+ let login;
23024
+ try {
23025
+ login = execFileSync2(bin, [...probe], {
23026
+ cwd: root,
23027
+ encoding: "utf8",
23028
+ stdio: ["pipe", "pipe", "pipe"]
23029
+ }).trim();
23030
+ } catch {
23031
+ return { ok: false, error: `${bin} identity could not be resolved` };
23010
23032
  }
23011
- return left.length - right.length;
23012
- };
23013
- var gitPaths = (root) => {
23014
- const result = spawnSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
23015
- cwd: root,
23016
- encoding: "buffer"
23017
- });
23018
- if (result.status !== 0) {
23019
- const stderr = result.stderr?.toString("utf8") ?? "";
23033
+ if (!login || login.toLowerCase() !== account.toLowerCase())
23020
23034
  return {
23021
- paths: [],
23022
- uncertain: !/not a git repository/i.test(stderr),
23023
- git: /not a git repository/i.test(stderr) ? false : true
23035
+ ok: false,
23036
+ error: `push identity ${login || "(unknown)"} does not match area account ${account}`
23024
23037
  };
23025
- }
23026
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
23027
- const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
23028
- if (stagedDeleted.status !== 0) {
23029
- const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
23030
- if (!/not a git repository/i.test(stderr))
23031
- return { paths, uncertain: true, git: true };
23032
- } else if (stagedDeleted.stdout) {
23033
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
23034
- }
23035
- return { paths: [...new Set(paths)], uncertain: false, git: true };
23038
+ return { ok: true };
23036
23039
  };
23037
- var walk = (root, directory, output) => {
23038
- let names;
23040
+
23041
+ // packages/workit-core/src/core/auto-approval.ts
23042
+ var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
23043
+ var OPERATION_CLASS = {
23044
+ "git.branch_setup": "branch",
23045
+ "git.commit": "commit",
23046
+ "git.push": "push",
23047
+ "hosting.pull_request": "pr",
23048
+ "hosting.merge": "merge"
23049
+ };
23050
+ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null;
23051
+ var digestOf = (configDirPath) => {
23039
23052
  try {
23040
- names = fs6.readdirSync(directory);
23053
+ const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
23054
+ return createHash3("sha256").update(raw).digest("hex");
23041
23055
  } catch {
23042
- return true;
23056
+ return null;
23043
23057
  }
23044
- let uncertain = false;
23045
- for (const name of names) {
23046
- if (name === ".git" || name === ".workit")
23047
- continue;
23048
- const target = path6.join(directory, name);
23049
- let stat;
23050
- try {
23051
- stat = fs6.lstatSync(target);
23052
- } catch {
23053
- uncertain = true;
23054
- continue;
23055
- }
23056
- const item = relative(root, target);
23057
- output.add(item);
23058
- if (stat.isDirectory() && !stat.isSymbolicLink())
23059
- uncertain = walk(root, target, output) || uncertain;
23058
+ };
23059
+ var normalizeClasses = (value) => {
23060
+ if (value === true)
23061
+ return [...AUTO_CLASSES];
23062
+ if (!Array.isArray(value))
23063
+ return [];
23064
+ const seen = new Set;
23065
+ for (const item of value) {
23066
+ if (typeof item === "string" && AUTO_CLASSES.includes(item))
23067
+ seen.add(item);
23060
23068
  }
23061
- return uncertain;
23069
+ return [...seen];
23062
23070
  };
23063
- var scopeRoots = (root, scope) => {
23064
- const roots = [];
23065
- for (const item of scope.paths.length ? scope.paths : ["."]) {
23066
- const target = path6.resolve(root, item);
23067
- if (!inside(root, target))
23068
- 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` }] });
23069
- let ancestor = target;
23070
- while (!fs6.existsSync(ancestor) && ancestor !== root)
23071
- ancestor = path6.dirname(ancestor);
23072
- try {
23073
- if (!inside(root, fs6.realpathSync(ancestor)))
23074
- 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` }] });
23075
- } catch {
23076
- return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
23077
- }
23078
- roots.push(target);
23071
+ var resolveAutoApproval = (root, dir = configDir()) => {
23072
+ let match;
23073
+ try {
23074
+ match = resolveWorkspaceFrom(root, dir);
23075
+ } catch {
23076
+ return { status: "off" };
23079
23077
  }
23080
- return success(null, null, roots);
23078
+ if (!match)
23079
+ return { status: "off" };
23080
+ const classes = normalizeClasses(match.autoApprove);
23081
+ if (classes.length === 0)
23082
+ return { status: "off" };
23083
+ return {
23084
+ status: "on",
23085
+ workspace: match.name,
23086
+ classes,
23087
+ account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
23088
+ configDigest: digestOf(dir)
23089
+ };
23081
23090
  };
23082
- function captureCandidate(root, scope, environment = []) {
23083
- let checkout;
23091
+ var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
23092
+ var standingApprovalLive = (root, receipt, cls, dir) => {
23093
+ if (!isStandingReceipt(receipt))
23094
+ return false;
23095
+ if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
23096
+ return false;
23097
+ if (receipt.class !== cls)
23098
+ return false;
23099
+ const resolved = resolveAutoApproval(root, dir);
23100
+ if (resolved.status !== "on")
23101
+ return false;
23102
+ if (resolved.workspace !== receipt.workspace)
23103
+ return false;
23104
+ return resolved.classes.includes(cls);
23105
+ };
23106
+ var standingReceiptFor = (root, workspace, cls, dir) => {
23107
+ const resolved = resolveAutoApproval(root, dir);
23108
+ return {
23109
+ kind: "standing",
23110
+ workspace,
23111
+ class: cls,
23112
+ configDigest: resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null
23113
+ };
23114
+ };
23115
+ var verifyStandingApproval = (root, task, caller, binding) => {
23116
+ if (task.status !== "active")
23117
+ return failure("invalid_transition", "only active tasks can authorize actions");
23118
+ const session = task.intent?.provenance?.session;
23119
+ if (!session || session.kind !== "host" || session.host !== caller.host || session.handle !== caller.actor)
23120
+ return failure("permission_denied", "standing approval requires the lead session");
23121
+ const standing = binding.standing;
23122
+ if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
23123
+ return failure("invalid_input", "standing approval needs a workspace and class");
23124
+ let operation;
23084
23125
  try {
23085
- checkout = fs6.realpathSync(root);
23126
+ operation = typeof binding.approvedContent === "string" ? JSON.parse(binding.approvedContent).operation : null;
23086
23127
  } catch {
23087
- return failure("invalid_input", "candidate root does not exist");
23128
+ operation = null;
23129
+ }
23130
+ const cls = operationAutoClass(operation);
23131
+ if (!cls || cls !== standing.class)
23132
+ return failure("permission_denied", "standing approval does not cover this operation");
23133
+ const resolved = resolveAutoApproval(root);
23134
+ if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
23135
+ return failure("permission_denied", "standing auto-approval is not live for this operation");
23136
+ if (cls === "push") {
23137
+ const workspace = resolveWorkspaceFrom(root, configDir());
23138
+ const provider = workspace?.vcs?.provider;
23139
+ const account = resolved.account;
23140
+ if (!provider || !account)
23141
+ return failure("permission_denied", "auto-push requires a workspace provider and area account");
23142
+ const identity = verifyPushIdentity(root, provider, account);
23143
+ if (!identity.ok)
23144
+ return failure("permission_denied", identity.error);
23088
23145
  }
23089
- const normalizedScope = canonicalScope(scope);
23090
- if (!normalizedScope)
23091
- 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" }] });
23092
- const roots = scopeRoots(checkout, normalizedScope);
23093
- if (!roots.ok)
23094
- return roots;
23095
- const git = gitPaths(checkout);
23096
- const names = new Set;
23097
- let uncertain = git.uncertain;
23098
- for (const target of roots.data) {
23099
- const item = relative(checkout, target);
23100
- if (item === ".git" || item === ".workit")
23101
- continue;
23102
- names.add(item);
23103
- if (!fs6.existsSync(target))
23104
- continue;
23105
- let stat;
23106
- try {
23107
- stat = fs6.lstatSync(target);
23108
- } catch {
23109
- uncertain = true;
23146
+ return success(null, null, {
23147
+ kind: "host_observed",
23148
+ host: caller.host,
23149
+ session: { kind: "host", host: caller.host, handle: caller.actor },
23150
+ workerId: null,
23151
+ receipts: [standingReceiptFor(root, standing.workspace, cls)]
23152
+ });
23153
+ };
23154
+
23155
+ // packages/workit-core/src/core/authority.ts
23156
+ var verifiedAuthorities = new WeakMap;
23157
+ var trustedAuthority = (kind, provenance, expected, binding, caller) => {
23158
+ const token = {};
23159
+ verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
23160
+ return token;
23161
+ };
23162
+ var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
23163
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23164
+ var normalizeChainSteps = (steps) => {
23165
+ if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
23166
+ return null;
23167
+ const normalized = [];
23168
+ for (const step of steps) {
23169
+ if (typeof step === "string") {
23170
+ if (!step.trim())
23171
+ return null;
23172
+ normalized.push({ kind: "commit", message: step });
23110
23173
  continue;
23111
23174
  }
23112
- if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
23113
- uncertain = walk(checkout, target, names) || uncertain;
23114
- }
23115
- for (const item of git.paths)
23116
- if (scopeMatches(item, normalizedScope))
23117
- names.add(item);
23118
- const files = [];
23119
- let completeness = uncertain ? "uncertain" : "known";
23120
- for (const item of [...names].sort()) {
23121
- if (!scopeMatches(item, normalizedScope))
23175
+ if (!isRecord(step))
23176
+ return null;
23177
+ if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
23178
+ normalized.push({ kind: "branch", target: step.branch });
23179
+ continue;
23180
+ }
23181
+ if (step.pr === true && Object.keys(step).length === 1) {
23182
+ normalized.push({ kind: "pr" });
23122
23183
  continue;
23123
- const target = path6.join(checkout, item);
23124
- try {
23125
- const stat = fs6.lstatSync(target);
23126
- if (stat.isSymbolicLink()) {
23127
- const link = fs6.readlinkSync(target);
23128
- files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
23129
- } else if (stat.isFile()) {
23130
- files.push({
23131
- path: item,
23132
- kind: "file",
23133
- digest: digestBytes3(fs6.readFileSync(target)),
23134
- executable: (stat.mode & 73) !== 0
23135
- });
23136
- } else if (!stat.isDirectory()) {
23137
- completeness = "uncertain";
23138
- } else if (git.git && git.paths.includes(item)) {
23139
- completeness = "uncertain";
23140
- }
23141
- } catch (error) {
23142
- if (error?.code === "ENOENT") {
23143
- files.push({ path: item, kind: "absent", digest: null, executable: null });
23144
- } else
23145
- completeness = "uncertain";
23146
23184
  }
23185
+ return null;
23147
23186
  }
23148
- 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) => ({
23149
- name,
23150
- value: environment[name] ?? null
23151
- }));
23152
- const namesByEnvironment = supplied.map(({ name }) => name);
23153
- if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
23154
- return failure("invalid_input", "candidate environment names must be unique and non-empty");
23155
- const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
23156
- if (values.some(({ value }) => value === null))
23157
- completeness = "uncertain";
23158
- const headResult = spawnSync("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
23159
- if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
23160
- completeness = "uncertain";
23161
- const head = headResult.status === 0 ? headResult.stdout.trim() : null;
23162
- const partial = {
23163
- id: "0".repeat(64),
23164
- scope: normalizedScope,
23165
- completeness,
23166
- files,
23167
- environment: values,
23168
- head
23169
- };
23170
- const candidate = { ...partial, id: candidateDigest(partial) };
23171
- const parsed = candidateSchema.safeParse(candidate);
23172
- return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
23173
- }
23174
- var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
23175
- var changedPaths = (before, after) => {
23176
- const paths = new Set([...before.files, ...after.files].map((file) => file.path));
23177
- const left = fileMap(before);
23178
- const right = fileMap(after);
23179
- const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
23180
- if (before.head !== after.head)
23181
- changed.push(".");
23182
- if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
23183
- changed.push(".");
23184
- return changed;
23187
+ const keys = normalized.map(chainStepKey);
23188
+ if (new Set(keys).size !== keys.length)
23189
+ return null;
23190
+ return normalized;
23185
23191
  };
23186
- var sessionFromRef = (value) => {
23187
- if (typeof value !== "object" || value === null || value.kind !== "host")
23192
+ var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
23193
+ var parseSteps = (content) => {
23194
+ let value;
23195
+ try {
23196
+ value = JSON.parse(content);
23197
+ } catch {
23198
+ return [];
23199
+ }
23200
+ if (typeof value !== "object" || value === null || !("steps" in value))
23201
+ return [];
23202
+ const steps = value.steps;
23203
+ if (!Array.isArray(steps))
23188
23204
  return null;
23189
- return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
23205
+ const normalized = normalizeChainSteps(steps);
23206
+ return normalized;
23190
23207
  };
23191
- var sameSession = (left, right) => {
23192
- const other = sessionFromRef(right);
23193
- return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
23208
+ var decisionMatches = (entry, purpose, binding) => {
23209
+ const decision = entry.data;
23210
+ 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);
23194
23211
  };
23195
- var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
23196
- function evaluateEvidence(task, candidate) {
23197
- const current = candidate ?? task.candidates.at(-1) ?? null;
23198
- return task.evidence.map((entry) => {
23199
- const evidence = entry.data;
23200
- if (evidence.result === "failed")
23201
- return {
23202
- evidenceId: entry.id,
23203
- status: "failed",
23204
- reason: "failed evidence remains historical"
23205
- };
23206
- if (evidence.kind === "check" || evidence.kind === "review") {
23207
- if (!evidence.beforeCandidateId || !evidence.candidateId)
23208
- return {
23209
- evidenceId: entry.id,
23210
- status: "stale",
23211
- reason: "check or review evidence lacks start and end candidates"
23212
- };
23213
- if (evidence.beforeCandidateId !== evidence.candidateId)
23214
- return {
23215
- evidenceId: entry.id,
23216
- status: "stale",
23217
- reason: "candidate changed during the check or review"
23218
- };
23219
- }
23220
- if (!evidence.candidateId || !current)
23221
- return {
23222
- evidenceId: entry.id,
23223
- status: evidence.result,
23224
- reason: "evidence has no candidate binding"
23225
- };
23226
- const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
23227
- if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
23228
- return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
23229
- if (observed.id === current.id)
23230
- return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
23231
- const scopes = evidenceScopes(task, evidence.requirementIds);
23232
- const changed = changedPaths(observed, current);
23233
- const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
23234
- if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
23235
- return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
23236
- return {
23237
- evidenceId: entry.id,
23238
- status: evidence.result,
23239
- reason: "candidate changed outside evidence scope"
23240
- };
23241
- });
23242
- }
23243
- var scopeCovers2 = scopeCovers;
23244
- var checkPin = (assignment, beforeCandidateId, candidateId) => {
23245
- const pin = assignment.candidateId ?? null;
23246
- if (pin === null)
23247
- return true;
23248
- return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
23212
+ var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
23213
+ var validProvenance = (value, caller) => {
23214
+ if (!provenanceSchema.safeParse(value).success)
23215
+ return false;
23216
+ const provenance = value;
23217
+ 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);
23249
23218
  };
23250
- var resolveBinding = (evidence, pin, currentCandidateId) => {
23251
- const bound = evidence.kind === "check" || evidence.kind === "review";
23252
- const fallback = bound ? pin ?? currentCandidateId : null;
23253
- return {
23254
- beforeCandidateId: evidence.beforeCandidateId ?? fallback,
23255
- candidateId: evidence.candidateId ?? fallback
23256
- };
23219
+ var verifyNativeDecision = (verifier, input, binding) => {
23220
+ if (!verifier)
23221
+ return failure("permission_denied", "native authority verifier is unavailable");
23222
+ const result = verifier.verifyDecision(input);
23223
+ if (!result.ok || !validProvenance(result.data, input.caller))
23224
+ return failure("permission_denied", "native decision observation was not attested");
23225
+ return success(null, null, trustedAuthority("decision", result.data, {
23226
+ taskId: input.expected.taskId,
23227
+ workspaceId: input.expected.workspaceId,
23228
+ purpose: input.expected.purpose,
23229
+ response: input.expected.response,
23230
+ binding: input.expected.binding,
23231
+ bindingBytes: input.expected.bindingBytes,
23232
+ digest: input.expected.digest,
23233
+ requirementIds: [...input.expected.requirementIds]
23234
+ }, binding, input.caller));
23257
23235
  };
23258
- var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
23259
- 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")));
23260
- 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 }));
23261
- var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
23262
- if (kind !== "passed")
23263
- return false;
23264
- if (dimension === "testing" || dimension === "verification")
23265
- return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
23266
- if (dimension === "review")
23267
- return evidenceKind === "review";
23268
- if (dimension === "investigation" || dimension === "challenge")
23269
- return evidenceKind === "investigation";
23270
- if (dimension === "artifacts" || dimension === "continuity")
23271
- return evidenceKind === "artifact";
23272
- return false;
23236
+ var verifyNativeAction = (verifier, input, binding) => {
23237
+ if (!verifier)
23238
+ return failure("permission_denied", "native authority verifier is unavailable");
23239
+ const result = verifier.verifyAction(input);
23240
+ if (!result.ok || !validProvenance(result.data, input.caller))
23241
+ return failure("permission_denied", "native action observation was not attested");
23242
+ return success(null, null, trustedAuthority("action", result.data, {
23243
+ taskId: input.expected.taskId,
23244
+ workspaceId: input.expected.workspaceId,
23245
+ decisionId: input.expected.decisionId,
23246
+ actionRef: input.expected.actionRef,
23247
+ taskRevision: input.expected.taskRevision,
23248
+ workspaceRevision: input.expected.workspaceRevision,
23249
+ outcome: input.expected.outcome,
23250
+ purpose: input.expected.decision.purpose,
23251
+ response: input.expected.decision.response,
23252
+ binding: input.expected.decision.binding,
23253
+ digest: input.expected.decision.digest,
23254
+ requirementIds: [...input.expected.decision.requirementIds]
23255
+ }, binding, input.caller));
23273
23256
  };
23274
- var expectedKinds = (dimension, ruleId) => {
23275
- if (dimension === "testing")
23276
- return "check";
23277
- if (dimension === "verification")
23278
- return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
23279
- if (dimension === "review")
23280
- return "review";
23281
- if (dimension === "investigation" || dimension === "challenge")
23282
- return "investigation";
23283
- if (dimension === "artifacts" || dimension === "continuity")
23284
- return "artifact";
23285
- if (dimension === "decisions")
23286
- return "decision";
23287
- return "worker report";
23257
+ var verifyNativeReconciliation = (verifier, input, binding) => {
23258
+ if (!verifier?.verifyReconciliation)
23259
+ return failure("permission_denied", "native reconciliation authority is unavailable");
23260
+ const result = verifier.verifyReconciliation(input);
23261
+ if (!result.ok || !validProvenance(result.data, input.caller))
23262
+ return failure("permission_denied", "native reconciliation observation was not attested");
23263
+ return success(null, null, trustedAuthority("reconciliation", result.data, {
23264
+ taskId: input.expected.taskId,
23265
+ workspaceId: input.expected.workspaceId,
23266
+ decisionId: input.expected.decisionId,
23267
+ actionRef: input.expected.actionRef,
23268
+ taskRevision: input.expected.taskRevision,
23269
+ workspaceRevision: input.expected.workspaceRevision,
23270
+ outcome: input.expected.outcome,
23271
+ evidenceDigest: input.expected.evidenceDigest,
23272
+ step: input.expected.step,
23273
+ purpose: input.expected.decision.purpose,
23274
+ response: input.expected.decision.response,
23275
+ binding: input.expected.decision.binding,
23276
+ digest: input.expected.decision.digest,
23277
+ requirementIds: [...input.expected.decision.requirementIds]
23278
+ }, binding, input.caller));
23288
23279
  };
23289
- function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
23290
- const evidence = evaluateEvidence(task, candidate);
23291
- if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
23292
- return task.policy.requirements.map((requirement) => ({
23293
- requirementId: requirement.id,
23294
- status: "unsatisfied",
23295
- evidenceIds: [],
23296
- decisionIds: [],
23297
- reason: "stored policy version is unsupported; reassessment is required"
23298
- }));
23299
- return (task.policy?.requirements ?? []).map((requirement) => {
23300
- const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
23301
- const passed = related.filter(({ entry, evaluation }) => {
23302
- if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
23303
- return false;
23304
- if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
23305
- return false;
23306
- if (requirement.dimension !== "review")
23307
- return true;
23308
- const reviewSession = sessionFromRef(entry.data.reviewContext);
23309
- const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
23310
- 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));
23311
- return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
23312
- });
23313
- if (passed.length) {
23314
- const currentId = candidate?.id ?? null;
23315
- 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;
23316
- if (withRed.length)
23317
- return {
23318
- requirementId: requirement.id,
23319
- status: "satisfied",
23320
- evidenceIds: withRed.map(({ entry }) => entry.id),
23321
- decisionIds: [],
23322
- reason: "fresh applicable evidence passed"
23323
- };
23324
- if (requirement.dimension === "testing")
23325
- return {
23326
- requirementId: requirement.id,
23327
- status: "unsatisfied",
23328
- evidenceIds: [],
23329
- decisionIds: [],
23330
- reason: "testing requirement has GREEN evidence but no preceding RED"
23331
- };
23332
- }
23333
- if (requirement.dimension === "delegation") {
23334
- const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
23335
- 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))));
23336
- if (reconciled)
23337
- return {
23338
- requirementId: requirement.id,
23339
- status: "satisfied",
23340
- evidenceIds: [],
23341
- decisionIds: [],
23342
- reason: "bounded helper completed and its result is recorded"
23343
- };
23344
- }
23345
- const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
23346
- if (decisions.length)
23347
- return {
23348
- requirementId: requirement.id,
23349
- status: "satisfied",
23350
- evidenceIds: [],
23351
- decisionIds: decisions.map((decision) => decision.id),
23352
- reason: "an applicable approved or stated decision satisfies the requirement"
23353
- };
23354
- const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
23355
- if (limitations.length)
23356
- return {
23357
- requirementId: requirement.id,
23358
- status: "accepted_limitation",
23359
- evidenceIds: [],
23360
- decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
23361
- reason: "an applicable approved limitation permits the missing evidence"
23362
- };
23363
- const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
23364
- if (!unavailable && related.length > 0 && !passed.length)
23365
- return {
23366
- requirementId: requirement.id,
23367
- status: "unsatisfied",
23368
- evidenceIds: related.map(({ entry }) => entry.id),
23369
- decisionIds: [],
23370
- reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
23371
- };
23372
- return {
23373
- requirementId: requirement.id,
23374
- status: unavailable ? "unavailable" : "unsatisfied",
23375
- evidenceIds: related.map(({ entry }) => entry.id),
23376
- decisionIds: [],
23377
- reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
23378
- };
23379
- });
23280
+ var takeAuthority = (authority, binding, caller) => {
23281
+ const record = verifiedAuthorities.get(authority);
23282
+ verifiedAuthorities.delete(authority);
23283
+ if (!record)
23284
+ return null;
23285
+ if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
23286
+ return null;
23287
+ return record;
23288
+ };
23289
+ var retireNativeAuthority = (authority) => {
23290
+ const record = verifiedAuthorities.get(authority);
23291
+ verifiedAuthorities.delete(authority);
23292
+ return record?.provenance ?? null;
23293
+ };
23294
+ 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;
23295
+ 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);
23296
+ var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
23297
+ function applicableDecision(task, purpose, binding, checkoutRoot) {
23298
+ if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
23299
+ return [];
23300
+ 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);
23380
23301
  }
23381
- function evaluateClosure(requestedOutcome, view) {
23382
- if (requestedOutcome !== "stopped" && !view.task.policy)
23383
- return failure("requirements_unsatisfied", "task has not been assessed");
23384
- if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
23385
- return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
23386
- const evaluations = view.requirements;
23387
- const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
23388
- if (requestedOutcome !== "stopped" && openFindings.length)
23389
- return failure("requirements_unsatisfied", "open findings must be resolved before closure");
23390
- if (requestedOutcome !== "stopped") {
23391
- const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
23392
- for (const entry of view.task.findings) {
23393
- if (entry.data.disposition === "fixed") {
23394
- const verified = entry.data.resolution?.evidenceIds.some((id) => {
23395
- const evidence = view.task.evidence.find((item) => item.id === id);
23396
- if (!evidence)
23397
- return false;
23398
- return findingVerificationPasses(entry.data.candidateId, {
23399
- kind: evidence.data.kind,
23400
- candidateId: evidence.data.candidateId,
23401
- status: evidenceById.get(id) ?? "missing"
23302
+ var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
23303
+ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
23304
+ let root;
23305
+ try {
23306
+ root = fs6.realpathSync(checkoutRoot);
23307
+ } catch {
23308
+ return failure("permission_denied", "checkout root is unavailable");
23309
+ }
23310
+ for (const reference of binding.contentRefs) {
23311
+ if (reference.kind !== "file")
23312
+ continue;
23313
+ if (!reference.digest)
23314
+ return failure("invalid_input", "document references require a byte digest");
23315
+ const target = path8.resolve(root, reference.path);
23316
+ if (target !== root && !target.startsWith(`${root}${path8.sep}`))
23317
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23318
+ fields: [
23319
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
23320
+ ]
23321
+ });
23322
+ try {
23323
+ const relative = path8.relative(root, target);
23324
+ let current = root;
23325
+ for (const segment of relative.split(path8.sep)) {
23326
+ if (!segment)
23327
+ continue;
23328
+ current = path8.join(current, segment);
23329
+ const stat = fs6.lstatSync(current);
23330
+ const real = fs6.realpathSync(current);
23331
+ if (real !== root && !real.startsWith(`${root}${path8.sep}`))
23332
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
23333
+ fields: [
23334
+ {
23335
+ path: "contentRefs.path",
23336
+ reason: `${reference.path} resolves outside the checkout`
23337
+ }
23338
+ ]
23402
23339
  });
23403
- });
23404
- if (!verified)
23405
- return failure("requirements_unsatisfied", "fixed findings require current verification");
23340
+ if (stat.isSymbolicLink())
23341
+ return failure("invalid_input", "document reference uses a symlink");
23342
+ if (current === target && !stat.isFile())
23343
+ return failure("invalid_input", "document reference is not a regular file");
23344
+ if (current !== target && !stat.isDirectory())
23345
+ return failure("invalid_input", "document reference ancestor is not a directory");
23406
23346
  }
23407
- if (entry.data.disposition === "deferred") {
23408
- const valid = entry.data.resolution?.decisionIds.some((id) => {
23409
- const decisionEntry = view.task.decisions.find((item) => item.id === id);
23410
- const decision = decisionEntry?.data;
23411
- 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)));
23412
- });
23413
- if (!valid)
23414
- return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
23347
+ if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
23348
+ return failure("permission_denied", "approved document bytes have changed");
23349
+ } catch {
23350
+ return failure("invalid_input", "approved document is unavailable");
23351
+ }
23352
+ }
23353
+ return success(null, null, null);
23354
+ };
23355
+ var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
23356
+ 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);
23357
+ var validateAction = (store, task, workspaceId, input, authority) => {
23358
+ if (task.status !== "active")
23359
+ return failure("invalid_transition", "only active tasks can authorize actions");
23360
+ if (!refSchema.safeParse(input.actionRef).success)
23361
+ return failure("invalid_input", "action reference is invalid");
23362
+ const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
23363
+ if (!entry)
23364
+ return failure("not_found", "decision not found");
23365
+ const decision = entry.data;
23366
+ if (decision.purpose !== "action")
23367
+ return failure("permission_denied", "decision is not an action approval");
23368
+ if (!authorityMatches(authority, {
23369
+ taskId: task.id,
23370
+ workspaceId,
23371
+ decisionId: input.decisionId,
23372
+ actionRef: input.actionRef,
23373
+ taskRevision: input.expectedRevision,
23374
+ workspaceRevision: input.expectedWorkspaceRevision,
23375
+ outcome: "reserve"
23376
+ }))
23377
+ return failure("permission_denied", "native reservation authority is not bound");
23378
+ if (!authorityDecisionMatches(authority, decision))
23379
+ return failure("permission_denied", "native reservation authority does not match decision");
23380
+ if (decision.response !== "approved")
23381
+ return failure("permission_denied", "decision was rejected");
23382
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
23383
+ return failure("permission_denied", "action approval lacks native receipt assurance");
23384
+ const receipts = entry.provenance.receipts;
23385
+ const standing = receipts.filter((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "standing");
23386
+ const questionBacked = receipts.some((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "host");
23387
+ if (!questionBacked && standing.length > 0) {
23388
+ let operation;
23389
+ try {
23390
+ operation = JSON.parse(decision.binding.approvedContent).operation;
23391
+ } catch {
23392
+ operation = null;
23393
+ }
23394
+ const cls = operationAutoClass(operation);
23395
+ if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
23396
+ return failure("permission_denied", "standing auto-approval is not live for this operation");
23397
+ }
23398
+ if (decision.revoked)
23399
+ return failure("permission_denied", "decision is revoked");
23400
+ if (decision.digest !== decisionDigest(decision))
23401
+ return failure("permission_denied", "decision binding is invalid");
23402
+ if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
23403
+ return failure("permission_denied", "decision task or workspace binding is invalid");
23404
+ if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
23405
+ return failure("permission_denied", "action binding does not match the approved decision");
23406
+ const content = verifyContentRefs(store, decision.binding);
23407
+ if (!content.ok)
23408
+ return content;
23409
+ const requestedSteps = parseSteps(decision.binding.approvedContent);
23410
+ if (requestedSteps === null)
23411
+ return failure("invalid_input", "bounded action steps must be a unique non-empty array");
23412
+ if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
23413
+ return failure("permission_denied", "bounded action steps do not match approved content");
23414
+ const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
23415
+ const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
23416
+ if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
23417
+ return failure("permission_denied", "bounded action scope changed");
23418
+ return success(null, null, { entry, workflow });
23419
+ };
23420
+ function reserveAction(input) {
23421
+ if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
23422
+ return failure("invalid_input", "action reservation preconditions are required");
23423
+ const authority = takeAuthority(input.authority, {
23424
+ owner: input.authorityOwner,
23425
+ store: input.store,
23426
+ root: input.store.root
23427
+ }, input.authorityCaller);
23428
+ if (!authority)
23429
+ return failure("permission_denied", "native reservation authority is not verified");
23430
+ const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
23431
+ const workspace = input.store.readWorkspace();
23432
+ if (!workspace.ok)
23433
+ return workspace;
23434
+ if (!workspace.data)
23435
+ return failure("not_found", "workspace not found");
23436
+ if (workspace.data.revision !== input.expectedWorkspaceRevision)
23437
+ return failure("revision_conflict", "workspace revision does not match", {
23438
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
23439
+ actualWorkspaceRevision: workspace.data.revision
23440
+ });
23441
+ const valid = validateAction(input.store, current, workspace.data.id, input, authority);
23442
+ if (!valid.ok)
23443
+ return valid;
23444
+ const { entry, workflow } = valid.data;
23445
+ const existing = entry.data.consumption;
23446
+ if (existing?.state === "uncertain")
23447
+ return failure("external_outcome_unknown", "previous action outcome is unknown", {
23448
+ operation: "reserve_action",
23449
+ outcome: "unknown"
23450
+ });
23451
+ if (existing?.state === "consumed")
23452
+ return failure("permission_denied", "action approval is consumed");
23453
+ if (existing?.state === "reserved" && workflow.completed.length === 0)
23454
+ return failure("permission_denied", "action approval is already reserved");
23455
+ const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
23456
+ if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
23457
+ return failure("permission_denied", "action step is outside the approved remaining workflow");
23458
+ if (requestedStep && workflow.completed.includes(requestedStep))
23459
+ return failure("permission_denied", "action step was already completed");
23460
+ const nextDecision = {
23461
+ ...entry.data,
23462
+ consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
23463
+ };
23464
+ const actionProgress = workflow.steps.length ? [
23465
+ ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
23466
+ {
23467
+ decisionId: input.decisionId,
23468
+ steps: [...workflow.steps],
23469
+ completedSteps: [...workflow.completed]
23415
23470
  }
23416
- }
23417
- }
23418
- const blocking = evaluations.filter((item) => {
23419
- if (item.status !== "unsatisfied" && item.status !== "unavailable")
23420
- return false;
23421
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
23422
- return !requirement || requirement.before === "close";
23423
- });
23424
- if (requestedOutcome !== "stopped" && blocking.length)
23425
- return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
23426
- requirementIds: blocking.map((item) => item.requirementId),
23427
- requirements: blocking.map((item) => {
23428
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
23429
- return {
23430
- requirementId: item.requirementId,
23431
- ruleId: requirement?.ruleId ?? "unknown",
23432
- reason: item.reason,
23433
- satisfaction: requirement?.satisfaction ?? "",
23434
- dependentAction: requirement?.dependentAction ?? null
23435
- };
23436
- })
23437
- });
23438
- const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
23439
- if (requestedOutcome === "verified" && accepted.length)
23440
- return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
23441
- requirementIds: accepted.map((item) => item.requirementId)
23442
- });
23443
- const evidenceIds = [
23444
- ...new Set([
23445
- ...evaluations.flatMap((item) => item.evidenceIds),
23446
- ...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
23447
- ])
23448
- ];
23449
- const decisionIds = [
23450
- ...new Set([
23451
- ...evaluations.flatMap((item) => item.decisionIds),
23452
- ...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
23453
- ])
23454
- ];
23455
- return success(null, null, {
23456
- outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
23457
- evidenceIds,
23458
- decisionIds,
23459
- requirementIds: evaluations.map((item) => item.requirementId)
23460
- });
23461
- }
23462
-
23463
- // packages/workit-core/src/core/task-context.ts
23464
- function reconcileResume(view, observations = []) {
23465
- if (observations.length > 0)
23466
- return failure("permission_denied", "worker observations require native host verification");
23467
- const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
23468
- if (!candidate.ok)
23469
- return candidate;
23470
- const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
23471
- const blockers = [...view.task.progress.blockers];
23472
- if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
23473
- blockers.push({
23474
- reason: "worker state requires reconciliation",
23475
- dependentAction: "resume",
23476
- refs: []
23471
+ ] : current.actionProgress ?? [];
23472
+ return success(mutation.revision, null, {
23473
+ ...current,
23474
+ actionProgress,
23475
+ decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
23477
23476
  });
23478
- return success(null, null, {
23479
- candidate: candidate.data,
23480
- staleEvidenceIds,
23481
- workerUpdates: [],
23482
- blockers,
23483
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
23477
+ }, input.native?.now);
23478
+ if (!changed.ok)
23479
+ return changed;
23480
+ const workspace = input.store.readWorkspace();
23481
+ if (!workspace.ok || !workspace.data)
23482
+ return failure("recovery_required", "workspace disappeared after reservation");
23483
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
23484
+ if (!entry)
23485
+ return failure("recovery_required", "reserved decision disappeared");
23486
+ const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
23487
+ steps: [],
23488
+ completedSteps: []
23489
+ };
23490
+ return success(changed.data.revision, workspace.data.revision, {
23491
+ taskId: input.taskId,
23492
+ decisionId: input.decisionId,
23493
+ actionRef: input.actionRef,
23494
+ taskRevision: changed.data.revision,
23495
+ workspaceRevision: workspace.data.revision,
23496
+ completedSteps: [...workflow.completedSteps],
23497
+ remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
23484
23498
  });
23485
23499
  }
23486
- var exportDigest = (bundle) => sha256({
23487
- schemaVersion: bundle.schemaVersion,
23488
- exportedAt: bundle.exportedAt,
23489
- sourceWorkspaceId: bundle.sourceWorkspaceId,
23490
- task: bundle.task
23491
- });
23492
- // packages/workit-core/src/core/git.ts
23493
- import { execFileSync } from "node:child_process";
23494
- var run = (cwd, args) => {
23495
- try {
23496
- const stdout = execFileSync("git", args, {
23497
- cwd,
23498
- encoding: "utf8",
23499
- stdio: ["pipe", "pipe", "pipe"]
23500
- });
23501
- return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
23502
- } catch (error) {
23503
- const e = error;
23504
- return {
23505
- stdout: (e.stdout ?? "").trimEnd(),
23506
- stderr: (e.stderr ?? "").trimEnd(),
23507
- exitCode: e.status ?? 1
23500
+ function settleAction(input) {
23501
+ if (!input.store || !input.taskRevision || !input.workspaceRevision)
23502
+ return failure("invalid_input", "action settlement preconditions are required");
23503
+ const authority = takeAuthority(input.authority, {
23504
+ owner: input.authorityOwner,
23505
+ store: input.store,
23506
+ root: input.store.root
23507
+ }, input.authorityCaller);
23508
+ if (!authority)
23509
+ return failure("permission_denied", "native settlement authority is not verified");
23510
+ if (!refSchema.safeParse(input.actionRef).success)
23511
+ return failure("invalid_input", "action reference is invalid");
23512
+ if (!authorityMatches(authority, {
23513
+ taskId: input.taskId,
23514
+ workspaceId: authority.workspaceId,
23515
+ decisionId: input.decisionId,
23516
+ actionRef: input.actionRef,
23517
+ taskRevision: input.taskRevision,
23518
+ workspaceRevision: input.workspaceRevision,
23519
+ outcome: input.outcome
23520
+ }))
23521
+ return failure("permission_denied", "native settlement authority is not verified");
23522
+ let outcomeResult = null;
23523
+ const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
23524
+ const workspace = input.store.readWorkspace();
23525
+ if (!workspace.ok)
23526
+ return workspace;
23527
+ if (!workspace.data)
23528
+ return failure("not_found", "workspace not found");
23529
+ if (!authorityMatches(authority, {
23530
+ taskId: input.taskId,
23531
+ workspaceId: workspace.data.id,
23532
+ decisionId: input.decisionId,
23533
+ actionRef: input.actionRef,
23534
+ taskRevision: input.taskRevision,
23535
+ workspaceRevision: input.workspaceRevision,
23536
+ outcome: input.outcome
23537
+ }))
23538
+ return failure("permission_denied", "native settlement authority does not match context");
23539
+ if (workspace.data.revision !== input.workspaceRevision)
23540
+ return failure("revision_conflict", "workspace revision does not match", {
23541
+ expectedWorkspaceRevision: input.workspaceRevision,
23542
+ actualWorkspaceRevision: workspace.data.revision
23543
+ });
23544
+ const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
23545
+ if (!entry)
23546
+ return failure("not_found", "decision not found");
23547
+ if (!authorityDecisionMatches(authority, entry.data))
23548
+ return failure("permission_denied", "native settlement authority does not match decision");
23549
+ if (!provenanceMatches(entry.provenance, authority.provenance))
23550
+ return failure("permission_denied", "native settlement caller does not match approval");
23551
+ if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
23552
+ return failure("permission_denied", "decision binding is invalid");
23553
+ if (entry.data.consumption?.state === "uncertain")
23554
+ return failure("external_outcome_unknown", "previous action outcome is unknown", {
23555
+ operation: "settle_action",
23556
+ outcome: "unknown"
23557
+ });
23558
+ if (entry.data.consumption?.state !== "reserved")
23559
+ return failure("permission_denied", "action is not reserved");
23560
+ if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
23561
+ return failure("permission_denied", "settlement reference does not match reservation");
23562
+ const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
23563
+ const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
23564
+ if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
23565
+ return failure("recovery_required", "bounded action progress is missing");
23566
+ const currentStep = input.step ?? workflow.steps[workflow.completed.length];
23567
+ if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
23568
+ return failure("permission_denied", "settlement step is outside the approved workflow");
23569
+ if (input.outcome === "unknown") {
23570
+ outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
23571
+ operation: "settle_action",
23572
+ outcome: "unknown"
23573
+ });
23574
+ const uncertain = {
23575
+ ...entry.data,
23576
+ consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
23577
+ };
23578
+ return success(mutation.revision, null, {
23579
+ ...current,
23580
+ decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
23581
+ });
23582
+ }
23583
+ if (input.outcome === "not_started") {
23584
+ const released = { ...entry.data, consumption: null };
23585
+ return success(mutation.revision, null, {
23586
+ ...current,
23587
+ decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
23588
+ });
23589
+ }
23590
+ if (currentStep)
23591
+ workflow.completed.push(currentStep);
23592
+ const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
23593
+ const settled = {
23594
+ ...entry.data,
23595
+ consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
23596
+ };
23597
+ const actionProgress = workflow.steps.length ? [
23598
+ ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
23599
+ {
23600
+ decisionId: input.decisionId,
23601
+ steps: [...workflow.steps],
23602
+ completedSteps: [...workflow.completed]
23603
+ }
23604
+ ] : current.actionProgress ?? [];
23605
+ const next = {
23606
+ ...current,
23607
+ actionProgress,
23608
+ decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
23508
23609
  };
23509
- }
23510
- };
23511
- var gitContext = (workspaceRoot, paths = []) => {
23512
- const cwd = workspaceRoot;
23513
- let failure;
23514
- const runHere = (args) => {
23515
- const result = run(cwd, args);
23516
- if (!failure && result.exitCode !== 0) {
23517
- failure = { stderr: result.stderr, exitCode: result.exitCode };
23610
+ if (!complete) {
23611
+ outcomeResult = success(mutation.revision, input.workspaceRevision, {
23612
+ ...entry,
23613
+ data: settled
23614
+ });
23518
23615
  }
23519
- return result.stdout;
23520
- };
23521
- const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
23522
- const status_short = runHere(["status", "--porcelain"]);
23523
- const staged = [];
23524
- const unstaged = [];
23525
- const untracked = [];
23526
- for (const line of status_short.split(`
23527
- `).filter(Boolean)) {
23528
- const code = line.slice(0, 2);
23529
- const file = line.slice(3);
23530
- if (code === "??") {
23531
- untracked.push(file);
23532
- } else {
23533
- if (code[0] !== " " && code[0] !== "?")
23534
- staged.push(file);
23535
- if (code[1] !== " " && code[1] !== "?")
23536
- unstaged.push(file);
23616
+ return success(mutation.revision, null, next);
23617
+ }, input.native?.now);
23618
+ if (!changed.ok)
23619
+ return changed;
23620
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
23621
+ if (!entry)
23622
+ return failure("recovery_required", "settled decision disappeared");
23623
+ if (outcomeResult)
23624
+ return outcomeResult;
23625
+ return success(changed.data.revision, input.workspaceRevision, entry);
23626
+ }
23627
+ function reconcileAction(input) {
23628
+ if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
23629
+ return failure("invalid_input", "action reconciliation preconditions are required");
23630
+ const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
23631
+ if (!authority || authority.kind !== "reconciliation")
23632
+ return failure("permission_denied", "native reconciliation authority is not verified");
23633
+ 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)
23634
+ return failure("permission_denied", "native reconciliation authority is not bound");
23635
+ const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
23636
+ const workspace = input.store.readWorkspace();
23637
+ if (!workspace.ok)
23638
+ return workspace;
23639
+ if (!workspace.data)
23640
+ return failure("not_found", "workspace not found");
23641
+ if (authority.workspaceId !== workspace.data.id)
23642
+ return failure("permission_denied", "native reconciliation workspace does not match");
23643
+ if (workspace.data.revision !== input.workspaceRevision)
23644
+ return failure("revision_conflict", "workspace revision does not match");
23645
+ const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
23646
+ if (!entry || entry.data.purpose !== "action")
23647
+ return failure("not_found", "decision not found");
23648
+ if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
23649
+ return failure("permission_denied", "native reconciliation caller does not match approval");
23650
+ 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)
23651
+ return failure("permission_denied", "reconciliation decision binding is invalid");
23652
+ if (entry.data.consumption?.state !== "uncertain")
23653
+ return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
23654
+ if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
23655
+ return failure("permission_denied", "reconciliation reference does not match reservation");
23656
+ const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
23657
+ const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
23658
+ const currentStep = input.step ?? workflow.steps[workflow.completed.length];
23659
+ if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
23660
+ return failure("permission_denied", "reconciliation step is outside the approved workflow");
23661
+ if (input.outcome === "not_started") {
23662
+ return success(mutation.revision, null, {
23663
+ ...current,
23664
+ decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
23665
+ });
23537
23666
  }
23538
- }
23539
- const pathArgs = paths.length ? ["--", ...paths] : [];
23540
- const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
23541
- const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
23542
- const stagedSet = new Set(staged);
23543
- const unstagedSet = new Set(unstaged);
23544
- const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
23545
- return {
23546
- workspace_root: cwd,
23547
- branch,
23548
- status_short,
23549
- staged,
23550
- unstaged,
23551
- untracked,
23552
- diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
23553
- `),
23554
- partial_staged: partial_staged.length > 0,
23555
- partial_staged_files: partial_staged,
23556
- ...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
23557
- };
23558
- };
23559
-
23560
- // packages/workit-core/src/core/config.ts
23561
- import {
23562
- copyFileSync,
23563
- cpSync,
23564
- existsSync as existsSync4,
23565
- mkdirSync as mkdirSync2,
23566
- readFileSync as readFileSync4,
23567
- readdirSync as readdirSync3,
23568
- writeFileSync as writeFileSync2
23569
- } from "node:fs";
23570
- import os from "node:os";
23571
- import path7 from "node:path";
23572
-
23573
- // packages/workit-core/src/core/boundary.ts
23574
- import { statSync } from "node:fs";
23575
- var EVENT = {
23576
- initialization: "initialization",
23577
- provenance: "provenance",
23578
- assets: "assets",
23579
- configurationSource: "configuration_source",
23580
- hooks: "hooks",
23581
- mcpConnection: "mcp_connection",
23582
- migration: "migration",
23583
- installSteps: "install_steps",
23584
- uncaughtFailure: "uncaught_failure",
23585
- toolsFailed: "tools_failed",
23586
- doctor: "doctor"
23587
- };
23588
- var errorDetail = (err) => {
23589
- if (err instanceof Error) {
23590
- return { error_name: err.name, error: err.message };
23591
- }
23592
- return { error_name: "unknown", error: String(err) };
23593
- };
23594
- var markSourcesLoaded = (files, loadedAt = Date.now()) => ({
23595
- loadedAt,
23596
- files: [...files]
23597
- });
23598
- var changedSourcesSinceLoad = (marker) => {
23599
- const changed = [];
23600
- for (const file of marker.files) {
23601
- try {
23602
- if (statSync(file).mtimeMs > marker.loadedAt)
23603
- changed.push(file);
23604
- } catch {}
23605
- }
23606
- return changed;
23607
- };
23667
+ if (currentStep)
23668
+ workflow.completed.push(currentStep);
23669
+ const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
23670
+ const nextDecision = {
23671
+ ...entry.data,
23672
+ consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
23673
+ };
23674
+ const actionProgress = workflow.steps.length ? [
23675
+ ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
23676
+ {
23677
+ decisionId: input.decisionId,
23678
+ steps: workflow.steps,
23679
+ completedSteps: workflow.completed
23680
+ }
23681
+ ] : current.actionProgress ?? [];
23682
+ return success(mutation.revision, null, {
23683
+ ...current,
23684
+ actionProgress,
23685
+ decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
23686
+ });
23687
+ }, input.native?.now);
23688
+ if (!changed.ok)
23689
+ return changed;
23690
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
23691
+ return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
23692
+ }
23693
+ var bindingCovers = scopeCovers;
23694
+ var verifyDecisionContent = verifyContentRefs;
23608
23695
 
23609
- // packages/workit-core/src/core/config.ts
23610
- var diagnosticLogger;
23611
- var PRESETS = {
23612
- gitflow: {
23613
- allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
23614
- protected: ["main", "develop", "master", "prod", "production"]
23615
- },
23616
- "github-flow": { allowed: ["*"], protected: ["main"] },
23617
- "trunk-based": { allowed: ["*"], protected: ["main"] },
23618
- custom: { allowed: [], protected: [] }
23619
- };
23620
- var mergePreset = (preset, input = {}, current = {
23621
- branchPolicy: { preset, allowed: [], protected: [] }
23622
- }) => {
23623
- const defs = PRESETS[preset];
23624
- return {
23625
- preset,
23626
- allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
23627
- protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
23628
- };
23629
- };
23630
- 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");
23631
- var migratedDir = null;
23632
- var migrationFailed = false;
23633
- var ensureConfigDir = (dir = resolveConfigDir()) => {
23634
- if (migratedDir === dir)
23635
- return dir;
23636
- if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
23637
- migratedDir = dir;
23638
- return dir;
23639
- }
23640
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
23641
- if (!existsSync4(legacy)) {
23642
- migratedDir = dir;
23643
- return dir;
23644
- }
23645
- if (!migrationFailed && existsSync4(dir)) {
23646
- migratedDir = dir;
23647
- return dir;
23648
- }
23649
- migrationFailed = false;
23650
- mkdirSync2(dir, { recursive: true });
23651
- diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
23652
- for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
23653
- const src = path7.join(legacy, entry.name);
23654
- const dest = path7.join(dir, entry.name);
23655
- if (existsSync4(dest))
23656
- continue;
23657
- try {
23658
- if (entry.isDirectory())
23659
- cpSync(src, dest, { recursive: true });
23660
- else if (entry.isFile())
23661
- copyFileSync(src, dest);
23662
- } catch (err) {
23663
- migrationFailed = true;
23664
- diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
23665
- console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
23666
- }
23667
- }
23668
- if (!migrationFailed)
23669
- migratedDir = dir;
23670
- return dir;
23671
- };
23672
- var configDir = () => ensureConfigDir();
23673
- var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
23674
- var DEFAULTS = {
23675
- locale: "en",
23676
- localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
23677
- timezone: "America/Santiago",
23678
- branchPolicy: {
23679
- preset: "gitflow",
23680
- allowed: [...PRESETS.gitflow.allowed],
23681
- protected: [...PRESETS.gitflow.protected]
23682
- },
23683
- commitPolicy: { preset: "conventional" }
23684
- };
23685
- var readSafe = (p) => {
23686
- try {
23687
- return readFileSync4(p, "utf8");
23688
- } catch {
23696
+ // packages/workit-core/src/core/task-evaluation.ts
23697
+ var digestBytes3 = (value) => createHash5("sha256").update(value).digest("hex");
23698
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path9.sep}`);
23699
+ var relative = (root, candidate) => path9.relative(root, candidate).split(path9.sep).join("/") || ".";
23700
+ var canonicalPath = (value) => {
23701
+ if (typeof value !== "string" || !value)
23702
+ return null;
23703
+ if (value.includes("\\") || value.split("/").includes(".."))
23704
+ return null;
23705
+ const normalized = path9.posix.normalize(value);
23706
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
23689
23707
  return null;
23690
- }
23708
+ return normalized === "." ? "." : normalized.replace(/\/$/, "");
23691
23709
  };
23692
- var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23693
- var parseConfigResult = (raw, file) => {
23694
- if (raw === null)
23695
- return { status: "missing", path: file };
23696
- let parsed;
23697
- try {
23698
- parsed = JSON.parse(raw);
23699
- } catch {
23700
- return { status: "malformed", path: file, error: `${file} is not valid JSON` };
23701
- }
23702
- if (!isConfigObject(parsed)) {
23703
- return { status: "malformed", path: file, error: `${file} is not a JSON object` };
23704
- }
23705
- const input = parsed;
23706
- const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
23707
- const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
23708
- const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
23710
+ var canonicalScope = (scope) => {
23711
+ const paths = scope.paths.length ? scope.paths : ["."];
23712
+ const normalizedPaths = paths.map(canonicalPath);
23713
+ const normalizedExclusions = scope.exclusions.map(canonicalPath);
23714
+ if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
23715
+ return null;
23709
23716
  return {
23710
- status: "valid",
23711
- path: file,
23712
- config: {
23713
- locale,
23714
- localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
23715
- timezone: input.timezone ?? DEFAULTS.timezone,
23716
- branchPolicy: mergePreset(preset, {
23717
- allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
23718
- protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
23719
- }, DEFAULTS),
23720
- commitPolicy: {
23721
- preset: commitPreset,
23722
- ...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
23723
- }
23724
- }
23717
+ description: scope.description,
23718
+ paths: [...new Set(normalizedPaths)],
23719
+ exclusions: [...new Set(normalizedExclusions)]
23725
23720
  };
23726
23721
  };
23727
- var readConfigTyped = (dir) => {
23728
- const file = path7.join(dir ?? configDir(), "config.json");
23729
- return parseConfigResult(readSafe(file), file);
23730
- };
23731
- var readConfig = () => {
23732
- const result = readConfigTyped();
23733
- if (result.status === "malformed")
23734
- throw new Error(result.error);
23735
- return result.config ?? DEFAULTS;
23722
+ var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
23723
+ var scopeMatches = (value, scope) => {
23724
+ const normalized = canonicalScope(scope);
23725
+ if (!normalized)
23726
+ return false;
23727
+ const paths = normalized.paths;
23728
+ return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
23736
23729
  };
23737
- var resolveBranchPolicy = (config, workspace) => {
23738
- const wp = workspace?.branchPolicy ?? {};
23739
- const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
23740
- const merged = mergePreset(preset, {
23741
- allowed: wp.allowed,
23742
- protectedNames: wp.protected
23743
- }, config);
23744
- const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
23745
- return {
23746
- preset,
23747
- allowed,
23748
- protected: new Set(merged.protected.map((p) => p.toLowerCase())),
23749
- integration: wp.integration === "merge" ? "merge" : "pr",
23750
- defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
23751
- };
23730
+ var pathRelevant = (value, scope) => scopeMatches(value, scope);
23731
+ var compareCodeUnits2 = (left, right) => {
23732
+ const length = Math.min(left.length, right.length);
23733
+ for (let index = 0;index < length; index += 1) {
23734
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
23735
+ if (difference !== 0)
23736
+ return difference;
23737
+ }
23738
+ return left.length - right.length;
23752
23739
  };
23753
- var COMMIT_PRESETS = [
23754
- "conventional",
23755
- "gitmoji",
23756
- "ticket-prefix",
23757
- "freeform",
23758
- "custom",
23759
- "auto"
23760
- ];
23761
-
23762
- // packages/workit-core/src/core/workspaces.ts
23763
- import { readFileSync as readFileSync5, realpathSync as realpathSync4 } from "node:fs";
23764
- import path8 from "node:path";
23765
- var readWorkspacesResult = (dir = configDir()) => {
23766
- const file = path8.join(dir, "workspaces.json");
23767
- let raw;
23768
- try {
23769
- raw = readFileSync5(file, "utf8");
23770
- } catch {
23771
- return { status: "missing", path: file, entries: [] };
23740
+ var gitPaths = (root) => {
23741
+ const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
23742
+ cwd: root,
23743
+ encoding: "buffer"
23744
+ });
23745
+ if (result.status !== 0) {
23746
+ const stderr = result.stderr?.toString("utf8") ?? "";
23747
+ return {
23748
+ paths: [],
23749
+ uncertain: !/not a git repository/i.test(stderr),
23750
+ git: /not a git repository/i.test(stderr) ? false : true
23751
+ };
23772
23752
  }
23773
- let parsed;
23753
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")) : [];
23754
+ const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
23755
+ if (stagedDeleted.status !== 0) {
23756
+ const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
23757
+ if (!/not a git repository/i.test(stderr))
23758
+ return { paths, uncertain: true, git: true };
23759
+ } else if (stagedDeleted.stdout) {
23760
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")));
23761
+ }
23762
+ return { paths: [...new Set(paths)], uncertain: false, git: true };
23763
+ };
23764
+ var walk = (root, directory, output) => {
23765
+ let names;
23774
23766
  try {
23775
- parsed = JSON.parse(raw);
23767
+ names = fs7.readdirSync(directory);
23776
23768
  } catch {
23777
- return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
23769
+ return true;
23778
23770
  }
23779
- if (!isConfigObject(parsed)) {
23780
- return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
23771
+ let uncertain = false;
23772
+ for (const name of names) {
23773
+ if (name === ".git" || name === ".workit")
23774
+ continue;
23775
+ const target = path9.join(directory, name);
23776
+ let stat;
23777
+ try {
23778
+ stat = fs7.lstatSync(target);
23779
+ } catch {
23780
+ uncertain = true;
23781
+ continue;
23782
+ }
23783
+ const item = relative(root, target);
23784
+ output.add(item);
23785
+ if (stat.isDirectory() && !stat.isSymbolicLink())
23786
+ uncertain = walk(root, target, output) || uncertain;
23781
23787
  }
23782
- const list = parsed.workspaces;
23783
- return {
23784
- status: "valid",
23785
- path: file,
23786
- entries: Array.isArray(list) ? list : []
23787
- };
23788
+ return uncertain;
23788
23789
  };
23789
- var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
23790
- var globToRegExp = (glob) => {
23791
- let out = "";
23792
- for (let i = 0;i < glob.length; i++) {
23793
- const c = glob[i];
23794
- if (c === "*") {
23795
- if (glob[i + 1] === "*") {
23796
- if (glob[i + 2] === "/") {
23797
- if (out === "")
23798
- out += "/?";
23799
- out += "(?:[^/]+/)*";
23800
- i += 2;
23801
- } else {
23802
- if (i + 2 >= glob.length) {
23803
- if (out.endsWith("/")) {
23804
- out = out.slice(0, -1);
23805
- out += "(?:/.*)?";
23806
- } else {
23807
- out += ".*";
23808
- }
23809
- } else {
23810
- out += ".*";
23811
- }
23812
- i++;
23813
- }
23814
- } else {
23815
- out += "[^/]*";
23816
- }
23817
- } else {
23818
- out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
23790
+ var scopeRoots = (root, scope) => {
23791
+ const roots = [];
23792
+ for (const item of scope.paths.length ? scope.paths : ["."]) {
23793
+ const target = path9.resolve(root, item);
23794
+ if (!inside(root, target))
23795
+ 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` }] });
23796
+ let ancestor = target;
23797
+ while (!fs7.existsSync(ancestor) && ancestor !== root)
23798
+ ancestor = path9.dirname(ancestor);
23799
+ try {
23800
+ if (!inside(root, fs7.realpathSync(ancestor)))
23801
+ 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` }] });
23802
+ } catch {
23803
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
23819
23804
  }
23805
+ roots.push(target);
23820
23806
  }
23821
- return new RegExp(`^${out}$`);
23807
+ return success(null, null, roots);
23822
23808
  };
23823
- var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
23824
- var realpathOf = (p) => {
23825
- const real = realpathSync4.native ?? realpathSync4;
23809
+ function captureCandidate(root, scope, environment = []) {
23810
+ let checkout;
23826
23811
  try {
23827
- return real(p);
23812
+ checkout = fs7.realpathSync(root);
23828
23813
  } catch {
23829
- return p;
23814
+ return failure("invalid_input", "candidate root does not exist");
23830
23815
  }
23831
- };
23832
- var canonicalGlob = (glob) => {
23833
- const m = /[*?[\]{]/.exec(glob);
23834
- const prefix = m ? glob.slice(0, m.index) : glob;
23835
- const rest = m ? glob.slice(m.index) : "";
23836
- if (!prefix)
23837
- return glob;
23838
- const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
23839
- return real + (prefix.endsWith("/") ? "/" : "") + rest;
23840
- };
23841
- var resolveWorkspaceFrom = (cwd, dir) => {
23842
- const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
23843
- for (const entry of loadWorkspacesFrom(dir)) {
23844
- if (!entry || typeof entry !== "object")
23816
+ const normalizedScope = canonicalScope(scope);
23817
+ if (!normalizedScope)
23818
+ 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" }] });
23819
+ const roots = scopeRoots(checkout, normalizedScope);
23820
+ if (!roots.ok)
23821
+ return roots;
23822
+ const git = gitPaths(checkout);
23823
+ const names = new Set;
23824
+ let uncertain = git.uncertain;
23825
+ for (const target of roots.data) {
23826
+ const item = relative(checkout, target);
23827
+ if (item === ".git" || item === ".workit")
23845
23828
  continue;
23846
- const ws = entry;
23847
- if (typeof ws.glob !== "string" || !ws.glob)
23829
+ names.add(item);
23830
+ if (!fs7.existsSync(target))
23848
23831
  continue;
23849
- const glob = ws.glob.replaceAll("\\", "/");
23850
- const canonical = canonicalGlob(glob);
23851
- for (const target of targets) {
23852
- if (matchWorkspace(glob, target))
23853
- return ws;
23854
- if (canonical !== glob && matchWorkspace(canonical, target))
23855
- return ws;
23832
+ let stat;
23833
+ try {
23834
+ stat = fs7.lstatSync(target);
23835
+ } catch {
23836
+ uncertain = true;
23837
+ continue;
23838
+ }
23839
+ if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
23840
+ uncertain = walk(checkout, target, names) || uncertain;
23841
+ }
23842
+ for (const item of git.paths)
23843
+ if (scopeMatches(item, normalizedScope))
23844
+ names.add(item);
23845
+ const files = [];
23846
+ let completeness = uncertain ? "uncertain" : "known";
23847
+ for (const item of [...names].sort()) {
23848
+ if (!scopeMatches(item, normalizedScope))
23849
+ continue;
23850
+ const target = path9.join(checkout, item);
23851
+ try {
23852
+ const stat = fs7.lstatSync(target);
23853
+ if (stat.isSymbolicLink()) {
23854
+ const link = fs7.readlinkSync(target);
23855
+ files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
23856
+ } else if (stat.isFile()) {
23857
+ files.push({
23858
+ path: item,
23859
+ kind: "file",
23860
+ digest: digestBytes3(fs7.readFileSync(target)),
23861
+ executable: (stat.mode & 73) !== 0
23862
+ });
23863
+ } else if (!stat.isDirectory()) {
23864
+ completeness = "uncertain";
23865
+ } else if (git.git && git.paths.includes(item)) {
23866
+ completeness = "uncertain";
23867
+ }
23868
+ } catch (error) {
23869
+ if (error?.code === "ENOENT") {
23870
+ files.push({ path: item, kind: "absent", digest: null, executable: null });
23871
+ } else
23872
+ completeness = "uncertain";
23856
23873
  }
23857
23874
  }
23858
- return null;
23875
+ 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) => ({
23876
+ name,
23877
+ value: environment[name] ?? null
23878
+ }));
23879
+ const namesByEnvironment = supplied.map(({ name }) => name);
23880
+ if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
23881
+ return failure("invalid_input", "candidate environment names must be unique and non-empty");
23882
+ const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
23883
+ if (values.some(({ value }) => value === null))
23884
+ completeness = "uncertain";
23885
+ const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
23886
+ if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
23887
+ completeness = "uncertain";
23888
+ const head = headResult.status === 0 ? headResult.stdout.trim() : null;
23889
+ const partial = {
23890
+ id: "0".repeat(64),
23891
+ scope: normalizedScope,
23892
+ completeness,
23893
+ files,
23894
+ environment: values,
23895
+ head
23896
+ };
23897
+ const candidate = { ...partial, id: candidateDigest(partial) };
23898
+ const parsed = candidateSchema.safeParse(candidate);
23899
+ return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
23900
+ }
23901
+ var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
23902
+ var changedPaths = (before, after) => {
23903
+ const paths = new Set([...before.files, ...after.files].map((file) => file.path));
23904
+ const left = fileMap(before);
23905
+ const right = fileMap(after);
23906
+ const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
23907
+ if (before.head !== after.head)
23908
+ changed.push(".");
23909
+ if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
23910
+ changed.push(".");
23911
+ return changed;
23859
23912
  };
23860
- var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
23861
-
23862
- // packages/workit-core/src/core/vcs-config.ts
23863
- import fs7 from "node:fs";
23864
- import path9 from "node:path";
23865
- import { spawnSync as spawnSync2 } from "node:child_process";
23866
- var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
23867
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
23868
- var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
23869
- var remoteProvider = (cwd) => {
23870
- const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
23871
- if (r.status !== 0)
23913
+ var sessionFromRef = (value) => {
23914
+ if (typeof value !== "object" || value === null || value.kind !== "host")
23872
23915
  return null;
23873
- const url = (r.stdout ?? "").trim();
23874
- if (/github\.com[:/]/.test(url))
23875
- return "github";
23876
- if (/gitlab\.com[:/]/.test(url))
23877
- return "gitlab";
23878
- return null;
23916
+ return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
23879
23917
  };
23880
- var readVcsConfig = () => {
23881
- const file = vcsConfigPath();
23882
- let raw;
23883
- try {
23884
- raw = fs7.readFileSync(file, "utf8");
23885
- } catch {
23886
- return { status: "missing", path: file, config: {} };
23887
- }
23888
- let parsed;
23889
- try {
23890
- parsed = JSON.parse(raw);
23891
- } catch {
23892
- return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
23893
- }
23894
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23895
- return { status: "valid", path: file, config: parsed };
23896
- }
23897
- return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
23918
+ var sameSession = (left, right) => {
23919
+ const other = sessionFromRef(right);
23920
+ return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
23898
23921
  };
23899
- function vcsConfig(mode, cwd) {
23900
- const ws = resolveWorkspace(vcsCwd(cwd));
23901
- const wsVcs = ws?.vcs ?? {};
23902
- const wsYt = ws?.youtrack ?? {};
23903
- const wsIssues = ws?.issues ?? {};
23904
- const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
23905
- const root = vcsCwd(cwd);
23906
- const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
23907
- const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
23908
- const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
23909
- const wp = ws?.branchPolicy ?? {};
23910
- const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
23911
- const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
23912
- const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
23913
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
23914
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
23915
- let issuesProvider = null;
23916
- let linkOnPr = null;
23917
- if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
23918
- issuesProvider = "github";
23919
- linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
23920
- }
23921
- if (mode === "resolve") {
23922
- if (cfgStatus === "malformed")
23923
- return { ok: false, error: cfgError, configPath: cfgPath };
23922
+ var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
23923
+ function evaluateEvidence(task, candidate) {
23924
+ const current = candidate ?? task.candidates.at(-1) ?? null;
23925
+ return task.evidence.map((entry) => {
23926
+ const evidence = entry.data;
23927
+ if (evidence.result === "failed")
23928
+ return {
23929
+ evidenceId: entry.id,
23930
+ status: "failed",
23931
+ reason: "failed evidence remains historical"
23932
+ };
23933
+ if (evidence.kind === "check" || evidence.kind === "review") {
23934
+ if (!evidence.beforeCandidateId || !evidence.candidateId)
23935
+ return {
23936
+ evidenceId: entry.id,
23937
+ status: "stale",
23938
+ reason: "check or review evidence lacks start and end candidates"
23939
+ };
23940
+ if (evidence.beforeCandidateId !== evidence.candidateId)
23941
+ return {
23942
+ evidenceId: entry.id,
23943
+ status: "stale",
23944
+ reason: "candidate changed during the check or review"
23945
+ };
23946
+ }
23947
+ if (!evidence.candidateId || !current)
23948
+ return {
23949
+ evidenceId: entry.id,
23950
+ status: evidence.result,
23951
+ reason: "evidence has no candidate binding"
23952
+ };
23953
+ const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
23954
+ if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
23955
+ return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
23956
+ if (observed.id === current.id)
23957
+ return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
23958
+ const scopes = evidenceScopes(task, evidence.requirementIds);
23959
+ const changed = changedPaths(observed, current);
23960
+ const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
23961
+ if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
23962
+ return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
23924
23963
  return {
23925
- ok: true,
23926
- workspace_name: ws?.name ?? null,
23927
- provider,
23928
- defaultTargetBranch: defaultTarget,
23929
- link_issues: linkIssues,
23930
- youtrack_base_url: youtrackBaseUrl,
23931
- issues_provider: issuesProvider,
23932
- link_on_pr: linkOnPr
23964
+ evidenceId: entry.id,
23965
+ status: evidence.result,
23966
+ reason: "candidate changed outside evidence scope"
23933
23967
  };
23934
- }
23935
- if (cfgStatus === "malformed") {
23936
- return { ok: false, error: cfgError, configPath: cfgPath };
23937
- }
23938
- if (cfgStatus === "missing") {
23939
- return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
23940
- }
23941
- if (provider === null) {
23968
+ });
23969
+ }
23970
+ var scopeCovers2 = scopeCovers;
23971
+ var checkPin = (assignment, beforeCandidateId, candidateId) => {
23972
+ const pin = assignment.candidateId ?? null;
23973
+ if (pin === null)
23974
+ return true;
23975
+ return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
23976
+ };
23977
+ var resolveBinding = (evidence, pin, currentCandidateId) => {
23978
+ const bound = evidence.kind === "check" || evidence.kind === "review";
23979
+ const fallback = bound ? pin ?? currentCandidateId : null;
23980
+ return {
23981
+ beforeCandidateId: evidence.beforeCandidateId ?? fallback,
23982
+ candidateId: evidence.candidateId ?? fallback
23983
+ };
23984
+ };
23985
+ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
23986
+ 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")));
23987
+ 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 }));
23988
+ var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
23989
+ if (kind !== "passed")
23990
+ return false;
23991
+ if (dimension === "testing" || dimension === "verification")
23992
+ return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
23993
+ if (dimension === "review")
23994
+ return evidenceKind === "review";
23995
+ if (dimension === "investigation" || dimension === "challenge")
23996
+ return evidenceKind === "investigation";
23997
+ if (dimension === "artifacts" || dimension === "continuity")
23998
+ return evidenceKind === "artifact";
23999
+ return false;
24000
+ };
24001
+ var expectedKinds = (dimension, ruleId) => {
24002
+ if (dimension === "testing")
24003
+ return "check";
24004
+ if (dimension === "verification")
24005
+ return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
24006
+ if (dimension === "review")
24007
+ return "review";
24008
+ if (dimension === "investigation" || dimension === "challenge")
24009
+ return "investigation";
24010
+ if (dimension === "artifacts" || dimension === "continuity")
24011
+ return "artifact";
24012
+ if (dimension === "decisions")
24013
+ return "decision";
24014
+ return "worker report";
24015
+ };
24016
+ function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
24017
+ const evidence = evaluateEvidence(task, candidate);
24018
+ if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
24019
+ return task.policy.requirements.map((requirement) => ({
24020
+ requirementId: requirement.id,
24021
+ status: "unsatisfied",
24022
+ evidenceIds: [],
24023
+ decisionIds: [],
24024
+ reason: "stored policy version is unsupported; reassessment is required"
24025
+ }));
24026
+ return (task.policy?.requirements ?? []).map((requirement) => {
24027
+ const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
24028
+ const passed = related.filter(({ entry, evaluation }) => {
24029
+ if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
24030
+ return false;
24031
+ if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
24032
+ return false;
24033
+ if (requirement.dimension !== "review")
24034
+ return true;
24035
+ const reviewSession = sessionFromRef(entry.data.reviewContext);
24036
+ const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
24037
+ 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));
24038
+ return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
24039
+ });
24040
+ if (passed.length) {
24041
+ const currentId = candidate?.id ?? null;
24042
+ 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;
24043
+ if (withRed.length)
24044
+ return {
24045
+ requirementId: requirement.id,
24046
+ status: "satisfied",
24047
+ evidenceIds: withRed.map(({ entry }) => entry.id),
24048
+ decisionIds: [],
24049
+ reason: "fresh applicable evidence passed"
24050
+ };
24051
+ if (requirement.dimension === "testing")
24052
+ return {
24053
+ requirementId: requirement.id,
24054
+ status: "unsatisfied",
24055
+ evidenceIds: [],
24056
+ decisionIds: [],
24057
+ reason: "testing requirement has GREEN evidence but no preceding RED"
24058
+ };
24059
+ }
24060
+ if (requirement.dimension === "delegation") {
24061
+ const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
24062
+ 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))));
24063
+ if (reconciled)
24064
+ return {
24065
+ requirementId: requirement.id,
24066
+ status: "satisfied",
24067
+ evidenceIds: [],
24068
+ decisionIds: [],
24069
+ reason: "bounded helper completed and its result is recorded"
24070
+ };
24071
+ }
24072
+ const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
24073
+ if (decisions.length)
24074
+ return {
24075
+ requirementId: requirement.id,
24076
+ status: "satisfied",
24077
+ evidenceIds: [],
24078
+ decisionIds: decisions.map((decision) => decision.id),
24079
+ reason: "an applicable approved or stated decision satisfies the requirement"
24080
+ };
24081
+ const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
24082
+ if (limitations.length)
24083
+ return {
24084
+ requirementId: requirement.id,
24085
+ status: "accepted_limitation",
24086
+ evidenceIds: [],
24087
+ decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
24088
+ reason: "an applicable approved limitation permits the missing evidence"
24089
+ };
24090
+ const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
24091
+ if (!unavailable && related.length > 0 && !passed.length)
24092
+ return {
24093
+ requirementId: requirement.id,
24094
+ status: "unsatisfied",
24095
+ evidenceIds: related.map(({ entry }) => entry.id),
24096
+ decisionIds: [],
24097
+ reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
24098
+ };
23942
24099
  return {
23943
- ok: false,
23944
- error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
23945
- configPath: cfgPath
23946
- };
23947
- }
23948
- const prov = cfg[provider] ?? {};
23949
- const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
23950
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
23951
- const tokenPath = path9.resolve(tokenFile);
23952
- let tokenOk = false;
23953
- if (fs7.existsSync(tokenPath)) {
23954
- const token = fs7.readFileSync(tokenPath, "utf8").trim();
23955
- const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
23956
- tokenOk = !placeholder;
23957
- }
23958
- const out = {
23959
- ok: true,
23960
- configPath: path9.resolve(cfgPath),
23961
- provider,
23962
- defaultTargetBranch: defaultTarget,
23963
- pr: cfg.pr ?? {},
23964
- tokenPath,
23965
- tokenPresent: fs7.existsSync(tokenPath),
23966
- tokenReady: tokenOk,
23967
- workspace_name: ws?.name ?? null,
23968
- link_issues: linkIssues,
23969
- youtrack_base_url: youtrackBaseUrl,
23970
- issues_provider: issuesProvider,
23971
- link_on_pr: linkOnPr
23972
- };
23973
- if (provider === "gitlab") {
23974
- out.gitlab = {
23975
- host: prov.host ?? "gitlab.com",
23976
- apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
24100
+ requirementId: requirement.id,
24101
+ status: unavailable ? "unavailable" : "unsatisfied",
24102
+ evidenceIds: related.map(({ entry }) => entry.id),
24103
+ decisionIds: [],
24104
+ reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
23977
24105
  };
23978
- } else if (provider === "github") {
23979
- out.github = { host: prov.host ?? "github.com" };
23980
- }
23981
- if (mode === "summary")
23982
- delete out.tokenPath;
23983
- return out;
23984
- }
23985
- function mergedPrStyle(limit = 6, cwd) {
23986
- const cfg = vcsConfig("load", cwd);
23987
- if (!cfg.ok || !cfg.tokenReady)
23988
- return { ok: false, error: "vcs not configured" };
23989
- const provider = cfg.provider;
23990
- const token = fs7.readFileSync(cfg.tokenPath, "utf8").trim();
23991
- const examples = [];
23992
- const descInfo = (desc, caseInsensitiveNotes = false) => ({
23993
- hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
23994
- sections: desc.split(`
23995
- `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
23996
- descriptionPreview: desc.slice(0, 600)
23997
24106
  });
23998
- if (provider === "gitlab") {
23999
- const remote = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
24000
- if (remote.status !== 0)
24001
- return { ok: false, error: "no origin remote" };
24002
- const url = (remote.stdout ?? "").trim();
24003
- const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
24004
- if (!m)
24005
- return { ok: false, error: "not a gitlab.com origin" };
24006
- const project = m[1];
24007
- const env = { ...process.env, GITLAB_TOKEN: token };
24008
- const run = (args) => spawnSync2("glab", ["api", ...args], { cwd, encoding: "utf8", env });
24009
- let r = run([
24010
- `projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
24011
- ]);
24012
- if (r.status !== 0) {
24013
- r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
24014
- }
24015
- if (r.status !== 0)
24016
- return { ok: false, error: "could not list merge requests" };
24017
- for (const mr of JSON.parse(r.stdout ?? "[]")) {
24018
- const desc = String(mr.description ?? "").trim();
24019
- examples.push({
24020
- title: mr.title,
24021
- url: mr.web_url,
24022
- squash: mr.squash,
24023
- ...descInfo(desc, true)
24024
- });
24025
- }
24026
- } else if (provider === "github") {
24027
- const r = spawnSync2("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
24028
- if (r.status !== 0)
24029
- return { ok: false, error: "could not list pull requests" };
24030
- for (const pr of JSON.parse(r.stdout ?? "[]")) {
24031
- const desc = String(pr.body ?? "").trim();
24032
- examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
24107
+ }
24108
+ function evaluateClosure(requestedOutcome, view) {
24109
+ if (requestedOutcome !== "stopped" && !view.task.policy)
24110
+ return failure("requirements_unsatisfied", "task has not been assessed");
24111
+ if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
24112
+ return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
24113
+ const evaluations = view.requirements;
24114
+ const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
24115
+ if (requestedOutcome !== "stopped" && openFindings.length)
24116
+ return failure("requirements_unsatisfied", "open findings must be resolved before closure");
24117
+ if (requestedOutcome !== "stopped") {
24118
+ const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
24119
+ for (const entry of view.task.findings) {
24120
+ if (entry.data.disposition === "fixed") {
24121
+ const verified = entry.data.resolution?.evidenceIds.some((id) => {
24122
+ const evidence = view.task.evidence.find((item) => item.id === id);
24123
+ if (!evidence)
24124
+ return false;
24125
+ return findingVerificationPasses(entry.data.candidateId, {
24126
+ kind: evidence.data.kind,
24127
+ candidateId: evidence.data.candidateId,
24128
+ status: evidenceById.get(id) ?? "missing"
24129
+ });
24130
+ });
24131
+ if (!verified)
24132
+ return failure("requirements_unsatisfied", "fixed findings require current verification");
24133
+ }
24134
+ if (entry.data.disposition === "deferred") {
24135
+ const valid = entry.data.resolution?.decisionIds.some((id) => {
24136
+ const decisionEntry = view.task.decisions.find((item) => item.id === id);
24137
+ const decision = decisionEntry?.data;
24138
+ 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)));
24139
+ });
24140
+ if (!valid)
24141
+ return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
24142
+ }
24033
24143
  }
24034
24144
  }
24035
- return {
24036
- ok: true,
24037
- provider,
24038
- count: examples.length,
24039
- styleHints: [
24040
- "Prefer ## Summary bullets + ## Validation or ## Test plan only",
24041
- "Do not add ## Notes with branch names, commit counts, or diff stats",
24042
- "Do not paste commit log or diff stat into the body"
24043
- ],
24044
- examples
24045
- };
24145
+ const blocking = evaluations.filter((item) => {
24146
+ if (item.status !== "unsatisfied" && item.status !== "unavailable")
24147
+ return false;
24148
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
24149
+ return !requirement || requirement.before === "close";
24150
+ });
24151
+ if (requestedOutcome !== "stopped" && blocking.length)
24152
+ return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
24153
+ requirementIds: blocking.map((item) => item.requirementId),
24154
+ requirements: blocking.map((item) => {
24155
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
24156
+ return {
24157
+ requirementId: item.requirementId,
24158
+ ruleId: requirement?.ruleId ?? "unknown",
24159
+ reason: item.reason,
24160
+ satisfaction: requirement?.satisfaction ?? "",
24161
+ dependentAction: requirement?.dependentAction ?? null
24162
+ };
24163
+ })
24164
+ });
24165
+ const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
24166
+ if (requestedOutcome === "verified" && accepted.length)
24167
+ return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
24168
+ requirementIds: accepted.map((item) => item.requirementId)
24169
+ });
24170
+ const evidenceIds = [
24171
+ ...new Set([
24172
+ ...evaluations.flatMap((item) => item.evidenceIds),
24173
+ ...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
24174
+ ])
24175
+ ];
24176
+ const decisionIds = [
24177
+ ...new Set([
24178
+ ...evaluations.flatMap((item) => item.decisionIds),
24179
+ ...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
24180
+ ])
24181
+ ];
24182
+ return success(null, null, {
24183
+ outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
24184
+ evidenceIds,
24185
+ decisionIds,
24186
+ requirementIds: evaluations.map((item) => item.requirementId)
24187
+ });
24046
24188
  }
24047
24189
 
24048
- // packages/workit-core/src/core/branch.ts
24049
- var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
24190
+ // packages/workit-core/src/core/task-context.ts
24191
+ function reconcileResume(view, observations = []) {
24192
+ if (observations.length > 0)
24193
+ return failure("permission_denied", "worker observations require native host verification");
24194
+ const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
24195
+ if (!candidate.ok)
24196
+ return candidate;
24197
+ const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
24198
+ const blockers = [...view.task.progress.blockers];
24199
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
24200
+ blockers.push({
24201
+ reason: "worker state requires reconciliation",
24202
+ dependentAction: "resume",
24203
+ refs: []
24204
+ });
24205
+ return success(null, null, {
24206
+ candidate: candidate.data,
24207
+ staleEvidenceIds,
24208
+ workerUpdates: [],
24209
+ blockers,
24210
+ reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
24211
+ });
24212
+ }
24213
+ var exportDigest = (bundle) => sha256({
24214
+ schemaVersion: bundle.schemaVersion,
24215
+ exportedAt: bundle.exportedAt,
24216
+ sourceWorkspaceId: bundle.sourceWorkspaceId,
24217
+ task: bundle.task
24218
+ });
24050
24219
  // packages/workit-core/src/core/external-action.ts
24051
24220
  var externalActionSchema = discriminatedUnion2("operation", [
24052
24221
  object4({
@@ -24080,6 +24249,13 @@ var externalActionSchema = discriminatedUnion2("operation", [
24080
24249
  babysit: boolean5().optional()
24081
24250
  }).strict()
24082
24251
  }).strict(),
24252
+ object4({
24253
+ operation: literal3("hosting.merge"),
24254
+ payload: object4({
24255
+ target_branch: string5().min(1).optional(),
24256
+ source_branch: string5().min(1).optional()
24257
+ }).strict()
24258
+ }).strict(),
24083
24259
  object4({
24084
24260
  operation: literal3("youtrack.update"),
24085
24261
  payload: object4({
@@ -25193,7 +25369,10 @@ class WorkitCore {
25193
25369
  observeDecision(request, observation) {
25194
25370
  return this.recordDecision(request, observation, true);
25195
25371
  }
25196
- recordDecision(request, nativeObservation, nativeRequired = false) {
25372
+ observeStandingDecision(request) {
25373
+ return this.recordDecision(request, undefined, true, true);
25374
+ }
25375
+ recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
25197
25376
  const root = this.contextRootError();
25198
25377
  if (!root.ok)
25199
25378
  return root;
@@ -25208,8 +25387,10 @@ class WorkitCore {
25208
25387
  return failure("permission_denied", "stated choices cannot authorize mutating actions");
25209
25388
  if (stated && !input.binding?.statedChoice)
25210
25389
  return failure("invalid_input", "stated choices require binding.statedChoice");
25211
- if (nativeRequired && nativeObservation === undefined && !stated)
25390
+ if (nativeRequired && nativeObservation === undefined && !stated && !standing)
25212
25391
  return failure("permission_denied", "native decision observation is required");
25392
+ if (standing && (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved"))
25393
+ return failure("invalid_input", "standing approvals need an approved action binding with a standing rule");
25213
25394
  const task = this.store.readTask(input.taskId);
25214
25395
  if (!task.ok)
25215
25396
  return task;
@@ -25241,7 +25422,10 @@ class WorkitCore {
25241
25422
  consumption: null
25242
25423
  };
25243
25424
  const data = { ...base, digest: decisionDigest(base) };
25244
- const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
25425
+ const standingApproval = standing && !stated ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding) : null;
25426
+ if (standingApproval && !standingApproval.ok)
25427
+ return standingApproval;
25428
+ const native = nativeRequired && !stated && !standingApproval ? verifyNativeDecision(this.context.nativeAuthority, {
25245
25429
  observation: nativeObservation,
25246
25430
  expected: {
25247
25431
  taskId: task.data.id,
@@ -25272,7 +25456,7 @@ class WorkitCore {
25272
25456
  const entry = {
25273
25457
  id: newId(),
25274
25458
  recordedAt: mutation.now,
25275
- provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
25459
+ provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
25276
25460
  data
25277
25461
  };
25278
25462
  return success(mutation.revision, null, {
@@ -26491,7 +26675,7 @@ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "gla
26491
26675
 
26492
26676
  // packages/workit-core/src/core/repo-context.ts
26493
26677
  import { spawnSync as spawnSync4 } from "node:child_process";
26494
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
26678
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
26495
26679
  import path12 from "node:path";
26496
26680
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
26497
26681
  try {
@@ -26661,7 +26845,7 @@ var packageScripts = (cwd, keys) => {
26661
26845
  if (!existsSync6(pkgPath))
26662
26846
  return [];
26663
26847
  try {
26664
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
26848
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
26665
26849
  return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
26666
26850
  } catch {
26667
26851
  return [];
@@ -26695,7 +26879,7 @@ var documentationFiles = (cwd) => {
26695
26879
  var readTrimmed = (file, maxLines) => {
26696
26880
  if (!existsSync6(file))
26697
26881
  return "";
26698
- const lines = readFileSync6(file, "utf8").split(`
26882
+ const lines = readFileSync7(file, "utf8").split(`
26699
26883
  `);
26700
26884
  return lines.slice(0, maxLines).join(`
26701
26885
  `);
@@ -26883,7 +27067,7 @@ function docsRefreshContext(root, range) {
26883
27067
  const pkgPath = path12.join(cwd, "package.json");
26884
27068
  if (existsSync6(pkgPath)) {
26885
27069
  try {
26886
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
27070
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
26887
27071
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
26888
27072
  `;
26889
27073
  } catch {}
@@ -27581,7 +27765,7 @@ var TOOL_CAPABILITIES = [
27581
27765
  ];
27582
27766
  var VERSION = (() => {
27583
27767
  try {
27584
- const packageJson = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
27768
+ const packageJson = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8"));
27585
27769
  return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
27586
27770
  } catch {
27587
27771
  return "0.0.0";