@brainervirus/workit-opencode 8.15.0 → 8.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/plugin.js +641 -33
  2. package/package.json +2 -2
package/dist/plugin.js CHANGED
@@ -12384,6 +12384,7 @@ class TaskStore {
12384
12384
  var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
12385
12385
  var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
12386
12386
  var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
12387
+ var holdsLiveState = (state, dirty) => state === "running" || dirty === true;
12387
12388
 
12388
12389
  // packages/workit-core/src/core/task-evaluation.ts
12389
12390
  import { createHash as createHash10 } from "node:crypto";
@@ -12630,6 +12631,36 @@ var line = (run) => {
12630
12631
  return value ? value : null;
12631
12632
  };
12632
12633
  var safeArg = (value) => value.length > 0 && !value.startsWith("-");
12634
+ var UNPARSEABLE = "<unparseable remote>";
12635
+ function redactRemote(raw) {
12636
+ const value = raw.trim();
12637
+ if (!value)
12638
+ return UNPARSEABLE;
12639
+ if (/^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
12640
+ return UNPARSEABLE;
12641
+ if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith("."))
12642
+ return value;
12643
+ if (!/[:@]/u.test(value))
12644
+ return value;
12645
+ if (!value.includes("://")) {
12646
+ if (/^[^/@]*:[^/]*@/u.test(value))
12647
+ return UNPARSEABLE;
12648
+ const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):([^?#]+)/u.exec(value);
12649
+ return scp ? `${scp[1] ? `${scp[1]}@` : ""}${scp[2]}:${scp[3]}` : UNPARSEABLE;
12650
+ }
12651
+ try {
12652
+ const url = new URL(value);
12653
+ const ssh = /^(?:ssh|git\+ssh|ssh\+git):$/u.test(url.protocol);
12654
+ url.password = "";
12655
+ if (!ssh)
12656
+ url.username = "";
12657
+ url.search = "";
12658
+ url.hash = "";
12659
+ return url.toString();
12660
+ } catch {
12661
+ return UNPARSEABLE;
12662
+ }
12663
+ }
12633
12664
  function headSha(cwd) {
12634
12665
  return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
12635
12666
  }
@@ -12776,6 +12807,52 @@ function pushRemoteName(cwd, branch) {
12776
12807
  return "origin";
12777
12808
  return remotes.length === 1 ? remotes[0] : null;
12778
12809
  }
12810
+ function pushUrl(cwd, remote) {
12811
+ if (!safeArg(remote))
12812
+ return null;
12813
+ const urls = git2(cwd, ["remote", "get-url", "--push", "--all", remote]).stdout.split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
12814
+ return urls.length === 1 ? urls[0] : null;
12815
+ }
12816
+ var cleanPath = (value) => value.replace(/^\/+/u, "").replace(/\/+$/u, "").replace(/\.git$/u, "");
12817
+ function parseRemoteUrl(raw) {
12818
+ const value = raw.trim();
12819
+ if (!value || /^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
12820
+ return null;
12821
+ if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith(".")) {
12822
+ return { protocol: "file", user: null, host: "", port: null, path: value };
12823
+ }
12824
+ if (!value.includes("://")) {
12825
+ if (/^[^/@]*:[^/]*@/u.test(value))
12826
+ return null;
12827
+ const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):(.+)$/u.exec(value);
12828
+ if (!scp)
12829
+ return null;
12830
+ return {
12831
+ protocol: "ssh",
12832
+ user: scp[1] ?? null,
12833
+ host: scp[2],
12834
+ port: null,
12835
+ path: cleanPath(scp[3])
12836
+ };
12837
+ }
12838
+ let url;
12839
+ try {
12840
+ url = new URL(value);
12841
+ } catch {
12842
+ return null;
12843
+ }
12844
+ const scheme = url.protocol.replace(/:$/u, "");
12845
+ const protocol = scheme === "ssh" || scheme === "git+ssh" || scheme === "ssh+git" ? "ssh" : scheme === "https" || scheme === "http" || scheme === "git" || scheme === "file" ? scheme : null;
12846
+ if (!protocol)
12847
+ return null;
12848
+ return {
12849
+ protocol,
12850
+ user: protocol === "ssh" && url.username ? decodeURIComponent(url.username) : null,
12851
+ host: url.hostname.replace(/\.$/u, ""),
12852
+ port: url.port || null,
12853
+ path: cleanPath(decodeURIComponent(url.pathname))
12854
+ };
12855
+ }
12779
12856
 
12780
12857
  // packages/workit-core/src/core/policy/derive.ts
12781
12858
  import { existsSync as existsSync4 } from "node:fs";
@@ -16502,6 +16579,15 @@ var releaseBranchesOf = (cwd) => {
16502
16579
  }
16503
16580
  };
16504
16581
  var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
16582
+ var policy = (root) => resolveBranchPolicyFor(root);
16583
+ var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
16584
+ var isProtectedTarget = (root, name) => {
16585
+ try {
16586
+ return isProtected(root, name);
16587
+ } catch {
16588
+ return true;
16589
+ }
16590
+ };
16505
16591
 
16506
16592
  // packages/workit-core/src/hooks/session-marker.ts
16507
16593
  import { createHash as createHash12 } from "node:crypto";
