@brainervirus/workit-cli 0.8.1 → 0.8.2

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/index.js CHANGED
@@ -42527,6 +42527,22 @@ var readConfigFromDir = (dir) => {
42527
42527
  throw new Error(result2.error);
42528
42528
  return result2.config ?? DEFAULTS;
42529
42529
  };
42530
+ var resolveBranchPolicy = (config, workspace) => {
42531
+ const wp = workspace?.branchPolicy ?? {};
42532
+ const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
42533
+ const merged = mergePreset(preset, {
42534
+ allowed: wp.allowed,
42535
+ protectedNames: wp.protected
42536
+ }, config);
42537
+ const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
42538
+ return {
42539
+ preset,
42540
+ allowed,
42541
+ protected: new Set(merged.protected.map((p) => p.toLowerCase())),
42542
+ integration: wp.integration === "merge" ? "merge" : "pr",
42543
+ defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
42544
+ };
42545
+ };
42530
42546
 
42531
42547
  // packages/workit-core/src/core/workspaces.ts
42532
42548
  import { readFileSync as readFileSync3, realpathSync } from "node:fs";
@@ -42650,6 +42666,7 @@ var resolveWorkspaceFrom = (cwd2, dir) => {
42650
42666
  }
42651
42667
  return null;
42652
42668
  };
42669
+ var resolveWorkspace = (cwd2) => resolveWorkspaceFrom(cwd2, configDir());
42653
42670
 
42654
42671
  // packages/workit-core/src/core/gitignore.ts
