@brainervirus/workit-cli 0.8.0 → 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);
@@ -42954,15 +43026,17 @@ function mergeCursorMcp(mcp, serverName, server) {
42954
43026
  base2.mcpServers = servers;
42955
43027
  return { config: base2, changed };
42956
43028
  }
43029
+ var CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@0.8.0";
42957
43030
  function cursorMcpServerEntry(_packageDir) {
42958
43031
  return {
42959
43032
  command: "npx",
42960
- args: [
42961
- "-y",
42962
- "--package=@brainervirus/workit-cursor@latest",
42963
- "workit-cursor-mcp",
42964
- "${workspaceFolder}"
42965
- ]
43033
+ args: ["-y", `--package=${CURSOR_RUNTIME_PACKAGE}`, "workit-cursor-mcp", "${workspaceFolder}"]
43034
+ };
43035
+ }
43036
+ function cursorHooksEntry(_packageDir) {
43037
+ return {
43038
+ command: `npx -y --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
43039
+ args: []
42966
43040
  };
42967
43041
  }
42968
43042
 
@@ -42970,7 +43044,7 @@ function cursorMcpServerEntry(_packageDir) {
42970
43044
  import { spawnSync as spawnSync2 } from "node:child_process";
42971
43045
  import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
42972
43046
  import os4 from "node:os";
42973
- import path8 from "node:path";
43047
+ import path9 from "node:path";
42974
43048
 
42975
43049
  // packages/workit-core/src/core/support-matrix.ts
42976
43050
  var SUPPORT_MATRIX = {
@@ -42982,7 +43056,7 @@ var SUPPORT_MATRIX = {
42982
43056
 
42983
43057
  // packages/workit-core/src/core/skill-manifests.ts
42984
43058
  import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync } from "node:fs";
42985
- import path7 from "node:path";
43059
+ import path8 from "node:path";
42986
43060
  var CANONICAL_SKILLS = {
42987
43061
  superpowers: [
42988
43062
  "brainstorming",
@@ -43015,7 +43089,7 @@ var CANONICAL_SKILLS = {
43015
43089
  "wk-verify"
43016
43090
  ]
43017
43091
  };
43018
- 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() : [];
43019
43093
  var validateSkillManifests = (root, expected, label) => {
43020
43094
  const actual = skillManifestNames(root);
43021
43095
  const missing = expected.filter((name) => !actual.includes(name));
@@ -43023,10 +43097,10 @@ var validateSkillManifests = (root, expected, label) => {
43023
43097
  return missing.length === 0 && extra.length === 0 ? null : `${label} mismatch at ${root} (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`;
43024
43098
  };
43025
43099
  var validateCursorSkills = (pluginDir) => {
43026
- 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");
43027
43101
  if (workit)
43028
43102
  return workit;
43029
- const vendor = path7.join(pluginDir, "vendor/superpowers/skills");
43103
+ const vendor = path8.join(pluginDir, "vendor/superpowers/skills");
43030
43104
  const superpowers = validateSkillManifests(vendor, CANONICAL_SKILLS.superpowers, "Cursor Superpowers skills");
43031
43105
  if (superpowers)
43032
43106
  return superpowers;
@@ -43034,7 +43108,7 @@ var validateCursorSkills = (pluginDir) => {
43034
43108
  while (pending.length > 0) {
43035
43109
  const dir = pending.pop();
43036
43110
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
43037
- const file = path7.join(dir, entry.name);
43111
+ const file = path8.join(dir, entry.name);
43038
43112
  if (entry.isDirectory())
43039
43113
  pending.push(file);
43040
43114
  else if ((statSync(file).mode & 73) !== 0 || readFileSync6(file).subarray(0, 2).toString("latin1") === "#!") {
@@ -43049,11 +43123,11 @@ if (false) {}
43049
43123
  // packages/workit-core/src/core/doctor.ts
43050
43124
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
43051
43125
  var findDevFromCwd = (cwd2) => {
43052
- let dir = path8.resolve(cwd2);
43126
+ let dir = path9.resolve(cwd2);
43053
43127
  while (true) {
43054
- if (existsSync7(path8.join(dir, "packages", "workit-core", "package.json")))
43128
+ if (existsSync7(path9.join(dir, "packages", "workit-core", "package.json")))
43055
43129
  return dir;
43056
- const parent = path8.dirname(dir);
43130
+ const parent = path9.dirname(dir);
43057
43131
  if (parent === dir)
43058
43132
  return null;
43059
43133
  dir = parent;
@@ -43062,8 +43136,8 @@ var findDevFromCwd = (cwd2) => {
43062
43136
  var resolve = (options) => {
43063
43137
  const env3 = options.env ?? process.env;
43064
43138
  const home = options.home ?? env3.HOME ?? os4.homedir();
43065
- const configDir2 = options.configDir ?? env3.WORKFLOW_TOOLKIT_CONFIG ?? env3.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path8.join(home, ".config", "workit");
43066
- 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");
43067
43141
  const cwd2 = options.cwd ?? process.cwd();
43068
43142
  const dev = options.dev ?? env3.WORKFLOW_TOOLKIT_DEV ?? findDevFromCwd(cwd2);
43069
43143
  return {
@@ -43073,10 +43147,10 @@ var resolve = (options) => {
43073
43147
  stateDir,
43074
43148
  cwd: cwd2,
43075
43149
  dev,
43076
- opencodeConfig: options.opencodeConfig ?? path8.join(home, ".config", "opencode", "opencode.json"),
43077
- cursorSettings: options.cursorSettings ?? path8.join(home, ".cursor", "settings.json"),
43078
- cursorMcp: options.cursorMcp ?? path8.join(home, ".cursor", "mcp.json"),
43079
- 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"),
43080
43154
  env: env3,
43081
43155
  installer: options.installer ?? false
43082
43156
  };
@@ -43102,13 +43176,13 @@ var pluginEntries = (cfg) => {
43102
43176
  return list.map(String).filter(isWorkitPlugin);
43103
43177
  };
43104
43178
  var commandOnPath = (name, env3) => {
43105
- const dirs = (env3.PATH ?? process.env.PATH ?? "").split(path8.delimiter);
43179
+ const dirs = (env3.PATH ?? process.env.PATH ?? "").split(path9.delimiter);
43106
43180
  const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
43107
43181
  for (const dir of dirs) {
43108
43182
  if (!dir)
43109
43183
  continue;
43110
43184
  for (const candidateName of names) {
43111
- const candidate = path8.join(dir, candidateName);
43185
+ const candidate = path9.join(dir, candidateName);
43112
43186
  try {
43113
43187
  const st = statSync2(candidate);
43114
43188
  if (process.platform !== "win32" && (st.mode & 73) === 0)
@@ -43140,7 +43214,7 @@ var resolveBun = (env3) => {
43140
43214
  if (env3.BUN && existsSync7(env3.BUN))
43141
43215
  return env3.BUN;
43142
43216
  for (const candidate of [
43143
- path8.join(os4.homedir(), ".bun/bin/bun"),
43217
+ path9.join(os4.homedir(), ".bun/bin/bun"),
43144
43218
  "/usr/local/bin/bun",
43145
43219
  "/usr/bin/bun"
43146
43220
  ]) {
@@ -43194,7 +43268,7 @@ var checkVersions = (res) => {
43194
43268
  detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping version parity"
43195
43269
  };
43196
43270
  }
43197
- 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"));
43198
43272
  if (!corePkg) {
43199
43273
  return {
43200
43274
  id: "versions",
@@ -43204,7 +43278,7 @@ var checkVersions = (res) => {
43204
43278
  }
43205
43279
  const refs = new Set;
43206
43280
  for (const name of ["workit-opencode", "workit-cursor", "workit-cli"]) {
43207
- const pkg = readJson(path8.join(res.dev, "packages", name, "package.json"));
43281
+ const pkg = readJson(path9.join(res.dev, "packages", name, "package.json"));
43208
43282
  const dep = pkg?.dependencies?.["@brainervirus/workit-core"];
43209
43283
  if (typeof dep === "string")
43210
43284
  refs.add(dep);
@@ -43220,7 +43294,7 @@ var checkVersions = (res) => {
43220
43294
  if (refs.size > 1) {
43221
43295
  problems.push(`adapters pin different core versions: ${[...refs].join(", ")}`);
43222
43296
  }
43223
- 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"));
43224
43298
  const sdk = opencodePkg?.dependencies?.["@opencode-ai/plugin"];
43225
43299
  const sdkVersion = typeof sdk === "string" ? (sdk.match(/^\d+(?:\.\d+){0,2}/) ?? [])[0] : null;
43226
43300
  if (sdkVersion && !semverAtLeast(sdkVersion, SUPPORT_MATRIX.opencode.minimum)) {
@@ -43241,23 +43315,23 @@ var checkVersions = (res) => {
43241
43315
  };
43242
43316
  };
43243
43317
  var assetPathsFor = (host, dev) => {
43244
- const pkg = path8.join(dev, "packages", `workit-${host}`);
43318
+ const pkg = path9.join(dev, "packages", `workit-${host}`);
43245
43319
  switch (host) {
43246
43320
  case "opencode":
43247
43321
  return [
43248
- path8.join(pkg, "assets", "commands", "wk-init.md"),
43249
- path8.join(pkg, "assets", "skills", "wk-init", "SKILL.md"),
43250
- path8.join(pkg, "assets", "templates", "spec-template.md"),
43251
- 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")
43252
43326
  ];
43253
43327
  case "cursor":
43254
43328
  return [
43255
- path8.join(pkg, "assets", "templates", "spec-template.md"),
43256
- path8.join(pkg, "mcp.json"),
43257
- 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")
43258
43332
  ];
43259
43333
  case "cli":
43260
- return [path8.join(pkg, "assets", "templates", "spec-template.md")];
43334
+ return [path9.join(pkg, "assets", "templates", "spec-template.md")];
43261
43335
  }
43262
43336
  };
43263
43337
  var hostsFor = (host) => host === "cli" ? ["opencode", "cursor", "cli"] : [host];
@@ -43291,17 +43365,17 @@ var checkAssets = (res) => {
43291
43365
  };
43292
43366
  };
43293
43367
  var launcherSlotsFor = (host, dev) => {
43294
- const pkg = path8.join(dev, "packages", `workit-${host}`);
43368
+ const pkg = path9.join(dev, "packages", `workit-${host}`);
43295
43369
  switch (host) {
43296
43370
  case "opencode":
43297
- 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")]];
43298
43372
  case "cursor":
43299
43373
  return [
43300
- [path8.join(pkg, "dist", "mcp-server.js")],
43301
- [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")]
43302
43376
  ];
43303
43377
  case "cli":
43304
- 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")]];
43305
43379
  }
43306
43380
  };
43307
43381
  var validNodeEntry = (entry, runtime, env3) => {
@@ -43330,11 +43404,11 @@ var registeredCursorLauncher = (res) => {
43330
43404
  if (typeof command !== "string" || !Array.isArray(args) || typeof args[0] !== "string") {
43331
43405
  return "invalid";
43332
43406
  }
43333
- const executable = path8.basename(command).toLowerCase();
43407
+ const executable = path9.basename(command).toLowerCase();
43334
43408
  if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
43335
43409
  if (args[0] !== "-y")
43336
43410
  return "invalid";
43337
- if (args[1] !== "--package=@brainervirus/workit-cursor@latest")
43411
+ if (args[1] !== `--package=${CURSOR_RUNTIME_PACKAGE}`)
43338
43412
  return "invalid";
43339
43413
  if (args[2] !== "workit-cursor-mcp")
43340
43414
  return "invalid";
@@ -43345,21 +43419,43 @@ var registeredCursorLauncher = (res) => {
43345
43419
  return {
43346
43420
  kind: "node",
43347
43421
  runtime: command,
43348
- 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])
43349
43423
  };
43350
43424
  };
43425
+ var canonicalCursorHook = cursorHooksEntry("").command;
43426
+ var registeredCursorHook = (res) => {
43427
+ const hooksFile = path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
43428
+ if (!existsSync7(hooksFile))
43429
+ return "invalid";
43430
+ const config = readJson(hooksFile);
43431
+ if (!config)
43432
+ return "invalid";
43433
+ const sessionStart = config.hooks?.sessionStart;
43434
+ if (!Array.isArray(sessionStart) || sessionStart.length !== 1)
43435
+ return "invalid";
43436
+ const entry = sessionStart[0];
43437
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
43438
+ return "invalid";
43439
+ return typeof entry.command === "string" ? entry.command : "invalid";
43440
+ };
43351
43441
  var checkLauncher = (res) => {
43352
43442
  const dev = res.dev;
43353
43443
  const hosts = hostsFor(res.host);
43354
43444
  const registered = hosts.includes("cursor") ? registeredCursorLauncher(res) : null;
43355
43445
  const runtime = registered && registered !== "invalid" && registered.kind === "node" ? registered.runtime : "node";
43356
- 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}`) : [];
43357
43447
  if (hosts.includes("cursor")) {
43358
43448
  if (registered === "invalid") {
43359
43449
  missing.push(`cursor: canonical workit MCP launcher in ${res.cursorMcp}`);
43360
43450
  } else if (registered && registered.kind === "node" && !validNodeEntry(registered.entry, registered.runtime, res.env)) {
43361
43451
  missing.push(`cursor: registered ${registered.entry}`);
43362
43452
  }
43453
+ const hook = registeredCursorHook(res);
43454
+ if (hook === "invalid") {
43455
+ missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
43456
+ } else if (hook !== null && hook !== canonicalCursorHook) {
43457
+ missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
43458
+ }
43363
43459
  }
43364
43460
  if (dev) {
43365
43461
  missing.push(...hosts.filter((h) => h !== "cursor").flatMap((h) => launcherSlotsFor(h, dev).filter((slot) => !slot.some((p) => existsSync7(p))).map((slot) => `${h}: ${slot.join(" or ")}`)));
@@ -43499,7 +43595,7 @@ var checkDuplicateRegistration = (res) => {
43499
43595
  var checkMalformedConfig = (res) => {
43500
43596
  const files = [];
43501
43597
  for (const name of ["config.json", "youtrack.json", "vcs.json", "workspaces.json"]) {
43502
- const p = path8.join(res.configDir, name);
43598
+ const p = path9.join(res.configDir, name);
43503
43599
  if (existsSync7(p))
43504
43600
  files.push(p);
43505
43601
  }
@@ -43522,7 +43618,7 @@ var checkMalformedConfig = (res) => {
43522
43618
  };
43523
43619
  };
43524
43620
  var checkWorkspaceMismatch = (res) => {
43525
- const file = path8.join(res.configDir, "workspaces.json");
43621
+ const file = path9.join(res.configDir, "workspaces.json");
43526
43622
  if (!existsSync7(file))
43527
43623
  return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
43528
43624
  const ws = readJson(file);
@@ -43552,20 +43648,20 @@ var isPlaceholder = (p) => {
43552
43648
  };
43553
43649
  var checkCredentialMetadata = (res) => {
43554
43650
  const tokenPaths = [];
43555
- const youtrackJson = readJson(path8.join(res.configDir, "youtrack.json"));
43651
+ const youtrackJson = readJson(path9.join(res.configDir, "youtrack.json"));
43556
43652
  if (youtrackJson) {
43557
- tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile : path8.join(res.configDir, "youtrack.token"));
43558
- } else if (existsSync7(path8.join(res.configDir, "youtrack.token"))) {
43559
- 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"));
43560
43656
  }
43561
- const vcsJson = readJson(path8.join(res.configDir, "vcs.json"));
43657
+ const vcsJson = readJson(path9.join(res.configDir, "vcs.json"));
43562
43658
  for (const key of ["gitlab", "github"]) {
43563
43659
  const provider = vcsJson?.[key];
43564
43660
  if (provider && typeof provider === "object") {
43565
43661
  const tf = provider.tokenFile;
43566
- tokenPaths.push(typeof tf === "string" ? tf : path8.join(res.configDir, `${key}.token`));
43567
- } else if (!vcsJson && existsSync7(path8.join(res.configDir, `${key}.token`))) {
43568
- 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`));
43569
43665
  }
43570
43666
  }
43571
43667
  if (tokenPaths.length === 0) {
@@ -43573,7 +43669,7 @@ var checkCredentialMetadata = (res) => {
43573
43669
  }
43574
43670
  const problems = [];
43575
43671
  for (const raw of tokenPaths) {
43576
- const p = path8.isAbsolute(raw) ? raw : path8.resolve(res.configDir, raw);
43672
+ const p = path9.isAbsolute(raw) ? raw : path9.resolve(res.configDir, raw);
43577
43673
  if (!existsSync7(p)) {
43578
43674
  problems.push(`${p} is missing`);
43579
43675
  continue;
@@ -43601,8 +43697,8 @@ var checkCredentialMetadata = (res) => {
43601
43697
  };
43602
43698
  };
43603
43699
  var checkLogWritable = (res) => {
43604
- const logsDir = path8.join(res.stateDir, "logs");
43605
- 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");
43606
43702
  try {
43607
43703
  mkdirSync2(logsDir, { recursive: true, mode: 448 });
43608
43704
  writeFileSync4(probe, `{"probe":true}
@@ -43790,7 +43886,7 @@ function buildSetupPreview(values2, opts = {}) {
43790
43886
  const current = readConfigFromDir(state.configDir);
43791
43887
  mutations.push({
43792
43888
  type: "merge-json",
43793
- path: path9.join(state.configDir, "config.json"),
43889
+ path: path10.join(state.configDir, "config.json"),
43794
43890
  value: {
43795
43891
  locale: values2.locale,
43796
43892
  localeOptions: current.localeOptions,
@@ -43815,16 +43911,16 @@ function buildSetupPreview(values2, opts = {}) {
43815
43911
  if (blocked.length === 0) {
43816
43912
  mutations.push({
43817
43913
  type: "update-workspaces",
43818
- path: path9.join(state.configDir, "workspaces.json"),
43914
+ path: path10.join(state.configDir, "workspaces.json"),
43819
43915
  entries: values2.workspaces
43820
43916
  });
43821
43917
  }
43822
43918
  }
43823
43919
  if (values2.baseUrl.trim()) {
43824
- const ytPath = path9.join(state.configDir, "youtrack.json");
43920
+ const ytPath = path10.join(state.configDir, "youtrack.json");
43825
43921
  const ytExisting = readConfigRecord(ytPath);
43826
43922
  const ytConfigured = configuredTokenPath(ytExisting, "tokenFile");
43827
- 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);
43828
43924
  const draft = youtrackDraft(values2, tokenPath);
43829
43925
  if (ytConfigured !== null && tokenPath !== ytConfigured) {
43830
43926
  delete draft.tokenFile;
@@ -43852,12 +43948,12 @@ function buildSetupPreview(values2, opts = {}) {
43852
43948
  });
43853
43949
  }
43854
43950
  if (values2.vcsProvider !== "skip") {
43855
- const vcsPath = path9.join(state.configDir, "vcs.json");
43951
+ const vcsPath = path10.join(state.configDir, "vcs.json");
43856
43952
  const vcsExisting = readConfigRecord(vcsPath);
43857
43953
  const glConfigured = configuredTokenPath(vcsExisting, "gitlab", "tokenFile");
43858
43954
  const ghConfigured = configuredTokenPath(vcsExisting, "github", "tokenFile");
43859
- const gitlabToken = resolveTokenPath(glConfigured, path9.join(state.configDir, "gitlab.token"), values2.tokenPaths?.gitlab);
43860
- 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);
43861
43957
  const draft = vcsDraft(values2.vcsProvider, gitlabToken, githubToken);
43862
43958
  for (const [configured, token, key] of [
43863
43959
  [glConfigured, gitlabToken, "gitlab.tokenFile"],
@@ -43904,8 +44000,8 @@ function buildSetupPreview(values2, opts = {}) {
43904
44000
  }
43905
44001
  }
43906
44002
  if (values2.applyProject) {
43907
- const root = path9.resolve(opts.cwd ?? process.cwd());
43908
- const gitignorePath = path9.join(root, ".gitignore");
44003
+ const root = path10.resolve(opts.cwd ?? process.cwd());
44004
+ const gitignorePath = path10.join(root, ".gitignore");
43909
44005
  const existing = existsSync8(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
43910
44006
  const existingLines = new Set(existing.split(`
43911
44007
  `).map((l) => l.trim()).filter(Boolean));
@@ -43934,10 +44030,10 @@ var resolveSetupPaths = (options, configDir2) => {
43934
44030
  configDir: configDir2,
43935
44031
  cwd: options.cwd ?? process.cwd(),
43936
44032
  env: env3,
43937
- opencodeConfig: options.opencodeConfig ?? path9.join(home, ".config", "opencode", "opencode.json"),
43938
- cursorSettings: options.cursorSettings ?? path9.join(home, ".cursor", "settings.json"),
43939
- cursorMcp: options.cursorMcp ?? path9.join(home, ".cursor", "mcp.json"),
43940
- 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")
43941
44037
  };
43942
44038
  };
43943
44039
  function resolveApply(preview, options) {
@@ -43983,7 +44079,7 @@ var readFileSafe = (p) => {
43983
44079
  }
43984
44080
  };
43985
44081
  function applyMutation(m) {
43986
- const dir = path9.dirname(m.path);
44082
+ const dir = path10.dirname(m.path);
43987
44083
  switch (m.type) {
43988
44084
  case "create-file": {
43989
44085
  mkdirSync3(dir, { recursive: true });
@@ -44087,7 +44183,7 @@ function applyMutation(m) {
44087
44183
  detail: "already configured"
44088
44184
  };
44089
44185
  }
44090
- mkdirSync3(path9.dirname(m.path), { recursive: true });
44186
+ mkdirSync3(path10.dirname(m.path), { recursive: true });
44091
44187
  writeFileSync5(m.path, JSON.stringify(merged, null, 2) + `
44092
44188
  `, "utf8");
44093
44189
  return existing.kind === "record" ? { platform: "core", file: m.path, status: "Configured" } : { platform: "core", file: m.path, status: "Installed" };
@@ -44096,7 +44192,7 @@ function applyMutation(m) {
44096
44192
  }
44097
44193
  var isAdapter = (root, platform2) => {
44098
44194
  try {
44099
- const pkg = JSON.parse(readFileSync8(path9.join(root, "package.json"), "utf8"));
44195
+ const pkg = JSON.parse(readFileSync8(path10.join(root, "package.json"), "utf8"));
44100
44196
  return pkg.name === `@brainervirus/workit-${platform2}`;
44101
44197
  } catch {
44102
44198
  return false;
@@ -44105,19 +44201,19 @@ var isAdapter = (root, platform2) => {
44105
44201
  function adapterRoot(platform2, res) {
44106
44202
  const candidates = [];
44107
44203
  if (res.dev) {
44108
- candidates.push(path9.join(res.dev, "packages", `workit-${platform2}`));
44204
+ candidates.push(path10.join(res.dev, "packages", `workit-${platform2}`));
44109
44205
  } else {
44110
- candidates.push(path9.join(packageRoot(), "..", `workit-${platform2}`));
44111
- let dir = path9.resolve(res.cwd);
44206
+ candidates.push(path10.join(packageRoot(), "..", `workit-${platform2}`));
44207
+ let dir = path10.resolve(res.cwd);
44112
44208
  while (true) {
44113
- candidates.push(path9.join(dir, "packages", `workit-${platform2}`));
44114
- const parent = path9.dirname(dir);
44209
+ candidates.push(path10.join(dir, "packages", `workit-${platform2}`));
44210
+ const parent = path10.dirname(dir);
44115
44211
  if (parent === dir)
44116
44212
  break;
44117
44213
  dir = parent;
44118
44214
  }
44119
44215
  }
44120
- 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}`));
44121
44217
  for (const candidate of candidates) {
44122
44218
  if (isAdapter(candidate, platform2))
44123
44219
  return candidate;
@@ -44126,7 +44222,7 @@ function adapterRoot(platform2, res) {
44126
44222
  }
44127
44223
  var opencodePin = (root) => {
44128
44224
  for (const rel of ["src/plugin.ts", "dist/plugin.js"]) {
44129
- const entry = path9.join(root, rel);
44225
+ const entry = path10.join(root, rel);
44130
44226
  if (existsSync8(entry))
44131
44227
  return `file://${entry}`;
44132
44228
  }
@@ -44142,7 +44238,7 @@ function applyOpenCode(root, res) {
44142
44238
  detail: "workit-opencode package has no loadable plugin entry (src/plugin.ts or dist/plugin.js)"
44143
44239
  };
44144
44240
  }
44145
- mkdirSync3(path9.dirname(res.opencodeConfig), { recursive: true });
44241
+ mkdirSync3(path10.dirname(res.opencodeConfig), { recursive: true });
44146
44242
  const existing = readExisting(res.opencodeConfig);
44147
44243
  if (existing.kind === "malformed") {
44148
44244
  return {
@@ -44180,19 +44276,19 @@ var samePluginContent = (src, dest, relative = "") => {
44180
44276
  for (const entry of readdirSync3(src, { withFileTypes: true })) {
44181
44277
  if (entry.name === "node_modules")
44182
44278
  continue;
44183
- const source = path9.join(src, entry.name);
44184
- const installed = path9.join(dest, entry.name);
44279
+ const source = path10.join(src, entry.name);
44280
+ const installed = path10.join(dest, entry.name);
44185
44281
  if (entry.isDirectory()) {
44186
- 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)))
44187
44283
  return false;
44188
44284
  } else if (!readFileSync8(source).equals(readFileSync8(installed))) {
44189
44285
  return false;
44190
44286
  }
44191
44287
  }
44192
44288
  for (const entry of readdirSync3(dest, { withFileTypes: true })) {
44193
- if (existsSync8(path9.join(src, entry.name)))
44289
+ if (existsSync8(path10.join(src, entry.name)))
44194
44290
  continue;
44195
- const rel = path9.join(relative, entry.name);
44291
+ const rel = path10.join(relative, entry.name);
44196
44292
  if (rel === ".workflow-toolkit-root")
44197
44293
  continue;
44198
44294
  if (relative === "rules" && entry.isFile() && entry.name.endsWith(".mdc"))
@@ -44206,41 +44302,41 @@ var samePluginContent = (src, dest, relative = "") => {
44206
44302
  };
44207
44303
  var preservedCursorRules = (src, dest) => {
44208
44304
  const preserved = new Map;
44209
- const sourceRules = path9.join(src, "rules");
44210
- const installedRules = path9.join(dest, "rules");
44305
+ const sourceRules = path10.join(src, "rules");
44306
+ const installedRules = path10.join(dest, "rules");
44211
44307
  try {
44212
44308
  for (const entry of readdirSync3(installedRules, { withFileTypes: true })) {
44213
44309
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
44214
44310
  continue;
44215
- if (existsSync8(path9.join(sourceRules, entry.name)))
44311
+ if (existsSync8(path10.join(sourceRules, entry.name)))
44216
44312
  continue;
44217
- preserved.set(entry.name, readFileSync8(path9.join(installedRules, entry.name)));
44313
+ preserved.set(entry.name, readFileSync8(path10.join(installedRules, entry.name)));
44218
44314
  }
44219
44315
  } catch {}
44220
44316
  return preserved;
44221
44317
  };
44222
44318
  function copyPluginDir(src, dest) {
44223
- const marker = path9.join(dest, ".workflow-toolkit-root");
44319
+ const marker = path10.join(dest, ".workflow-toolkit-root");
44224
44320
  const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest);
44225
44321
  if (synced)
44226
44322
  return "Skipped";
44227
44323
  const hadDir = existsSync8(dest);
44228
44324
  const rules = preservedCursorRules(src, dest);
44229
- const parent = path9.dirname(dest);
44325
+ const parent = path10.dirname(dest);
44230
44326
  mkdirSync3(parent, { recursive: true });
44231
- const swap = mkdtempSync(path9.join(parent, `.${path9.basename(dest)}.swap-`));
44232
- const stage = path9.join(swap, "stage");
44233
- 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");
44234
44330
  try {
44235
44331
  cpSync2(src, stage, {
44236
44332
  recursive: true,
44237
- 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")
44238
44334
  });
44239
44335
  for (const [name, content] of rules) {
44240
- mkdirSync3(path9.join(stage, "rules"), { recursive: true });
44241
- writeFileSync5(path9.join(stage, "rules", name), content);
44336
+ mkdirSync3(path10.join(stage, "rules"), { recursive: true });
44337
+ writeFileSync5(path10.join(stage, "rules", name), content);
44242
44338
  }
44243
- writeFileSync5(path9.join(stage, ".workflow-toolkit-root"), src + `
44339
+ writeFileSync5(path10.join(stage, ".workflow-toolkit-root"), src + `
44244
44340
  `, "utf8");
44245
44341
  if (!samePluginContent(src, stage))
44246
44342
  throw new Error("staged adapter content is incomplete");
@@ -44260,26 +44356,26 @@ function copyPluginDir(src, dest) {
44260
44356
  return hadDir ? "Configured" : "Installed";
44261
44357
  }
44262
44358
  var removeLegacyCursorDir = (res) => {
44263
- const legacy = path9.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
44359
+ const legacy = path10.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
44264
44360
  if (legacy === res.cursorPluginDir || !existsSync8(legacy))
44265
44361
  return;
44266
- const legacyRules = path9.join(legacy, "rules");
44362
+ const legacyRules = path10.join(legacy, "rules");
44267
44363
  if (existsSync8(legacyRules)) {
44268
44364
  try {
44269
- mkdirSync3(path9.join(res.cursorPluginDir, "rules"), { recursive: true });
44365
+ mkdirSync3(path10.join(res.cursorPluginDir, "rules"), { recursive: true });
44270
44366
  for (const entry of readdirSync3(legacyRules, { withFileTypes: true })) {
44271
44367
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
44272
44368
  continue;
44273
- const target = path9.join(res.cursorPluginDir, "rules", entry.name);
44369
+ const target = path10.join(res.cursorPluginDir, "rules", entry.name);
44274
44370
  if (!existsSync8(target))
44275
- copyFileSync2(path9.join(legacyRules, entry.name), target);
44371
+ copyFileSync2(path10.join(legacyRules, entry.name), target);
44276
44372
  }
44277
44373
  } catch {}
44278
44374
  }
44279
44375
  rmSync(legacy, { recursive: true, force: true });
44280
44376
  };
44281
44377
  function applyCursorSettings(root, res) {
44282
- mkdirSync3(path9.dirname(res.cursorSettings), { recursive: true });
44378
+ mkdirSync3(path10.dirname(res.cursorSettings), { recursive: true });
44283
44379
  const settingsExisting = readExisting(res.cursorSettings);
44284
44380
  if (settingsExisting.kind === "malformed") {
44285
44381
  return {
@@ -44307,7 +44403,7 @@ function applyCursorSettings(root, res) {
44307
44403
  };
44308
44404
  }
44309
44405
  function applyCursorMcp(root, res) {
44310
- mkdirSync3(path9.dirname(res.cursorMcp), { recursive: true });
44406
+ mkdirSync3(path10.dirname(res.cursorMcp), { recursive: true });
44311
44407
  const mcpExisting = readExisting(res.cursorMcp);
44312
44408
  if (mcpExisting.kind === "malformed") {
44313
44409
  return {
@@ -44496,12 +44592,12 @@ var setupCompletionGuidance = () => [
44496
44592
  ];
44497
44593
  function applyWorkspaceBranchPolicy(opts) {
44498
44594
  const { workspace_root, env: env3 = process.env } = opts;
44499
- const dir = path9.join(env3.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
44595
+ const dir = path10.join(env3.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
44500
44596
  const { status, path: wsPath, entries } = readWorkspacesResult(dir);
44501
44597
  if (status === "malformed")
44502
44598
  return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
44503
44599
  const detection = detectBranchPolicy(workspace_root);
44504
- const name = String(env3.WORKFLOW_BP_NAME ?? path9.basename(workspace_root));
44600
+ const name = String(env3.WORKFLOW_BP_NAME ?? path10.basename(workspace_root));
44505
44601
  const integration = env3.WORKFLOW_BP_INTEGRATION ?? detection.integration;
44506
44602
  const policy = {
44507
44603
  preset: detection.preset,
@@ -44526,7 +44622,7 @@ function applyWorkspaceBranchPolicy(opts) {
44526
44622
  };
44527
44623
  }
44528
44624
  const next = existing ? entries.map((w, i) => i === idx ? { ...w, branchPolicy: policy } : w) : [...entries, { name, glob, branchPolicy: policy }];
44529
- mkdirSync3(path9.dirname(wsPath), { recursive: true });
44625
+ mkdirSync3(path10.dirname(wsPath), { recursive: true });
44530
44626
  writeFileSync5(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
44531
44627
  `, "utf8");
44532
44628
  return {
@@ -44584,7 +44680,7 @@ function applyWizardBranchPolicy(branchPolicy, workspace_root, env3 = process.en
44584
44680
 
44585
44681
  // packages/workit-cli/src/wizard-state.ts
44586
44682
  import { existsSync as existsSync9 } from "node:fs";
44587
- import path10 from "node:path";
44683
+ import path11 from "node:path";
44588
44684
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
44589
44685
  var NEXT = {
44590
44686
  platforms: "locale",
@@ -44629,7 +44725,7 @@ var PREV = {
44629
44725
  exit: null
44630
44726
  };
44631
44727
  var skipsCustomBranch = (screen, preset) => (screen === "branchAllowed" || screen === "branchProtected") && preset !== "custom";
44632
- 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"));
44633
44729
  function nextScreen(screen, preset) {
44634
44730
  let next = NEXT[screen];
44635
44731
  while (next && (skipsCustomBranch(next, preset) || skipsBranchPolicy(next)))
@@ -45935,7 +46031,7 @@ function Screen({ draft, dispatch }) {
45935
46031
  // packages/workit-core/src/core/logger.ts
45936
46032
  import { appendFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
45937
46033
  import os6 from "node:os";
45938
- import path11 from "node:path";
46034
+ import path12 from "node:path";
45939
46035
  var REDACTED = "[REDACTED]";
45940
46036
  var DEFAULT_MAX_RATE = 20;
45941
46037
  var DEFAULT_RATE_WINDOW_MS = 1000;
@@ -45991,14 +46087,14 @@ var resolveStateDir = () => {
45991
46087
  return override;
45992
46088
  const home = os6.homedir();
45993
46089
  if (process.env.XDG_STATE_HOME)
45994
- return path11.join(process.env.XDG_STATE_HOME, "workit");
46090
+ return path12.join(process.env.XDG_STATE_HOME, "workit");
45995
46091
  switch (os6.platform()) {
45996
46092
  case "darwin":
45997
- return path11.join(home, "Library", "Application Support", "workit");
46093
+ return path12.join(home, "Library", "Application Support", "workit");
45998
46094
  case "win32":
45999
- 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");
46000
46096
  default:
46001
- return path11.join(process.env.HOME ?? home, ".local", "state", "workit");
46097
+ return path12.join(process.env.HOME ?? home, ".local", "state", "workit");
46002
46098
  }
46003
46099
  };
46004
46100
  var homePattern = new RegExp(`${escapeRegExp2(os6.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
@@ -46071,14 +46167,14 @@ var pruneOldFiles = (dir) => {
46071
46167
  const files = readdirSync4(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
46072
46168
  for (const file of files.slice(RETAINED_DAYS)) {
46073
46169
  try {
46074
- unlinkSync2(path11.join(dir, file));
46170
+ unlinkSync2(path12.join(dir, file));
46075
46171
  } catch {}
46076
46172
  }
46077
46173
  } catch {}
46078
46174
  };
46079
46175
  var createLogger = (options = {}) => {
46080
46176
  const stateDir = options.stateDir ?? resolveStateDir();
46081
- const logDir = path11.join(stateDir, "logs");
46177
+ const logDir = path12.join(stateDir, "logs");
46082
46178
  const now2 = options.now ?? (() => new Date);
46083
46179
  const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
46084
46180
  const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
@@ -46112,7 +46208,7 @@ var createLogger = (options = {}) => {
46112
46208
  const line = `${JSON.stringify(event)}
46113
46209
  `;
46114
46210
  mkdirSync4(logDir, { recursive: true, mode: 448 });
46115
- appendFileSync(path11.join(logDir, dailyFileName(date)), line, { mode: 384 });
46211
+ appendFileSync(path12.join(logDir, dailyFileName(date)), line, { mode: 384 });
46116
46212
  pruneOldFiles(logDir);
46117
46213
  } catch {
46118
46214
  try {
@@ -46123,7 +46219,7 @@ var createLogger = (options = {}) => {
46123
46219
  context: { redaction_failed: true }
46124
46220
  };
46125
46221
  mkdirSync4(logDir, { recursive: true, mode: 448 });
46126
- appendFileSync(path11.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
46222
+ appendFileSync(path12.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
46127
46223
  `, {
46128
46224
  mode: 384
46129
46225
  });
@@ -46158,21 +46254,2152 @@ var createLogger = (options = {}) => {
46158
46254
  };
46159
46255
  };
46160
46256
 
46161
- // packages/workit-cli/src/index.tsx
46162
- var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
46163
- var logger = createLogger({
46164
- stderr: (event) => {
46165
- if (event.level === "debug" || event.level === "info")
46166
- return;
46167
- process.stderr.write(`${JSON.stringify(event)}
46168
- `);
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
+ });
46169
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
46170
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
+ ];
46171
48396
  var HELP = `workit — workflow rails for agentic coding
46172
48397
 
46173
48398
  Usage:
46174
48399
  workit init Run the interactive setup wizard
46175
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
+ `)}
46176
48403
  workit Show this help
46177
48404
 
46178
48405
  Run \`npx workit init\` to configure platforms, YouTrack, VCS and project hygiene.
@@ -46268,14 +48495,18 @@ if (__require.main == __require.module) {
46268
48495
  setDiagnosticLogger(logger);
46269
48496
  logger.info(EVENT.initialization, { host: "cli", command: subcommand });
46270
48497
  process.on("unhandledRejection", (reason) => logger.error(EVENT.uncaughtFailure, { phase: "unhandledRejection", ...errorDetail(reason) }));
46271
- process.on("uncaughtException", (err) => {
46272
- logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(err) });
48498
+ process.on("uncaughtException", (err3) => {
48499
+ logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(err3) });
46273
48500
  process.exit(1);
46274
48501
  });
46275
48502
  if (subcommand === "init") {
46276
48503
  await runInit();
46277
48504
  } else if (subcommand === "doctor") {
46278
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)));
46279
48510
  } else {
46280
48511
  console.log(HELP);
46281
48512
  process.exit(0);