@brainervirus/workit-pi 8.25.0 → 8.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -6669,6 +6669,41 @@ var ENVELOPE = new Set([
6669
6669
  "judgment"
6670
6670
  ]);
6671
6671
  // packages/workit-core/src/core/config.ts
6672
+ import {
6673
+ copyFileSync,
6674
+ cpSync,
6675
+ existsSync,
6676
+ mkdirSync,
6677
+ readFileSync,
6678
+ readdirSync,
6679
+ writeFileSync
6680
+ } from "node:fs";
6681
+ import os from "node:os";
6682
+ import path from "node:path";
6683
+
6684
+ // packages/workit-core/src/core/boundary.ts
6685
+ var EVENT = {
6686
+ initialization: "initialization",
6687
+ provenance: "provenance",
6688
+ assets: "assets",
6689
+ configurationSource: "configuration_source",
6690
+ hooks: "hooks",
6691
+ mcpConnection: "mcp_connection",
6692
+ migration: "migration",
6693
+ installSteps: "install_steps",
6694
+ uncaughtFailure: "uncaught_failure",
6695
+ toolsFailed: "tools_failed",
6696
+ doctor: "doctor"
6697
+ };
6698
+ var errorDetail = (err) => {
6699
+ if (err instanceof Error) {
6700
+ return { error_name: err.name, error: err.message };
6701
+ }
6702
+ return { error_name: "unknown", error: String(err) };
6703
+ };
6704
+
6705
+ // packages/workit-core/src/core/config.ts
6706
+ var diagnosticLogger;
6672
6707
  var PRESETS = {
6673
6708
  gitflow: {
6674
6709
  allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
@@ -6678,6 +6713,49 @@ var PRESETS = {
6678
6713
  "trunk-based": { allowed: ["*"], protected: ["main"] },
6679
6714
  custom: { allowed: [], protected: [] }
6680
6715
  };
6716
+ var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workit");
6717
+ var migratedDir = null;
6718
+ var migrationFailed = false;
6719
+ var ensureConfigDir = (dir = resolveConfigDir()) => {
6720
+ if (migratedDir === dir)
6721
+ return dir;
6722
+ if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
6723
+ migratedDir = dir;
6724
+ return dir;
6725
+ }
6726
+ const legacy = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workflow-toolkit");
6727
+ if (!existsSync(legacy)) {
6728
+ migratedDir = dir;
6729
+ return dir;
6730
+ }
6731
+ if (!migrationFailed && existsSync(dir)) {
6732
+ migratedDir = dir;
6733
+ return dir;
6734
+ }
6735
+ migrationFailed = false;
6736
+ mkdirSync(dir, { recursive: true });
6737
+ diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
6738
+ for (const entry of readdirSync(legacy, { withFileTypes: true })) {
6739
+ const src = path.join(legacy, entry.name);
6740
+ const dest = path.join(dir, entry.name);
6741
+ if (existsSync(dest))
6742
+ continue;
6743
+ try {
6744
+ if (entry.isDirectory())
6745
+ cpSync(src, dest, { recursive: true });
6746
+ else if (entry.isFile())
6747
+ copyFileSync(src, dest);
6748
+ } catch (err) {
6749
+ migrationFailed = true;
6750
+ diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
6751
+ console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
6752
+ }
6753
+ }
6754
+ if (!migrationFailed)
6755
+ migratedDir = dir;
6756
+ return dir;
6757
+ };
6758
+ var configDir = () => ensureConfigDir();
6681
6759
  var DEFAULTS = {
6682
6760
  locale: "en",
6683
6761
  localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
@@ -6699,7 +6777,9 @@ var COMMIT_PRESETS = [
6699
6777
  ];
6700
6778
 
6701
6779
  // packages/workit-core/src/core/workspaces.ts
6702
- import path from "node:path";
6780
+ import { spawnSync } from "node:child_process";
6781
+ import { readFileSync as readFileSync2, realpathSync } from "node:fs";
6782
+ import path2 from "node:path";
6703
6783
  var nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
6704
6784
  var branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
6705
6785
  var commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
@@ -6757,7 +6837,7 @@ var releaseTrackSchema = object({
6757
6837
  context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
6758
6838
  }
6759
6839
  }
6760
- if (track.versionSource?.kind === "package-json" && (path.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
6840
+ if (track.versionSource?.kind === "package-json" && (path2.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
6761
6841
  context.addIssue({
6762
6842
  code: "custom",
6763
6843
  path: ["versionSource", "path"],
@@ -6781,6 +6861,7 @@ var workspaceConfigSchema = object({
6781
6861
  trailers: boolean2().optional(),
6782
6862
  liveSessionGuard: boolean2().optional(),
6783
6863
  fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
6864
+ models: object({ roles: unknown().optional() }).passthrough().optional(),
6784
6865
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
6785
6866
  kind,
6786
6867
  union([boolean2(), literal("verified")]).optional()
@@ -6807,7 +6888,10 @@ var workspaceConfigSchema = object({
6807
6888
  });
6808
6889
  }
6809
6890
  });
6810
- var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
6891
+ var workspacesFileSchema = object({
6892
+ workspaces: array(workspaceConfigSchema).optional(),
6893
+ models: object({ roles: unknown().optional() }).passthrough().optional()
6894
+ }).passthrough().superRefine((file, context) => {
6811
6895
  const names = new Set;
6812
6896
  for (const [index, workspace] of (file.workspaces ?? []).entries()) {
6813
6897
  if (names.has(workspace.name)) {
@@ -6820,10 +6904,213 @@ var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).opt
6820
6904
  names.add(workspace.name);
6821
6905
  }
6822
6906
  });
6907
+ var globToRegExp = (glob) => {
6908
+ let out = "";
6909
+ for (let i = 0;i < glob.length; i++) {
6910
+ const c = glob[i];
6911
+ if (c === "*") {
6912
+ if (glob[i + 1] === "*") {
6913
+ if (glob[i + 2] === "/") {
6914
+ if (out === "")
6915
+ out += "/?";
6916
+ out += "(?:[^/]+/)*";
6917
+ i += 2;
6918
+ } else {
6919
+ if (i + 2 >= glob.length) {
6920
+ if (out.endsWith("/")) {
6921
+ out = out.slice(0, -1);
6922
+ out += "(?:/.*)?";
6923
+ } else {
6924
+ out += ".*";
6925
+ }
6926
+ } else {
6927
+ out += ".*";
6928
+ }
6929
+ i++;
6930
+ }
6931
+ } else {
6932
+ out += "[^/]*";
6933
+ }
6934
+ } else {
6935
+ out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6936
+ }
6937
+ }
6938
+ return new RegExp(`^${out}$`);
6939
+ };
6940
+ var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
6941
+ var UNSUPPORTED_GLOB = /[?[\]{}]/;
6942
+ var UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
6943
+ var validateWorkspaceGlob = (glob) => {
6944
+ const trimmed = glob.trim();
6945
+ if (!trimmed)
6946
+ return { ok: false, error: "workspace pattern is required" };
6947
+ const m = UNSUPPORTED_GLOB.exec(trimmed);
6948
+ if (m) {
6949
+ return {
6950
+ ok: false,
6951
+ error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
6952
+ };
6953
+ }
6954
+ const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
6955
+ if (ext) {
6956
+ const token = ext[0];
6957
+ const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
6958
+ return {
6959
+ ok: false,
6960
+ error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
6961
+ };
6962
+ }
6963
+ return { ok: true };
6964
+ };
6965
+
6966
+ class WorkspaceConfigError extends Error {
6967
+ name = "WorkspaceConfigError";
6968
+ }
6823
6969
  var mainCheckouts = new Map;
6970
+ var mainCheckoutOf = (cwd) => {
6971
+ const cached = mainCheckouts.get(cwd);
6972
+ if (cached !== undefined)
6973
+ return cached;
6974
+ const env = { ...process.env };
6975
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
6976
+ delete env[name];
6977
+ const git = (args) => {
6978
+ const run = spawnSync("git", args, {
6979
+ cwd,
6980
+ env,
6981
+ encoding: "utf8",
6982
+ timeout: 5000,
6983
+ windowsHide: true
6984
+ });
6985
+ return run.status === 0 ? run.stdout : null;
6986
+ };
6987
+ let main = null;
6988
+ const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
6989
+ const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
6990
+ if (gitDir && common && path2.resolve(gitDir) !== path2.resolve(common)) {
6991
+ const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
6992
+ main = listed?.trim() || (path2.basename(common) === ".git" ? path2.dirname(common) : common);
6993
+ }
6994
+ mainCheckouts.set(cwd, main);
6995
+ return main;
6996
+ };
6997
+ var matchingEntries = (cwd, entries) => {
6998
+ const match = (where) => {
6999
+ const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
7000
+ return entries.filter((entry) => {
7001
+ const glob = entry.glob.replaceAll("\\", "/");
7002
+ const canonical = canonicalGlob(glob);
7003
+ return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
7004
+ });
7005
+ };
7006
+ const direct = match(cwd);
7007
+ if (direct.length)
7008
+ return direct;
7009
+ const main = mainCheckoutOf(cwd);
7010
+ return main ? match(main) : direct;
7011
+ };
7012
+ var realpathOf = (p) => {
7013
+ const real = realpathSync.native ?? realpathSync;
7014
+ try {
7015
+ return real(p);
7016
+ } catch {
7017
+ return p;
7018
+ }
7019
+ };
7020
+ var canonicalGlob = (glob) => {
7021
+ const m = /[*?[\]{]/.exec(glob);
7022
+ const prefix = m ? glob.slice(0, m.index) : glob;
7023
+ const rest = m ? glob.slice(m.index) : "";
7024
+ if (!prefix)
7025
+ return glob;
7026
+ const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
7027
+ return real + (prefix.endsWith("/") ? "/" : "") + rest;
7028
+ };
7029
+ var globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length;
7030
+ var selectWorkspaceMatch = (matches, cwd, workspaceName) => {
7031
+ if (workspaceName !== undefined) {
7032
+ const named = matches.filter((entry) => entry.name === workspaceName);
7033
+ if (named.length === 1)
7034
+ return named[0];
7035
+ if (named.length > 1)
7036
+ throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
7037
+ if (matches.length > 0) {
7038
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
7039
+ }
7040
+ throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
7041
+ }
7042
+ if (matches.length < 2)
7043
+ return matches[0];
7044
+ const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
7045
+ const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
7046
+ if (mostSpecific.length > 1) {
7047
+ throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
7048
+ }
7049
+ return mostSpecific[0];
7050
+ };
6824
7051
  var runtimeWorkspaceIndexSchema = object({
6825
7052
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
6826
7053
  }).passthrough();
7054
+ var matchRuntimeWorkspace = (cwd, dir = configDir()) => {
7055
+ const file = path2.join(dir, "workspaces.json");
7056
+ let raw;
7057
+ try {
7058
+ raw = readFileSync2(file, "utf8");
7059
+ } catch (error) {
7060
+ if (error.code === "ENOENT")
7061
+ return null;
7062
+ throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
7063
+ }
7064
+ let parsed;
7065
+ try {
7066
+ parsed = JSON.parse(raw);
7067
+ } catch {
7068
+ throw new WorkspaceConfigError(`${file} is not valid JSON`);
7069
+ }
7070
+ const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
7071
+ if (!validated.success) {
7072
+ const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
7073
+ throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
7074
+ }
7075
+ const entries = validated.data.workspaces;
7076
+ for (const entry of entries ?? []) {
7077
+ const glob = validateWorkspaceGlob(entry.glob);
7078
+ if (!glob.ok)
7079
+ throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
7080
+ }
7081
+ const matches = matchingEntries(cwd, entries ?? []);
7082
+ const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
7083
+ const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
7084
+ return selected ? { file, selected, matches } : null;
7085
+ };
7086
+ var modelSources = (cwd, dir) => {
7087
+ const out = [];
7088
+ try {
7089
+ const matched = matchRuntimeWorkspace(cwd, dir);
7090
+ if (matched) {
7091
+ const enclosing = matched.matches.filter((entry) => entry !== matched.selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
7092
+ for (const entry of [matched.selected, ...enclosing])
7093
+ out.push({
7094
+ name: entry.name,
7095
+ global: false,
7096
+ models: entry.models,
7097
+ tierModels: entry.fanout?.tierModels
7098
+ });
7099
+ }
7100
+ } catch {}
7101
+ try {
7102
+ const document = JSON.parse(readFileSync2(path2.join(dir ?? configDir(), "workspaces.json"), "utf8"));
7103
+ if (document && typeof document === "object" && !Array.isArray(document))
7104
+ out.push({
7105
+ name: "global",
7106
+ global: true,
7107
+ models: document.models,
7108
+ tierModels: undefined
7109
+ });
7110
+ } catch {}
7111
+ return out;
7112
+ };
7113
+ var hostFamily = (host) => host.replace(/_(?:cli|desktop)$/u, "");
6827
7114
  var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
6828
7115
  var TIER_HOSTS = new Set([
6829
7116
  "claude_code",
@@ -6837,11 +7124,11 @@ var TIER_HOSTS = new Set([
6837
7124
  ]);
6838
7125
 
6839
7126
  // packages/workit-core/src/git/rev.ts
6840
- import { spawnSync } from "node:child_process";
7127
+ import { spawnSync as spawnSync2 } from "node:child_process";
6841
7128
  var MAX_BUFFER = 256 * 1024 * 1024;
6842
7129
  var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
6843
7130
  var git = (cwd, args, options = {}) => {
6844
- const result = spawnSync("git", args, {
7131
+ const result = spawnSync2("git", args, {
6845
7132
  cwd,
6846
7133
  encoding: "utf8",
6847
7134
  env: options.env ?? process.env,
@@ -6890,7 +7177,7 @@ var memo2 = new Map;
6890
7177
  var TRACKERS = new Set(["github", "gitlab", "youtrack"]);
6891
7178
 
6892
7179
  // packages/workit-core/src/core/logger.ts
6893
- import os from "node:os";
7180
+ import os2 from "node:os";
6894
7181
  var SENSITIVE_WORDS = new Set([
6895
7182
  "token",
6896
7183
  "secret",
@@ -6929,15 +7216,15 @@ var SENSITIVE_WORDS = new Set([
6929
7216
  ]);
6930
7217
  var STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
6931
7218
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6932
- var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
7219
+ var homePattern = new RegExp(`${escapeRegExp(os2.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
6933
7220
 
6934
7221
  // packages/workit-core/src/store/paths.ts
6935
- import { spawnSync as spawnSync2 } from "node:child_process";
7222
+ import { spawnSync as spawnSync3 } from "node:child_process";
6936
7223
  import fs from "node:fs";
6937
- import path2 from "node:path";
7224
+ import path3 from "node:path";
6938
7225
  var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
6939
7226
  var gitRun = (cwd, args) => {
6940
- const run = spawnSync2("git", args, {
7227
+ const run = spawnSync3("git", args, {
6941
7228
  cwd,
6942
7229
  encoding: "utf8",
6943
7230
  timeout: 20000,
@@ -6975,14 +7262,14 @@ var DISCOVERY_ENV = [
6975
7262
  ];
6976
7263
  var isGitDir = (dir) => {
6977
7264
  try {
6978
- return fs.statSync(path2.join(dir, "HEAD")).isFile() && fs.statSync(path2.join(dir, "objects")).isDirectory() && fs.statSync(path2.join(dir, "refs")).isDirectory();
7265
+ return fs.statSync(path3.join(dir, "HEAD")).isFile() && fs.statSync(path3.join(dir, "objects")).isDirectory() && fs.statSync(path3.join(dir, "refs")).isDirectory();
6979
7266
  } catch {
6980
7267
  return false;
6981
7268
  }
6982
7269
  };
6983
7270
  var reftableRefs = (common) => {
6984
7271
  try {
6985
- return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs.readFileSync(path2.join(common, "config"), "utf8"));
7272
+ return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs.readFileSync(path3.join(common, "config"), "utf8"));
6986
7273
  } catch {
6987
7274
  return false;
6988
7275
  }
@@ -7002,7 +7289,7 @@ function fastGit(root) {
7002
7289
  if (device !== null && stat.dev !== device)
7003
7290
  return { kind: "none" };
7004
7291
  device = stat.dev;
7005
- const dotGit = path2.join(dir, ".git");
7292
+ const dotGit = path3.join(dir, ".git");
7006
7293
  let entry = null;
7007
7294
  try {
7008
7295
  entry = fs.statSync(dotGit);
@@ -7023,19 +7310,19 @@ function fastGit(root) {
7023
7310
  const match = /^gitdir: (.+)$/mu.exec(text);
7024
7311
  if (!match)
7025
7312
  return null;
7026
- gitDir = path2.resolve(dir, match[1].trim());
7313
+ gitDir = path3.resolve(dir, match[1].trim());
7027
7314
  } else
7028
7315
  return null;
7029
- if (!fs.existsSync(path2.join(gitDir, "HEAD")))
7316
+ if (!fs.existsSync(path3.join(gitDir, "HEAD")))
7030
7317
  return null;
7031
7318
  } else if (isGitDir(dir))
7032
7319
  gitDir = dir;
7033
7320
  if (gitDir) {
7034
7321
  let common = gitDir;
7035
7322
  try {
7036
- common = path2.resolve(gitDir, fs.readFileSync(path2.join(gitDir, "commondir"), "utf8").trim());
7323
+ common = path3.resolve(gitDir, fs.readFileSync(path3.join(gitDir, "commondir"), "utf8").trim());
7037
7324
  } catch {}
7038
- if (!fs.existsSync(path2.join(common, "objects")))
7325
+ if (!fs.existsSync(path3.join(common, "objects")))
7039
7326
  return null;
7040
7327
  return {
7041
7328
  kind: "repo",
@@ -7045,26 +7332,26 @@ function fastGit(root) {
7045
7332
  reftable: reftableRefs(common)
7046
7333
  };
7047
7334
  }
7048
- const parent = path2.dirname(dir);
7335
+ const parent = path3.dirname(dir);
7049
7336
  if (parent === dir)
7050
7337
  return { kind: "none" };
7051
7338
  dir = parent;
7052
7339
  }
7053
7340
  }
7054
7341
  function resolveStore(root) {
7055
- root = path2.resolve(root);
7342
+ root = path3.resolve(root);
7056
7343
  let directory = false;
7057
7344
  try {
7058
7345
  directory = fs.statSync(root).isDirectory();
7059
7346
  } catch {}
7060
7347
  if (!directory)
7061
- return { dir: path2.join(root, ".workit"), shared: false, top: null };
7348
+ return { dir: path3.join(root, ".workit"), shared: false, top: null };
7062
7349
  const fast = fastGit(root);
7063
7350
  if (fast?.kind === "none")
7064
- return { dir: path2.join(root, ".workit"), shared: false, top: null };
7351
+ return { dir: path3.join(root, ".workit"), shared: false, top: null };
7065
7352
  if (fast?.kind === "repo")
7066
7353
  return {
7067
- dir: path2.join(fast.common, "workit"),
7354
+ dir: path3.join(fast.common, "workit"),
7068
7355
  shared: true,
7069
7356
  top: fast.top,
7070
7357
  git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
@@ -7074,13 +7361,13 @@ function resolveStore(root) {
7074
7361
  if (run.ok && common) {
7075
7362
  const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
7076
7363
  return {
7077
- dir: path2.join(real(path2.resolve(root, common)), "workit"),
7364
+ dir: path3.join(real(path3.resolve(root, common)), "workit"),
7078
7365
  shared: true,
7079
7366
  top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
7080
7367
  };
7081
7368
  }
7082
7369
  if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
7083
- return { dir: path2.join(root, ".workit"), shared: false, top: null };
7370
+ return { dir: path3.join(root, ".workit"), shared: false, top: null };
7084
7371
  return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
7085
7372
  `)[0] || "no output"}); fix the repository (git status should work here)`);
7086
7373
  }
@@ -7095,22 +7382,22 @@ function childSessionId(lead, child) {
7095
7382
  }
7096
7383
 
7097
7384
  // packages/workit-core/src/core/package-root.ts
7098
- import { existsSync } from "node:fs";
7099
- import path3 from "node:path";
7385
+ import { existsSync as existsSync2 } from "node:fs";
7386
+ import path4 from "node:path";
7100
7387
  import { fileURLToPath } from "node:url";
7101
7388
  var packageRoot = () => {
7102
- let dir = path3.dirname(fileURLToPath(import.meta.url));
7389
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
7103
7390
  while (true) {
7104
- const parent = path3.dirname(dir);
7105
- if (existsSync(path3.join(dir, "package.json")) || parent === dir)
7391
+ const parent = path4.dirname(dir);
7392
+ if (existsSync2(path4.join(dir, "package.json")) || parent === dir)
7106
7393
  return dir;
7107
7394
  dir = parent;
7108
7395
  }
7109
7396
  };
7110
7397
  var assetRoot = () => {
7111
7398
  const root = packageRoot();
7112
- const assets = path3.join(root, "assets");
7113
- return existsSync(assets) ? assets : root;
7399
+ const assets = path4.join(root, "assets");
7400
+ return existsSync2(assets) ? assets : root;
7114
7401
  };
7115
7402
 
7116
7403
  // packages/workit-core/src/core/store-lock.ts
@@ -7162,10 +7449,10 @@ var STALE_TEMPORARY_MS = 60 * 60000;
7162
7449
  var origins = new Map;
7163
7450
  var heldInProcess = new Map;
7164
7451
  // packages/workit-core/src/ledger.ts
7165
- import { spawnSync as spawnSync3 } from "node:child_process";
7452
+ import { spawnSync as spawnSync4 } from "node:child_process";
7166
7453
  import { createHash as createHash3, randomBytes } from "node:crypto";
7167
7454
  import fs3 from "node:fs";
7168
- import path5 from "node:path";
7455
+ import path6 from "node:path";
7169
7456
 
7170
7457
  // packages/workit-core/src/core/policy/substance.ts
7171
7458
  var SLASH_BLOCK = [["/*", "*/"]];
@@ -7268,33 +7555,33 @@ var TEST_ONLY_KINDS = new Set(["doc", "cosmetic", "generated", "test"]);
7268
7555
  // packages/workit-core/src/hooks/standing-signal.ts
7269
7556
  import { createHash as createHash2 } from "node:crypto";
7270
7557
  import fs2 from "node:fs";
7271
- import path4 from "node:path";
7558
+ import path5 from "node:path";
7272
7559
  var gitStoreDir = (cwd) => {
7273
7560
  try {
7274
- for (let dir = path4.resolve(cwd);; dir = path4.dirname(dir)) {
7275
- const dotGit = path4.join(dir, ".git");
7561
+ for (let dir = path5.resolve(cwd);; dir = path5.dirname(dir)) {
7562
+ const dotGit = path5.join(dir, ".git");
7276
7563
  if (fs2.existsSync(dotGit)) {
7277
7564
  let gitDir = dotGit;
7278
7565
  if (fs2.statSync(dotGit).isFile()) {
7279
7566
  const target = /^gitdir:\s*(.+)$/mu.exec(fs2.readFileSync(dotGit, "utf8"))?.[1]?.trim();
7280
7567
  if (!target)
7281
7568
  return null;
7282
- gitDir = path4.resolve(dir, target);
7283
- const common = path4.join(gitDir, "commondir");
7569
+ gitDir = path5.resolve(dir, target);
7570
+ const common = path5.join(gitDir, "commondir");
7284
7571
  if (fs2.existsSync(common))
7285
- gitDir = path4.resolve(gitDir, fs2.readFileSync(common, "utf8").trim());
7572
+ gitDir = path5.resolve(gitDir, fs2.readFileSync(common, "utf8").trim());
7286
7573
  }
7287
- return path4.join(gitDir, "workit");
7574
+ return path5.join(gitDir, "workit");
7288
7575
  }
7289
- if (path4.dirname(dir) === dir)
7576
+ if (path5.dirname(dir) === dir)
7290
7577
  return null;
7291
7578
  }
7292
7579
  } catch {
7293
7580
  return null;
7294
7581
  }
7295
7582
  };
7296
- var standingRevFile = (store) => path4.join(store, "standing", "rev");
7297
- var standingSeenFile = (store, session, agentId) => path4.join(store, "standing", "seen", `${createHash2("sha256").update(`${session}\x00${agentId ?? ""}`).digest("hex").slice(0, 32)}.json`);
7583
+ var standingRevFile = (store) => path5.join(store, "standing", "rev");
7584
+ var standingSeenFile = (store, session, agentId) => path5.join(store, "standing", "seen", `${createHash2("sha256").update(`${session}\x00${agentId ?? ""}`).digest("hex").slice(0, 32)}.json`);
7298
7585
  var readStandingRev = (store) => {
7299
7586
  try {
7300
7587
  return fs2.readFileSync(standingRevFile(store), "utf8").trim() || null;
@@ -7304,7 +7591,7 @@ var readStandingRev = (store) => {
7304
7591
  };
7305
7592
  var writeStandingSeen = (file, seen) => {
7306
7593
  try {
7307
- fs2.mkdirSync(path4.dirname(file), { recursive: true });
7594
+ fs2.mkdirSync(path5.dirname(file), { recursive: true });
7308
7595
  fs2.writeFileSync(file, JSON.stringify(seen));
7309
7596
  } catch {}
7310
7597
  };
@@ -7327,7 +7614,7 @@ var err = (code, error, unblock) => ({
7327
7614
  ...unblock ? { unblock } : {}
7328
7615
  });
7329
7616
  var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
7330
- const run = spawnSync3("git", args, {
7617
+ const run = spawnSync4("git", args, {
7331
7618
  cwd,
7332
7619
  encoding: "utf8",
7333
7620
  timeout: timeoutMs,
@@ -7355,14 +7642,14 @@ var git2 = (cwd, args) => {
7355
7642
  };
7356
7643
  var safeRef = (value) => value.length > 0 && !value.startsWith("-");
7357
7644
  function storeRoot(cwd) {
7358
- const location = resolveStore(path5.resolve(cwd));
7645
+ const location = resolveStore(path6.resolve(cwd));
7359
7646
  if (location instanceof Error)
7360
7647
  return err("unavailable", location.message.replace(/; fix the repository.*$/u, ""), "fix the repository (git status should work here) or run from a non-git directory");
7361
7648
  return { ok: true, value: { root: location.dir, shared: location.shared } };
7362
7649
  }
7363
7650
  function ledgerPath(cwd) {
7364
7651
  const root = storeRoot(cwd);
7365
- return root.ok ? { ok: true, value: path5.join(root.value.root, "ledger", "ledger.jsonl") } : root;
7652
+ return root.ok ? { ok: true, value: path6.join(root.value.root, "ledger", "ledger.jsonl") } : root;
7366
7653
  }
7367
7654
  var NETWORK_FS_TYPES = new Set([
7368
7655
  26985,
@@ -7821,11 +8108,97 @@ var contextReadSchema = object({
7821
8108
  planPath: string2().optional(),
7822
8109
  cwd: string2().min(1).optional()
7823
8110
  }).strict();
8111
+ // packages/workit-core/src/core/role-models.ts
8112
+ var MODEL_ROLES = [
8113
+ "decider",
8114
+ "implementer",
8115
+ "verifier",
8116
+ "reviewer",
8117
+ "explore",
8118
+ "babysit"
8119
+ ];
8120
+ var MODEL_TIERS = ["mundane", "standard", "hard"];
8121
+ var HOST_KEYS = new Set([
8122
+ "claude_code",
8123
+ "opencode",
8124
+ "codex",
8125
+ "codex_cli",
8126
+ "codex_desktop",
8127
+ "cursor",
8128
+ "pi",
8129
+ "default"
8130
+ ]);
8131
+ var BUILT_IN_CLAUDE_CODE = {
8132
+ decider: { model: "fable", effort: "high" },
8133
+ "implementer.hard": { model: "opus" },
8134
+ "implementer.standard": { model: "sonnet" },
8135
+ "implementer.mundane": { model: "haiku" },
8136
+ verifier: { model: "sonnet" },
8137
+ reviewer: { model: "opus" },
8138
+ explore: { model: "haiku" },
8139
+ babysit: { model: "haiku" }
8140
+ };
8141
+ var isObject2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
8142
+ var name = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
8143
+ var leaf = (value) => {
8144
+ const text = name(value);
8145
+ if (text)
8146
+ return { model: text, effort: null };
8147
+ if (!isObject2(value))
8148
+ return null;
8149
+ const model = name(value.model);
8150
+ return model ? { model, effort: name(value.effort) } : null;
8151
+ };
8152
+ var isFlat = (value) => typeof value === "string" || isObject2(value) && Object.hasOwn(value, "model");
8153
+ var forHost = (value, host) => {
8154
+ if (isFlat(value))
8155
+ return leaf(value);
8156
+ if (!isObject2(value))
8157
+ return null;
8158
+ const family = host ? hostFamily(host) : null;
8159
+ for (const key of [family, host, "default"])
8160
+ if (key && Object.hasOwn(value, key)) {
8161
+ const found = leaf(value[key]);
8162
+ if (found)
8163
+ return found;
8164
+ }
8165
+ return null;
8166
+ };
8167
+ var rolesOf = (models) => isObject2(models) && isObject2(models.roles) ? models.roles : null;
8168
+ var resolveRoleModel = (cwd, selector, host, dir) => {
8169
+ const { role, tier } = selector;
8170
+ for (const source of modelSources(cwd, dir)) {
8171
+ const where = source.global ? "the global default" : `workspace ${source.name}`;
8172
+ const roles = rolesOf(source.models);
8173
+ const configured = roles && Object.hasOwn(roles, role) ? roles[role] : undefined;
8174
+ const fromRoles = role === "implementer" ? tier && isObject2(configured) && Object.hasOwn(configured, tier) ? forHost(configured[tier], host) : null : forHost(configured, host);
8175
+ if (fromRoles)
8176
+ return { ...fromRoles, source: `models.roles in ${where}` };
8177
+ if (role === "implementer" && tier && isObject2(source.tierModels)) {
8178
+ const fromTiers = Object.hasOwn(source.tierModels, tier) ? forHost(source.tierModels[tier], host) : null;
8179
+ if (fromTiers)
8180
+ return { model: fromTiers.model, effort: null, source: `fanout.tierModels in ${where}` };
8181
+ }
8182
+ }
8183
+ if (host && hostFamily(host) === "claude_code") {
8184
+ const builtIn = BUILT_IN_CLAUDE_CODE[tier ? `${role}.${tier}` : role];
8185
+ if (builtIn)
8186
+ return { model: builtIn.model, effort: builtIn.effort ?? null, source: "built-in" };
8187
+ }
8188
+ return null;
8189
+ };
8190
+ var uniformImplementerModel = (cwd, host, dir) => {
8191
+ const tiers = MODEL_TIERS.map((tier) => resolveRoleModel(cwd, { role: "implementer", tier }, host, dir));
8192
+ const [first] = tiers;
8193
+ return first && tiers.every((t) => t?.model === first.model && t.effort === first.effort) ? first : null;
8194
+ };
8195
+ var ROLE_SELECTORS = MODEL_ROLES.flatMap((role) => role === "implementer" ? MODEL_TIERS.map((tier) => ({ role, tier })) : [{ role }]);
8196
+
7824
8197
  // packages/workit-core/src/hooks/standing.ts
7825
8198
  import fs4 from "node:fs";
7826
- import path6 from "node:path";
8199
+ import path7 from "node:path";
7827
8200
  var readPlans = (store) => {
7828
- const dir = path6.join(store, "fanouts");
8201
+ const dir = path7.join(store, "fanouts");
7829
8202
  let names;
7830
8203
  try {
7831
8204
  names = fs4.readdirSync(dir).filter((name) => name.endsWith(".json"));
@@ -7835,7 +8208,7 @@ var readPlans = (store) => {
7835
8208
  const plans = [];
7836
8209
  for (const name of names) {
7837
8210
  try {
7838
- const raw = JSON.parse(fs4.readFileSync(path6.join(dir, name), "utf8"));
8211
+ const raw = JSON.parse(fs4.readFileSync(path7.join(dir, name), "utf8"));
7839
8212
  if (typeof raw.name !== "string" || !Array.isArray(raw.slices))
7840
8213
  continue;
7841
8214
  const texts = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "") : [];
@@ -7962,7 +8335,9 @@ var roleTools = (role) => {
7962
8335
  return [...readOnly, "write", "edit", ...familyTools];
7963
8336
  return [...readOnly, ...familyTools];
7964
8337
  };
8338
+ var roleModel = (cwd, role) => role === "implementer" ? uniformImplementerModel(cwd, "pi") : resolveRoleModel(cwd, { role: role === "investigator" ? "explore" : "reviewer" }, "pi");
7965
8339
  function workerCommand(runtime, assignment) {
8340
+ const configured = roleModel(runtime.root, assignment.role);
7966
8341
  return {
7967
8342
  command: runtime.node,
7968
8343
  args: [
@@ -7973,6 +8348,8 @@ function workerCommand(runtime, assignment) {
7973
8348
  ...runtime.sessionId ? ["--session-id", runtime.sessionId] : [],
7974
8349
  "--approve",
7975
8350
  "--offline",
8351
+ ...configured ? ["--model", configured.model] : [],
8352
+ ...configured?.effort ? ["--thinking", configured.effort] : [],
7976
8353
  "--no-context-files",
7977
8354
  "--tools",
7978
8355
  roleTools(assignment.role).join(","),
package/dist/workit.js CHANGED
@@ -8407,6 +8407,7 @@ var workspaceConfigSchema = object({
8407
8407
  trailers: boolean2().optional(),
8408
8408
  liveSessionGuard: boolean2().optional(),
8409
8409
  fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
8410
+ models: object({ roles: unknown().optional() }).passthrough().optional(),
8410
8411
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
8411
8412
  kind,
8412
8413
  union([boolean2(), literal("verified")]).optional()
@@ -8433,7 +8434,10 @@ var workspaceConfigSchema = object({
8433
8434
  });
8434
8435
  }
8435
8436
  });
8436
- var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
8437
+ var workspacesFileSchema = object({
8438
+ workspaces: array(workspaceConfigSchema).optional(),
8439
+ models: object({ roles: unknown().optional() }).passthrough().optional()
8440
+ }).passthrough().superRefine((file, context) => {
8437
8441
  const names = new Set;
8438
8442
  for (const [index, workspace] of (file.workspaces ?? []).entries()) {
8439
8443
  if (names.has(workspace.name)) {
@@ -8745,6 +8749,34 @@ var workspaceTrailers = (cwd, dir) => {
8745
8749
  };
8746
8750
  };
8747
8751
  var workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false;
8752
+ var modelSources = (cwd, dir) => {
8753
+ const out = [];
8754
+ try {
8755
+ const matched = matchRuntimeWorkspace(cwd, dir);
8756
+ if (matched) {
8757
+ const enclosing = matched.matches.filter((entry) => entry !== matched.selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
8758
+ for (const entry of [matched.selected, ...enclosing])
8759
+ out.push({
8760
+ name: entry.name,
8761
+ global: false,
8762
+ models: entry.models,
8763
+ tierModels: entry.fanout?.tierModels
8764
+ });
8765
+ }
8766
+ } catch {}
8767
+ try {
8768
+ const document = JSON.parse(readFileSync2(path4.join(dir ?? configDir(), "workspaces.json"), "utf8"));
8769
+ if (document && typeof document === "object" && !Array.isArray(document))
8770
+ out.push({
8771
+ name: "global",
8772
+ global: true,
8773
+ models: document.models,
8774
+ tierModels: undefined
8775
+ });
8776
+ } catch {}
8777
+ return out;
8778
+ };
8779
+ var hostFamily = (host) => host.replace(/_(?:cli|desktop)$/u, "");
8748
8780
  var TIER_KEYS = new Set(["mundane", "standard", "hard"]);
8749
8781
  var TIER_HOSTS = new Set([
8750
8782
  "claude_code",
@@ -24669,6 +24701,92 @@ var cancelHint = (workerId) => `cancel it first: workit_worker {"action":"cancel
24669
24701
  import { spawn as nodeSpawn } from "node:child_process";
24670
24702
  import { randomUUID as randomUUID4 } from "node:crypto";
24671
24703
 
24704
+ // packages/workit-core/src/core/role-models.ts
24705
+ var MODEL_ROLES = [
24706
+ "decider",
24707
+ "implementer",
24708
+ "verifier",
24709
+ "reviewer",
24710
+ "explore",
24711
+ "babysit"
24712
+ ];
24713
+ var MODEL_TIERS = ["mundane", "standard", "hard"];
24714
+ var HOST_KEYS = new Set([
24715
+ "claude_code",
24716
+ "opencode",
24717
+ "codex",
24718
+ "codex_cli",
24719
+ "codex_desktop",
24720
+ "cursor",
24721
+ "pi",
24722
+ "default"
24723
+ ]);
24724
+ var BUILT_IN_CLAUDE_CODE = {
24725
+ decider: { model: "fable", effort: "high" },
24726
+ "implementer.hard": { model: "opus" },
24727
+ "implementer.standard": { model: "sonnet" },
24728
+ "implementer.mundane": { model: "haiku" },
24729
+ verifier: { model: "sonnet" },
24730
+ reviewer: { model: "opus" },
24731
+ explore: { model: "haiku" },
24732
+ babysit: { model: "haiku" }
24733
+ };
24734
+ var isObject7 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
24735
+ var name = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
24736
+ var leaf = (value) => {
24737
+ const text = name(value);
24738
+ if (text)
24739
+ return { model: text, effort: null };
24740
+ if (!isObject7(value))
24741
+ return null;
24742
+ const model = name(value.model);
24743
+ return model ? { model, effort: name(value.effort) } : null;
24744
+ };
24745
+ var isFlat = (value) => typeof value === "string" || isObject7(value) && Object.hasOwn(value, "model");
24746
+ var forHost = (value, host) => {
24747
+ if (isFlat(value))
24748
+ return leaf(value);
24749
+ if (!isObject7(value))
24750
+ return null;
24751
+ const family = host ? hostFamily(host) : null;
24752
+ for (const key of [family, host, "default"])
24753
+ if (key && Object.hasOwn(value, key)) {
24754
+ const found = leaf(value[key]);
24755
+ if (found)
24756
+ return found;
24757
+ }
24758
+ return null;
24759
+ };
24760
+ var rolesOf = (models) => isObject7(models) && isObject7(models.roles) ? models.roles : null;
24761
+ var resolveRoleModel = (cwd, selector, host, dir) => {
24762
+ const { role, tier } = selector;
24763
+ for (const source of modelSources(cwd, dir)) {
24764
+ const where = source.global ? "the global default" : `workspace ${source.name}`;
24765
+ const roles = rolesOf(source.models);
24766
+ const configured = roles && Object.hasOwn(roles, role) ? roles[role] : undefined;
24767
+ const fromRoles = role === "implementer" ? tier && isObject7(configured) && Object.hasOwn(configured, tier) ? forHost(configured[tier], host) : null : forHost(configured, host);
24768
+ if (fromRoles)
24769
+ return { ...fromRoles, source: `models.roles in ${where}` };
24770
+ if (role === "implementer" && tier && isObject7(source.tierModels)) {
24771
+ const fromTiers = Object.hasOwn(source.tierModels, tier) ? forHost(source.tierModels[tier], host) : null;
24772
+ if (fromTiers)
24773
+ return { model: fromTiers.model, effort: null, source: `fanout.tierModels in ${where}` };
24774
+ }
24775
+ }
24776
+ if (host && hostFamily(host) === "claude_code") {
24777
+ const builtIn = BUILT_IN_CLAUDE_CODE[tier ? `${role}.${tier}` : role];
24778
+ if (builtIn)
24779
+ return { model: builtIn.model, effort: builtIn.effort ?? null, source: "built-in" };
24780
+ }
24781
+ return null;
24782
+ };
24783
+ var uniformImplementerModel = (cwd, host, dir) => {
24784
+ const tiers = MODEL_TIERS.map((tier) => resolveRoleModel(cwd, { role: "implementer", tier }, host, dir));
24785
+ const [first] = tiers;
24786
+ return first && tiers.every((t) => t?.model === first.model && t.effort === first.effort) ? first : null;
24787
+ };
24788
+ var ROLE_SELECTORS = MODEL_ROLES.flatMap((role) => role === "implementer" ? MODEL_TIERS.map((tier) => ({ role, tier })) : [{ role }]);
24789
+
24672
24790
  // packages/workit-pi/src/worker-protocol.ts
24673
24791
  var MAX_WORKER_LINE_BYTES = 64 * 1024;
24674
24792
  var MAX_WORKER_STDERR_BYTES = 64 * 1024;
@@ -24740,7 +24858,9 @@ var roleTools = (role) => {
24740
24858
  return [...readOnly, "write", "edit", ...familyTools];
24741
24859
  return [...readOnly, ...familyTools];
24742
24860
  };
24861
+ var roleModel = (cwd, role) => role === "implementer" ? uniformImplementerModel(cwd, "pi") : resolveRoleModel(cwd, { role: role === "investigator" ? "explore" : "reviewer" }, "pi");
24743
24862
  function workerCommand2(runtime, assignment) {
24863
+ const configured = roleModel(runtime.root, assignment.role);
24744
24864
  return {
24745
24865
  command: runtime.node,
24746
24866
  args: [
@@ -24751,6 +24871,8 @@ function workerCommand2(runtime, assignment) {
24751
24871
  ...runtime.sessionId ? ["--session-id", runtime.sessionId] : [],
24752
24872
  "--approve",
24753
24873
  "--offline",
24874
+ ...configured ? ["--model", configured.model] : [],
24875
+ ...configured?.effort ? ["--thinking", configured.effort] : [],
24754
24876
  "--no-context-files",
24755
24877
  "--tools",
24756
24878
  roleTools(assignment.role).join(","),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-pi",
3
- "version": "8.25.0",
3
+ "version": "8.26.0",
4
4
  "description": "Workit native extension for Pi — tools, method skills, and session continuity",
5
5
  "keywords": [
6
6
  "agentic-coding",
@@ -25,8 +25,8 @@ units of ~100+ lines each. Coupled work stays with one owner. Workit never spawn
25
25
  Never hand-edit it; change the plan or the standing orders and render again.
26
26
  4. **Spawn in a rolling window**, in the background, each in its own worktree:
27
27
  keep 4-6 in flight and refill from `spawnable` in `workit fanout status` as
28
- each one finishes. Model: the brief's `model:` line, else TIER
29
- (`references/orchestration.md`). Claude Code: the `implementer` agent, never
28
+ each one finishes. Model: the brief's `model:` and `effort:` lines (`workit models show`),
29
+ else TIER (`references/orchestration.md`). Claude Code: the `implementer` agent, never
30
30
  with `isolation: "worktree"`; Workit's hooks confine it to its WORKTREE.
31
31
  5. **Judge liveness by side effects only:** `workit fanout status` (head age,
32
32
  PR, CI, verdict, landed; STUCK past the TIMEBOX). Stop a stuck worker and
@@ -40,12 +40,12 @@ units of ~100+ lines each. Coupled work stays with one owner. Workit never spawn
40
40
  chain resumes. A fix round and recheck that both fail: a decider, not a
41
41
  third round; product choices go to the user (`references/orchestration.md`).
42
42
  7. **Verify by the workspace `verification` setting** (`workit grant show`).
43
- A report is a pointer, never evidence. `proportional` (default): each slice's lane (`workit pr status`) decides; none needs no verdict, self the author's `--self`, focused (cheap model: Claude Code `model: sonnet`) and full a verifier after the last push; one verdict at a stack's top covers its unchanged lower PRs, and a stale one with `reverify: focused` re-checks only the new diff, reusing green CI. `self`: a verifier that wrote none
43
+ A report is a pointer, never evidence. `proportional` (default): each slice's lane (`workit pr status`) decides; none needs no verdict, self the author's `--self`, focused and full a verifier (Claude Code: the `model` and `effort` of `workit models show --role verifier`) after the last push; one verdict at a stack's top covers its unchanged lower PRs, and a stale one with `reverify: focused` re-checks only the new diff, reusing green CI. `self`: a verifier that wrote none
44
44
  of the slices, or you if you wrote none. `independent` or high risk: a
45
45
  separate verifier session, never yours (doctrine: the ledger only refuses
46
46
  authors). One verifier may take a batch of slices, one verdict per branch
47
47
  (`workit ledger verdict <result> --branch <b> --how "<evidence>"`), in a
48
- session never chosen by the author. A review panel on separate models only at high risk.
48
+ session never chosen by the author. Spawn it with the brief's `verifier:` model (`workit models show --role verifier`). A review panel on separate models only at high risk.
49
49
  8. **Fan in** with `workit fanout check`: out-of-scope files (any file outside
50
50
  it stops the fan-in) and `git merge-tree` conflicts with trunk and between
51
51
  siblings. Fix what it names until it exits 0; a merged slice reads as
@@ -5,22 +5,31 @@ and fans in. Workit records and checks; the host's own subagents run the work.
5
5
 
6
6
  ## TIER to model
7
7
 
8
- TIER is a field of each slice and of its brief. When the workspace sets
9
- `fanout.tierModels`, the brief has a `model:` line under TIER and `workit
10
- fanout status` names the model next to each spawnable slice: use that model.
11
- Without one, map TIER where the host lets you pick a model per spawn:
12
-
13
- | TIER | Claude Code (`model` on the Agent call) | Hosts without a per-spawn model |
14
- | --- | --- | --- |
15
- | scouting (read-only search, not a slice) | `haiku` | host default |
16
- | mundane | `sonnet` | host default |
17
- | standard | omit it (inherits yours) | host default |
18
- | hard | omit it (inherits yours); consider a race | host default |
19
-
20
- OpenCode takes a subagent's model from its agent config: pick a configured
21
- agent whose model fits the TIER, else the default. Codex, Cursor and Pi run
22
- every worker on the host's model, so TIER only sizes the timebox and decides
23
- whether to race.
8
+ TIER is a field of each slice and of its brief. Role models come from the
9
+ workspace (`models.roles`; `fanout.tierModels` still feeds the implementer
10
+ tiers) and, on Claude Code, built-in defaults: the brief has a `model:` line
11
+ (with `effort:` when set) under TIER and a `verifier:` line; `workit fanout
12
+ status` names the model next to each spawnable slice; `workit models show
13
+ [--role r] [--host h] --json` lists every role (decider, implementer tiers,
14
+ verifier, reviewer, explore, babysit). Pass exactly that `model` (and `effort`
15
+ where the host's spawn call takes one) on the spawn call: a subagent that
16
+ passes none inherits your model. Scouting and PR babysitting use the `explore`
17
+ and `babysit` roles. A role with no model (other hosts without config) runs
18
+ on the host's model, and TIER only sizes the timebox and decides whether to
19
+ race. Claude Code's built-in defaults, when nothing is configured:
20
+
21
+ | Role or TIER | `model` on the Agent call |
22
+ | --- | --- |
23
+ | decider | `fable`, effort `high` |
24
+ | hard | `opus` |
25
+ | standard | `sonnet` |
26
+ | mundane | `haiku` |
27
+ | verifier | `sonnet` |
28
+ | reviewer | `opus` |
29
+ | explore, babysit | `haiku` |
30
+
31
+ OpenCode takes a subagent's model from its agent config: pick a
32
+ configured agent whose model fits the TIER, else the default.
24
33
 
25
34
  ## Rolling window
26
35
 
@@ -53,7 +62,7 @@ reversibility (the option cheapest to undo wins a tie), and the exact change
53
62
  list. The fixer applies that list once, then one focused recheck follows.
54
63
 
55
64
  - Hard call (shared contract, security, migration): two deciders on different
56
- models (Claude Code: a different `model` on the Agent call; OpenCode: a
65
+ models (Claude Code: the `decider` role model, the other a different `model` on the Agent call; OpenCode: a
57
66
  child session on another configured agent; Codex, Cursor and Pi: one
58
67
  decider, since the host picks no model). If they disagree, you take the
59
68
  more reversible option.
@@ -40,7 +40,7 @@ description: Build a requested change in small verified steps - follow local pat
40
40
  status`). Lane none: no verdict. Lane self: after `workit check test` passes,
41
41
  record `workit ledger verdict tests-verified --self --how "<what you ran>"`;
42
42
  it reads self-reviewed, never verified, and is enough. Focused, full, high
43
- risk or `verification: self|independent` merges: one fresh verifier per PR after the last push, its own host-derived session (Claude Code: the `verifier` agent, `model: sonnet` when focused; Codex:
43
+ risk or `verification: self|independent` merges: one fresh verifier per PR after the last push, its own host-derived session (Claude Code: the `verifier` agent with the `model` (and `effort`) of `workit models show --role verifier`; Codex:
44
44
  the `verifier` agent; OpenCode: a child session; Pi: a supervised worker),
45
45
  never a hand-minted id. Before saying done, reconcile
46
46
  every named deliverable against the target checkout and observe it (for a push:
@@ -9,7 +9,8 @@ Review the candidate, never the author's summary of it. A session that wrote a
9
9
  commit on the branch cannot record a passing verdict: if that is you, hand the
10
10
  review to a fresh agent with its own host-derived session: Claude Code and
11
11
  Codex, the `reviewer` or `verifier` agent (`--session` and `--token` from SubagentStart);
12
- OpenCode, a child session; Pi, a supervised worker. Never hand-mint one such
12
+ OpenCode, a child session; Pi, a supervised worker. On Claude Code pass the
13
+ `model` and `effort` of `workit models show --role <verifier|reviewer>`. Never hand-mint one such
13
14
  as `<lead>-v1`: it is refused as `forged_session`.
14
15
  An author's `--self` verdict reads self-reviewed and never counts as
15
16
  independent; `type-check-only` never proves a behavior change.
@@ -51,7 +51,7 @@ the same check. Fix round and recheck both failed: a decider (read the `workit-f
51
51
  allows; no sleep loop. Red: read `logTail`, classify. Flake or infra: one
52
52
  `workit ci rerun --failed --reason flake|infra` per head. Real: reproduce
53
53
  with `workit check`, fix the root cause, batch every fix in one push.
54
- 6. **Verified.** The lane in `workit pr status` decides: none, no verdict; self, your `--self` verdict; focused or full (or `verification: self|independent`), one non-author verifier per PR after the last push, never per push (read the `workit-review` skill's SKILL.md and follow it), on Claude Code `model: sonnet` when focused. A docs-only push (or test-only under a unit verdict) keeps the verdict (`carried`); `reverify: focused` re-checks only the new diff, reusing green CI; one verdict at a stack's top covers its unchanged lower PRs. Land only when
54
+ 6. **Verified.** The lane in `workit pr status` decides: none, no verdict; self, your `--self` verdict; focused or full (or `verification: self|independent`), one non-author verifier per PR after the last push, never per push (read the `workit-review` skill's SKILL.md and follow it), on Claude Code with the `model` and `effort` of `workit models show --role verifier`. A docs-only push (or test-only under a unit verdict) keeps the verdict (`carried`); `reverify: focused` re-checks only the new diff, reusing green CI; one verdict at a stack's top covers its unchanged lower PRs. Land only when
55
55
  granted: `workit stack land` or `workit pr merge`; `--unverified --reason` if user-asked.
56
56
  7. **Observe it landed:** `workit verify-delivery pr` or `merge`. After a failed
57
57
  verdict or a check red 3+ times, at any endpoint: offer `/wk-retro` in one