@@ -17951,7 +18037,7 @@ var rewrittenUrl = (url, config) => {
17951
18037
  }
17952
18038
  return url;
17953
18039
  };
17954
- var pushUrl = (config, remote, dir) => {
18040
+ var pushUrl2 = (config, remote, dir) => {
17955
18041
  const configured = configValue(config, `remote.${remote}.pushurl`) ?? configValue(config, `remote.${remote}.url`);
17956
18042
  return normalizedUrl(rewrittenUrl(configured ?? remote, config), dir);
17957
18043
  };
@@ -17979,7 +18065,7 @@ var toPushRemote = (repo, remote, config) => {
17979
18065
  const pushRemote = pushRemoteOf(config, repo.branch);
17980
18066
  if (pushRemote === null || pushRemote === remote)
17981
18067
  return true;
17982
- return pushUrl(config, remote, repo.dir) === pushUrl(config, pushRemote, repo.dir);
18068
+ return pushUrl2(config, remote, repo.dir) === pushUrl2(config, pushRemote, repo.dir);
17983
18069
  };
17984
18070
  var quote = (text) => `\`${text}\``;
17985
18071
  var refusal = (action, branch, dir) => {
@@ -21011,6 +21097,7 @@ import { spawnSync as spawnSync15 } from "node:child_process";
21011
21097
  import fs16 from "node:fs";
21012
21098
  import path22 from "node:path";
21013
21099
  import { spawnSync as spawnSync14 } from "node:child_process";
21100
+ import { pathToFileURL } from "node:url";
21014
21101
  // packages/workit-core/src/forge/pr-body.ts
21015
21102
  function parseGhIssue(value) {
21016
21103
  const m = /issues\/(\d+)/.exec(value);
@@ -21044,6 +21131,39 @@ var pushRemote = (cwd) => {
21044
21131
  const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
21045
21132
  return urls.length === 1 ? urls[0] : null;
21046
21133
  };
21134
+ var safePushUrl = (remote) => {
21135
+ const raw = remote.trim();
21136
+ if (path22.isAbsolute(raw))
21137
+ return pathToFileURL(raw).href;
21138
+ if (!raw.includes("://")) {
21139
+ const scp = /^(?:([^@\s]+)@)?([^:]+):(.+)$/u.exec(raw);
21140
+ if (scp)
21141
+ return `${scp[1] || "git"}@${scp[2]}:${scp[3]}`;
21142
+ }
21143
+ try {
21144
+ const url = new URL(raw);
21145
+ if (!["file:", "git:", "http:", "https:", "ssh:"].includes(url.protocol))
21146
+ return null;
21147
+ url.password = "";
21148
+ if (url.protocol !== "ssh:")
21149
+ url.username = "";
21150
+ url.search = "";
21151
+ url.hash = "";
21152
+ return url.toString();
21153
+ } catch {
21154
+ return null;
21155
+ }
21156
+ };
21157
+ var pushTargetIsStable = (cwd, remote) => {
21158
+ const target = safePushUrl(remote);
21159
+ if (!target)
21160
+ return false;
21161
+ const result = spawnSync14("git", ["config", "--get-regexp", "^url\\..*\\.(insteadof|pushinsteadof)$"], { cwd, encoding: "utf8" });
21162
+ if (result.status !== 0 && result.status !== 1)
21163
+ return false;
21164
+ const prefixes = (result.stdout ?? "").split(/\r?\n/u).map((line) => line.trim().slice(line.trim().indexOf(" ") + 1)).filter(Boolean);
21165
+ return !prefixes.some((prefix) => target.startsWith(prefix));
21166
+ };
21047
21167
 
21048
21168
  // packages/workit-core/src/core/tracker-issues.ts
21049
21169
  var repoPathFromRemote = (remote) => {
@@ -23496,10 +23616,494 @@ var workerCoordinatorFor = (task, worker) => {
23496
23616
  return coordinator?.kind === "host" && coordinator.host === "opencode" ? coordinator.handle : null;
23497
23617
  };
23498
23618
 
23619
+ // packages/workit-core/src/fanout.ts
23620
+ import { createHash as createHash16 } from "node:crypto";
23621
+ import fs21 from "node:fs";
23622
+ import path37 from "node:path";
23623
+
23624
+ // packages/workit-core/src/stack.ts
23625
+ import { spawnSync as spawnSync21 } from "node:child_process";
23626
+ import { createHash as createHash15, randomBytes as randomBytes4 } from "node:crypto";
23627
+ import fs20 from "node:fs";
23628
+ import path36 from "node:path";
23629
+
23630
+ // packages/workit-core/src/git/ops.ts
23631
+ import { spawnSync as spawnSync20 } from "node:child_process";
23632
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
23633
+ import path35 from "node:path";
23634
+
23635
+ // packages/workit-core/src/forge/redact.ts
23636
+ var KEY = String.raw`[A-Za-z0-9_.-]*?(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|account[_-]?key|private[_-]?key|client[_-]?secret|credentials?|authorization|cookie|signature)[A-Za-z0-9_.-]*`;
23637
+ var ASSIGNMENT2 = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
23638
+ var QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
23639
+ var ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
23640
+
23641
+ // packages/workit-core/src/forge/types.ts
23642
+ var success2 = (data) => ({ ok: true, data });
23643
+ var failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
23644
+
23645
+ // packages/workit-core/src/git/ops.ts
23646
+ var COMMIT_TIMEOUT_MS = 10 * 60000;
23647
+ var git5 = (cwd, args, options = {}) => {
23648
+ const result = spawnSync20("git", args, {
23649
+ cwd,
23650
+ encoding: "utf8",
23651
+ env: { ...options.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
23652
+ maxBuffer: 64 * 1024 * 1024,
23653
+ timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
23654
+ killSignal: "SIGKILL",
23655
+ windowsHide: true,
23656
+ ...options.input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input: options.input, stdio: ["pipe", "pipe", "pipe"] }
23657
+ });
23658
+ return {
23659
+ ok: result.status === 0,
23660
+ status: result.status,
23661
+ stdout: result.stdout ?? "",
23662
+ stderr: result.stderr ?? ""
23663
+ };
23664
+ };
23665
+ function operationInProgress(cwd) {
23666
+ const markers = [
23667
+ ["rebase-merge", "rebase"],
23668
+ ["rebase-apply", "rebase"],
23669
+ ["MERGE_HEAD", "merge"],
23670
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
23671
+ ["REVERT_HEAD", "revert"]
23672
+ ];
23673
+ const run = git5(cwd, ["rev-parse", ...markers.flatMap(([name]) => ["--git-path", name])]);
23674
+ if (!run.ok)
23675
+ return null;
23676
+ const found = run.stdout.split(`
23677
+ `).map((line) => path35.resolve(cwd, line.trim()));
23678
+ const conflicted = () => git5(cwd, ["ls-files", "-u"]).stdout.trim() !== "";
23679
+ for (const [index, [, operation]] of markers.entries()) {
23680
+ const marker = found[index];
23681
+ if (!marker || !existsSync11(marker))
23682
+ continue;
23683
+ if (index === 1 && existsSync11(path35.join(marker, "applying")))
23684
+ return { operation: "am", commitAllowed: false, branch: null };
23685
+ if (index === 0 && existsSync11(path35.join(marker, "amend")) && !conflicted()) {
23686
+ let headName = "";
23687
+ try {
23688
+ headName = readFileSync9(path35.join(marker, "head-name"), "utf8").trim();
23689
+ } catch {}
23690
+ const branch = headName.startsWith("refs/heads/") ? headName.slice("refs/heads/".length) : null;
23691
+ return { operation, commitAllowed: branch !== null, branch };
23692
+ }
23693
+ if ((operation === "revert" || operation === "merge") && !conflicted())
23694
+ return { operation, commitAllowed: true, branch: null };
23695
+ return { operation, commitAllowed: false, branch: null };
23696
+ }
23697
+ return null;
23698
+ }
23699
+ function inProgressFailure(operation, verb) {
23700
+ const command = operation === "am" ? "git am" : `git ${operation}`;
23701
+ return failure3("blocked", `${operation.replace("-", "_")}_in_progress: a ${operation === "am" ? "git am" : operation} is in progress; finish or abort it before workit can ${verb}`, `resolve the conflicts, git add <files>, then ${command} --continue # or ${command} --abort to return to where you started`);
23702
+ }
23703
+ function pushPreflight(cwd, options = {}) {
23704
+ const progress = operationInProgress(cwd);
23705
+ if (progress)
23706
+ return inProgressFailure(progress.operation, "push");
23707
+ const branch = options.branch ?? currentBranch(cwd);
23708
+ if (!branch)
23709
+ return failure3("invalid_input", "HEAD is detached; push a branch");
23710
+ if (isProtectedTarget(cwd, branch))
23711
+ return failure3("blocked", `protected_branch: ${branch} is protected by the workspace branch policy; workit git push never pushes to it`, releaseBranch(cwd, branch) ? `workit git push --release ${branch} # finishing a release or hotfix; needs the release grant` : "workit git branch <feature/name> # push a feature branch and open a PR");
23712
+ const sha = resolveRef(cwd, `refs/heads/${branch}`);
23713
+ if (!sha)
23714
+ return failure3("not_found", `branch ${branch} has no commit to push`);
23715
+ const remote = pushRemoteName(cwd, branch);
23716
+ if (!remote)
23717
+ return failure3("not_found", "no push remote is configured for this branch", "git remote add origin <url>");
23718
+ const raw = pushUrl(cwd, remote);
23719
+ if (!raw)
23720
+ return failure3("blocked", `remote "${remote}" has no single push URL`, `git remote get-url --push --all ${remote} # keep exactly one push URL`);
23721
+ const parsed = parseRemoteUrl(raw);
23722
+ const local = parsed?.protocol === "file";
23723
+ if (!local && !pushTargetIsStable(cwd, raw))
23724
+ return failure3("blocked", `push_target_rewritten: the push URL of "${remote}" is rewritten by a url.*.insteadOf rule, so the destination is not the one checked`, "git config --get-regexp '^url\\.' # remove the rewrite for this remote");
23725
+ return success2({
23726
+ branch,
23727
+ sha,
23728
+ remote,
23729
+ url: redactRemote(raw),
23730
+ rawUrl: raw,
23731
+ local
23732
+ });
23733
+ }
23734
+
23735
+ // packages/workit-core/src/forge/report.ts
23736
+ var GITLAB_MERGEABLE = new Set(["mergeable"]);
23737
+ var GITHUB_MERGEABLE = new Set(["clean", "unstable", "has_hooks"]);
23738
+
23739
+ // packages/workit-core/src/stack.ts
23740
+ var stackFail = (code, error, unblock, data) => ({
23741
+ ok: false,
23742
+ code,
23743
+ error,
23744
+ ...unblock ? { unblock } : {},
23745
+ ...data ? { data } : {}
23746
+ });
23747
+ var stackDeps = {
23748
+ pushPlan: (cwd, branch) => pushPreflight(cwd, { branch }),
23749
+ adapter: { name: "git", restack: (cwd, input) => gitRestack(cwd, input) },
23750
+ rebaseTimeoutMs: Number(process.env.WORKIT_STACK_REBASE_TIMEOUT_MS) || 10 * 60000
23751
+ };
23752
+ var gitRun3 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
23753
+ const run = spawnSync21("git", args, {
23754
+ cwd,
23755
+ encoding: "utf8",
23756
+ timeout: timeoutMs,
23757
+ killSignal: "SIGKILL",
23758
+ windowsHide: true,
23759
+ maxBuffer: 64 * 1024 * 1024,
23760
+ env: {
23761
+ ...process.env,
23762
+ GIT_TERMINAL_PROMPT: "0",
23763
+ GIT_EDITOR: "true",
23764
+ GIT_SEQUENCE_EDITOR: "true",
23765
+ LC_ALL: "C",
23766
+ LANGUAGE: "C"
23767
+ }
23768
+ });
23769
+ const failure = run.error;
23770
+ return {
23771
+ ok: run.status === 0,
23772
+ stdout: run.stdout ?? "",
23773
+ stderr: run.error ? String(run.error.message) : run.stderr ?? "",
23774
+ timedOut: failure?.code === "ETIMEDOUT"
23775
+ };
23776
+ };
23777
+ var branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`);
23778
+ function worktreeOf(cwd, branch) {
23779
+ const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
23780
+ if (!list.ok)
23781
+ return null;
23782
+ let current = null;
23783
+ for (const line of list.stdout.split(`
23784
+ `)) {
23785
+ if (line.startsWith("worktree "))
23786
+ current = line.slice("worktree ".length);
23787
+ else if (line === `branch refs/heads/${branch}` && current)
23788
+ return current;
23789
+ else if (line === "detached" && current && rebasingBranch2(current) === `refs/heads/${branch}`)
23790
+ return current;
23791
+ }
23792
+ return null;
23793
+ }
23794
+ function rebasingBranch2(dir) {
23795
+ for (const name of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
23796
+ const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
23797
+ if (!found.ok)
23798
+ continue;
23799
+ try {
23800
+ return fs20.readFileSync(found.stdout.trim(), "utf8").trim();
23801
+ } catch {}
23802
+ }
23803
+ return null;
23804
+ }
23805
+ function rebaseInProgress(dir) {
23806
+ for (const name of ["rebase-merge", "rebase-apply"]) {
23807
+ const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
23808
+ if (found.ok && fs20.existsSync(found.stdout.trim()))
23809
+ return true;
23810
+ }
23811
+ return false;
23812
+ }
23813
+ var scratchDir = (cwd) => {
23814
+ const dir = stacksDir(cwd);
23815
+ return dir.ok ? path36.join(dir.data, "worktrees") : null;
23816
+ };
23817
+ function gitRestack(cwd, input) {
23818
+ const owner = worktreeOf(cwd, input.branch);
23819
+ let dir = owner;
23820
+ if (!dir) {
23821
+ const scratch = scratchDir(cwd);
23822
+ if (!scratch)
23823
+ return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
23824
+ fs20.mkdirSync(scratch, { recursive: true });
23825
+ dir = path36.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes4(4).toString("hex")}`);
23826
+ const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
23827
+ if (!added.ok)
23828
+ return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
23829
+ }
23830
+ const run = gitRun3(dir, [
23831
+ "-c",
23832
+ "rebase.autoStash=false",
23833
+ "-c",
23834
+ "rebase.updateRefs=false",
23835
+ "rebase",
23836
+ "--no-autosquash",
23837
+ "--onto",
23838
+ input.onto,
23839
+ input.from
23840
+ ], stackDeps.rebaseTimeoutMs);
23841
+ if (!run.ok) {
23842
+ if (run.timedOut)
23843
+ return {
23844
+ ok: false,
23845
+ conflict: false,
23846
+ timedOut: true,
23847
+ worktree: dir,
23848
+ error: `git rebase did not finish within ${stackDeps.rebaseTimeoutMs} ms`
23849
+ };
23850
+ if (rebaseInProgress(dir))
23851
+ return { ok: false, conflict: true, worktree: dir, error: firstLine(run) };
23852
+ if (!owner)
23853
+ gitRun3(cwd, ["worktree", "remove", "--force", dir]);
23854
+ return { ok: false, conflict: false, worktree: dir, error: firstLine(run) };
23855
+ }
23856
+ if (!owner)
23857
+ gitRun3(cwd, ["worktree", "remove", "--force", dir]);
23858
+ const head = branchTip(cwd, input.branch);
23859
+ return head ? { ok: true, head } : { ok: false, conflict: false, worktree: dir, error: `${input.branch} vanished` };
23860
+ }
23861
+ var firstLine = (run) => (`${run.stderr}
23862
+ ${run.stdout}`.split(`
23863
+ `).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300);
23864
+ var stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`;
23865
+ function stacksDir(cwd) {
23866
+ const root = storeRoot(cwd);
23867
+ if (!root.ok)
23868
+ return stackFail(root.code, root.error, root.unblock);
23869
+ return { ok: true, data: path36.join(root.value.root, "stacks") };
23870
+ }
23871
+ var HEARTBEAT_STALE_MS = 2 * 60000;
23872
+
23873
+ // packages/workit-core/src/fanout.ts
23874
+ var FANOUT_VERSION = 1;
23875
+ var TIERS = ["mundane", "standard", "hard"];
23876
+ var fanoutFail = (code, error, unblock, data) => ({
23877
+ ok: false,
23878
+ code,
23879
+ error,
23880
+ ...unblock ? { unblock } : {},
23881
+ ...data ? { data } : {}
23882
+ });
23883
+ var compiled = new Map;
23884
+ function normalizePattern(raw) {
23885
+ let pattern = raw.trim();
23886
+ if (pattern.includes("\\")) {
23887
+ if (/\\[*?{}]/u.test(pattern))
23888
+ return {
23889
+ error: "has \\*, \\?, \\{ or \\}: only \\[ and \\] are escapes; use / as the separator (src/**/*.ts)"
23890
+ };
23891
+ const brackets = /\\[[\]]/u.test(pattern);
23892
+ if (brackets && pattern.replace(/\\[[\]]/gu, "").includes("\\"))
23893
+ return {
23894
+ error: "mixes \\[ or \\] escapes with other backslashes, which is ambiguous: use / as the separator (app/\\[id\\]/page.tsx)"
23895
+ };
23896
+ if (!brackets)
23897
+ pattern = pattern.replaceAll("\\", "/");
23898
+ }
23899
+ while (pattern.startsWith("./"))
23900
+ pattern = pattern.slice(2);
23901
+ if (pattern === "." || pattern === "")
23902
+ pattern = raw.trim() ? "**" : "";
23903
+ if (pattern.endsWith("/"))
23904
+ pattern = `${pattern}**`;
23905
+ if (!pattern)
23906
+ return { error: "is empty" };
23907
+ if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
23908
+ return { error: "must be relative to the repository root" };
23909
+ if (pattern.split("/").includes(".."))
23910
+ return { error: "must not contain .." };
23911
+ const bare = pattern.replace(/\\./gu, "");
23912
+ if (/[[\]]/u.test(bare))
23913
+ return {
23914
+ error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
23915
+ };
23916
+ if (bare.startsWith("!"))
23917
+ return { error: "uses ! (negation is not supported)" };
23918
+ let depth = 0;
23919
+ for (const char of bare) {
23920
+ if (char === "{")
23921
+ depth += 1;
23922
+ if (char === "}")
23923
+ depth -= 1;
23924
+ if (depth < 0 || depth > 1)
23925
+ return { error: "has unbalanced or nested braces" };
23926
+ }
23927
+ if (depth !== 0)
23928
+ return { error: "has unbalanced or nested braces" };
23929
+ return { pattern };
23930
+ }
23931
+ var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23932
+ function findCycle(slices) {
23933
+ const byId = new Map(slices.map((slice) => [slice.id, slice]));
23934
+ const state = new Map;
23935
+ const trail = [];
23936
+ const visit = (id) => {
23937
+ if (state.get(id) === "done")
23938
+ return null;
23939
+ if (state.get(id) === "open")
23940
+ return [...trail.slice(trail.indexOf(id)), id];
23941
+ state.set(id, "open");
23942
+ trail.push(id);
23943
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
23944
+ if (!byId.has(dep) || dep === id)
23945
+ continue;
23946
+ const found = visit(dep);
23947
+ if (found)
23948
+ return found;
23949
+ }
23950
+ trail.pop();
23951
+ state.set(id, "done");
23952
+ return null;
23953
+ };
23954
+ for (const slice of slices) {
23955
+ const found = visit(slice.id);
23956
+ if (found)
23957
+ return found;
23958
+ }
23959
+ return null;
23960
+ }
23961
+ function sliceHash(slice) {
23962
+ const {
23963
+ worktree: _worktree,
23964
+ hash: _hash,
23965
+ hashSince: _since,
23966
+ tier: _tier,
23967
+ timebox: _timebox,
23968
+ context: _context,
23969
+ issues: _issues,
23970
+ ...definition
23971
+ } = slice;
23972
+ return createHash16("sha256").update(JSON.stringify(definition)).digest("hex").slice(0, 16);
23973
+ }
23974
+ function fanoutsDir(cwd) {
23975
+ const root = storeRoot(cwd);
23976
+ if (!root.ok)
23977
+ return fanoutFail(root.code, root.error, root.unblock);
23978
+ return { ok: true, data: path37.join(root.value.root, "fanouts") };
23979
+ }
23980
+ function parseStored(raw) {
23981
+ if (!isRecord5(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
23982
+ return null;
23983
+ if (!Array.isArray(raw.slices))
23984
+ return null;
23985
+ const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
23986
+ const text = (value) => typeof value === "string" ? value : "";
23987
+ let globsValid = true;
23988
+ const globs = (value) => list(value).map((entry) => {
23989
+ const normalized = normalizePattern(entry);
23990
+ if ("error" in normalized) {
23991
+ globsValid = false;
23992
+ return entry;
23993
+ }
23994
+ return normalized.pattern;
23995
+ });
23996
+ const slices = [];
23997
+ for (const item of raw.slices) {
23998
+ if (!isRecord5(item) || typeof item.id !== "string" || typeof item.branch !== "string")
23999
+ return null;
24000
+ slices.push({
24001
+ id: item.id,
24002
+ branch: item.branch,
24003
+ base: text(item.base) || raw.trunk,
24004
+ worktree: text(item.worktree),
24005
+ tier: TIERS.includes(item.tier) ? item.tier : "standard",
24006
+ dependsOn: list(item.dependsOn),
24007
+ scope: globs(item.scope),
24008
+ owns: globs(item.owns),
24009
+ goal: text(item.goal),
24010
+ acceptance: list(item.acceptance),
24011
+ verify: list(item.verify),
24012
+ forbidden: list(item.forbidden),
24013
+ context: typeof item.context === "string" ? item.context : null,
24014
+ timebox: typeof item.timebox === "string" ? item.timebox : null,
24015
+ issues: list(item.issues),
24016
+ hash: text(item.hash),
24017
+ hashSince: text(item.hashSince) || text(raw.createdAt)
24018
+ });
24019
+ }
24020
+ for (const slice of slices)
24021
+ slice.hash ||= sliceHash(slice);
24022
+ const shared = globs(raw.shared);
24023
+ const ids = new Set(slices.map((slice) => slice.id));
24024
+ if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
24025
+ return null;
24026
+ const fanIn = raw.fanIn === "integration" ? "integration" : "prs";
24027
+ const createdAt = text(raw.createdAt);
24028
+ return {
24029
+ v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
24030
+ name: raw.name,
24031
+ trunk: raw.trunk,
24032
+ fanIn,
24033
+ shared,
24034
+ slices,
24035
+ leadSession: typeof raw.leadSession === "string" && raw.leadSession ? raw.leadSession : null,
24036
+ formerLeads: list(raw.formerLeads),
24037
+ createdAt,
24038
+ updatedAt: text(raw.updatedAt)
24039
+ };
24040
+ }
24041
+ function readFanout(cwd, name) {
24042
+ const dir = fanoutsDir(cwd);
24043
+ if (!dir.ok)
24044
+ return dir;
24045
+ const file = path37.join(dir.data, stackFileName(name));
24046
+ let parsed = null;
24047
+ try {
24048
+ parsed = parseStored(JSON.parse(fs21.readFileSync(file, "utf8")));
24049
+ } catch (error) {
24050
+ if (error.code === "ENOENT")
24051
+ return { ok: true, data: null };
24052
+ parsed = null;
24053
+ }
24054
+ return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
24055
+ }
24056
+ function listFanouts(cwd) {
24057
+ const dir = fanoutsDir(cwd);
24058
+ if (!dir.ok)
24059
+ return dir;
24060
+ let names = [];
24061
+ try {
24062
+ names = fs21.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
24063
+ } catch {
24064
+ return { ok: true, data: [] };
24065
+ }
24066
+ const out = [];
24067
+ for (const entry of names) {
24068
+ let parsed = null;
24069
+ try {
24070
+ parsed = parseStored(JSON.parse(fs21.readFileSync(path37.join(dir.data, entry), "utf8")));
24071
+ } catch {
24072
+ parsed = null;
24073
+ }
24074
+ if (parsed && stackFileName(parsed.name) === entry)
24075
+ out.push(parsed);
24076
+ }
24077
+ return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
24078
+ }
24079
+ function selectFanout(cwd, name, current) {
24080
+ if (name) {
24081
+ const read = readFanout(cwd, name);
24082
+ if (!read.ok)
24083
+ return read;
24084
+ return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
24085
+ }
24086
+ const all = listFanouts(cwd);
24087
+ if (!all.ok)
24088
+ return all;
24089
+ const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
24090
+ if (holding.length === 1)
24091
+ return { ok: true, data: holding[0] };
24092
+ if (all.data.length === 1)
24093
+ return { ok: true, data: all.data[0] };
24094
+ return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
24095
+ }
24096
+
23499
24097
  // packages/workit-opencode/src/v2/lifecycle.ts
23500
24098
  var record2 = (value) => typeof value === "object" && value !== null ? value : {};
23501
24099
  var sessionIDOf = (value) => typeof value === "string" && value.length > 0 ? value : null;
23502
24100
  var parentOf = (session) => typeof session?.parentID === "string" && session.parentID ? session.parentID : undefined;
24101
+ var resumeBriefCommand = (root, directory) => {
24102
+ const branch = directory ? currentBranch(directory) : null;
24103
+ const plan = branch ? selectFanout(root, null, branch) : null;
24104
+ const slice = branch && plan?.ok ? plan.data.slices.find((entry) => entry.branch === branch || branch.startsWith(`${entry.branch}-try`) && /^\d+$/u.test(branch.slice(entry.branch.length + 4))) : undefined;
24105
+ return `workit fanout brief ${slice?.id ?? "<slice>"} --mode resume`;
24106
+ };
23503
24107
  var contentText = (content) => {
23504
24108
  if (typeof content === "string")
23505
24109
  return content;
@@ -23755,8 +24359,12 @@ var createV2Lifecycle = (deps) => {
23755
24359
  throw new Error("delegation_lineage_denied: a continuation sessionID must name an existing direct child of the coordinator");
23756
24360
  const listed = new TaskStore(root).listTasks();
23757
24361
  const bound = listed.ok ? listed.data.filter((task) => task.workers.some((worker) => worker.data.state === "running" && worker.data.session?.kind === "host" && worker.data.session.handle === childID && workerCoordinatorFor(task, worker) === coordinator)).length : 0;
23758
- if (bound !== 1 && ownsActiveTask(coordinator))
23759
- throw new Error("delegation_lineage_denied: a continuation sessionID must name a running Workit worker");
24362
+ if (bound !== 1 && ownsActiveTask(coordinator)) {
24363
+ const reported = listed.ok && listed.data.some((task) => task.workers.some((worker) => worker.data.state === "stopped" && worker.data.session?.kind === "host" && worker.data.session.handle === childID && workerCoordinatorFor(task, worker) === coordinator));
24364
+ const dirty = reported && child.directory ? worktreeTree(child.directory)?.dirty ?? null : null;
24365
+ if (!reported || !holdsLiveState("stopped", dirty))
24366
+ throw new Error(reported ? `delegation_lineage_denied: this worker already reported and holds no live state (no running process, no uncommitted changes); continue the slice with a fresh worker: ${resumeBriefCommand(root, child.directory)}` : "delegation_lineage_denied: a continuation sessionID must name a running Workit worker");
24367
+ }
23760
24368
  return;
23761
24369
  }
23762
24370
  if (!ownsActiveTask(coordinator)) {
@@ -24060,7 +24668,7 @@ var childTaskMissing = (store, input) => {
24060
24668
  var childDenial = (identity) => `OpenCode child session has no validated Workit worker: as ${identity} it may only record evidence or findings (workit_evidence/workit_finding action "record") on an existing task; for anything else the lead assigns it a worker with workit_worker before launching it`;
24061
24669
 
24062
24670
  // packages/workit-opencode/src/v2/shell.ts
24063
- import path35 from "node:path";
24671
+ import path38 from "node:path";
24064
24672
  var commandOf = (input) => {
24065
24673
  if (input === null || typeof input !== "object")
24066
24674
  return null;
@@ -24069,7 +24677,7 @@ var commandOf = (input) => {
24069
24677
  };
24070
24678
  var hookInput = (root, sessionID, id, shell) => ({
24071
24679
  host: "opencode",
24072
- cwd: shell.workdir ? path35.resolve(root, shell.workdir) : root,
24680
+ cwd: shell.workdir ? path38.resolve(root, shell.workdir) : root,
24073
24681
  session: { id: sessionID, agentId: null, agentType: null, parentId: null },
24074
24682
  permissionMode: null,
24075
24683
  transcriptPath: null,
@@ -24253,38 +24861,38 @@ var createStopControl = (root, port) => {
24253
24861
  };
24254
24862
 
24255
24863
  // packages/workit-opencode/src/v2/registry.ts
24256
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
24257
- import path37 from "node:path";
24864
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
24865
+ import path40 from "node:path";
24258
24866
 
24259
24867
  // packages/workit-opencode/src/shared/assets.ts
24260
- import { existsSync as existsSync11, realpathSync as realpathSync9 } from "node:fs";
24261
- import path36 from "node:path";
24868
+ import { existsSync as existsSync12, realpathSync as realpathSync9 } from "node:fs";
24869
+ import path39 from "node:path";
24262
24870
  import { fileURLToPath as fileURLToPath2 } from "node:url";
24263
24871
  var moduleDir = () => {
24264
24872
  try {
24265
- return realpathSync9(path36.dirname(fileURLToPath2(import.meta.url)));
24873
+ return realpathSync9(path39.dirname(fileURLToPath2(import.meta.url)));
24266
24874
  } catch {
24267
- return path36.dirname(fileURLToPath2(import.meta.url));
24875
+ return path39.dirname(fileURLToPath2(import.meta.url));
24268
24876
  }
24269
24877
  };
24270
24878
  var packageRoot2 = () => {
24271
24879
  const dir = moduleDir();
24272
24880
  for (const candidate of [
24273
- path36.resolve(dir, ".."),
24274
- path36.resolve(dir, "..", ".."),
24275
- path36.resolve(dir, "..", "..", "..")
24881
+ path39.resolve(dir, ".."),
24882
+ path39.resolve(dir, "..", ".."),
24883
+ path39.resolve(dir, "..", "..", "..")
24276
24884
  ]) {
24277
- if (existsSync11(path36.join(candidate, "package.json")))
24885
+ if (existsSync12(path39.join(candidate, "package.json")))
24278
24886
  return candidate;
24279
24887
  }
24280
- return path36.resolve(dir, "..");
24888
+ return path39.resolve(dir, "..");
24281
24889
  };
24282
24890
  var skillsRoot = () => {
24283
24891
  const root = packageRoot2();
24284
- const canonical = path36.resolve(root, "..", "workit-core", "skills");
24285
- if (existsSync11(path36.join(root, "src")) && existsSync11(canonical))
24892
+ const canonical = path39.resolve(root, "..", "workit-core", "skills");
24893
+ if (existsSync12(path39.join(root, "src")) && existsSync12(canonical))
24286
24894
  return canonical;
24287
- return path36.join(root, "assets", "skills");
24895
+ return path39.join(root, "assets", "skills");
24288
24896
  };
24289
24897
 
24290
24898
  // packages/workit-opencode/src/v2/registry.ts
@@ -24308,15 +24916,15 @@ var parseFrontmatter = (markdown) => {
24308
24916
  };
24309
24917
  var registerSkills = async (ctx) => {
24310
24918
  const skillsDir = skillsRoot();
24311
- if (!existsSync12(skillsDir))
24919
+ if (!existsSync13(skillsDir))
24312
24920
  return new Set;
24313
24921
  const listed = await ctx.skill.list().catch(() => ({ data: [] }));
24314
- const own = (skill) => typeof skill.path === "string" && path37.resolve(skill.path).startsWith(skillsDir + path37.sep);
24922
+ const own = (skill) => typeof skill.path === "string" && path40.resolve(skill.path).startsWith(skillsDir + path40.sep);
24315
24923
  const userSkills = new Set((listed?.data ?? []).filter((skill) => !own(skill)).map((skill) => skill.id));
24316
24924
  const packaged = new Map;
24317
24925
  for (const id of WORKIT_METHOD_SKILLS) {
24318
- const file = path37.join(skillsDir, id, "SKILL.md");
24319
- if (!userSkills.has(id) && existsSync12(file))
24926
+ const file = path40.join(skillsDir, id, "SKILL.md");
24927
+ if (!userSkills.has(id) && existsSync13(file))
24320
24928
  packaged.set(id, file);
24321
24929
  }
24322
24930
  await ctx.skill.transform((editor) => {
@@ -24324,7 +24932,7 @@ var registerSkills = async (ctx) => {
24324
24932
  for (const [id, file] of packaged) {
24325
24933
  if (existing.has(id))
24326
24934
  continue;
24327
- const parsed = parseFrontmatter(readFileSync9(file, "utf8"));
24935
+ const parsed = parseFrontmatter(readFileSync10(file, "utf8"));
24328
24936
  editor.add({
24329
24937
  id,
24330
24938
  name: parsed.name || id,
@@ -24362,25 +24970,25 @@ var registerCommands = async (ctx, registeredSkills) => {
24362
24970
  };
24363
24971
 
24364
24972
  // packages/workit-opencode/src/stale-sources.ts
24365
- import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
24366
- import path38 from "node:path";
24973
+ import { existsSync as existsSync14, readdirSync as readdirSync6 } from "node:fs";
24974
+ import path41 from "node:path";
24367
24975
  import { fileURLToPath as fileURLToPath3 } from "node:url";
24368
24976
  var typescriptFiles = (directory) => {
24369
- if (!existsSync13(directory))
24977
+ if (!existsSync14(directory))
24370
24978
  return [];
24371
24979
  return readdirSync6(directory, { withFileTypes: true }).flatMap((entry) => {
24372
- const file = path38.join(directory, entry.name);
24980
+ const file = path41.join(directory, entry.name);
24373
24981
  return entry.isDirectory() ? typescriptFiles(file) : entry.isFile() && file.endsWith(".ts") ? [file] : [];
24374
24982
  });
24375
24983
  };
24376
24984
  var pluginSourceFiles = [
24377
24985
  ...new Set([
24378
24986
  fileURLToPath3(import.meta.url),
24379
- ...typescriptFiles(path38.join(packageRoot2(), "src")),
24380
- path38.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
24381
- ...typescriptFiles(path38.join(packageRoot2(), "..", "workit-core", "src", "core"))
24987
+ ...typescriptFiles(path41.join(packageRoot2(), "src")),
24988
+ path41.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
24989
+ ...typescriptFiles(path41.join(packageRoot2(), "..", "workit-core", "src", "core"))
24382
24990
  ])
24383
- ].filter((file) => existsSync13(file)).toSorted();
24991
+ ].filter((file) => existsSync14(file)).toSorted();
24384
24992
 
24385
24993
  // packages/workit-opencode/src/v2/plugin.ts
24386
24994
  var sessionFacts = async (ctx, sessionID) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "8.15.0",
3
+ "version": "8.15.1",
4
4
  "private": false,
5
5
  "description": "Workit OpenCode plugin — native tools, adaptive policy, and sixteen method skills",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "typecheck": "tsc --noEmit"
40
40
  },
41
41
  "devDependencies": {
42
- "@brainervirus/workit-core": "^8.15.0",
42
+ "@brainervirus/workit-core": "^8.15.1",
43
43
  "@opencode/plugin": "2.0.18"
44
44
  },
45
45
  "engines": {