42655
42672
  var GITIGNORE_ENTRIES = [
@@ -42671,6 +42688,10 @@ var GITIGNORE_ENTRIES = [
42671
42688
 
42672
42689
  // packages/workit-core/src/core/hygiene.ts
42673
42690
  import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
42691
+ import path5 from "node:path";
42692
+
42693
+ // packages/workit-core/src/core/changelog.ts
42694
+ import fs3 from "node:fs";
42674
42695
  import path4 from "node:path";
42675
42696
 
42676
42697
  // packages/workit-core/src/core/package-root.ts
@@ -42695,26 +42716,49 @@ var assetRoot = () => {
42695
42716
  // packages/workit-core/src/core/scripts.ts
42696
42717
  var PLUGIN_ROOT = packageRoot();
42697
42718
  var ASSET_ROOT = assetRoot();
42719
+ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
42720
+
42721
+ // packages/workit-core/src/core/changelog.ts
42722
+ function changelogUnreleasedStats(workspace_root, changelogPath = "CHANGELOG.md") {
42723
+ const cwd2 = resolveWorkspaceRoot(workspace_root);
42724
+ const abs = path4.isAbsolute(changelogPath) ? changelogPath : path4.join(cwd2, changelogPath);
42725
+ if (!fs3.existsSync(abs))
42726
+ return { exists: false };
42727
+ const text = fs3.readFileSync(abs, "utf8");
42728
+ const m = text.match(/##\s+\[Unreleased\]([\s\S]*?)(?=\n##\s+\[|$)/i);
42729
+ if (!m)
42730
+ return { exists: true, has_unreleased: false };
42731
+ const body = m[1];
42732
+ const headings = [...body.matchAll(/^###\s+(\w+)\s*$/gim)].map((x) => x[1]);
42733
+ const dupes = headings.filter((h, i) => headings.slice(0, i).some((p) => p.toLowerCase() === h.toLowerCase()));
42734
+ return {
42735
+ exists: true,
42736
+ has_unreleased: true,
42737
+ category_headings: headings,
42738
+ duplicate_category_headings: dupes,
42739
+ needs_normalize: dupes.length > 0
42740
+ };
42741
+ }
42698
42742
 
42699
42743
  // packages/workit-core/src/core/hygiene.ts
42700
42744
  var repoRoot = assetRoot();
42701
- var templatesDir = () => path4.join(repoRoot, "templates", "hygiene");
42745
+ var templatesDir = () => path5.join(repoRoot, "templates", "hygiene");
42702
42746
  var packageJson = (root) => {
42703
- if (!existsSync4(path4.join(root, "package.json")))
42747
+ if (!existsSync4(path5.join(root, "package.json")))
42704
42748
  return null;
42705
42749
  try {
42706
- return JSON.parse(readFileSync4(path4.join(root, "package.json"), "utf8"));
42750
+ return JSON.parse(readFileSync4(path5.join(root, "package.json"), "utf8"));
42707
42751
  } catch {
42708
42752
  return null;
42709
42753
  }
42710
42754
  };
42711
42755
  var isOpenSource = (root) => {
42712
- if (existsSync4(path4.join(root, "LICENSE")))
42756
+ if (existsSync4(path5.join(root, "LICENSE")))
42713
42757
  return true;
42714
42758
  const pkg = packageJson(root);
42715
42759
  if (pkg && !pkg.private)
42716
42760
  return true;
42717
- return path4.basename(root) === "workflow-toolkit";
42761
+ return path5.basename(root) === "workflow-toolkit";
42718
42762
  };
42719
42763
  var licenseHolder = (root) => {
42720
42764
  const pkg = packageJson(root);
@@ -42730,6 +42774,34 @@ var licenseHolder = (root) => {
42730
42774
  }
42731
42775
  return "";
42732
42776
  };
42777
+ var hygieneFiles = (root) => {
42778
+ const openSource = isOpenSource(root);
42779
+ const state = {};
42780
+ for (const file of [
42781
+ "CHANGELOG.md",
42782
+ "README.md",
42783
+ ".editorconfig",
42784
+ ".gitattributes",
42785
+ "LICENSE",
42786
+ "CONTRIBUTING.md"
42787
+ ]) {
42788
+ if (file === "LICENSE" || file === "CONTRIBUTING.md") {
42789
+ state[file] = openSource ? existsSync4(path5.join(root, file)) ? "ok" : "missing" : "skip";
42790
+ continue;
42791
+ }
42792
+ if (!existsSync4(path5.join(root, file))) {
42793
+ state[file] = "missing";
42794
+ continue;
42795
+ }
42796
+ if (file === "CHANGELOG.md") {
42797
+ const stats = changelogUnreleasedStats(root);
42798
+ state[file] = stats.exists && stats.has_unreleased ? "ok" : "invalid";
42799
+ continue;
42800
+ }
42801
+ state[file] = "ok";
42802
+ }
42803
+ return { state, openSource };
42804
+ };
42733
42805
  var planHygieneFiles = (root, opts = {}) => {
42734
42806
  const openSource = opts.includeOpenSource ?? isOpenSource(root);
42735
42807
  const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"];
@@ -42737,13 +42809,13 @@ var planHygieneFiles = (root, opts = {}) => {
42737
42809
  files.push("LICENSE", "CONTRIBUTING.md");
42738
42810
  const planned = [];
42739
42811
  for (const file of files) {
42740
- if (existsSync4(path4.join(root, file)))
42812
+ if (existsSync4(path5.join(root, file)))
42741
42813
  continue;
42742
- const tpl = path4.join(templatesDir(), file);
42814
+ const tpl = path5.join(templatesDir(), file);
42743
42815
  if (!existsSync4(tpl))
42744
42816
  continue;
42745
- const content = readFileSync4(tpl, "utf8").replace(/<PROJECT>/g, path4.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
42746
- planned.push({ path: path4.join(root, file), content });
42817
+ const content = readFileSync4(tpl, "utf8").replace(/<PROJECT>/g, path5.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
42818
+ planned.push({ path: path5.join(root, file), content });
42747
42819
  }
42748
42820
  return planned;
42749
42821
  };
@@ -42776,7 +42848,7 @@ import {
42776
42848
  writeFileSync as writeFileSync5
42777
42849
  } from "node:fs";
42778
42850
  import os5 from "node:os";
42779
- import path9 from "node:path";
42851
+ import path10 from "node:path";
42780
42852
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
42781
42853
 
42782
42854
  // packages/workit-core/src/core/branch-policy.ts
@@ -42841,9 +42913,9 @@ var detectBranchPolicy = (workspaceRoot) => {
42841
42913
 
42842
42914
  // packages/workit-core/src/core/setup-state.ts
42843
42915
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
42844
- import path5 from "node:path";
42916
+ import path6 from "node:path";
42845
42917
  var classifySetupFile = (dir, name) => {
42846
- const file = path5.join(dir, name);
42918
+ const file = path6.join(dir, name);
42847
42919
  let raw;
42848
42920
  try {
42849
42921
  raw = readFileSync5(file, "utf8");
@@ -42872,7 +42944,7 @@ var readSetupState = (dir = resolveConfigDir()) => ({
42872
42944
  });
42873
42945
 
42874
42946
  // packages/workit-core/src/core/registration.ts
42875
- import path6 from "node:path";
42947
+ import path7 from "node:path";
42876
42948
  var isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
42877
42949
  var named = (s, name) => s === name || s.startsWith(`${name}@`);
42878
42950
  function isWorkitPlugin(value) {
@@ -42918,11 +42990,11 @@ function mergeCursorEnabledPlugins(enabled) {
42918
42990
  }
42919
42991
  function mergeCursorPluginDirs(pluginDirs, pluginDir) {
42920
42992
  const strip = (p) => {
42921
- const j = path6.join(p);
42922
- return path6.dirname(j) === j ? j : j.replace(/[\\/]+$/, "");
42993
+ const j = path7.join(p);
42994
+ return path7.dirname(j) === j ? j : j.replace(/[\\/]+$/, "");
42923
42995
  };
42924
42996
  const normalized = strip(pluginDir);
42925
- const legacy = strip(path6.join(path6.dirname(pluginDir), "workflow-toolkit"));
42997
+ const legacy = strip(path7.join(path7.dirname(pluginDir), "workflow-toolkit"));
42926
42998
  const prev = Array.isArray(pluginDirs) ? pluginDirs.map(String) : [];
42927
42999
  const kept = prev.filter((d) => strip(d) !== legacy);
42928
43000
  const exists = kept.some((d) => strip(d) === normalized);
@@ -42972,7 +43044,7 @@ function cursorHooksEntry(_packageDir) {
42972
43044
  import { spawnSync as spawnSync2 } from "node:child_process";
42973
43045
  import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
42974
43046
  import os4 from "node:os";
42975
- import path8 from "node:path";
43047
+ import path9 from "node:path";
42976
43048
 
42977
43049
  // packages/workit-core/src/core/support-matrix.ts
42978
43050
  var SUPPORT_MATRIX = {
@@ -42984,7 +43056,7 @@ var SUPPORT_MATRIX = {
42984
43056
 
42985
43057
  // packages/workit-core/src/core/skill-manifests.ts
42986
43058
  import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync } from "node:fs";
42987
- import path7 from "node:path";
43059
+ import path8 from "node:path";
42988
43060
  var CANONICAL_SKILLS = {
42989
43061
  superpowers: [
42990
43062
  "brainstorming",
@@ -43017,7 +43089,7 @@ var CANONICAL_SKILLS = {
43017
43089
  "wk-verify"
43018
43090
  ]
43019
43091
  };
43020
- var skillManifestNames = (root) => existsSync6(root) ? readdirSync2(root).filter((name) => existsSync6(path7.join(root, name, "SKILL.md"))).sort() : [];
43092
+ var skillManifestNames = (root) => existsSync6(root) ? readdirSync2(root).filter((name) => existsSync6(path8.join(root, name, "SKILL.md"))).sort() : [];
43021
43093
  var validateSkillManifests = (root, expected, label) => {
43022
43094
  const actual = skillManifestNames(root);
43023
43095
  const missing = expected.filter((name) => !actual.includes(name));
@@ -43025,10 +43097,10 @@ var validateSkillManifests = (root, expected, label) => {
43025
43097
  return missing.length === 0 && extra.length === 0 ? null : `${label} mismatch at ${root} (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`;
43026
43098
  };
43027
43099
  var validateCursorSkills = (pluginDir) => {
43028
- const workit = validateSkillManifests(path7.join(pluginDir, "skills"), CANONICAL_SKILLS.workit, "Cursor Workit skills");
43100
+ const workit = validateSkillManifests(path8.join(pluginDir, "skills"), CANONICAL_SKILLS.workit, "Cursor Workit skills");
43029
43101
  if (workit)
43030
43102
  return workit;
43031
- const vendor = path7.join(pluginDir, "vendor/superpowers/skills");
43103
+ const vendor = path8.join(pluginDir, "vendor/superpowers/skills");
43032
43104
  const superpowers = validateSkillManifests(vendor, CANONICAL_SKILLS.superpowers, "Cursor Superpowers skills");
43033
43105
  if (superpowers)
43034
43106
  return superpowers;
@@ -43036,7 +43108,7 @@ var validateCursorSkills = (pluginDir) => {
43036
43108
  while (pending.length > 0) {
43037
43109
  const dir = pending.pop();
43038
43110
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
43039
- const file = path7.join(dir, entry.name);
43111
+ const file = path8.join(dir, entry.name);
43040
43112
  if (entry.isDirectory())
43041
43113
  pending.push(file);
43042
43114
  else if ((statSync(file).mode & 73) !== 0 || readFileSync6(file).subarray(0, 2).toString("latin1") === "#!") {
@@ -43051,11 +43123,11 @@ if (false) {}
43051
43123
  // packages/workit-core/src/core/doctor.ts
43052
43124
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
43053
43125
  var findDevFromCwd = (cwd2) => {
43054
- let dir = path8.resolve(cwd2);
43126
+ let dir = path9.resolve(cwd2);
43055
43127
  while (true) {
43056
- if (existsSync7(path8.join(dir, "packages", "workit-core", "package.json")))
43128
+ if (existsSync7(path9.join(dir, "packages", "workit-core", "package.json")))
43057
43129
  return dir;
43058
- const parent = path8.dirname(dir);
43130
+ const parent = path9.dirname(dir);
43059
43131
  if (parent === dir)
43060
43132
  return null;
43061
43133
  dir = parent;
@@ -43064,8 +43136,8 @@ var findDevFromCwd = (cwd2) => {
43064
43136
  var resolve = (options) => {
43065
43137
  const env3 = options.env ?? process.env;
43066
43138
  const home = options.home ?? env3.HOME ?? os4.homedir();
43067
- const configDir2 = options.configDir ?? env3.WORKFLOW_TOOLKIT_CONFIG ?? env3.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path8.join(home, ".config", "workit");
43068
- const stateDir = options.stateDir ?? env3.WORKFLOW_TOOLKIT_STATE ?? path8.join(home, ".local", "state", "workit");
43139
+ const configDir2 = options.configDir ?? env3.WORKFLOW_TOOLKIT_CONFIG ?? env3.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path9.join(home, ".config", "workit");
43140
+ const stateDir = options.stateDir ?? env3.WORKFLOW_TOOLKIT_STATE ?? path9.join(home, ".local", "state", "workit");
43069
43141
  const cwd2 = options.cwd ?? process.cwd();
43070
43142
  const dev = options.dev ?? env3.WORKFLOW_TOOLKIT_DEV ?? findDevFromCwd(cwd2);
43071
43143
  return {
@@ -43075,10 +43147,10 @@ var resolve = (options) => {
43075
43147
  stateDir,
43076
43148
  cwd: cwd2,
43077
43149
  dev,
43078
- opencodeConfig: options.opencodeConfig ?? path8.join(home, ".config", "opencode", "opencode.json"),
43079
- cursorSettings: options.cursorSettings ?? path8.join(home, ".cursor", "settings.json"),
43080
- cursorMcp: options.cursorMcp ?? path8.join(home, ".cursor", "mcp.json"),
43081
- cursorPluginDir: options.cursorPluginDir ?? path8.join(home, ".cursor", "plugins", "local", "workit"),
43150
+ opencodeConfig: options.opencodeConfig ?? path9.join(home, ".config", "opencode", "opencode.json"),
43151
+ cursorSettings: options.cursorSettings ?? path9.join(home, ".cursor", "settings.json"),
43152
+ cursorMcp: options.cursorMcp ?? path9.join(home, ".cursor", "mcp.json"),
43153
+ cursorPluginDir: options.cursorPluginDir ?? path9.join(home, ".cursor", "plugins", "local", "workit"),
43082
43154
  env: env3,
43083
43155
  installer: options.installer ?? false
43084
43156
  };
@@ -43104,13 +43176,13 @@ var pluginEntries = (cfg) => {
43104
43176
  return list.map(String).filter(isWorkitPlugin);
43105
43177
  };
43106
43178
  var commandOnPath = (name, env3) => {
43107
- const dirs = (env3.PATH ?? process.env.PATH ?? "").split(path8.delimiter);
43179
+ const dirs = (env3.PATH ?? process.env.PATH ?? "").split(path9.delimiter);
43108
43180
  const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
43109
43181
  for (const dir of dirs) {
43110
43182
  if (!dir)
43111
43183
  continue;
43112
43184
  for (const candidateName of names) {
43113
- const candidate = path8.join(dir, candidateName);
43185
+ const candidate = path9.join(dir, candidateName);
43114
43186
  try {
43115
43187
  const st = statSync2(candidate);
43116
43188
  if (process.platform !== "win32" && (st.mode & 73) === 0)
@@ -43142,7 +43214,7 @@ var resolveBun = (env3) => {
43142
43214
  if (env3.BUN && existsSync7(env3.BUN))
43143
43215
  return env3.BUN;
43144
43216
  for (const candidate of [
43145
- path8.join(os4.homedir(), ".bun/bin/bun"),
43217
+ path9.join(os4.homedir(), ".bun/bin/bun"),
43146
43218
  "/usr/local/bin/bun",
43147
43219
  "/usr/bin/bun"
43148
43220
  ]) {
@@ -43196,7 +43268,7 @@ var checkVersions = (res) => {
43196
43268
  detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping version parity"
43197
43269
  };
43198
43270
  }
43199
- const corePkg = readJson(path8.join(res.dev, "packages/workit-core/package.json"));
43271
+ const corePkg = readJson(path9.join(res.dev, "packages/workit-core/package.json"));
43200
43272
  if (!corePkg) {
43201
43273
  return {
43202
43274
  id: "versions",
@@ -43206,7 +43278,7 @@ var checkVersions = (res) => {
43206
43278
  }
43207
43279
  const refs = new Set;
43208
43280
  for (const name of ["workit-opencode", "workit-cursor", "workit-cli"]) {
43209
- const pkg = readJson(path8.join(res.dev, "packages", name, "package.json"));
43281
+ const pkg = readJson(path9.join(res.dev, "packages", name, "package.json"));
43210
43282
  const dep = pkg?.dependencies?.["@brainervirus/workit-core"];
43211
43283
  if (typeof dep === "string")
43212
43284
  refs.add(dep);
@@ -43222,7 +43294,7 @@ var checkVersions = (res) => {
43222
43294
  if (refs.size > 1) {
43223
43295
  problems.push(`adapters pin different core versions: ${[...refs].join(", ")}`);
43224
43296
  }
43225
- const opencodePkg = readJson(path8.join(res.dev, "packages/workit-opencode/package.json"));
43297
+ const opencodePkg = readJson(path9.join(res.dev, "packages/workit-opencode/package.json"));
43226
43298
  const sdk = opencodePkg?.dependencies?.["@opencode-ai/plugin"];
43227
43299
  const sdkVersion = typeof sdk === "string" ? (sdk.match(/^\d+(?:\.\d+){0,2}/) ?? [])[0] : null;
43228
43300
  if (sdkVersion && !semverAtLeast(sdkVersion, SUPPORT_MATRIX.opencode.minimum)) {
@@ -43243,23 +43315,23 @@ var checkVersions = (res) => {
43243
43315
  };
43244
43316
  };
43245
43317
  var assetPathsFor = (host, dev) => {
43246
- const pkg = path8.join(dev, "packages", `workit-${host}`);
43318
+ const pkg = path9.join(dev, "packages", `workit-${host}`);
43247
43319
  switch (host) {
43248
43320
  case "opencode":
43249
43321
  return [
43250
- path8.join(pkg, "assets", "commands", "wk-init.md"),
43251
- path8.join(pkg, "assets", "skills", "wk-init", "SKILL.md"),
43252
- path8.join(pkg, "assets", "templates", "spec-template.md"),
43253
- path8.join(pkg, "assets", "vendor", "superpowers", "skills", "brainstorming", "SKILL.md")
43322
+ path9.join(pkg, "assets", "commands", "wk-init.md"),
43323
+ path9.join(pkg, "assets", "skills", "wk-init", "SKILL.md"),
43324
+ path9.join(pkg, "assets", "templates", "spec-template.md"),
43325
+ path9.join(pkg, "assets", "vendor", "superpowers", "skills", "brainstorming", "SKILL.md")
43254
43326
  ];
43255
43327
  case "cursor":
43256
43328
  return [
43257
- path8.join(pkg, "assets", "templates", "spec-template.md"),
43258
- path8.join(pkg, "mcp.json"),
43259
- path8.join(pkg, ".cursor-plugin")
43329
+ path9.join(pkg, "assets", "templates", "spec-template.md"),
43330
+ path9.join(pkg, "mcp.json"),
43331
+ path9.join(pkg, ".cursor-plugin")
43260
43332
  ];
43261
43333
  case "cli":
43262
- return [path8.join(pkg, "assets", "templates", "spec-template.md")];
43334
+ return [path9.join(pkg, "assets", "templates", "spec-template.md")];
43263
43335
  }
43264
43336
  };
43265
43337
  var hostsFor = (host) => host === "cli" ? ["opencode", "cursor", "cli"] : [host];
@@ -43293,17 +43365,17 @@ var checkAssets = (res) => {
43293
43365
  };
43294
43366
  };
43295
43367
  var launcherSlotsFor = (host, dev) => {
43296
- const pkg = path8.join(dev, "packages", `workit-${host}`);
43368
+ const pkg = path9.join(dev, "packages", `workit-${host}`);
43297
43369
  switch (host) {
43298
43370
  case "opencode":
43299
- return [[path8.join(pkg, "src", "plugin.ts"), path8.join(pkg, "dist", "plugin.js")]];
43371
+ return [[path9.join(pkg, "src", "plugin.ts"), path9.join(pkg, "dist", "plugin.js")]];
43300
43372
  case "cursor":
43301
43373
  return [
43302
- [path8.join(pkg, "dist", "mcp-server.js")],
43303
- [path8.join(pkg, "dist", "cursor-session-start.js")]
43374
+ [path9.join(pkg, "dist", "mcp-server.js")],
43375
+ [path9.join(pkg, "dist", "cursor-session-start.js")]
43304
43376
  ];
43305
43377
  case "cli":
43306
- return [[path8.join(pkg, "src", "index.tsx"), path8.join(pkg, "dist", "index.js")]];
43378
+ return [[path9.join(pkg, "src", "index.tsx"), path9.join(pkg, "dist", "index.js")]];
43307
43379
  }
43308
43380
  };
43309
43381
  var validNodeEntry = (entry, runtime, env3) => {
@@ -43332,7 +43404,7 @@ var registeredCursorLauncher = (res) => {
43332
43404
  if (typeof command !== "string" || !Array.isArray(args) || typeof args[0] !== "string") {
43333
43405
  return "invalid";
43334
43406
  }
43335
- const executable = path8.basename(command).toLowerCase();
43407
+ const executable = path9.basename(command).toLowerCase();
43336
43408
  if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
43337
43409
  if (args[0] !== "-y")
43338
43410
  return "invalid";
@@ -43347,12 +43419,12 @@ var registeredCursorLauncher = (res) => {
43347
43419
  return {
43348
43420
  kind: "node",
43349
43421
  runtime: command,
43350
- entry: path8.isAbsolute(args[0]) ? args[0] : path8.resolve(path8.dirname(res.cursorMcp), args[0])
43422
+ entry: path9.isAbsolute(args[0]) ? args[0] : path9.resolve(path9.dirname(res.cursorMcp), args[0])
43351
43423
  };
43352
43424
  };
43353
43425
  var canonicalCursorHook = cursorHooksEntry("").command;
43354
43426
  var registeredCursorHook = (res) => {
43355
- const hooksFile = path8.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
43427
+ const hooksFile = path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
43356
43428
  if (!existsSync7(hooksFile))
43357
43429
  return "invalid";
43358
43430
  const config = readJson(hooksFile);
@@ -43371,7 +43443,7 @@ var checkLauncher = (res) => {
43371
43443
  const hosts = hostsFor(res.host);
43372
43444
  const registered = hosts.includes("cursor") ? registeredCursorLauncher(res) : null;
43373
43445
  const runtime = registered && registered !== "invalid" && registered.kind === "node" ? registered.runtime : "node";
43374
- const missing = hosts.includes("cursor") ? ["dist/mcp-server.js", "dist/cursor-session-start.js"].map((rel) => path8.join(res.cursorPluginDir, rel)).filter((p) => !validNodeEntry(p, runtime, res.env)).map((p) => `cursor: ${p}`) : [];
43446
+ const missing = hosts.includes("cursor") ? ["dist/mcp-server.js", "dist/cursor-session-start.js"].map((rel) => path9.join(res.cursorPluginDir, rel)).filter((p) => !validNodeEntry(p, runtime, res.env)).map((p) => `cursor: ${p}`) : [];
43375
43447
  if (hosts.includes("cursor")) {
43376
43448
  if (registered === "invalid") {
43377
43449
  missing.push(`cursor: canonical workit MCP launcher in ${res.cursorMcp}`);
@@ -43380,9 +43452,9 @@ var checkLauncher = (res) => {
43380
43452
  }
43381
43453
  const hook = registeredCursorHook(res);
43382
43454
  if (hook === "invalid") {
43383
- missing.push(`cursor: canonical session-start hook in ${path8.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
43455
+ missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
43384
43456
  } else if (hook !== null && hook !== canonicalCursorHook) {
43385
- missing.push(`cursor: canonical session-start hook in ${path8.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
43457
+ missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
43386
43458
  }
43387
43459
  }
43388
43460
  if (dev) {
@@ -43523,7 +43595,7 @@ var checkDuplicateRegistration = (res) => {
43523
43595
  var checkMalformedConfig = (res) => {
43524
43596
  const files = [];
43525
43597
  for (const name of ["config.json", "youtrack.json", "vcs.json", "workspaces.json"]) {
43526
- const p = path8.join(res.configDir, name);
43598
+ const p = path9.join(res.configDir, name);
43527
43599
  if (existsSync7(p))
43528
43600
  files.push(p);
43529
43601
  }
@@ -43546,7 +43618,7 @@ var checkMalformedConfig = (res) => {
43546
43618
  };
43547
43619
  };
43548
43620
  var checkWorkspaceMismatch = (res) => {
43549
- const file = path8.join(res.configDir, "workspaces.json");
43621
+ const file = path9.join(res.configDir, "workspaces.json");
43550
43622
  if (!existsSync7(file))
43551
43623
  return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
43552
43624
  const ws = readJson(file);
@@ -43576,20 +43648,20 @@ var isPlaceholder = (p) => {
43576
43648
  };
43577
43649
  var checkCredentialMetadata = (res) => {
43578
43650
  const tokenPaths = [];
43579
- const youtrackJson = readJson(path8.join(res.configDir, "youtrack.json"));
43651
+ const youtrackJson = readJson(path9.join(res.configDir, "youtrack.json"));
43580
43652
  if (youtrackJson) {
43581
- tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile : path8.join(res.configDir, "youtrack.token"));
43582
- } else if (existsSync7(path8.join(res.configDir, "youtrack.token"))) {
43583
- tokenPaths.push(path8.join(res.configDir, "youtrack.token"));
43653
+ tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile : path9.join(res.configDir, "youtrack.token"));
43654
+ } else if (existsSync7(path9.join(res.configDir, "youtrack.token"))) {
43655
+ tokenPaths.push(path9.join(res.configDir, "youtrack.token"));
43584
43656
  }
43585
- const vcsJson = readJson(path8.join(res.configDir, "vcs.json"));
43657
+ const vcsJson = readJson(path9.join(res.configDir, "vcs.json"));
43586
43658
  for (const key of ["gitlab", "github"]) {
43587
43659
  const provider = vcsJson?.[key];
43588
43660
  if (provider && typeof provider === "object") {
43589
43661
  const tf = provider.tokenFile;
43590
- tokenPaths.push(typeof tf === "string" ? tf : path8.join(res.configDir, `${key}.token`));
43591
- } else if (!vcsJson && existsSync7(path8.join(res.configDir, `${key}.token`))) {
43592
- tokenPaths.push(path8.join(res.configDir, `${key}.token`));
43662
+ tokenPaths.push(typeof tf === "string" ? tf : path9.join(res.configDir, `${key}.token`));
43663
+ } else if (!vcsJson && existsSync7(path9.join(res.configDir, `${key}.token`))) {
43664
+ tokenPaths.push(path9.join(res.configDir, `${key}.token`));
43593
43665
  }
43594
43666
  }
43595
43667
  if (tokenPaths.length === 0) {
@@ -43597,7 +43669,7 @@ var checkCredentialMetadata = (res) => {
43597
43669
  }
43598
43670
  const problems = [];
43599
43671
  for (const raw of tokenPaths) {
43600
- const p = path8.isAbsolute(raw) ? raw : path8.resolve(res.configDir, raw);
43672
+ const p = path9.isAbsolute(raw) ? raw : path9.resolve(res.configDir, raw);
43601
43673
  if (!existsSync7(p)) {
43602
43674
  problems.push(`${p} is missing`);
43603
43675
  continue;
@@ -43625,8 +43697,8 @@ var checkCredentialMetadata = (res) => {
43625
43697
  };
43626
43698
  };
43627
43699
  var checkLogWritable = (res) => {
43628
- const logsDir = path8.join(res.stateDir, "logs");
43629
- const probe = path8.join(logsDir, "doctor-probe.tmp");
43700
+ const logsDir = path9.join(res.stateDir, "logs");
43701
+ const probe = path9.join(logsDir, "doctor-probe.tmp");
43630
43702
  try {
43631
43703
  mkdirSync2(logsDir, { recursive: true, mode: 448 });
43632
43704
  writeFileSync4(probe, `{"probe":true}
@@ -43814,7 +43886,7 @@ function buildSetupPreview(values2, opts = {}) {
43814
43886
  const current = readConfigFromDir(state.configDir);
43815
43887
  mutations.push({
43816
43888
  type: "merge-json",
43817
- path: path9.join(state.configDir, "config.json"),
43889
+ path: path10.join(state.configDir, "config.json"),
43818
43890
  value: {
43819
43891
  locale: values2.locale,
43820
43892
  localeOptions: current.localeOptions,
@@ -43839,16 +43911,16 @@ function buildSetupPreview(values2, opts = {}) {
43839
43911
  if (blocked.length === 0) {
43840
43912
  mutations.push({
43841
43913
  type: "update-workspaces",
43842
- path: path9.join(state.configDir, "workspaces.json"),
43914
+ path: path10.join(state.configDir, "workspaces.json"),
43843
43915
  entries: values2.workspaces
43844
43916
  });
43845
43917
  }
43846
43918
  }
43847
43919
  if (values2.baseUrl.trim()) {
43848
- const ytPath = path9.join(state.configDir, "youtrack.json");
43920
+ const ytPath = path10.join(state.configDir, "youtrack.json");
43849
43921
  const ytExisting = readConfigRecord(ytPath);
43850
43922
  const ytConfigured = configuredTokenPath(ytExisting, "tokenFile");
43851
- const tokenPath = resolveTokenPath(ytConfigured, path9.join(state.configDir, "youtrack.token"), values2.tokenPaths?.youtrack);
43923
+ const tokenPath = resolveTokenPath(ytConfigured, path10.join(state.configDir, "youtrack.token"), values2.tokenPaths?.youtrack);
43852
43924
  const draft = youtrackDraft(values2, tokenPath);
43853
43925
  if (ytConfigured !== null && tokenPath !== ytConfigured) {
43854
43926
  delete draft.tokenFile;
@@ -43876,12 +43948,12 @@ function buildSetupPreview(values2, opts = {}) {
43876
43948
  });
43877
43949
  }
43878
43950
  if (values2.vcsProvider !== "skip") {
43879
- const vcsPath = path9.join(state.configDir, "vcs.json");
43951
+ const vcsPath = path10.join(state.configDir, "vcs.json");
43880
43952
  const vcsExisting = readConfigRecord(vcsPath);
43881
43953
  const glConfigured = configuredTokenPath(vcsExisting, "gitlab", "tokenFile");
43882
43954
  const ghConfigured = configuredTokenPath(vcsExisting, "github", "tokenFile");
43883
- const gitlabToken = resolveTokenPath(glConfigured, path9.join(state.configDir, "gitlab.token"), values2.tokenPaths?.gitlab);
43884
- const githubToken = resolveTokenPath(ghConfigured, path9.join(state.configDir, "github.token"), values2.tokenPaths?.github);
43955
+ const gitlabToken = resolveTokenPath(glConfigured, path10.join(state.configDir, "gitlab.token"), values2.tokenPaths?.gitlab);
43956
+ const githubToken = resolveTokenPath(ghConfigured, path10.join(state.configDir, "github.token"), values2.tokenPaths?.github);
43885
43957
  const draft = vcsDraft(values2.vcsProvider, gitlabToken, githubToken);
43886
43958
  for (const [configured, token, key] of [
43887
43959
  [glConfigured, gitlabToken, "gitlab.tokenFile"],
@@ -43928,8 +44000,8 @@ function buildSetupPreview(values2, opts = {}) {
43928
44000
  }
43929
44001
  }
43930
44002
  if (values2.applyProject) {
43931
- const root = path9.resolve(opts.cwd ?? process.cwd());
43932
- const gitignorePath = path9.join(root, ".gitignore");
44003
+ const root = path10.resolve(opts.cwd ?? process.cwd());
44004
+ const gitignorePath = path10.join(root, ".gitignore");
43933
44005
  const existing = existsSync8(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
43934
44006
  const existingLines = new Set(existing.split(`
43935
44007
  `).map((l) => l.trim()).filter(Boolean));
@@ -43958,10 +44030,10 @@ var resolveSetupPaths = (options, configDir2) => {
43958
44030
  configDir: configDir2,
43959
44031
  cwd: options.cwd ?? process.cwd(),
43960
44032
  env: env3,
43961
- opencodeConfig: options.opencodeConfig ?? path9.join(home, ".config", "opencode", "opencode.json"),
43962
- cursorSettings: options.cursorSettings ?? path9.join(home, ".cursor", "settings.json"),
43963
- cursorMcp: options.cursorMcp ?? path9.join(home, ".cursor", "mcp.json"),
43964
- cursorPluginDir: options.cursorPluginDir ?? path9.join(home, ".cursor", "plugins", "local", "workit")
44033
+ opencodeConfig: options.opencodeConfig ?? path10.join(home, ".config", "opencode", "opencode.json"),
44034
+ cursorSettings: options.cursorSettings ?? path10.join(home, ".cursor", "settings.json"),
44035
+ cursorMcp: options.cursorMcp ?? path10.join(home, ".cursor", "mcp.json"),
44036
+ cursorPluginDir: options.cursorPluginDir ?? path10.join(home, ".cursor", "plugins", "local", "workit")
43965
44037
  };
43966
44038
  };
43967
44039
  function resolveApply(preview, options) {
@@ -44007,7 +44079,7 @@ var readFileSafe = (p) => {
44007
44079
  }
44008
44080
  };
44009
44081
  function applyMutation(m) {
44010
- const dir = path9.dirname(m.path);
44082
+ const dir = path10.dirname(m.path);
44011
44083
  switch (m.type) {
44012
44084
  case "create-file": {
44013
44085
  mkdirSync3(dir, { recursive: true });
@@ -44111,7 +44183,7 @@ function applyMutation(m) {
44111
44183
  detail: "already configured"
44112
44184
  };
44113
44185
  }
44114
- mkdirSync3(path9.dirname(m.path), { recursive: true });
44186
+ mkdirSync3(path10.dirname(m.path), { recursive: true });
44115
44187
  writeFileSync5(m.path, JSON.stringify(merged, null, 2) + `
44116
44188
  `, "utf8");
44117
44189
  return existing.kind === "record" ? { platform: "core", file: m.path, status: "Configured" } : { platform: "core", file: m.path, status: "Installed" };
@@ -44120,7 +44192,7 @@ function applyMutation(m) {
44120
44192
  }
44121
44193
  var isAdapter = (root, platform2) => {
44122
44194
  try {
44123
- const pkg = JSON.parse(readFileSync8(path9.join(root, "package.json"), "utf8"));
44195
+ const pkg = JSON.parse(readFileSync8(path10.join(root, "package.json"), "utf8"));
44124
44196
  return pkg.name === `@brainervirus/workit-${platform2}`;
44125
44197
  } catch {
44126
44198
  return false;
@@ -44129,19 +44201,19 @@ var isAdapter = (root, platform2) => {
44129
44201
  function adapterRoot(platform2, res) {
44130
44202
  const candidates = [];
44131
44203
  if (res.dev) {
44132
- candidates.push(path9.join(res.dev, "packages", `workit-${platform2}`));
44204
+ candidates.push(path10.join(res.dev, "packages", `workit-${platform2}`));
44133
44205
  } else {
44134
- candidates.push(path9.join(packageRoot(), "..", `workit-${platform2}`));
44135
- let dir = path9.resolve(res.cwd);
44206
+ candidates.push(path10.join(packageRoot(), "..", `workit-${platform2}`));
44207
+ let dir = path10.resolve(res.cwd);
44136
44208
  while (true) {
44137
- candidates.push(path9.join(dir, "packages", `workit-${platform2}`));
44138
- const parent = path9.dirname(dir);
44209
+ candidates.push(path10.join(dir, "packages", `workit-${platform2}`));
44210
+ const parent = path10.dirname(dir);
44139
44211
  if (parent === dir)
44140
44212
  break;
44141
44213
  dir = parent;
44142
44214
  }
44143
44215
  }
44144
- candidates.push(path9.join(res.home, ".local", "share", "workflow-toolkit", "packages", `workit-${platform2}`));
44216
+ candidates.push(path10.join(res.home, ".local", "share", "workflow-toolkit", "packages", `workit-${platform2}`));
44145
44217
  for (const candidate of candidates) {
44146
44218
  if (isAdapter(candidate, platform2))
44147
44219
  return candidate;
@@ -44150,7 +44222,7 @@ function adapterRoot(platform2, res) {
44150
44222
  }
44151
44223
  var opencodePin = (root) => {
44152
44224
  for (const rel of ["src/plugin.ts", "dist/plugin.js"]) {
44153
- const entry = path9.join(root, rel);
44225
+ const entry = path10.join(root, rel);
44154
44226
  if (existsSync8(entry))
44155
44227
  return `file://${entry}`;
44156
44228
  }
@@ -44166,7 +44238,7 @@ function applyOpenCode(root, res) {
44166
44238
  detail: "workit-opencode package has no loadable plugin entry (src/plugin.ts or dist/plugin.js)"
44167
44239
  };
44168
44240
  }
44169
- mkdirSync3(path9.dirname(res.opencodeConfig), { recursive: true });
44241
+ mkdirSync3(path10.dirname(res.opencodeConfig), { recursive: true });
44170
44242
  const existing = readExisting(res.opencodeConfig);
44171
44243
  if (existing.kind === "malformed") {
44172
44244
  return {
@@ -44204,19 +44276,19 @@ var samePluginContent = (src, dest, relative = "") => {
44204
44276
  for (const entry of readdirSync3(src, { withFileTypes: true })) {
44205
44277
  if (entry.name === "node_modules")
44206
44278
  continue;
44207
- const source = path9.join(src, entry.name);
44208
- const installed = path9.join(dest, entry.name);
44279
+ const source = path10.join(src, entry.name);
44280
+ const installed = path10.join(dest, entry.name);
44209
44281
  if (entry.isDirectory()) {
44210
- if (!statSync3(installed).isDirectory() || !samePluginContent(source, installed, path9.join(relative, entry.name)))
44282
+ if (!statSync3(installed).isDirectory() || !samePluginContent(source, installed, path10.join(relative, entry.name)))
44211
44283
  return false;
44212
44284
  } else if (!readFileSync8(source).equals(readFileSync8(installed))) {
44213
44285
  return false;
44214
44286
  }
44215
44287
  }
44216
44288
  for (const entry of readdirSync3(dest, { withFileTypes: true })) {
44217
- if (existsSync8(path9.join(src, entry.name)))
44289
+ if (existsSync8(path10.join(src, entry.name)))
44218
44290
  continue;
44219
- const rel = path9.join(relative, entry.name);
44291
+ const rel = path10.join(relative, entry.name);
44220
44292
  if (rel === ".workflow-toolkit-root")
44221
44293
  continue;
44222
44294
  if (relative === "rules" && entry.isFile() && entry.name.endsWith(".mdc"))
@@ -44230,41 +44302,41 @@ var samePluginContent = (src, dest, relative = "") => {
44230
44302
  };
44231
44303
  var preservedCursorRules = (src, dest) => {
44232
44304
  const preserved = new Map;
44233
- const sourceRules = path9.join(src, "rules");
44234
- const installedRules = path9.join(dest, "rules");
44305
+ const sourceRules = path10.join(src, "rules");
44306
+ const installedRules = path10.join(dest, "rules");
44235
44307
  try {
44236
44308
  for (const entry of readdirSync3(installedRules, { withFileTypes: true })) {
44237
44309
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
44238
44310
  continue;
44239
- if (existsSync8(path9.join(sourceRules, entry.name)))
44311
+ if (existsSync8(path10.join(sourceRules, entry.name)))
44240
44312
  continue;
44241
- preserved.set(entry.name, readFileSync8(path9.join(installedRules, entry.name)));
44313
+ preserved.set(entry.name, readFileSync8(path10.join(installedRules, entry.name)));
44242
44314
  }
44243
44315
  } catch {}
44244
44316
  return preserved;
44245
44317
  };
44246
44318
  function copyPluginDir(src, dest) {
44247
- const marker = path9.join(dest, ".workflow-toolkit-root");
44319
+ const marker = path10.join(dest, ".workflow-toolkit-root");
44248
44320
  const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest);
44249
44321
  if (synced)
44250
44322
  return "Skipped";
44251
44323
  const hadDir = existsSync8(dest);
44252
44324
  const rules = preservedCursorRules(src, dest);
44253
- const parent = path9.dirname(dest);
44325
+ const parent = path10.dirname(dest);
44254
44326
  mkdirSync3(parent, { recursive: true });
44255
- const swap = mkdtempSync(path9.join(parent, `.${path9.basename(dest)}.swap-`));
44256
- const stage = path9.join(swap, "stage");
44257
- const backup = path9.join(swap, "backup");
44327
+ const swap = mkdtempSync(path10.join(parent, `.${path10.basename(dest)}.swap-`));
44328
+ const stage = path10.join(swap, "stage");
44329
+ const backup = path10.join(swap, "backup");
44258
44330
  try {
44259
44331
  cpSync2(src, stage, {
44260
44332
  recursive: true,
44261
- filter: (entry) => !path9.relative(src, entry).split(path9.sep).includes("node_modules")
44333
+ filter: (entry) => !path10.relative(src, entry).split(path10.sep).includes("node_modules")
44262
44334
  });
44263
44335
  for (const [name, content] of rules) {
44264
- mkdirSync3(path9.join(stage, "rules"), { recursive: true });
44265
- writeFileSync5(path9.join(stage, "rules", name), content);
44336
+ mkdirSync3(path10.join(stage, "rules"), { recursive: true });
44337
+ writeFileSync5(path10.join(stage, "rules", name), content);
44266
44338
  }
44267
- writeFileSync5(path9.join(stage, ".workflow-toolkit-root"), src + `
44339
+ writeFileSync5(path10.join(stage, ".workflow-toolkit-root"), src + `
44268
44340
  `, "utf8");
44269
44341
  if (!samePluginContent(src, stage))
44270
44342
  throw new Error("staged adapter content is incomplete");
@@ -44284,26 +44356,26 @@ function copyPluginDir(src, dest) {
44284
44356
  return hadDir ? "Configured" : "Installed";
44285
44357
  }
44286
44358
  var removeLegacyCursorDir = (res) => {
44287
- const legacy = path9.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
44359
+ const legacy = path10.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
44288
44360
  if (legacy === res.cursorPluginDir || !existsSync8(legacy))
44289
44361
  return;
44290
- const legacyRules = path9.join(legacy, "rules");
44362
+ const legacyRules = path10.join(legacy, "rules");
44291
44363
  if (existsSync8(legacyRules)) {
44292
44364
  try {
44293
- mkdirSync3(path9.join(res.cursorPluginDir, "rules"), { recursive: true });
44365
+ mkdirSync3(path10.join(res.cursorPluginDir, "rules"), { recursive: true });
44294
44366
  for (const entry of readdirSync3(legacyRules, { withFileTypes: true })) {
44295
44367
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
44296
44368
  continue;
44297
- const target = path9.join(res.cursorPluginDir, "rules", entry.name);
44369
+ const target = path10.join(res.cursorPluginDir, "rules", entry.name);
44298
44370
  if (!existsSync8(target))
44299
- copyFileSync2(path9.join(legacyRules, entry.name), target);
44371
+ copyFileSync2(path10.join(legacyRules, entry.name), target);
44300
44372
  }
44301
44373
  } catch {}
44302
44374
  }
44303
44375
  rmSync(legacy, { recursive: true, force: true });
44304
44376
  };
44305
44377
  function applyCursorSettings(root, res) {
44306
- mkdirSync3(path9.dirname(res.cursorSettings), { recursive: true });
44378
+ mkdirSync3(path10.dirname(res.cursorSettings), { recursive: true });
44307
44379
  const settingsExisting = readExisting(res.cursorSettings);
44308
44380
  if (settingsExisting.kind === "malformed") {
44309
44381
  return {
@@ -44331,7 +44403,7 @@ function applyCursorSettings(root, res) {
44331
44403
  };
44332
44404
  }
44333
44405
  function applyCursorMcp(root, res) {
44334
- mkdirSync3(path9.dirname(res.cursorMcp), { recursive: true });
44406
+ mkdirSync3(path10.dirname(res.cursorMcp), { recursive: true });
44335
44407
  const mcpExisting = readExisting(res.cursorMcp);
44336
44408
  if (mcpExisting.kind === "malformed") {
44337
44409
  return {
@@ -44520,12 +44592,12 @@ var setupCompletionGuidance = () => [
44520
44592
  ];
44521
44593
  function applyWorkspaceBranchPolicy(opts) {
44522
44594
  const { workspace_root, env: env3 = process.env } = opts;
44523
- const dir = path9.join(env3.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
44595
+ const dir = path10.join(env3.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
44524
44596
  const { status, path: wsPath, entries } = readWorkspacesResult(dir);
44525
44597
  if (status === "malformed")
44526
44598
  return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
44527
44599
  const detection = detectBranchPolicy(workspace_root);
44528
- const name = String(env3.WORKFLOW_BP_NAME ?? path9.basename(workspace_root));
44600
+ const name = String(env3.WORKFLOW_BP_NAME ?? path10.basename(workspace_root));
44529
44601
  const integration = env3.WORKFLOW_BP_INTEGRATION ?? detection.integration;
44530
44602
  const policy = {
44531
44603
  preset: detection.preset,
@@ -44550,7 +44622,7 @@ function applyWorkspaceBranchPolicy(opts) {
44550
44622
  };
44551
44623
  }
44552
44624
  const next = existing ? entries.map((w, i) => i === idx ? { ...w, branchPolicy: policy } : w) : [...entries, { name, glob, branchPolicy: policy }];
44553
- mkdirSync3(path9.dirname(wsPath), { recursive: true });
44625
+ mkdirSync3(path10.dirname(wsPath), { recursive: true });
44554
44626
  writeFileSync5(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
44555
44627
  `, "utf8");
44556
44628
  return {
@@ -44608,7 +44680,7 @@ function applyWizardBranchPolicy(branchPolicy, workspace_root, env3 = process.en
44608
44680
 
44609
44681
  // packages/workit-cli/src/wizard-state.ts
44610
44682
  import { existsSync as existsSync9 } from "node:fs";
44611
- import path10 from "node:path";
44683
+ import path11 from "node:path";
44612
44684
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
44613
44685
  var NEXT = {
44614
44686
  platforms: "locale",
@@ -44653,7 +44725,7 @@ var PREV = {
44653
44725
  exit: null
44654
44726
  };
44655
44727
  var skipsCustomBranch = (screen, preset) => (screen === "branchAllowed" || screen === "branchProtected") && preset !== "custom";
44656
- var skipsBranchPolicy = (screen) => (screen === "branchPolicy" || screen === "branchPolicyDevelop") && !existsSync9(path10.join(process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), ".git"));
44728
+ var skipsBranchPolicy = (screen) => (screen === "branchPolicy" || screen === "branchPolicyDevelop") && !existsSync9(path11.join(process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), ".git"));
44657
44729
  function nextScreen(screen, preset) {
44658
44730
  let next = NEXT[screen];
44659
44731
  while (next && (skipsCustomBranch(next, preset) || skipsBranchPolicy(next)))
@@ -45959,7 +46031,7 @@ function Screen({ draft, dispatch }) {
45959
46031
  // packages/workit-core/src/core/logger.ts
45960
46032
  import { appendFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
45961
46033
  import os6 from "node:os";
45962
- import path11 from "node:path";
46034
+ import path12 from "node:path";
45963
46035
  var REDACTED = "[REDACTED]";
45964
46036
  var DEFAULT_MAX_RATE = 20;
45965
46037
  var DEFAULT_RATE_WINDOW_MS = 1000;
@@ -46015,14 +46087,14 @@ var resolveStateDir = () => {
46015
46087
  return override;
46016
46088
  const home = os6.homedir();
46017
46089
  if (process.env.XDG_STATE_HOME)
46018
- return path11.join(process.env.XDG_STATE_HOME, "workit");
46090
+ return path12.join(process.env.XDG_STATE_HOME, "workit");
46019
46091
  switch (os6.platform()) {
46020
46092
  case "darwin":
46021
- return path11.join(home, "Library", "Application Support", "workit");
46093
+ return path12.join(home, "Library", "Application Support", "workit");
46022
46094
  case "win32":
46023
- return path11.join(process.env.LOCALAPPDATA ?? path11.join(home, "AppData", "Local"), "workit");
46095
+ return path12.join(process.env.LOCALAPPDATA ?? path12.join(home, "AppData", "Local"), "workit");
46024
46096
  default:
46025
- return path11.join(process.env.HOME ?? home, ".local", "state", "workit");
46097
+ return path12.join(process.env.HOME ?? home, ".local", "state", "workit");
46026
46098
  }
46027
46099
  };
46028
46100
  var homePattern = new RegExp(`${escapeRegExp2(os6.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
@@ -46095,14 +46167,14 @@ var pruneOldFiles = (dir) => {
46095
46167
  const files = readdirSync4(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
46096
46168
  for (const file of files.slice(RETAINED_DAYS)) {
46097
46169
  try {
46098
- unlinkSync2(path11.join(dir, file));
46170
+ unlinkSync2(path12.join(dir, file));
46099
46171
  } catch {}
46100
46172
  }
46101
46173
  } catch {}
46102
46174
  };
46103
46175
  var createLogger = (options = {}) => {
46104
46176
  const stateDir = options.stateDir ?? resolveStateDir();
46105
- const logDir = path11.join(stateDir, "logs");
46177
+ const logDir = path12.join(stateDir, "logs");
46106
46178
  const now2 = options.now ?? (() => new Date);
46107
46179
  const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
46108
46180
  const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
@@ -46136,7 +46208,7 @@ var createLogger = (options = {}) => {
46136
46208
  const line = `${JSON.stringify(event)}
46137
46209
  `;
46138
46210
  mkdirSync4(logDir, { recursive: true, mode: 448 });
46139
- appendFileSync(path11.join(logDir, dailyFileName(date)), line, { mode: 384 });
46211
+ appendFileSync(path12.join(logDir, dailyFileName(date)), line, { mode: 384 });
46140
46212
  pruneOldFiles(logDir);
46141
46213
  } catch {
46142
46214
  try {
@@ -46147,7 +46219,7 @@ var createLogger = (options = {}) => {
46147
46219
  context: { redaction_failed: true }
46148
46220
  };
46149
46221
  mkdirSync4(logDir, { recursive: true, mode: 448 });
46150
- appendFileSync(path11.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
46222
+ appendFileSync(path12.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
46151
46223
  `, {
46152
46224
  mode: 384
46153
46225
  });
@@ -46182,21 +46254,2152 @@ var createLogger = (options = {}) => {
46182
46254
  };
46183
46255
  };
46184
46256
 
46185
- // packages/workit-cli/src/index.tsx
46186
- var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
46187
- var logger = createLogger({
46188
- stderr: (event) => {
46189
- if (event.level === "debug" || event.level === "info")
46190
- return;
46191
- process.stderr.write(`${JSON.stringify(event)}
46192
- `);
46257
+ // packages/workit-cli/src/flow.ts
46258
+ import { createInterface } from "node:readline/promises";
46259
+ import path21 from "node:path";
46260
+
46261
+ // packages/workit-core/src/core/flow-state.ts
46262
+ import {
46263
+ closeSync,
46264
+ existsSync as existsSync14,
46265
+ fstatSync,
46266
+ fsyncSync,
46267
+ mkdirSync as mkdirSync8,
46268
+ openSync,
46269
+ readFileSync as readFileSync13,
46270
+ renameSync as renameSync2,
46271
+ rmSync as rmSync2,
46272
+ statSync as statSync7,
46273
+ unlinkSync as unlinkSync3,
46274
+ writeFileSync as writeFileSync8
46275
+ } from "node:fs";
46276
+ import { createHash } from "node:crypto";
46277
+ import path18 from "node:path";
46278
+
46279
+ // packages/workit-core/src/core/docs-validate.ts
46280
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
46281
+ import { execFileSync as execFileSync2 } from "node:child_process";
46282
+ import path14 from "node:path";
46283
+
46284
+ // packages/workit-core/src/core/docs-layout.ts
46285
+ import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
46286
+ import path13 from "node:path";
46287
+ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
46288
+ var LEGACY_SLUG = "superpowers";
46289
+ var posix = (p) => p.split(path13.sep).join("/");
46290
+ var canonicalize = (base2, candidate) => {
46291
+ const abs = path13.resolve(base2, candidate);
46292
+ let ancestor = abs;
46293
+ while (!existsSync10(ancestor))
46294
+ ancestor = path13.dirname(ancestor);
46295
+ let real;
46296
+ try {
46297
+ real = realpathSync2(ancestor);
46298
+ } catch (error) {
46299
+ if (error.code === "EACCES" && !lstatSync(ancestor).isSymbolicLink()) {
46300
+ real = path13.join(realpathSync2(path13.dirname(ancestor)), path13.basename(ancestor));
46301
+ } else {
46302
+ throw error;
46303
+ }
46304
+ }
46305
+ if (real !== base2 && !real.startsWith(base2 + path13.sep)) {
46306
+ throw new Error(`path must stay inside repository root: ${candidate}`);
46307
+ }
46308
+ return path13.join(real, path13.relative(ancestor, abs));
46309
+ };
46310
+ var buildLayout = (workspace, slug) => {
46311
+ const docs = canonicalize(workspace, "docs");
46312
+ const dir = canonicalize(workspace, path13.join(docs, slug));
46313
+ return {
46314
+ workspace,
46315
+ slug,
46316
+ docs,
46317
+ dir,
46318
+ spec: path13.join(dir, "spec.md"),
46319
+ plan: path13.join(dir, "plan.md"),
46320
+ sdd: path13.join(dir, "sdd")
46321
+ };
46322
+ };
46323
+ var resolveCanonicalLayout = (input) => {
46324
+ const { workspace_root, slug, spec_path, plan_path } = input;
46325
+ if (!workspace_root)
46326
+ return { ok: false, error: "workspace_root required" };
46327
+ let workspace;
46328
+ try {
46329
+ workspace = realpathSync2(path13.resolve(workspace_root));
46330
+ } catch {
46331
+ return { ok: false, error: `workspace root not found: ${workspace_root}` };
46332
+ }
46333
+ let derived = null;
46334
+ for (const [candidate, kind] of [
46335
+ [spec_path, "spec"],
46336
+ [plan_path, "plan"]
46337
+ ]) {
46338
+ if (!candidate)
46339
+ continue;
46340
+ if (path13.isAbsolute(candidate)) {
46341
+ return { ok: false, error: `absolute path not allowed: ${candidate}` };
46342
+ }
46343
+ const spelling = posix(candidate);
46344
+ const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
46345
+ if (!match) {
46346
+ return {
46347
+ ok: false,
46348
+ error: `path must be docs/<slug>/(spec|plan).md inside workspace_root: ${candidate}`
46349
+ };
46350
+ }
46351
+ const pathSlug = match[1];
46352
+ if (pathSlug === LEGACY_SLUG) {
46353
+ return { ok: false, error: `legacy path not allowed: ${candidate}` };
46354
+ }
46355
+ if (!SLUG_RE.test(pathSlug)) {
46356
+ return { ok: false, error: `invalid slug derived from path: ${JSON.stringify(pathSlug)}` };
46357
+ }
46358
+ if (match[2] !== kind) {
46359
+ return {
46360
+ ok: false,
46361
+ error: `wrong basename for ${kind}: expected ${kind}.md, got ${path13.basename(candidate)}`
46362
+ };
46363
+ }
46364
+ if (derived && derived !== pathSlug) {
46365
+ return {
46366
+ ok: false,
46367
+ error: "cross-slug pair: spec_path and plan_path must share the same docs/<slug>/"
46368
+ };
46369
+ }
46370
+ derived = pathSlug;
46371
+ let abs;
46372
+ try {
46373
+ abs = canonicalize(workspace, candidate);
46374
+ } catch (error) {
46375
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
46376
+ }
46377
+ if (posix(path13.relative(workspace, abs)) !== spelling) {
46378
+ return {
46379
+ ok: false,
46380
+ error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`
46381
+ };
46382
+ }
46383
+ }
46384
+ let resolvedSlug = slug;
46385
+ if (resolvedSlug !== undefined) {
46386
+ if (!SLUG_RE.test(resolvedSlug)) {
46387
+ return { ok: false, error: `invalid slug: ${JSON.stringify(resolvedSlug)}` };
46388
+ }
46389
+ if (resolvedSlug === LEGACY_SLUG) {
46390
+ return { ok: false, error: `reserved slug: ${LEGACY_SLUG}` };
46391
+ }
46392
+ if (derived && derived !== resolvedSlug) {
46393
+ return {
46394
+ ok: false,
46395
+ error: `slug ${JSON.stringify(resolvedSlug)} does not match docs path ${JSON.stringify(derived)}`
46396
+ };
46397
+ }
46398
+ } else if (derived) {
46399
+ resolvedSlug = derived;
46400
+ } else {
46401
+ return { ok: false, error: "slug or spec_path/plan_path required" };
46402
+ }
46403
+ try {
46404
+ return { ok: true, layout: buildLayout(workspace, resolvedSlug) };
46405
+ } catch (error) {
46406
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
46407
+ }
46408
+ };
46409
+ var resolveDocsPath = (input) => {
46410
+ if (path13.isAbsolute(input.path)) {
46411
+ return { ok: false, error: `absolute path not allowed: ${input.path}` };
46412
+ }
46413
+ let workspace;
46414
+ try {
46415
+ workspace = realpathSync2(path13.resolve(input.workspace_root));
46416
+ } catch {
46417
+ return { ok: false, error: `workspace root not found: ${input.workspace_root}` };
46418
+ }
46419
+ try {
46420
+ const abs = canonicalize(workspace, input.path);
46421
+ const relative = posix(path13.relative(workspace, abs));
46422
+ if (!relative.startsWith("docs/") || relative === `docs/${LEGACY_SLUG}` || relative.startsWith(`docs/${LEGACY_SLUG}/`)) {
46423
+ return {
46424
+ ok: false,
46425
+ error: `path must live under docs/ and not under docs/${LEGACY_SLUG}/: ${input.path}`
46426
+ };
46427
+ }
46428
+ return { ok: true, path: abs, relative, base: workspace };
46429
+ } catch (error) {
46430
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
46431
+ }
46432
+ };
46433
+
46434
+ // packages/workit-core/src/core/docs-validate.ts
46435
+ var BRANCH_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/im;
46436
+ var SPEC_LINK_RE = /^\s*\*+Spec:\*+\s*(?:`([^`]+)`|(\S+))\s*$/im;
46437
+ var TASK_RE = /^###\s+Task\s+(\d+):\s*(.*)$/i;
46438
+ var err = (code, message, path15) => {
46439
+ const item = { code, message };
46440
+ if (path15)
46441
+ item.path = path15;
46442
+ return item;
46443
+ };
46444
+ var failValidate = (errors) => ({
46445
+ ok: false,
46446
+ errors,
46447
+ error: errors.map((e) => e.message).filter(Boolean).join("; ") || "docs validation failed"
46448
+ });
46449
+ var readBranch = (text, label) => {
46450
+ const match = text.match(BRANCH_RE);
46451
+ if (!match)
46452
+ return [null, err("missing_branch", `**Branch:** feature/* or bugfix/* required in ${label}`)];
46453
+ return [match[1].trim().replace(/`/g, ""), null];
46454
+ };
46455
+ var scanTaskHeadings = (planText) => {
46456
+ const ids = [];
46457
+ const titles = [];
46458
+ let inFence = false;
46459
+ for (const line of planText.split(`
46460
+ `)) {
46461
+ if (line.startsWith("```")) {
46462
+ inFence = !inFence;
46463
+ continue;
46464
+ }
46465
+ if (inFence)
46466
+ continue;
46467
+ const match = line.match(TASK_RE);
46468
+ if (match) {
46469
+ ids.push(Number(match[1]));
46470
+ titles.push(match[2].trim());
46471
+ }
46472
+ }
46473
+ if (ids.length === 0)
46474
+ return [ids, titles, err("task_order", "no ### Task N sections found outside fences")];
46475
+ const expected = ids.map((_, i) => i + 1);
46476
+ const sorted = [...ids].sort((a, b) => a - b);
46477
+ if (JSON.stringify(sorted) !== JSON.stringify(expected) || new Set(ids).size !== ids.length) {
46478
+ return [
46479
+ ids,
46480
+ titles,
46481
+ err("task_order", `task headings must be contiguous from 1..${ids.length}; found ${ids}`)
46482
+ ];
46483
+ }
46484
+ return [ids, titles, null];
46485
+ };
46486
+ var parseTasksFromPlan = (planText) => {
46487
+ const tasks = [];
46488
+ let current = null;
46489
+ let inFence = false;
46490
+ for (const line of planText.split(`
46491
+ `)) {
46492
+ if (line.startsWith("```")) {
46493
+ inFence = !inFence;
46494
+ continue;
46495
+ }
46496
+ if (inFence)
46497
+ continue;
46498
+ const match = line.match(TASK_RE);
46499
+ if (match) {
46500
+ if (current)
46501
+ tasks.push({ id: current.id, title: current.title, section_text: current.body.join(`
46502
+ `) });
46503
+ current = { id: Number(match[1]), title: match[2].trim(), body: [] };
46504
+ } else if (current) {
46505
+ current.body.push(line);
46506
+ }
46507
+ }
46508
+ if (current)
46509
+ tasks.push({ id: current.id, title: current.title, section_text: current.body.join(`
46510
+ `) });
46511
+ return tasks;
46512
+ };
46513
+ var docsValidate = ({
46514
+ spec_path,
46515
+ plan_path,
46516
+ workspace_root
46517
+ }) => {
46518
+ const resolved = resolveCanonicalLayout({ workspace_root, spec_path, plan_path });
46519
+ if (!resolved.ok)
46520
+ return failValidate([err("path_contract", resolved.error)]);
46521
+ const cwd2 = resolved.layout.workspace;
46522
+ const specAbs = resolved.layout.spec;
46523
+ const planAbs = resolved.layout.plan;
46524
+ const slug = resolved.layout.slug;
46525
+ const errors = [];
46526
+ const read = (p) => {
46527
+ try {
46528
+ return readFileSync9(p, "utf8");
46529
+ } catch {
46530
+ return null;
46531
+ }
46532
+ };
46533
+ const specText = read(specAbs);
46534
+ const planText = read(planAbs);
46535
+ if (specText === null)
46536
+ errors.push(err("missing_file", `spec not found: ${spec_path}`, spec_path));
46537
+ if (planText === null)
46538
+ errors.push(err("missing_file", `plan not found: ${plan_path}`, plan_path));
46539
+ if (errors.length)
46540
+ return failValidate(errors);
46541
+ const [specBranch, specErr] = readBranch(specText, "spec");
46542
+ if (specErr)
46543
+ errors.push(specErr);
46544
+ const [planBranch, planErr] = readBranch(planText, "plan");
46545
+ if (planErr)
46546
+ errors.push(planErr);
46547
+ const linkMatch = planText.match(SPEC_LINK_RE);
46548
+ if (!linkMatch) {
46549
+ errors.push(err("missing_spec_link", "**Spec:** link required in plan", plan_path));
46550
+ } else {
46551
+ const linked = (linkMatch[1] ?? linkMatch[2] ?? "").trim();
46552
+ const linkedResolved = resolveDocsPath({ workspace_root: cwd2, path: linked });
46553
+ if (!linkedResolved.ok) {
46554
+ errors.push(err("spec_link_escape", `plan **Spec:** ${linked} is not a contained docs path`, plan_path));
46555
+ } else if (linkedResolved.path !== specAbs) {
46556
+ errors.push(err("spec_mismatch", `plan **Spec:** ${linked} does not match spec_path ${spec_path}`, plan_path));
46557
+ }
46558
+ }
46559
+ if (specBranch && planBranch && specBranch !== planBranch) {
46560
+ errors.push(err("branch_mismatch", `spec branch ${JSON.stringify(specBranch)} != plan branch ${JSON.stringify(planBranch)}`, plan_path));
46561
+ }
46562
+ const [, , taskErr] = scanTaskHeadings(planText);
46563
+ if (taskErr)
46564
+ errors.push(taskErr);
46565
+ if (errors.length)
46566
+ return failValidate(errors);
46567
+ const tasks = parseTasksFromPlan(planText);
46568
+ const [headingIds, headingTitles, headingErr] = scanTaskHeadings(planText);
46569
+ if (headingErr)
46570
+ return failValidate([headingErr]);
46571
+ if (tasks.length !== headingIds.length) {
46572
+ return failValidate([
46573
+ err("task_order", `parse count ${tasks.length} != heading count ${headingIds.length}`, plan_path)
46574
+ ]);
46575
+ }
46576
+ for (let i = 0;i < tasks.length; i++) {
46577
+ if (String(tasks[i].id) !== String(headingIds[i]) || tasks[i].title.trim() !== headingTitles[i]) {
46578
+ return failValidate([err("task_order", `task mismatch at position ${i + 1}`, plan_path)]);
46579
+ }
46580
+ }
46581
+ const relSpec = path14.isAbsolute(spec_path) ? path14.relative(cwd2, specAbs) : spec_path;
46582
+ const relPlan = path14.isAbsolute(plan_path) ? path14.relative(cwd2, planAbs) : plan_path;
46583
+ const quality = qualitySpec(specText);
46584
+ if (!sddIgnored(cwd2, slug)) {
46585
+ quality.push({
46586
+ code: "sdd_not_ignored",
46587
+ message: `docs/${slug}/sdd/ exists but is not gitignored — add 'docs/*/sdd/' to .gitignore (or run wk-init)`,
46588
+ severity: "hard"
46589
+ });
46193
46590
  }
46591
+ const hygiene = hygieneFiles(cwd2);
46592
+ const hyState = hygiene.state;
46593
+ if (hyState["CHANGELOG.md"] === "missing")
46594
+ quality.push({
46595
+ code: "changelog_missing",
46596
+ message: "CHANGELOG.md missing — create it with Keep a Changelog format (run wk-init hygiene)",
46597
+ severity: "warning"
46598
+ });
46599
+ if (hyState["CHANGELOG.md"] === "invalid")
46600
+ quality.push({
46601
+ code: "changelog_invalid_format",
46602
+ message: "CHANGELOG.md lacks ## [Unreleased] — Keep a Changelog format required",
46603
+ severity: "warning"
46604
+ });
46605
+ if (hyState["README.md"] === "missing")
46606
+ quality.push({ code: "readme_missing", message: "README.md missing", severity: "warning" });
46607
+ if (hyState[".editorconfig"] === "missing")
46608
+ quality.push({
46609
+ code: "editorconfig_missing",
46610
+ message: ".editorconfig missing",
46611
+ severity: "warning"
46612
+ });
46613
+ if (hyState[".gitattributes"] === "missing")
46614
+ quality.push({
46615
+ code: "gitattributes_missing",
46616
+ message: ".gitattributes missing",
46617
+ severity: "warning"
46618
+ });
46619
+ if (hygiene.openSource && hyState.LICENSE === "missing")
46620
+ quality.push({
46621
+ code: "license_missing",
46622
+ message: "LICENSE missing (open-source repo)",
46623
+ severity: "warning"
46624
+ });
46625
+ if (hygiene.openSource && hyState["CONTRIBUTING.md"] === "missing")
46626
+ quality.push({
46627
+ code: "contributing_missing",
46628
+ message: "CONTRIBUTING.md missing (open-source repo)",
46629
+ severity: "warning"
46630
+ });
46631
+ return {
46632
+ ok: true,
46633
+ spec: relSpec,
46634
+ plan: relPlan,
46635
+ branch: specBranch,
46636
+ task_count: tasks.length,
46637
+ quality
46638
+ };
46639
+ };
46640
+ var REQUIRED_SECTIONS = [
46641
+ "## Context",
46642
+ "## Goals",
46643
+ "## Non-goals",
46644
+ "## Architecture",
46645
+ "## Acceptance criteria"
46646
+ ];
46647
+ var UI_KEYWORDS = [
46648
+ /\bui\b/,
46649
+ /\binterface\b/,
46650
+ /\bscreen\b/,
46651
+ /\bmodal\b/,
46652
+ /\bform\b/,
46653
+ /\bcomponent\b/
46654
+ ];
46655
+ var FLOW_KEYWORDS = [/\bflow\b/, /\bpipeline\b/, /\bsequence\b/, /\bdiagram\b/];
46656
+ var GLOSSARY_KEYWORDS = [/\bglossary\b/, /\bcontracts?\b/, /\bscope\b/];
46657
+ var finding = (code, message, severity) => ({
46658
+ code,
46659
+ message,
46660
+ severity
46194
46661
  });
46662
+ var stripFences = (text) => {
46663
+ const lines = text.split(`
46664
+ `);
46665
+ const out = [];
46666
+ let inFence = false;
46667
+ for (const line of lines) {
46668
+ if (line.startsWith("```")) {
46669
+ if (!inFence)
46670
+ out.push(line);
46671
+ inFence = !inFence;
46672
+ continue;
46673
+ }
46674
+ if (!inFence)
46675
+ out.push(line);
46676
+ }
46677
+ return out.join(`
46678
+ `);
46679
+ };
46680
+ var qualitySpec = (text) => {
46681
+ const findings = [];
46682
+ const body = stripFences(text);
46683
+ const lower = body.toLowerCase();
46684
+ for (const section of REQUIRED_SECTIONS) {
46685
+ if (!body.includes(section)) {
46686
+ findings.push(finding("missing_section", `required section ${section} missing`, "hard"));
46687
+ }
46688
+ }
46689
+ const hasCa = /^\s*(?:- CA-\d+|CA-\d+[.:])/m.test(body);
46690
+ if (!hasCa) {
46691
+ findings.push(finding("missing_acceptance_criteria", "no enumerable CA-XX acceptance criteria found", "hard"));
46692
+ }
46693
+ const hasAsciiFence = /```(?:text|ascii)/.test(body);
46694
+ const mentionsUi = UI_KEYWORDS.some((k) => k.test(lower));
46695
+ if (mentionsUi && !hasAsciiFence) {
46696
+ findings.push(finding("missing_ascii_for_ui", "spec mentions UI but has no ASCII wireframe fence", "warning"));
46697
+ }
46698
+ const hasMermaid = /```mermaid/.test(body);
46699
+ const explicitlyNoFlow = /\bno (?:flow|pipeline|sequence|diagram)\b/.test(lower);
46700
+ const mentionsFlow = FLOW_KEYWORDS.some((k) => k.test(lower));
46701
+ if (mentionsFlow && !hasMermaid && !explicitlyNoFlow) {
46702
+ findings.push(finding("missing_mermaid_for_flow", "spec describes a flow/pipeline/sequence but has no mermaid fence", "warning"));
46703
+ }
46704
+ const hasTable = /^\s*\|.+\|.+\|/m.test(body);
46705
+ const mentionsGlossary = GLOSSARY_KEYWORDS.some((k) => k.test(lower));
46706
+ const onlyOutOfScope = /\bout of scope\b/.test(lower) && !/\bglossary\b/.test(lower) && !/\bcontracts?\b/.test(lower);
46707
+ if (mentionsGlossary && !hasTable && !onlyOutOfScope) {
46708
+ findings.push(finding("missing_table", "spec has glossary/contract/scope content but no markdown table", "warning"));
46709
+ }
46710
+ return findings;
46711
+ };
46712
+ var sddIgnored = (cwd2, slug) => {
46713
+ const sddDir = path14.join(cwd2, "docs", slug, "sdd");
46714
+ if (!existsSync11(sddDir))
46715
+ return true;
46716
+ try {
46717
+ execFileSync2("git", ["-C", cwd2, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
46718
+ } catch {
46719
+ return true;
46720
+ }
46721
+ try {
46722
+ execFileSync2("git", ["-C", cwd2, "check-ignore", path14.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
46723
+ return true;
46724
+ } catch {
46725
+ return false;
46726
+ }
46727
+ };
46728
+
46729
+ // packages/workit-core/src/core/sdd.ts
46730
+ import {
46731
+ appendFileSync as appendFileSync2,
46732
+ existsSync as existsSync12,
46733
+ mkdirSync as mkdirSync6,
46734
+ readFileSync as readFileSync10,
46735
+ statSync as statSync5,
46736
+ writeFileSync as writeFileSync6
46737
+ } from "node:fs";
46738
+ import path15 from "node:path";
46739
+ function ledgerCompletion(root, slug) {
46740
+ let started = false;
46741
+ const completed = [];
46742
+ const absProgress = path15.join(root, "docs", slug, "sdd", "progress.md");
46743
+ if (existsSync12(absProgress)) {
46744
+ try {
46745
+ for (const line of readFileSync10(absProgress, "utf8").split(`
46746
+ `)) {
46747
+ const match = /^Task\s+(\d+):/i.exec(line);
46748
+ if (match) {
46749
+ started = true;
46750
+ if (/^Task\s+\d+:\s*complete\b/i.test(line))
46751
+ completed.push(Number(match[1]));
46752
+ }
46753
+ }
46754
+ } catch {}
46755
+ }
46756
+ const required = [];
46757
+ try {
46758
+ const absPlan = path15.join(root, "docs", slug, "plan.md");
46759
+ if (existsSync12(absPlan)) {
46760
+ for (const task of parseTasksFromPlan(readFileSync10(absPlan, "utf8")))
46761
+ required.push(task.id);
46762
+ }
46763
+ } catch {}
46764
+ const completedSet = new Set(completed);
46765
+ const missing = required.filter((id) => !completedSet.has(id));
46766
+ return {
46767
+ started,
46768
+ complete: required.length > 0 && missing.length === 0,
46769
+ required,
46770
+ completed,
46771
+ missing
46772
+ };
46773
+ }
46774
+
46775
+ // packages/workit-core/src/core/verify-project.ts
46776
+ import { spawnSync as spawnSync4 } from "node:child_process";
46777
+ import { existsSync as existsSync13, readFileSync as readFileSync12, statSync as statSync6 } from "node:fs";
46778
+ import path17 from "node:path";
46779
+
46780
+ // packages/workit-core/src/core/repo-context.ts
46781
+ import { spawnSync as spawnSync3 } from "node:child_process";
46782
+
46783
+ // packages/workit-core/src/core/branch.ts
46784
+ import { readFileSync as readFileSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
46785
+ import path16 from "node:path";
46786
+
46787
+ // packages/workit-core/src/core/git.ts
46788
+ import { execFileSync as execFileSync3 } from "node:child_process";
46789
+ var run = (cwd2, args) => {
46790
+ try {
46791
+ const stdout = execFileSync3("git", args, {
46792
+ cwd: cwd2,
46793
+ encoding: "utf8",
46794
+ stdio: ["pipe", "pipe", "pipe"]
46795
+ });
46796
+ return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
46797
+ } catch (error) {
46798
+ const e = error;
46799
+ return {
46800
+ stdout: (e.stdout ?? "").trimEnd(),
46801
+ stderr: (e.stderr ?? "").trimEnd(),
46802
+ exitCode: e.status ?? 1
46803
+ };
46804
+ }
46805
+ };
46806
+ var gitContext = (workspaceRoot, paths = []) => {
46807
+ const cwd2 = workspaceRoot;
46808
+ let failure;
46809
+ const runHere = (args) => {
46810
+ const result2 = run(cwd2, args);
46811
+ if (!failure && result2.exitCode !== 0) {
46812
+ failure = { stderr: result2.stderr, exitCode: result2.exitCode };
46813
+ }
46814
+ return result2.stdout;
46815
+ };
46816
+ const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
46817
+ const status_short = runHere(["status", "--porcelain"]);
46818
+ const staged = [];
46819
+ const unstaged = [];
46820
+ const untracked = [];
46821
+ for (const line of status_short.split(`
46822
+ `).filter(Boolean)) {
46823
+ const code = line.slice(0, 2);
46824
+ const file = line.slice(3);
46825
+ if (code === "??") {
46826
+ untracked.push(file);
46827
+ } else {
46828
+ if (code[0] !== " " && code[0] !== "?")
46829
+ staged.push(file);
46830
+ if (code[1] !== " " && code[1] !== "?")
46831
+ unstaged.push(file);
46832
+ }
46833
+ }
46834
+ const pathArgs = paths.length ? ["--", ...paths] : [];
46835
+ const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
46836
+ const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
46837
+ const stagedSet = new Set(staged);
46838
+ const unstagedSet = new Set(unstaged);
46839
+ const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
46840
+ return {
46841
+ workspace_root: cwd2,
46842
+ branch,
46843
+ status_short,
46844
+ staged,
46845
+ unstaged,
46846
+ untracked,
46847
+ diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
46848
+ `),
46849
+ partial_staged: partial_staged.length > 0,
46850
+ partial_staged_files: partial_staged,
46851
+ ...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
46852
+ };
46853
+ };
46854
+
46855
+ // packages/workit-core/src/core/branch.ts
46856
+ var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
46857
+ var policy = (root) => resolveBranchPolicyFor(root);
46858
+ var allowedBranch = (root, name) => policy(root).allowed.some((r) => r.test(name));
46859
+ var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
46860
+ var DECLARE_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/gim;
46861
+ var USE_CURRENT_RE = /^\s*\*+Branch:\*+\s*use-current\s*$/im;
46862
+ var readSafe2 = (p) => {
46863
+ try {
46864
+ return readFileSync11(p, "utf8");
46865
+ } catch {
46866
+ return null;
46867
+ }
46868
+ };
46869
+ var normalizeBranch = (root, name) => {
46870
+ const n = name.trim().replace(/`/g, "").replace(/\.+$/, "");
46871
+ if (isProtected(root, n))
46872
+ return null;
46873
+ if (!allowedBranch(root, n))
46874
+ return null;
46875
+ const parts = n.toLowerCase().split("/").map((p) => p.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-"));
46876
+ if (parts.some((p) => !p))
46877
+ return null;
46878
+ return parts.join("/");
46879
+ };
46880
+ var deriveSlug = (planPath) => {
46881
+ const dirName = path16.basename(path16.dirname(planPath));
46882
+ return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
46883
+ };
46884
+ var deriveKind = (planPath, fallback = "feature") => {
46885
+ const slug = deriveSlug(planPath);
46886
+ const text = readSafe2(planPath) ?? "";
46887
+ let kind = fallback;
46888
+ if (/\bbugfix\b/i.test(slug) || /^fix-/i.test(slug)) {
46889
+ kind = "bugfix";
46890
+ } else {
46891
+ const goal = text.split(`
46892
+ `).find((line) => line.startsWith("**Goal:**"))?.toLowerCase() ?? "";
46893
+ if (/\b(bugfix|bug fix)\b/.test(goal) && !/\b(feat|feature|upgrade|add)\b/.test(goal)) {
46894
+ kind = "bugfix";
46895
+ }
46896
+ }
46897
+ return kind;
46898
+ };
46899
+ var resolveBranch = ({
46900
+ spec_path,
46901
+ plan_path,
46902
+ workspace_root
46903
+ }) => {
46904
+ const cwd2 = path16.resolve(workspace_root);
46905
+ const abs = (p) => path16.isAbsolute(p) ? p : path16.join(cwd2, p);
46906
+ const spec = abs(spec_path);
46907
+ const plan = abs(plan_path);
46908
+ const git = gitContext(cwd2);
46909
+ const current = git.branch;
46910
+ const finish = (branch, source) => ({
46911
+ branch,
46912
+ source,
46913
+ current_branch: current,
46914
+ dirty: Boolean(git.status_short.trim()),
46915
+ needs_checkout: current !== branch
46916
+ });
46917
+ for (const file of [spec, plan]) {
46918
+ const text = readSafe2(file);
46919
+ if (!text)
46920
+ continue;
46921
+ if (USE_CURRENT_RE.test(text)) {
46922
+ if (!current || !allowedBranch(cwd2, current) || isProtected(cwd2, current)) {
46923
+ return { error: `use-current but HEAD ${current} is not an allowed branch` };
46924
+ }
46925
+ return finish(current, "use-current");
46926
+ }
46927
+ }
46928
+ if (current && allowedBranch(cwd2, current) && !isProtected(cwd2, current))
46929
+ return finish(current, "keep-current");
46930
+ let declaredButInvalid = null;
46931
+ for (const file of [spec, plan]) {
46932
+ const text = readSafe2(file);
46933
+ if (!text)
46934
+ continue;
46935
+ for (const match of text.matchAll(DECLARE_RE)) {
46936
+ const normalized = normalizeBranch(cwd2, match[1]);
46937
+ if (normalized)
46938
+ return finish(normalized, file === spec ? "spec" : "plan");
46939
+ declaredButInvalid ??= match[1];
46940
+ }
46941
+ }
46942
+ if (declaredButInvalid) {
46943
+ return {
46944
+ error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy`
46945
+ };
46946
+ }
46947
+ const slug = deriveSlug(plan);
46948
+ if (!slug)
46949
+ return { error: `cannot derive branch slug from plan ${plan}` };
46950
+ const kind = deriveKind(plan);
46951
+ return finish(`${kind}/${slug}`, "derived");
46952
+ };
46953
+
46954
+ // packages/workit-core/src/core/repo-context.ts
46955
+ var runGit = (cwd2, args) => {
46956
+ const result2 = spawnSync3("git", args, { cwd: cwd2, encoding: "utf8" });
46957
+ return {
46958
+ stdout: (result2.stdout ?? "").trimEnd(),
46959
+ stderr: (result2.stderr ?? "").trimEnd(),
46960
+ exitCode: result2.status ?? 1
46961
+ };
46962
+ };
46963
+ var repoRoot2 = (cwd2) => {
46964
+ const r = runGit(cwd2, ["rev-parse", "--show-toplevel"]);
46965
+ return r.exitCode === 0 && r.stdout ? r.stdout : cwd2;
46966
+ };
46967
+
46968
+ // packages/workit-core/src/core/verify-project.ts
46969
+ var commandOnPath2 = (name) => {
46970
+ const dirs = (process.env.PATH ?? "").split(path17.delimiter);
46971
+ const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
46972
+ for (const dir of dirs) {
46973
+ if (!dir)
46974
+ continue;
46975
+ for (const candidateName of names) {
46976
+ const candidate = path17.join(dir, candidateName);
46977
+ try {
46978
+ statSync6(candidate);
46979
+ if (process.platform !== "win32") {
46980
+ const mode = statSync6(candidate).mode & 73;
46981
+ if (mode === 0)
46982
+ continue;
46983
+ }
46984
+ return true;
46985
+ } catch {}
46986
+ }
46987
+ }
46988
+ return false;
46989
+ };
46990
+ var hasScript = (cwd2, name) => {
46991
+ const pkgPath = path17.join(cwd2, "package.json");
46992
+ if (!existsSync13(pkgPath))
46993
+ return false;
46994
+ try {
46995
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
46996
+ return Boolean(pkg.scripts && typeof pkg.scripts[name] === "string");
46997
+ } catch {
46998
+ return false;
46999
+ }
47000
+ };
47001
+ var packageRunner = (cwd2) => {
47002
+ if (existsSync13(path17.join(cwd2, "pnpm-lock.yaml")))
47003
+ return "pnpm";
47004
+ const pkgPath = path17.join(cwd2, "package.json");
47005
+ if (existsSync13(pkgPath)) {
47006
+ try {
47007
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
47008
+ if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
47009
+ return "pnpm";
47010
+ }
47011
+ } catch {}
47012
+ }
47013
+ if (existsSync13(path17.join(cwd2, "yarn.lock")))
47014
+ return "yarn";
47015
+ return "npm";
47016
+ };
47017
+ function runVerifyProject(root, dryRun = false) {
47018
+ const cwd2 = repoRoot2(root);
47019
+ let passed = 0;
47020
+ let failed = 0;
47021
+ let skipped = 0;
47022
+ const lines = [];
47023
+ const stderrParts = [];
47024
+ const runCheck = (label, cmd, args) => {
47025
+ lines.push(`
47026
+ ## ${label}
47027
+
47028
+ `);
47029
+ lines.push(`command: ${[cmd, ...args].join(" ")}
47030
+
47031
+ `);
47032
+ if (dryRun) {
47033
+ lines.push(`status: skipped (dry run)
47034
+ `);
47035
+ skipped += 1;
47036
+ return;
47037
+ }
47038
+ const r = spawnSync4(cmd, args, { cwd: cwd2, encoding: "utf8" });
47039
+ const so = (r.stdout ?? "").trimEnd();
47040
+ if (so)
47041
+ lines.push(so + `
47042
+ `);
47043
+ const se = (r.stderr ?? "").trimEnd();
47044
+ if (se)
47045
+ stderrParts.push(se);
47046
+ if (r.status === 0) {
47047
+ lines.push(`
47048
+ status: pass
47049
+ `);
47050
+ passed += 1;
47051
+ } else {
47052
+ lines.push(`
47053
+ status: fail (exit ${r.status})
47054
+ `);
47055
+ failed += 1;
47056
+ }
47057
+ };
47058
+ const skipCheck = (label, reason) => {
47059
+ lines.push(`
47060
+ ## ${label}
47061
+
47062
+ status: skipped (${reason})
47063
+ `);
47064
+ skipped += 1;
47065
+ };
47066
+ lines.push(`# Verification Context
47067
+ `);
47068
+ lines.push(`
47069
+ root: ${cwd2}
47070
+ `);
47071
+ lines.push(`dry_run: ${String(dryRun)}
47072
+ `);
47073
+ const pkgPath = path17.join(cwd2, "package.json");
47074
+ if (existsSync13(pkgPath)) {
47075
+ const runner = packageRunner(cwd2);
47076
+ for (const script of ["lint", "format:check", "test", "build"]) {
47077
+ if (hasScript(cwd2, script)) {
47078
+ if (runner === "npm")
47079
+ runCheck(script, "npm", ["run", script]);
47080
+ else
47081
+ runCheck(script, runner, [script]);
47082
+ } else {
47083
+ skipCheck(script, `package.json has no ${script} script`);
47084
+ }
47085
+ }
47086
+ } else {
47087
+ skipCheck("javascript", "package.json not found");
47088
+ }
47089
+ const cargoManifest = existsSync13(path17.join(cwd2, "Cargo.toml")) ? "Cargo.toml" : existsSync13(path17.join(cwd2, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
47090
+ if (cargoManifest) {
47091
+ runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
47092
+ runCheck("cargo clippy", "cargo", [
47093
+ "clippy",
47094
+ "--manifest-path",
47095
+ cargoManifest,
47096
+ "--all-targets",
47097
+ "--",
47098
+ "-D",
47099
+ "warnings"
47100
+ ]);
47101
+ runCheck("cargo test", "cargo", ["test", "--manifest-path", cargoManifest, "--all-targets"]);
47102
+ } else {
47103
+ skipCheck("rust", "Cargo.toml not found");
47104
+ }
47105
+ if (existsSync13(path17.join(cwd2, "pyproject.toml")) || existsSync13(path17.join(cwd2, "pytest.ini")) || existsSync13(path17.join(cwd2, "tests"))) {
47106
+ if (commandOnPath2("pytest"))
47107
+ runCheck("pytest", "pytest", []);
47108
+ else
47109
+ skipCheck("pytest", "pytest not available");
47110
+ if (commandOnPath2("ruff"))
47111
+ runCheck("ruff check", "ruff", ["check", "."]);
47112
+ else
47113
+ skipCheck("ruff check", "ruff not available");
47114
+ }
47115
+ lines.push(`
47116
+ ## CHANGELOG.md format
47117
+
47118
+ `);
47119
+ lines.push(`command: grep -qi "## [Unreleased]" CHANGELOG.md
47120
+
47121
+ `);
47122
+ const changelogPath = path17.join(cwd2, "CHANGELOG.md");
47123
+ if (dryRun) {
47124
+ lines.push(`status: skipped (dry run)
47125
+ `);
47126
+ skipped += 1;
47127
+ } else if (existsSync13(changelogPath)) {
47128
+ if (/## \[Unreleased\]/i.test(readFileSync12(changelogPath, "utf8"))) {
47129
+ lines.push(`status: pass
47130
+ `);
47131
+ passed += 1;
47132
+ } else {
47133
+ lines.push(`status: fail (missing ## [Unreleased])
47134
+ `);
47135
+ failed += 1;
47136
+ }
47137
+ } else {
47138
+ lines.push(`status: fail (missing CHANGELOG.md)
47139
+ `);
47140
+ failed += 1;
47141
+ }
47142
+ lines.push(`
47143
+ # Summary
47144
+
47145
+ `);
47146
+ lines.push(`passed: ${passed}
47147
+ `);
47148
+ lines.push(`failed: ${failed}
47149
+ `);
47150
+ lines.push(`skipped: ${skipped}
47151
+ `);
47152
+ return {
47153
+ stdout: lines.join(""),
47154
+ stderr: stderrParts.join(`
47155
+ `),
47156
+ exitCode: failed > 0 ? 1 : 0,
47157
+ cwd: cwd2
47158
+ };
47159
+ }
47160
+
47161
+ // packages/workit-core/src/core/flow-state.ts
47162
+ var COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations (task briefs, progress, review packages) to an " + "authenticated delegated worker via `task` / `wk-implement` instead of " + "editing in the coordinator session.";
47163
+ var CURSOR_SUBAGENT_UNSUPPORTED_TEXT = "Cursor cannot execute subagent-driven plans: the MCP has no child-session " + "support. Choose Inline, Handoff, or a review option in this session, or " + "run the plan in OpenCode with `wk-implement`.";
47164
+ var MENU_CHOICES = [
47165
+ "subagent-driven",
47166
+ "inline",
47167
+ "handoff",
47168
+ "review-spec",
47169
+ "review-plan"
47170
+ ];
47171
+ var err2 = (code, error, details) => ({
47172
+ ok: false,
47173
+ code,
47174
+ error,
47175
+ ...details ? { details } : {}
47176
+ });
47177
+ var SLUG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
47178
+ var flowPath = (root, slug) => {
47179
+ if (!SLUG_RE2.test(slug))
47180
+ throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
47181
+ return path18.join(root, "docs", slug, "sdd", "flow.json");
47182
+ };
47183
+ var resolveDoc = (root, slug, docPath, kind) => {
47184
+ const resolved = resolveCanonicalLayout({
47185
+ workspace_root: root,
47186
+ ...slug ? { slug } : {},
47187
+ [kind === "spec" ? "spec_path" : "plan_path"]: docPath
47188
+ });
47189
+ if (!resolved.ok)
47190
+ return { ok: false, error: resolved.error };
47191
+ return { ok: true, path: resolved.layout[kind === "spec" ? "spec" : "plan"] };
47192
+ };
47193
+ var HEX64_RE = /^[0-9a-f]{64}$/;
47194
+ var FLOW_STATUSES = ["draft", "self_reviewed", "approved"];
47195
+ var EXECUTION_STATUSES = ["pending", "active", "paused", "completed"];
47196
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
47197
+ var validateEvidenceValue = (v, allowCli) => {
47198
+ if (v === null)
47199
+ return true;
47200
+ if (!isRecord3(v))
47201
+ return false;
47202
+ if (v.host === "opencode") {
47203
+ return v.attested === true && typeof v.callID === "string" && typeof v.selectedLabel === "string" && typeof v.recordedAt === "number";
47204
+ }
47205
+ if (v.host === "cursor")
47206
+ return v.attested === false && v.confirmation === "contract";
47207
+ if (allowCli && v.host === "cli") {
47208
+ return v.attested === false && (v.confirmation === "flag" || v.confirmation === "tty");
47209
+ }
47210
+ return false;
47211
+ };
47212
+ var validateState = (parsed, slug) => {
47213
+ if (!isRecord3(parsed))
47214
+ return { ok: false, error: "flow state must be a JSON object" };
47215
+ if (parsed.slug !== undefined && (typeof parsed.slug !== "string" || parsed.slug !== slug)) {
47216
+ return { ok: false, error: `flow state slug must be ${JSON.stringify(slug)}` };
47217
+ }
47218
+ if (parsed.activated !== undefined && typeof parsed.activated !== "boolean") {
47219
+ return { ok: false, error: "flow state activated must be a boolean" };
47220
+ }
47221
+ if (parsed.handoff_destination !== undefined && typeof parsed.handoff_destination !== "boolean") {
47222
+ return { ok: false, error: "flow state handoff_destination must be a boolean" };
47223
+ }
47224
+ if (parsed.updated_at !== undefined && (typeof parsed.updated_at !== "number" || !Number.isFinite(parsed.updated_at))) {
47225
+ return { ok: false, error: "flow state updated_at must be a finite number" };
47226
+ }
47227
+ const doc = (value, name) => {
47228
+ const p = isRecord3(value) ? value : {};
47229
+ if (!isRecord3(value) && value !== undefined) {
47230
+ return `flow state ${name} must be an object`;
47231
+ }
47232
+ if (p.status !== undefined && !FLOW_STATUSES.includes(p.status)) {
47233
+ return `flow state ${name}.status must be draft, self_reviewed, or approved`;
47234
+ }
47235
+ if (p.path !== undefined && typeof p.path !== "string") {
47236
+ return `flow state ${name}.path must be a string`;
47237
+ }
47238
+ if (p.approved_digest !== undefined && p.approved_digest !== null && (typeof p.approved_digest !== "string" || !HEX64_RE.test(p.approved_digest))) {
47239
+ return `flow state ${name}.approved_digest must be 64-char lowercase hex or null`;
47240
+ }
47241
+ if (p.evidence !== undefined && !validateEvidenceValue(p.evidence, false)) {
47242
+ return `flow state ${name}.evidence has an unsupported shape`;
47243
+ }
47244
+ return {
47245
+ path: p.path ?? "",
47246
+ status: p.status ?? "draft",
47247
+ evidence: p.evidence ?? null,
47248
+ approved_digest: p.approved_digest ?? null
47249
+ };
47250
+ };
47251
+ const spec = doc(parsed.spec, "spec");
47252
+ if (typeof spec === "string")
47253
+ return { ok: false, error: spec };
47254
+ const plan = doc(parsed.plan, "plan");
47255
+ if (typeof plan === "string")
47256
+ return { ok: false, error: plan };
47257
+ const menuRaw = isRecord3(parsed.menu) ? parsed.menu : undefined;
47258
+ if (parsed.menu !== undefined && !isRecord3(parsed.menu)) {
47259
+ return { ok: false, error: "flow state menu must be an object" };
47260
+ }
47261
+ if (menuRaw?.presented !== undefined && typeof menuRaw.presented !== "boolean") {
47262
+ return { ok: false, error: "flow state menu.presented must be a boolean" };
47263
+ }
47264
+ if (menuRaw?.chosen !== undefined && typeof menuRaw.chosen !== "string") {
47265
+ return { ok: false, error: "flow state menu.chosen must be a string" };
47266
+ }
47267
+ if (menuRaw?.chosen !== undefined && menuRaw.chosen !== "" && !MENU_CHOICES.includes(menuRaw.chosen)) {
47268
+ return {
47269
+ ok: false,
47270
+ error: `flow state menu.chosen must be one of: ${MENU_CHOICES.join(", ")} (or an empty string when the menu is unpresented)`
47271
+ };
47272
+ }
47273
+ if (menuRaw?.evidence !== undefined && !validateEvidenceValue(menuRaw.evidence, false)) {
47274
+ return { ok: false, error: "flow state menu.evidence has an unsupported shape" };
47275
+ }
47276
+ const execRaw = isRecord3(parsed.execution) ? parsed.execution : undefined;
47277
+ if (parsed.execution !== undefined && !isRecord3(parsed.execution)) {
47278
+ return { ok: false, error: "flow state execution must be an object" };
47279
+ }
47280
+ if (execRaw?.status !== undefined && !EXECUTION_STATUSES.includes(execRaw.status)) {
47281
+ return {
47282
+ ok: false,
47283
+ error: "flow state execution.status must be pending, active, paused, or completed"
47284
+ };
47285
+ }
47286
+ if (execRaw?.mode !== undefined && execRaw.mode !== null && execRaw.mode !== "subagent-driven" && execRaw.mode !== "inline") {
47287
+ return {
47288
+ ok: false,
47289
+ error: "flow state execution.mode must be subagent-driven, inline, or null"
47290
+ };
47291
+ }
47292
+ if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
47293
+ return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
47294
+ }
47295
+ return {
47296
+ ok: true,
47297
+ state: {
47298
+ slug,
47299
+ activated: parsed.activated ?? true,
47300
+ spec,
47301
+ plan,
47302
+ menu: {
47303
+ presented: menuRaw?.presented ?? false,
47304
+ chosen: menuRaw?.chosen ?? "",
47305
+ evidence: menuRaw?.evidence ?? null
47306
+ },
47307
+ execution: {
47308
+ status: execRaw?.status ?? "pending",
47309
+ mode: execRaw?.mode ?? null,
47310
+ evidence: execRaw?.evidence ?? null
47311
+ },
47312
+ handoff_destination: parsed.handoff_destination ?? false,
47313
+ updated_at: parsed.updated_at ?? Date.now()
47314
+ }
47315
+ };
47316
+ };
47317
+ var readFlowStrict = (root, slug) => {
47318
+ const file = flowPath(root, slug);
47319
+ const rel = path18.posix.join("docs", slug, "sdd", "flow.json");
47320
+ if (!existsSync14(file)) {
47321
+ return err2("flow_not_activated", `flow not activated for ${slug} — run workflow_flow_status first`);
47322
+ }
47323
+ let text;
47324
+ try {
47325
+ text = readFileSync13(file, "utf8");
47326
+ } catch (error) {
47327
+ return err2("flow_io_error", `cannot read flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`);
47328
+ }
47329
+ let parsed;
47330
+ try {
47331
+ parsed = JSON.parse(text);
47332
+ } catch (error) {
47333
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`, { path: rel, original_bytes_preserved: true });
47334
+ }
47335
+ const validated = validateState(parsed, slug);
47336
+ if (!validated.ok) {
47337
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${validated.error}`, {
47338
+ path: rel,
47339
+ original_bytes_preserved: true
47340
+ });
47341
+ }
47342
+ return { ok: true, state: validated.state, raw: parsed };
47343
+ };
47344
+ var uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`;
47345
+ var writeFlowFileAtomic = (file, state) => {
47346
+ const text = JSON.stringify(state, null, 2) + `
47347
+ `;
47348
+ const tmp = uniqueTempPath(file);
47349
+ mkdirSync8(path18.dirname(file), { recursive: true });
47350
+ let fd = null;
47351
+ try {
47352
+ fd = openSync(tmp, "w");
47353
+ writeFileSync8(fd, text, "utf8");
47354
+ fsyncSync(fd);
47355
+ closeSync(fd);
47356
+ fd = null;
47357
+ renameSync2(tmp, file);
47358
+ } finally {
47359
+ try {
47360
+ if (fd !== null)
47361
+ closeSync(fd);
47362
+ } catch {}
47363
+ try {
47364
+ if (existsSync14(tmp))
47365
+ rmSync2(tmp, { force: true });
47366
+ } catch {}
47367
+ }
47368
+ };
47369
+ var MAX_WRITE_ATTEMPTS = 5;
47370
+ var STALE_LOCK_MS = 1000;
47371
+ var lockMtimeMs = (lock) => {
47372
+ try {
47373
+ return statSync7(lock).mtimeMs;
47374
+ } catch {
47375
+ return null;
47376
+ }
47377
+ };
47378
+ var lockOwnedBy = (fd, lock) => {
47379
+ try {
47380
+ return fstatSync(fd).ino === statSync7(lock).ino;
47381
+ } catch {
47382
+ return false;
47383
+ }
47384
+ };
47385
+ var writeFlowStateIfCurrent = (root, expected, next) => {
47386
+ const file = flowPath(root, next.slug);
47387
+ const expectedText = JSON.stringify(expected, null, 2) + `
47388
+ `;
47389
+ const nextText = JSON.stringify(next, null, 2) + `
47390
+ `;
47391
+ if (expectedText === nextText)
47392
+ return { ok: true };
47393
+ const tmp = uniqueTempPath(file);
47394
+ let fd = null;
47395
+ try {
47396
+ const currentText = existsSync14(file) ? readFileSync13(file, "utf8") : null;
47397
+ if (currentText !== expectedText)
47398
+ return { ok: false, conflict: true };
47399
+ mkdirSync8(path18.dirname(file), { recursive: true });
47400
+ fd = openSync(tmp, "w");
47401
+ writeFileSync8(fd, nextText, "utf8");
47402
+ fsyncSync(fd);
47403
+ closeSync(fd);
47404
+ fd = null;
47405
+ const reRead = existsSync14(file) ? readFileSync13(file, "utf8") : null;
47406
+ if (reRead !== expectedText)
47407
+ return { ok: false, conflict: true };
47408
+ renameSync2(tmp, file);
47409
+ return { ok: true };
47410
+ } catch (error) {
47411
+ return { ok: false, io_error: error instanceof Error ? error.message : String(error) };
47412
+ } finally {
47413
+ try {
47414
+ if (fd !== null)
47415
+ closeSync(fd);
47416
+ } catch {}
47417
+ try {
47418
+ if (existsSync14(tmp))
47419
+ rmSync2(tmp, { force: true });
47420
+ } catch {}
47421
+ }
47422
+ };
47423
+ var readCanonicalDigest = (root, rel) => {
47424
+ const abs = path18.join(root, ...rel.split("/"));
47425
+ let bytes;
47426
+ try {
47427
+ bytes = readFileSync13(abs);
47428
+ } catch (error) {
47429
+ if (error.code === "ENOENT") {
47430
+ return { ok: false, code: "document_missing" };
47431
+ }
47432
+ return { ok: false, code: "document_unreadable" };
47433
+ }
47434
+ let text;
47435
+ try {
47436
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
47437
+ } catch {
47438
+ return { ok: false, code: "document_unreadable" };
47439
+ }
47440
+ return { ok: true, text, digest: createHash("sha256").update(bytes).digest("hex") };
47441
+ };
47442
+ var resetForSpecDrift = (state) => ({
47443
+ ...state,
47444
+ spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
47445
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
47446
+ menu: { presented: false, chosen: "", evidence: null },
47447
+ execution: { status: "pending", mode: null, evidence: null },
47448
+ handoff_destination: false,
47449
+ updated_at: Date.now()
47450
+ });
47451
+ var resetForPlanDrift = (state) => ({
47452
+ ...state,
47453
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
47454
+ menu: { presented: false, chosen: "", evidence: null },
47455
+ execution: { status: "pending", mode: null, evidence: null },
47456
+ handoff_destination: false,
47457
+ updated_at: Date.now()
47458
+ });
47459
+ var driftCodeFor = (root, relPath, storedDigest) => {
47460
+ if (storedDigest === null)
47461
+ return "digest_missing";
47462
+ const current = readCanonicalDigest(root, relPath);
47463
+ if (!current.ok)
47464
+ return current.code;
47465
+ return current.digest !== storedDigest ? "digest_mismatch" : null;
47466
+ };
47467
+ var reconcileState = (root, slug, state) => {
47468
+ const specPath = path18.posix.join("docs", slug, "spec.md");
47469
+ const planPath = path18.posix.join("docs", slug, "plan.md");
47470
+ if (state.spec.status === "approved") {
47471
+ const code = driftCodeFor(root, specPath, state.spec.approved_digest);
47472
+ if (code) {
47473
+ return {
47474
+ state: resetForSpecDrift(state),
47475
+ drift: [{ document: "spec", code, path: specPath }]
47476
+ };
47477
+ }
47478
+ }
47479
+ if (state.plan.status === "approved") {
47480
+ const code = driftCodeFor(root, planPath, state.plan.approved_digest);
47481
+ if (code) {
47482
+ return {
47483
+ state: resetForPlanDrift(state),
47484
+ drift: [{ document: "plan", code, path: planPath }]
47485
+ };
47486
+ }
47487
+ }
47488
+ return { state, drift: [] };
47489
+ };
47490
+ var deriveLegacyExecution = (root, slug, state) => {
47491
+ const ledger = ledgerCompletion(root, slug);
47492
+ if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
47493
+ return { status: "active", mode: "subagent-driven", evidence: null };
47494
+ }
47495
+ return { status: "pending", mode: null, evidence: null };
47496
+ };
47497
+ var normalizeCompatibility = (root, slug, parsed, state) => {
47498
+ if (!isRecord3(parsed) || !("execution" in parsed)) {
47499
+ const derived = deriveLegacyExecution(root, slug, state);
47500
+ const current = state.execution;
47501
+ if (derived.status !== current.status || derived.mode !== current.mode) {
47502
+ return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
47503
+ }
47504
+ }
47505
+ return { state, changed: false };
47506
+ };
47507
+ var withFlowLock = (file, fn) => {
47508
+ const lock = `${file}.lock`;
47509
+ if (!existsSync14(path18.dirname(file)))
47510
+ return { locked: true, value: fn() };
47511
+ try {
47512
+ if (existsSync14(`${lock}.stale`))
47513
+ rmSync2(`${lock}.stale`, { force: true });
47514
+ } catch {}
47515
+ const wait = new Int32Array(new SharedArrayBuffer(4));
47516
+ let fd = null;
47517
+ for (let attempt2 = 0;attempt2 < MAX_WRITE_ATTEMPTS; attempt2++) {
47518
+ try {
47519
+ fd = openSync(lock, "wx");
47520
+ break;
47521
+ } catch (error) {
47522
+ const code = error.code;
47523
+ if (code !== "EEXIST") {
47524
+ return {
47525
+ locked: false,
47526
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${error instanceof Error ? error.message : String(error)}`)
47527
+ };
47528
+ }
47529
+ const mtime = lockMtimeMs(lock);
47530
+ if (mtime !== null && Date.now() - mtime > STALE_LOCK_MS) {
47531
+ try {
47532
+ renameSync2(lock, `${lock}.stale`);
47533
+ unlinkSync3(`${lock}.stale`);
47534
+ } catch {}
47535
+ try {
47536
+ fd = openSync(lock, "wx");
47537
+ break;
47538
+ } catch (innerError) {
47539
+ const innerCode = innerError.code;
47540
+ if (innerCode !== "EEXIST") {
47541
+ return {
47542
+ locked: false,
47543
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${innerError instanceof Error ? innerError.message : String(innerError)}`)
47544
+ };
47545
+ }
47546
+ }
47547
+ }
47548
+ if (attempt2 === MAX_WRITE_ATTEMPTS - 1) {
47549
+ return {
47550
+ locked: false,
47551
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path18.dirname(file)}: re-read the flow state and retry the transition`)
47552
+ };
47553
+ }
47554
+ Atomics.wait(wait, 0, 0, 10);
47555
+ }
47556
+ }
47557
+ if (fd === null) {
47558
+ return {
47559
+ locked: false,
47560
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path18.dirname(file)}: re-read the flow state and retry the transition`)
47561
+ };
47562
+ }
47563
+ try {
47564
+ return { locked: true, value: fn() };
47565
+ } finally {
47566
+ try {
47567
+ if (fd !== null && lockOwnedBy(fd, lock))
47568
+ rmSync2(lock, { force: true });
47569
+ } catch {}
47570
+ try {
47571
+ if (fd !== null)
47572
+ closeSync(fd);
47573
+ } catch {}
47574
+ }
47575
+ };
47576
+ var readEffectiveFlowState = (root, slug) => {
47577
+ const file = flowPath(root, slug);
47578
+ const rel = path18.posix.join("docs", slug, "sdd", "flow.json");
47579
+ const locked = withFlowLock(file, () => {
47580
+ const strict = readFlowStrict(root, slug);
47581
+ if (!strict.ok)
47582
+ return strict;
47583
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
47584
+ const { state, drift } = reconcileState(root, slug, normalized.state);
47585
+ if (normalized.changed || drift.length > 0) {
47586
+ try {
47587
+ writeFlowFileAtomic(file, state);
47588
+ } catch (error) {
47589
+ return err2("flow_io_error", `cannot persist reconciled flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`, { path: rel, original_bytes_preserved: true });
47590
+ }
47591
+ }
47592
+ return { ok: true, state, drift };
47593
+ });
47594
+ if (!locked.locked)
47595
+ return locked.error;
47596
+ return locked.value;
47597
+ };
47598
+ var readModifyWrite = (root, slug, mutate) => {
47599
+ const file = flowPath(root, slug);
47600
+ const locked = withFlowLock(file, () => {
47601
+ for (let attempt2 = 0;attempt2 < MAX_WRITE_ATTEMPTS; attempt2++) {
47602
+ const strict = readFlowStrict(root, slug);
47603
+ if (!strict.ok)
47604
+ return strict;
47605
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
47606
+ const reconciled = reconcileState(root, slug, normalized.state);
47607
+ const result2 = mutate(reconciled.state);
47608
+ if (!result2.ok)
47609
+ return result2;
47610
+ let baseline = strict.state;
47611
+ if (normalized.changed) {
47612
+ try {
47613
+ writeFlowFileAtomic(file, normalized.state);
47614
+ } catch (error) {
47615
+ return err2("flow_io_error", `cannot persist normalized flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`);
47616
+ }
47617
+ baseline = normalized.state;
47618
+ }
47619
+ const commit = writeFlowStateIfCurrent(root, baseline, result2.next);
47620
+ if (commit.ok)
47621
+ return { ok: true };
47622
+ if ("io_error" in commit) {
47623
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
47624
+ }
47625
+ }
47626
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
47627
+ });
47628
+ if (!locked.locked)
47629
+ return locked.error;
47630
+ return locked.value;
47631
+ };
47632
+ var assertMutationWorkspace = (root, ctx) => {
47633
+ if (ctx && ctx.hostWorkspace !== root) {
47634
+ return err2("workspace_mismatch", `mutation context workspace ${JSON.stringify(ctx.hostWorkspace)} does not match flow workspace ${JSON.stringify(root)}`);
47635
+ }
47636
+ return { ok: true };
47637
+ };
47638
+ var MAX_CLOCK_SKEW_MS = 60000;
47639
+ var MAX_RECEIPTS_PER_SESSION = 10;
47640
+ var RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
47641
+ var EVIDENCE_WINDOW_MS = 24 * 60 * 60 * 1000;
47642
+ var NEGATIVE_ANSWER_LABELS = [
47643
+ "no",
47644
+ "nope",
47645
+ "nah",
47646
+ "reject",
47647
+ "cancel",
47648
+ "decline",
47649
+ "not now",
47650
+ "not yet",
47651
+ "skip",
47652
+ "back",
47653
+ "deny"
47654
+ ];
47655
+ var isNegativeLabel = (label) => {
47656
+ const normalized = label.trim().toLowerCase();
47657
+ return NEGATIVE_ANSWER_LABELS.some((entry) => {
47658
+ const firstWord = normalized.split(/\s+/)[0] ?? "";
47659
+ if (entry.includes(" ")) {
47660
+ const plain = normalized.replace(/[^a-z ]/g, "");
47661
+ return plain === entry || plain.startsWith(`${entry} `);
47662
+ }
47663
+ return firstWord.replace(/[^a-z]/g, "") === entry;
47664
+ });
47665
+ };
47666
+
47667
+ class HostReceiptStore {
47668
+ #bySession = new Map;
47669
+ record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
47670
+ const label = selectedLabel.trim();
47671
+ if (!label)
47672
+ return;
47673
+ if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
47674
+ return;
47675
+ const queue = this.#bySession.get(sessionId) ?? [];
47676
+ if (queue.length >= MAX_RECEIPTS_PER_SESSION)
47677
+ queue.shift();
47678
+ queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
47679
+ this.#bySession.set(sessionId, queue);
47680
+ }
47681
+ count(sessionId) {
47682
+ return this.#bySession.get(sessionId)?.length ?? 0;
47683
+ }
47684
+ peek(sessionId, opts = {}) {
47685
+ return this.#take(sessionId, opts, false);
47686
+ }
47687
+ consume(sessionId, opts = {}) {
47688
+ return this.#take(sessionId, opts, true);
47689
+ }
47690
+ #take(sessionId, opts, remove2) {
47691
+ const queue = this.#bySession.get(sessionId);
47692
+ if (!queue || queue.length === 0) {
47693
+ return err2("receipt_missing", "no host-observed native-question receipt for this session — ask the native " + "`question` tool and have the user answer before calling this tool");
47694
+ }
47695
+ const index = queue.length - 1;
47696
+ const receipt = queue[index];
47697
+ if (isNegativeLabel(receipt.selectedLabel)) {
47698
+ this.#bySession.delete(sessionId);
47699
+ return err2("receipt_rejected", `the user's most recent answer (${JSON.stringify(receipt.selectedLabel)}) is a ` + "negative answer — it cannot authorize an approval; ask the native question again");
47700
+ }
47701
+ if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
47702
+ return err2("evidence_mismatch", `receipt selectedLabel does not match ${JSON.stringify(opts.label)} — fabricated menu choice rejected`);
47703
+ }
47704
+ if (Date.now() - receipt.recordedAt > RECEIPT_FRESHNESS_MS) {
47705
+ if (remove2) {
47706
+ queue.splice(index, 1);
47707
+ if (queue.length === 0)
47708
+ this.#bySession.delete(sessionId);
47709
+ }
47710
+ return err2("receipt_stale", "the question receipt is too old — ask the native question again and re-answer");
47711
+ }
47712
+ if (remove2) {
47713
+ queue.splice(index, 1);
47714
+ if (queue.length === 0)
47715
+ this.#bySession.delete(sessionId);
47716
+ }
47717
+ return { ok: true, receipt };
47718
+ }
47719
+ }
47720
+ var sameChoiceLabel = (a, b) => a.toLowerCase() === b.toLowerCase();
47721
+ var CURSOR_KEYS = ["attested", "confirmation", "host"];
47722
+ var assertEvidenceShape = (input) => {
47723
+ if (typeof input !== "object" || input === null) {
47724
+ return {
47725
+ ok: false,
47726
+ error: "native choice evidence required — bare booleans and other primitives are not approval evidence"
47727
+ };
47728
+ }
47729
+ const record = input;
47730
+ if (record.host === "cursor") {
47731
+ if (record.attested !== false || record.confirmation !== "contract") {
47732
+ return {
47733
+ ok: false,
47734
+ error: 'cursor confirmations are policy-only: exactly { host: "cursor", attested: false, confirmation: "contract" } — Cursor cannot attest a host-observed answer'
47735
+ };
47736
+ }
47737
+ const keys2 = Object.keys(record).sort();
47738
+ if (keys2.length !== CURSOR_KEYS.length || !CURSOR_KEYS.every((key) => keys2.includes(key))) {
47739
+ return {
47740
+ ok: false,
47741
+ error: "cursor confirmations carry no caller-supplied question data — the attested: false constant only"
47742
+ };
47743
+ }
47744
+ return { ok: true, evidence: { host: "cursor", attested: false, confirmation: "contract" } };
47745
+ }
47746
+ if (record.host !== "opencode") {
47747
+ return {
47748
+ ok: false,
47749
+ error: `evidence host must be 'opencode' or 'cursor', got ${JSON.stringify(record.host)}`
47750
+ };
47751
+ }
47752
+ if (record.attested !== true) {
47753
+ return {
47754
+ ok: false,
47755
+ error: "opencode evidence requires host attestation (attested: true) — only host-observed question receipts are accepted"
47756
+ };
47757
+ }
47758
+ const { callID, selectedLabel, recordedAt } = record;
47759
+ if (typeof callID !== "string" || callID.trim() === "") {
47760
+ return {
47761
+ ok: false,
47762
+ error: "opencode evidence callID must be a non-empty string (host question tool call)"
47763
+ };
47764
+ }
47765
+ if (typeof selectedLabel !== "string" || selectedLabel.trim() === "") {
47766
+ return {
47767
+ ok: false,
47768
+ error: "opencode evidence selectedLabel must be the exact label the user selected on the native question"
47769
+ };
47770
+ }
47771
+ if (typeof recordedAt !== "number" || !Number.isFinite(recordedAt) || recordedAt <= 0) {
47772
+ return {
47773
+ ok: false,
47774
+ error: "opencode evidence recordedAt must be a positive epoch-ms timestamp"
47775
+ };
47776
+ }
47777
+ const now2 = Date.now();
47778
+ if (recordedAt > now2 + MAX_CLOCK_SKEW_MS) {
47779
+ return {
47780
+ ok: false,
47781
+ error: "opencode evidence recordedAt is in the future — forged evidence is rejected"
47782
+ };
47783
+ }
47784
+ if (now2 - recordedAt > EVIDENCE_WINDOW_MS) {
47785
+ return {
47786
+ ok: false,
47787
+ error: "opencode evidence recordedAt is too old — ask the native question again and re-record the answer"
47788
+ };
47789
+ }
47790
+ return {
47791
+ ok: true,
47792
+ evidence: {
47793
+ host: "opencode",
47794
+ attested: true,
47795
+ callID: callID.trim(),
47796
+ selectedLabel: selectedLabel.trim(),
47797
+ recordedAt
47798
+ }
47799
+ };
47800
+ };
47801
+ var markHandoffDestination = (root, slug, planPath) => {
47802
+ const doc = resolveDoc(root, slug, planPath, "plan");
47803
+ if (!doc.ok)
47804
+ return err2("path_invalid", doc.error);
47805
+ return readModifyWrite(root, slug, (state) => {
47806
+ if (state.spec.status !== "approved")
47807
+ return err2("spec_not_approved", "spec must be approved before marking a handoff destination");
47808
+ if (state.plan.status !== "approved")
47809
+ return err2("plan_not_approved", "plan must be approved before marking a handoff destination");
47810
+ if (state.handoff_destination) {
47811
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
47812
+ }
47813
+ if (state.menu.chosen !== "handoff") {
47814
+ return err2("handoff_not_chosen", `source menu choice must be "handoff" to mark a handoff destination (chosen: ${JSON.stringify(state.menu.chosen)})`);
47815
+ }
47816
+ return {
47817
+ ok: true,
47818
+ next: {
47819
+ ...state,
47820
+ handoff_destination: true,
47821
+ menu: { presented: false, chosen: "", evidence: null },
47822
+ updated_at: Date.now()
47823
+ }
47824
+ };
47825
+ });
47826
+ };
47827
+ var CLI_CONFIRMATION_KEYS = ["attested", "confirmation", "host"];
47828
+ var validateLifecycleEvidence = (input) => {
47829
+ if (typeof input !== "object" || input === null) {
47830
+ return {
47831
+ ok: false,
47832
+ error: "lifecycle evidence required — native choice evidence or an exact CLI confirmation"
47833
+ };
47834
+ }
47835
+ const record = input;
47836
+ if (record.host === "cli") {
47837
+ const validValue = record.attested === false && (record.confirmation === "flag" || record.confirmation === "tty");
47838
+ const keys2 = Object.keys(record).sort();
47839
+ const exactShape = keys2.length === CLI_CONFIRMATION_KEYS.length && CLI_CONFIRMATION_KEYS.every((key) => keys2.includes(key));
47840
+ if (validValue && exactShape) {
47841
+ return {
47842
+ ok: true,
47843
+ evidence: {
47844
+ host: "cli",
47845
+ attested: false,
47846
+ confirmation: record.confirmation
47847
+ }
47848
+ };
47849
+ }
47850
+ return {
47851
+ ok: false,
47852
+ error: 'cli confirmations accept only the exact { host: "cli", attested: false, confirmation: "flag" | "tty" } shape'
47853
+ };
47854
+ }
47855
+ return assertEvidenceShape(input);
47856
+ };
47857
+ var errPendingFlow = (action) => err2("flow_not_active", `cannot ${action} a pending flow — the execution menu has not started it`);
47858
+ var errCompletedFlow = (action) => err2("flow_already_completed", `cannot ${action} a completed flow`);
47859
+ var completeExecution = (root, slug, deps) => {
47860
+ const file = flowPath(root, slug);
47861
+ const captured = readEffectiveFlowState(root, slug);
47862
+ if (!captured.ok)
47863
+ return captured;
47864
+ const exec3 = captured.state.execution;
47865
+ if (exec3.status === "pending")
47866
+ return errPendingFlow("complete");
47867
+ if (exec3.status === "completed")
47868
+ return errCompletedFlow("complete");
47869
+ const ledger = ledgerCompletion(root, slug);
47870
+ if (!ledger.complete) {
47871
+ return err2("execution_incomplete", `execution ledger incomplete for ${slug}: missing tasks ${ledger.missing.join(", ")}`, { required: ledger.required, completed: ledger.completed, missing: ledger.missing });
47872
+ }
47873
+ const verifier = deps?.verifyProject ?? runVerifyProject;
47874
+ const verify = verifier(root, false);
47875
+ if (verify.exitCode !== 0) {
47876
+ return err2("verification_failed", `repository verification failed for ${slug} (exit ${verify.exitCode}) — see the verification output`, { exitCode: verify.exitCode });
47877
+ }
47878
+ const locked = withFlowLock(file, () => {
47879
+ const strict = readFlowStrict(root, slug);
47880
+ if (!strict.ok)
47881
+ return strict;
47882
+ const reconciled = reconcileState(root, slug, strict.state);
47883
+ const currentExec = reconciled.state.execution;
47884
+ if (currentExec.status !== exec3.status || currentExec.mode !== exec3.mode) {
47885
+ return err2("flow_concurrent_conflict", `concurrent execution state change detected for ${slug}: re-read the flow state and retry completion`);
47886
+ }
47887
+ const next = {
47888
+ ...reconciled.state,
47889
+ execution: { ...exec3, status: "completed" },
47890
+ handoff_destination: false,
47891
+ updated_at: Date.now()
47892
+ };
47893
+ const commit = writeFlowStateIfCurrent(root, captured.state, next);
47894
+ if (commit.ok)
47895
+ return { ok: true };
47896
+ if ("io_error" in commit) {
47897
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
47898
+ }
47899
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry completion`);
47900
+ });
47901
+ if (!locked.locked)
47902
+ return locked.error;
47903
+ return locked.value;
47904
+ };
47905
+ var transitionExecution = (root, slug, planPath, action, evidence, ctx, deps) => {
47906
+ const bound = assertMutationWorkspace(root, ctx);
47907
+ if (!bound.ok)
47908
+ return bound;
47909
+ const validated = validateLifecycleEvidence(evidence);
47910
+ if (!validated.ok)
47911
+ return err2("evidence_invalid", validated.error);
47912
+ const doc = resolveDoc(root, slug, planPath, "plan");
47913
+ if (!doc.ok)
47914
+ return err2("path_invalid", doc.error);
47915
+ if (action === "complete")
47916
+ return completeExecution(root, slug, deps);
47917
+ if (action === "pause") {
47918
+ return readModifyWrite(root, slug, (state) => {
47919
+ const exec3 = state.execution;
47920
+ if (exec3.status === "pending")
47921
+ return errPendingFlow("pause");
47922
+ if (exec3.status === "completed")
47923
+ return errCompletedFlow("pause");
47924
+ if (exec3.status === "paused")
47925
+ return err2("flow_already_paused", "flow is already paused");
47926
+ return {
47927
+ ok: true,
47928
+ next: { ...state, execution: { ...exec3, status: "paused" }, updated_at: Date.now() }
47929
+ };
47930
+ });
47931
+ }
47932
+ return readModifyWrite(root, slug, (state) => {
47933
+ const exec3 = state.execution;
47934
+ if (exec3.status === "completed")
47935
+ return errCompletedFlow("resume");
47936
+ if (exec3.status !== "paused") {
47937
+ return err2("flow_not_paused", exec3.status === "active" ? "flow is already active — cannot resume" : "cannot resume a pending flow — the execution menu has not started it");
47938
+ }
47939
+ return {
47940
+ ok: true,
47941
+ next: { ...state, execution: { ...exec3, status: "active" }, updated_at: Date.now() }
47942
+ };
47943
+ });
47944
+ };
47945
+ var slugFromPath = (p) => {
47946
+ const dirName = path18.basename(path18.dirname(p));
47947
+ return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
47948
+ };
47949
+ var BASH_READ_TOKENS = new Set([
47950
+ "cat",
47951
+ "head",
47952
+ "tail",
47953
+ "less",
47954
+ "more",
47955
+ "grep",
47956
+ "rg",
47957
+ "ag",
47958
+ "find",
47959
+ "ls",
47960
+ "stat",
47961
+ "wc",
47962
+ "file",
47963
+ "diff",
47964
+ "sort",
47965
+ "uniq",
47966
+ "cut",
47967
+ "tr",
47968
+ "fold",
47969
+ "printf",
47970
+ "echo",
47971
+ "pwd",
47972
+ "date",
47973
+ "which",
47974
+ "type",
47975
+ "du",
47976
+ "df",
47977
+ "tree",
47978
+ "jq",
47979
+ "basename",
47980
+ "dirname",
47981
+ "realpath",
47982
+ "readlink",
47983
+ "rev",
47984
+ "comm",
47985
+ "paste",
47986
+ "nl",
47987
+ "od",
47988
+ "xxd",
47989
+ "awk",
47990
+ "gawk",
47991
+ "mawk",
47992
+ "test",
47993
+ "["
47994
+ ]);
47995
+ var BASH_GIT_READ_SUBCOMMANDS = new Set([
47996
+ "status",
47997
+ "log",
47998
+ "diff",
47999
+ "show",
48000
+ "branch",
48001
+ "rev-parse",
48002
+ "merge-base",
48003
+ "remote",
48004
+ "ls-files",
48005
+ "blame",
48006
+ "shortlog",
48007
+ "describe",
48008
+ "check-ignore",
48009
+ "name-rev",
48010
+ "stash",
48011
+ "grep",
48012
+ "tag"
48013
+ ]);
48014
+ var BASH_GIT_MUTABLE_SUBCOMMANDS = new Set(["branch", "remote", "tag"]);
48015
+ var BASH_GIT_READ_FLAGS = {
48016
+ branch: new Set([
48017
+ "-a",
48018
+ "--all",
48019
+ "-r",
48020
+ "--remotes",
48021
+ "-v",
48022
+ "--verbose",
48023
+ "-vv",
48024
+ "--show-current",
48025
+ "-l",
48026
+ "--list",
48027
+ "--merged",
48028
+ "--no-merged",
48029
+ "--contains",
48030
+ "--points-at",
48031
+ "--format",
48032
+ "--sort"
48033
+ ]),
48034
+ tag: new Set([
48035
+ "-l",
48036
+ "--list",
48037
+ "--sort",
48038
+ "--contains",
48039
+ "--points-at",
48040
+ "--merged",
48041
+ "--no-merged",
48042
+ "--format",
48043
+ "--column"
48044
+ ]),
48045
+ remote: new Set(["-v", "--verbose"])
48046
+ };
48047
+ var BASH_FIND_DENIED_FLAGS = new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
48048
+ var BASH_TEST_VERBS = new Set([
48049
+ "test",
48050
+ "check",
48051
+ "lint",
48052
+ "typecheck",
48053
+ "verify",
48054
+ "validate",
48055
+ "vitest",
48056
+ "jest",
48057
+ "mocha"
48058
+ ]);
48059
+ var BASH_DENIED_HEADS = new Set(["curl", "sudo", "tee", "wget"]);
48060
+ var BASH_OUTPUT_FLAG_VERBS = new Set(["sort", "tree", "comm", "diff", "jq"]);
48061
+ var BASH_PAREN_EXEMPT_HEADS = new Set(["echo", "printf", "jq"]);
48062
+ var BASH_GIT_VALUE_FLAGS = new Set([
48063
+ "--contains",
48064
+ "--points-at",
48065
+ "--merged",
48066
+ "--no-merged",
48067
+ "--sort",
48068
+ "--format"
48069
+ ]);
48070
+ var COORDINATOR_SHELL_DENIED_TEXT = "Coordinator shell commands are restricted while a subagent-driven plan is " + "active: only bounded read/test/review commands are allowed (the exact " + "allowlist is in flow-state.ts, isCoordinatorBashAllowed). " + COORDINATOR_RECOVERY_TEXT;
48071
+
48072
+ // packages/workit-core/src/core/handoff-tools.ts
48073
+ import path20 from "node:path";
48074
+
48075
+ // packages/workit-core/src/core/handoff-context.ts
48076
+ import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync8 } from "node:fs";
48077
+ import path19 from "node:path";
48078
+ var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
48079
+ var listMd = (dir) => {
48080
+ try {
48081
+ return readdirSync5(dir).filter((f) => f.endsWith(".md")).sort();
48082
+ } catch {
48083
+ return [];
48084
+ }
48085
+ };
48086
+ var extractMessagePaths = (message) => [...new Set(message.match(DOC_RE) ?? [])].sort();
48087
+ var resolveFromMessagePaths = (root, message) => {
48088
+ const paths = extractMessagePaths(message);
48089
+ if (paths.length === 0)
48090
+ return { error: "no paths" };
48091
+ const slugs = [...new Set(paths.map((p) => p.split("/")[1]))];
48092
+ if (slugs.length !== 1)
48093
+ return { error: "multiple features in message — use exactly one docs/<slug>/ pair" };
48094
+ const slug = slugs[0];
48095
+ const plan = `docs/${slug}/plan.md`;
48096
+ const spec = `docs/${slug}/spec.md`;
48097
+ if (!existsSync15(path19.join(root, plan)) || !existsSync15(path19.join(root, spec))) {
48098
+ return { error: `docs/${slug}/ must contain both plan.md and spec.md` };
48099
+ }
48100
+ return { spec, plan, source: "message_paths" };
48101
+ };
48102
+ var resolveActivePair = (root) => {
48103
+ const docsDir = path19.join(root, "docs");
48104
+ let best = null;
48105
+ let entries = [];
48106
+ try {
48107
+ entries = readdirSync5(docsDir);
48108
+ } catch {
48109
+ return { error: "no pair" };
48110
+ }
48111
+ for (const slug of entries) {
48112
+ if (slug.startsWith("."))
48113
+ continue;
48114
+ const plan = path19.join("docs", slug, "plan.md");
48115
+ const spec = path19.join("docs", slug, "spec.md");
48116
+ if (!existsSync15(path19.join(root, plan)) || !existsSync15(path19.join(root, spec)))
48117
+ continue;
48118
+ const score = Math.max(statSync8(path19.join(root, spec)).mtimeMs, statSync8(path19.join(root, plan)).mtimeMs);
48119
+ if (best === null || score > best.score || score === best.score && slug < best.spec.split("/")[1]) {
48120
+ best = { score, spec, plan, source: "active_pair" };
48121
+ }
48122
+ }
48123
+ if (best === null)
48124
+ return { error: "no pair" };
48125
+ return { spec: best.spec, plan: best.plan, source: best.source };
48126
+ };
48127
+ var resolveWorkflowPaths = (root, message) => {
48128
+ const fromMessage = resolveFromMessagePaths(root, message);
48129
+ if (!("error" in fromMessage))
48130
+ return fromMessage;
48131
+ if (fromMessage.error !== "no paths")
48132
+ return fromMessage;
48133
+ const active = resolveActivePair(root);
48134
+ if (!("error" in active))
48135
+ return active;
48136
+ const docsDir = path19.join(root, "docs");
48137
+ if (!existsSync15(docsDir) || listMd(docsDir).length === 0) {
48138
+ return { error: "no docs/<slug>/ features found under docs/" };
48139
+ }
48140
+ return {
48141
+ error: "could not resolve spec and plan — mention docs/<slug>/plan.md or create docs/<slug>/{spec.md,plan.md}"
48142
+ };
48143
+ };
48144
+ var buildHandoffContract = ({
48145
+ root,
48146
+ spec,
48147
+ plan,
48148
+ templatePath
48149
+ }) => {
48150
+ const validated = docsValidate({ spec_path: spec, plan_path: plan, workspace_root: root });
48151
+ if (validated.ok === false) {
48152
+ return {
48153
+ error: `docs validation failed
48154
+ ${JSON.stringify({ ok: false, errors: validated.errors })}`
48155
+ };
48156
+ }
48157
+ const branchResolved = resolveBranch({ spec_path: spec, plan_path: plan, workspace_root: root });
48158
+ if ("error" in branchResolved)
48159
+ return { error: branchResolved.error };
48160
+ const branch = branchResolved.branch;
48161
+ const slug = path19.basename(path19.dirname(plan));
48162
+ const sddDir = `docs/${slug}/sdd`;
48163
+ const planText = readFileSync14(path19.join(root, plan), "utf8");
48164
+ const tasks = parseTasksFromPlan(planText);
48165
+ const taskList = tasks.map((t) => `- Task ${t.id}: ${t.title}`).join(`
48166
+ `);
48167
+ let contract;
48168
+ try {
48169
+ contract = readFileSync14(templatePath, "utf8");
48170
+ } catch {
48171
+ return { error: "missing template templates/execution-contract.md" };
48172
+ }
48173
+ contract = contract.replace(/<SPEC_PATH>/g, spec).replace(/<PLAN_PATH>/g, plan).replace(/<BRANCH>/g, branch).replace(/<SLUG>/g, slug).replace(/<SDD_DIR>/g, sddDir).replace(/<TASK_LIST>/g, taskList);
48174
+ if (!/^<workflow-handoff-destination>true<\/workflow-handoff-destination>$/m.test(contract)) {
48175
+ return { error: "handoff destination contract missing its destination marker" };
48176
+ }
48177
+ return { prompt: contract };
48178
+ };
48179
+
48180
+ // packages/workit-core/src/core/handoff-tools.ts
48181
+ var buildHandoffPrompt = (root, message) => {
48182
+ const resolved = resolveWorkflowPaths(root, message);
48183
+ if ("error" in resolved)
48184
+ return { error: resolved.error };
48185
+ const templatePath = path20.join(assetRoot(), "templates", "execution-contract.md");
48186
+ const contract = buildHandoffContract({
48187
+ root,
48188
+ spec: resolved.spec,
48189
+ plan: resolved.plan,
48190
+ templatePath
48191
+ });
48192
+ if ("error" in contract)
48193
+ return { error: contract.error };
48194
+ const sdd = `docs/${path20.basename(path20.dirname(resolved.plan))}/sdd`;
48195
+ return { prompt: contract.prompt, spec: resolved.spec, plan: resolved.plan, sdd };
48196
+ };
48197
+
48198
+ // packages/workit-cli/src/flow.ts
48199
+ var FLOW_ACTIONS = ["status", "pause", "resume", "complete"];
48200
+ var COMMANDS = {
48201
+ status: "workit flow status --plan <path>",
48202
+ pause: "workit flow pause --plan <path> [--confirm]",
48203
+ resume: "workit flow resume --plan <path> [--confirm]",
48204
+ complete: "workit flow complete --plan <path> [--confirm]",
48205
+ handoff: "workit handoff --message <text>"
48206
+ };
48207
+ var FLOW_COMMANDS = {
48208
+ status: COMMANDS.status,
48209
+ pause: COMMANDS.pause,
48210
+ resume: COMMANDS.resume,
48211
+ complete: COMMANDS.complete
48212
+ };
48213
+ var CLI_FLAG_EVIDENCE = { host: "cli", attested: false, confirmation: "flag" };
48214
+ var CLI_TTY_EVIDENCE = { host: "cli", attested: false, confirmation: "tty" };
48215
+ var defaultIsTTY = () => process.stdin.isTTY === true;
48216
+ var defaultConfirm = async (out) => {
48217
+ const rl = createInterface({
48218
+ input: process.stdin,
48219
+ output: out ?? process.stdout
48220
+ });
48221
+ try {
48222
+ const answer = await rl.question("Proceed? [y/N] ");
48223
+ return /^y(es)?$/i.test(answer.trim());
48224
+ } finally {
48225
+ rl.close();
48226
+ }
48227
+ };
48228
+ var outStream = (deps) => deps.out ?? process.stdout;
48229
+ var errStream = (deps) => deps.err ?? process.stderr;
48230
+ var write = (stream, text) => stream.write(text.endsWith(`
48231
+ `) ? text : `${text}
48232
+ `);
48233
+ var writeJSON = (stream, value) => write(stream, JSON.stringify(value, null, 2));
48234
+ var isTTY = (deps) => deps.stdinIsTTY === undefined ? defaultIsTTY() : deps.stdinIsTTY();
48235
+ var workspaceRoot = (deps) => process.env.WORKFLOW_WORKSPACE_ROOT ?? deps.cwd ?? process.cwd();
48236
+ var domainFail = (err3, code, error, details) => {
48237
+ writeJSON(err3, { ok: false, error, code, ...details ? { details } : {} });
48238
+ return 1;
48239
+ };
48240
+ var usage = (err3, text) => {
48241
+ write(err3, text);
48242
+ return 2;
48243
+ };
48244
+ function parseFlowFlags(action, argv, err3) {
48245
+ let plan;
48246
+ let confirm = false;
48247
+ for (let i = 0;i < argv.length; i++) {
48248
+ const token = argv[i];
48249
+ if (token === "--plan") {
48250
+ if (plan !== undefined) {
48251
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — duplicate --plan flag`);
48252
+ return { ok: false };
48253
+ }
48254
+ const value = argv[i + 1];
48255
+ if (value === undefined || value.trim() === "") {
48256
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — --plan requires a non-empty path`);
48257
+ return { ok: false };
48258
+ }
48259
+ if (value.startsWith("--")) {
48260
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${value}`);
48261
+ return { ok: false };
48262
+ }
48263
+ plan = value;
48264
+ i += 1;
48265
+ } else if (token === "--confirm") {
48266
+ if (action === "status") {
48267
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — status is read-only and accepts no --confirm`);
48268
+ return { ok: false };
48269
+ }
48270
+ if (confirm) {
48271
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — duplicate --confirm flag`);
48272
+ return { ok: false };
48273
+ }
48274
+ confirm = true;
48275
+ } else {
48276
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${token}`);
48277
+ return { ok: false };
48278
+ }
48279
+ }
48280
+ if (plan === undefined) {
48281
+ usage(err3, `usage: ${FLOW_COMMANDS[action]} — --plan <path> required`);
48282
+ return { ok: false };
48283
+ }
48284
+ return { ok: true, parsed: { plan, confirm } };
48285
+ }
48286
+ async function resolveConfirmation(deps, confirmFlag, out, err3) {
48287
+ if (confirmFlag)
48288
+ return CLI_FLAG_EVIDENCE;
48289
+ if (!isTTY(deps)) {
48290
+ usage(err3, "--confirm required when stdin is not a TTY");
48291
+ return null;
48292
+ }
48293
+ const answered = deps.confirm ? await deps.confirm() : await defaultConfirm(out);
48294
+ if (!answered) {
48295
+ usage(err3, "cancelled — no confirmation");
48296
+ return null;
48297
+ }
48298
+ return CLI_TTY_EVIDENCE;
48299
+ }
48300
+ function statusCommand(root, plan, out, err3) {
48301
+ const resolved = resolveCanonicalLayout({ workspace_root: root, plan_path: plan });
48302
+ if (!resolved.ok)
48303
+ return domainFail(err3, "path_invalid", resolved.error);
48304
+ const effective = readEffectiveFlowState(root, resolved.layout.slug);
48305
+ if (!effective.ok)
48306
+ return domainFail(err3, effective.code, effective.error, effective.details);
48307
+ const { state, drift } = effective;
48308
+ writeJSON(out, {
48309
+ ok: true,
48310
+ slug: state.slug,
48311
+ spec: state.spec,
48312
+ plan: state.plan,
48313
+ menu: state.menu,
48314
+ execution: state.execution,
48315
+ handoff_destination: state.handoff_destination,
48316
+ drift,
48317
+ flow_path: path21.posix.join("docs", state.slug, "sdd", "flow.json")
48318
+ });
48319
+ return 0;
48320
+ }
48321
+ function mutateCommand(root, plan, action, evidence, deps, out, err3) {
48322
+ const resolved = resolveCanonicalLayout({ workspace_root: root, plan_path: plan });
48323
+ if (!resolved.ok)
48324
+ return domainFail(err3, "path_invalid", resolved.error);
48325
+ const result2 = transitionExecution(root, resolved.layout.slug, plan, action, evidence, undefined, deps.verifyProject ? { verifyProject: deps.verifyProject } : undefined);
48326
+ if (!result2.ok)
48327
+ return domainFail(err3, result2.code, result2.error, result2.details);
48328
+ const effective = readEffectiveFlowState(root, resolved.layout.slug);
48329
+ if (!effective.ok)
48330
+ return domainFail(err3, effective.code, effective.error, effective.details);
48331
+ writeJSON(out, {
48332
+ ok: true,
48333
+ plan,
48334
+ execution: effective.state.execution,
48335
+ drift: effective.drift
48336
+ });
48337
+ return 0;
48338
+ }
48339
+ async function runFlowCommand(argv, deps = {}) {
48340
+ const out = outStream(deps);
48341
+ const err3 = errStream(deps);
48342
+ const [action, ...rest2] = argv;
48343
+ if (!action || !FLOW_ACTIONS.includes(action)) {
48344
+ return usage(err3, "usage: workit flow <status|pause|resume|complete> --plan <path> [--confirm]");
48345
+ }
48346
+ const flowAction = action;
48347
+ const parsed = parseFlowFlags(flowAction, rest2, err3);
48348
+ if (!parsed.ok)
48349
+ return 2;
48350
+ const root = workspaceRoot(deps);
48351
+ if (flowAction === "status") {
48352
+ return statusCommand(root, parsed.parsed.plan, out, err3);
48353
+ }
48354
+ const evidence = await resolveConfirmation(deps, parsed.parsed.confirm, out, err3);
48355
+ if (evidence === null)
48356
+ return 2;
48357
+ return mutateCommand(root, parsed.parsed.plan, flowAction, evidence, deps, out, err3);
48358
+ }
48359
+ async function runHandoffCommand(argv, deps = {}) {
48360
+ const out = outStream(deps);
48361
+ const err3 = errStream(deps);
48362
+ const [flag, value, ...rest2] = argv;
48363
+ if (flag !== "--message" || value === undefined || value.trim() === "" || rest2.length > 0) {
48364
+ return usage(err3, `usage: ${COMMANDS.handoff}`);
48365
+ }
48366
+ const root = workspaceRoot(deps);
48367
+ const built = buildHandoffPrompt(root, value.trim());
48368
+ if ("error" in built) {
48369
+ writeJSON(err3, { ok: false, error: built.error, code: "handoff_build_failed" });
48370
+ return 1;
48371
+ }
48372
+ const marked = markHandoffDestination(root, slugFromPath(built.plan), built.plan);
48373
+ if (!marked.ok)
48374
+ return domainFail(err3, marked.code, marked.error, marked.details);
48375
+ write(out, built.prompt);
48376
+ return 0;
48377
+ }
48378
+
48379
+ // packages/workit-cli/src/index.tsx
48380
+ var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
48381
+ var logger = createLogger({
48382
+ stderr: (event) => {
48383
+ if (event.level === "debug" || event.level === "info")
48384
+ return;
48385
+ process.stderr.write(`${JSON.stringify(event)}
48386
+ `);
48387
+ }
48388
+ });
48389
+ var COMMAND_DESCRIPTIONS = [
48390
+ [COMMANDS.status, "Read the effective flow state for a plan"],
48391
+ [COMMANDS.pause, "Pause an active plan"],
48392
+ [COMMANDS.resume, "Resume a paused plan"],
48393
+ [COMMANDS.complete, "Complete a plan (ledger and verification gated)"],
48394
+ [COMMANDS.handoff, "Emit the destination handoff prompt for a plan"]
48395
+ ];
46195
48396
  var HELP = `workit — workflow rails for agentic coding
46196
48397
 
46197
48398
  Usage:
46198
48399
  workit init Run the interactive setup wizard
46199
48400
  workit doctor Verify the offline installation health (add --json for a machine-readable report)
48401
+ ${COMMAND_DESCRIPTIONS.map(([cmd, desc]) => ` ${cmd.padEnd(49)}${desc}`).join(`
48402
+ `)}
46200
48403
  workit Show this help
46201
48404
 
46202
48405
  Run \`npx workit init\` to configure platforms, YouTrack, VCS and project hygiene.
@@ -46292,14 +48495,18 @@ if (__require.main == __require.module) {
46292
48495
  setDiagnosticLogger(logger);
46293
48496
  logger.info(EVENT.initialization, { host: "cli", command: subcommand });
46294
48497
  process.on("unhandledRejection", (reason) => logger.error(EVENT.uncaughtFailure, { phase: "unhandledRejection", ...errorDetail(reason) }));
46295
- process.on("uncaughtException", (err) => {
46296
- logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(err) });
48498
+ process.on("uncaughtException", (err3) => {
48499
+ logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(err3) });
46297
48500
  process.exit(1);
46298
48501
  });
46299
48502
  if (subcommand === "init") {
46300
48503
  await runInit();
46301
48504
  } else if (subcommand === "doctor") {
46302
48505
  runDoctorCommand(args);
48506
+ } else if (subcommand === "flow") {
48507
+ process.exit(await runFlowCommand(args.slice(1)));
48508
+ } else if (subcommand === "handoff") {
48509
+ process.exit(await runHandoffCommand(args.slice(1)));
46303
48510
  } else {
46304
48511
  console.log(HELP);
46305
48512
  process.exit(0);