@brainervirus/workit-cursor 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.
@@ -21839,7 +21839,13 @@ function isWorkitPlugin(value) {
21839
21839
  const pkgPath = s.includes("/packages/workit-opencode/") || s.includes("/packages/workit-cursor/") || s.includes("/node_modules/@brainervirus/workit-opencode/") || s.includes("/node_modules/@brainervirus/workit-cursor/");
21840
21840
  return named(s, "workflow-toolkit") || named(s, "workflow-toolkit-opencode") || named(s, "local/workflow-toolkit") || named(s, "workit") || named(s, "local/workit") || named(s, "@brainervirus/workit-opencode") || named(s, "@brainervirus/workit-cursor") || url && pkgPath || s.startsWith("git+file://") && s.includes("workflow-toolkit");
21841
21841
  }
21842
- var named = (s, name) => s === name || s.startsWith(`${name}@`);
21842
+ function cursorHooksEntry(_packageDir) {
21843
+ return {
21844
+ command: `npx -y --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
21845
+ args: []
21846
+ };
21847
+ }
21848
+ var named = (s, name) => s === name || s.startsWith(`${name}@`), CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@0.8.0";
21843
21849
  var init_registration = () => {};
21844
21850
 
21845
21851
  // packages/workit-core/src/core/skill-manifests.ts
@@ -22183,7 +22189,7 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
22183
22189
  if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
22184
22190
  if (args[0] !== "-y")
22185
22191
  return "invalid";
22186
- if (args[1] !== "--package=@brainervirus/workit-cursor@latest")
22192
+ if (args[1] !== `--package=${CURSOR_RUNTIME_PACKAGE}`)
22187
22193
  return "invalid";
22188
22194
  if (args[2] !== "workit-cursor-mcp")
22189
22195
  return "invalid";
@@ -22196,6 +22202,20 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
22196
22202
  runtime: command,
22197
22203
  entry: path9.isAbsolute(args[0]) ? args[0] : path9.resolve(path9.dirname(res.cursorMcp), args[0])
22198
22204
  };
22205
+ }, canonicalCursorHook, registeredCursorHook = (res) => {
22206
+ const hooksFile = path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
22207
+ if (!existsSync5(hooksFile))
22208
+ return "invalid";
22209
+ const config2 = readJson(hooksFile);
22210
+ if (!config2)
22211
+ return "invalid";
22212
+ const sessionStart = config2.hooks?.sessionStart;
22213
+ if (!Array.isArray(sessionStart) || sessionStart.length !== 1)
22214
+ return "invalid";
22215
+ const entry = sessionStart[0];
22216
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
22217
+ return "invalid";
22218
+ return typeof entry.command === "string" ? entry.command : "invalid";
22199
22219
  }, checkLauncher = (res) => {
22200
22220
  const dev = res.dev;
22201
22221
  const hosts = hostsFor(res.host);
@@ -22208,6 +22228,12 @@ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE", findDevFromCwd = (cwd) => {
22208
22228
  } else if (registered && registered.kind === "node" && !validNodeEntry(registered.entry, registered.runtime, res.env)) {
22209
22229
  missing.push(`cursor: registered ${registered.entry}`);
22210
22230
  }
22231
+ const hook = registeredCursorHook(res);
22232
+ if (hook === "invalid") {
22233
+ missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
22234
+ } else if (hook !== null && hook !== canonicalCursorHook) {
22235
+ missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
22236
+ }
22211
22237
  }
22212
22238
  if (dev) {
22213
22239
  missing.push(...hosts.filter((h) => h !== "cursor").flatMap((h) => launcherSlotsFor(h, dev).filter((slot) => !slot.some((p) => existsSync5(p))).map((slot) => `${h}: ${slot.join(" or ")}`)));
@@ -22493,6 +22519,7 @@ var init_doctor = __esm(() => {
22493
22519
  init_registration();
22494
22520
  init_workspaces();
22495
22521
  init_skill_manifests();
22522
+ canonicalCursorHook = cursorHooksEntry("").command;
22496
22523
  RUN_CHECKS = [
22497
22524
  checkRuntime,
22498
22525
  checkVersions,
@@ -23182,14 +23209,23 @@ var init_hygiene = __esm(() => {
23182
23209
  });
23183
23210
 
23184
23211
  // packages/workit-core/src/core/docs-layout.ts
23185
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
23212
+ import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
23186
23213
  import path14 from "node:path";
23187
23214
  var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).join("/"), canonicalize = (base, candidate) => {
23188
23215
  const abs = path14.resolve(base, candidate);
23189
23216
  let ancestor = abs;
23190
23217
  while (!existsSync8(ancestor))
23191
23218
  ancestor = path14.dirname(ancestor);
23192
- const real = realpathSync2(ancestor);
23219
+ let real;
23220
+ try {
23221
+ real = realpathSync2(ancestor);
23222
+ } catch (error2) {
23223
+ if (error2.code === "EACCES" && !lstatSync(ancestor).isSymbolicLink()) {
23224
+ real = path14.join(realpathSync2(path14.dirname(ancestor)), path14.basename(ancestor));
23225
+ } else {
23226
+ throw error2;
23227
+ }
23228
+ }
23193
23229
  if (real !== base && !real.startsWith(base + path14.sep)) {
23194
23230
  throw new Error(`path must stay inside repository root: ${candidate}`);
23195
23231
  }
@@ -23229,14 +23265,8 @@ var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).joi
23229
23265
  if (path14.isAbsolute(candidate)) {
23230
23266
  return { ok: false, error: `absolute path not allowed: ${candidate}` };
23231
23267
  }
23232
- let abs;
23233
- try {
23234
- abs = canonicalize(workspace, candidate);
23235
- } catch (error2) {
23236
- return { ok: false, error: error2 instanceof Error ? error2.message : String(error2) };
23237
- }
23238
- const rel = posix(path14.relative(workspace, abs));
23239
- const match = rel.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
23268
+ const spelling = posix(candidate);
23269
+ const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
23240
23270
  if (!match) {
23241
23271
  return {
23242
23272
  ok: false,
@@ -23263,6 +23293,18 @@ var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).joi
23263
23293
  };
23264
23294
  }
23265
23295
  derived = pathSlug;
23296
+ let abs;
23297
+ try {
23298
+ abs = canonicalize(workspace, candidate);
23299
+ } catch (error2) {
23300
+ return { ok: false, error: error2 instanceof Error ? error2.message : String(error2) };
23301
+ }
23302
+ if (posix(path14.relative(workspace, abs)) !== spelling) {
23303
+ return {
23304
+ ok: false,
23305
+ error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`
23306
+ };
23307
+ }
23266
23308
  }
23267
23309
  let resolvedSlug = slug;
23268
23310
  if (resolvedSlug !== undefined) {
@@ -23771,6 +23813,9 @@ ${JSON.stringify({ ok: false, errors: validated.errors })}`
23771
23813
  return { error: "missing template templates/execution-contract.md" };
23772
23814
  }
23773
23815
  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);
23816
+ if (!/^<workflow-handoff-destination>true<\/workflow-handoff-destination>$/m.test(contract)) {
23817
+ return { error: "handoff destination contract missing its destination marker" };
23818
+ }
23774
23819
  return { prompt: contract };
23775
23820
  };
23776
23821
  var init_handoff_context = __esm(() => {
@@ -23804,9 +23849,240 @@ var init_handoff_tools = __esm(() => {
23804
23849
  init_package_root();
23805
23850
  });
23806
23851
 
23807
- // packages/workit-core/src/core/flow-state.ts
23808
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, renameSync, rmSync, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
23852
+ // packages/workit-core/src/core/sdd.ts
23853
+ import {
23854
+ appendFileSync as appendFileSync2,
23855
+ existsSync as existsSync11,
23856
+ mkdirSync as mkdirSync6,
23857
+ readFileSync as readFileSync12,
23858
+ statSync as statSync6,
23859
+ writeFileSync as writeFileSync5
23860
+ } from "node:fs";
23861
+ import { execFileSync as execFileSync4 } from "node:child_process";
23809
23862
  import path19 from "node:path";
23863
+ function todosFromTasks(tasks, completedTaskIds = []) {
23864
+ const done = new Set((completedTaskIds ?? []).map((id) => Number(id)));
23865
+ const todos = (tasks ?? []).map((t) => {
23866
+ const id = Number(t.id);
23867
+ return {
23868
+ id: `task-${id}`,
23869
+ content: `Task ${id}: ${t.title ?? ""}`.trim(),
23870
+ status: done.has(id) ? "completed" : "pending"
23871
+ };
23872
+ });
23873
+ const firstPending = todos.find((t) => t.status === "pending");
23874
+ if (firstPending)
23875
+ firstPending.status = "in_progress";
23876
+ return todos;
23877
+ }
23878
+ function ledgerCompletion(root, slug) {
23879
+ let started = false;
23880
+ const completed = [];
23881
+ const absProgress = path19.join(root, "docs", slug, "sdd", "progress.md");
23882
+ if (existsSync11(absProgress)) {
23883
+ try {
23884
+ for (const line of readFileSync12(absProgress, "utf8").split(`
23885
+ `)) {
23886
+ const match = /^Task\s+(\d+):/i.exec(line);
23887
+ if (match) {
23888
+ started = true;
23889
+ if (/^Task\s+\d+:\s*complete\b/i.test(line))
23890
+ completed.push(Number(match[1]));
23891
+ }
23892
+ }
23893
+ } catch {}
23894
+ }
23895
+ const required2 = [];
23896
+ try {
23897
+ const absPlan = path19.join(root, "docs", slug, "plan.md");
23898
+ if (existsSync11(absPlan)) {
23899
+ for (const task of parseTasksFromPlan(readFileSync12(absPlan, "utf8")))
23900
+ required2.push(task.id);
23901
+ }
23902
+ } catch {}
23903
+ const completedSet = new Set(completed);
23904
+ const missing = required2.filter((id) => !completedSet.has(id));
23905
+ return {
23906
+ started,
23907
+ complete: required2.length > 0 && missing.length === 0,
23908
+ required: required2,
23909
+ completed,
23910
+ missing
23911
+ };
23912
+ }
23913
+ function sddContext({
23914
+ slug,
23915
+ plan_path,
23916
+ workspace_root
23917
+ }) {
23918
+ const resolved = resolveCanonicalLayout({ workspace_root, slug, plan_path });
23919
+ if (!resolved.ok)
23920
+ return { error: resolved.error };
23921
+ const cwd = resolved.layout.workspace;
23922
+ const resolvedSlug = resolved.layout.slug;
23923
+ if (!resolvedSlug)
23924
+ return { error: "slug or plan_path required" };
23925
+ const sdd_dir = path19.posix.join("docs", resolvedSlug, "sdd");
23926
+ const progress_path = path19.posix.join(sdd_dir, "progress.md");
23927
+ const manifest_path = path19.posix.join(sdd_dir, "manifest.json");
23928
+ let progress_lines = [];
23929
+ let completed_task_ids = [];
23930
+ const absProgress = path19.join(resolved.layout.sdd, "progress.md");
23931
+ if (existsSync11(absProgress)) {
23932
+ progress_lines = readFileSync12(absProgress, "utf8").split(`
23933
+ `).map((ln) => ln.trim()).filter(Boolean);
23934
+ const pat = /^Task\s+(\d+):\s+complete\b/i;
23935
+ completed_task_ids = progress_lines.map((ln) => pat.exec(ln)?.[1]).filter(Boolean).map(Number);
23936
+ }
23937
+ let manifest = {};
23938
+ const absManifest = path19.join(resolved.layout.sdd, "manifest.json");
23939
+ if (existsSync11(absManifest)) {
23940
+ try {
23941
+ manifest = JSON.parse(readFileSync12(absManifest, "utf8"));
23942
+ } catch {
23943
+ manifest = {};
23944
+ }
23945
+ }
23946
+ const legacy_path = path19.join(cwd, ".superpowers/sdd");
23947
+ const legacy_exists = existsSync11(legacy_path);
23948
+ let todos = [];
23949
+ let task_count = 0;
23950
+ if (plan_path) {
23951
+ const planText = readFileSync12(resolved.layout.plan, "utf8");
23952
+ const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
23953
+ const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
23954
+ if (spec_path) {
23955
+ const validated = docsValidate({
23956
+ spec_path,
23957
+ plan_path: path19.relative(cwd, resolved.layout.plan),
23958
+ workspace_root: cwd
23959
+ });
23960
+ if (validated.ok === false)
23961
+ return { ok: false, errors: validated.errors, error: validated.error };
23962
+ }
23963
+ const tasks = parseTasksFromPlan(planText);
23964
+ if (tasks.length > 0) {
23965
+ task_count = tasks.length;
23966
+ todos = todosFromTasks(tasks, completed_task_ids);
23967
+ }
23968
+ }
23969
+ const flow = readFlowState(cwd, resolvedSlug);
23970
+ return {
23971
+ slug: resolvedSlug,
23972
+ sdd_dir,
23973
+ progress_path,
23974
+ manifest_path,
23975
+ progress_lines,
23976
+ completed_task_ids,
23977
+ manifest,
23978
+ created: existsSync11(path19.join(cwd, sdd_dir)),
23979
+ forbidden_legacy_path: ".superpowers/sdd",
23980
+ legacy_sdd_exists: legacy_exists,
23981
+ warning: legacy_exists ? "Ignore .superpowers/sdd — use sdd_dir from this tool only" : undefined,
23982
+ todos,
23983
+ task_count,
23984
+ flow: { spec: flow.spec, plan: flow.plan, menu: flow.menu },
23985
+ todowrite_required: true,
23986
+ todowrite_hint: "REQUIRED: Call OpenCode todowrite with todos from this result so the native task list shows progress. Before each task set status in_progress; after workflow_sdd_append_progress set it completed."
23987
+ };
23988
+ }
23989
+ function sddTaskBrief({
23990
+ sdd_dir,
23991
+ task_id,
23992
+ section_text,
23993
+ workspace_root
23994
+ }) {
23995
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
23996
+ if (!contained.ok)
23997
+ return { error: contained.error };
23998
+ const dir = contained.path;
23999
+ mkdirSync6(dir, { recursive: true });
24000
+ const out = path19.join(dir, `task-${task_id}-brief.md`);
24001
+ writeFileSync5(out, `# Task ${task_id} brief
24002
+
24003
+ ${section_text}
24004
+ `, "utf8");
24005
+ const rel = posix2(path19.relative(contained.base, out));
24006
+ return { brief_path: rel, task_id };
24007
+ }
24008
+ function sddReviewPackage({
24009
+ sdd_dir,
24010
+ base_sha,
24011
+ head_sha,
24012
+ workspace_root
24013
+ }) {
24014
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
24015
+ if (!contained.ok)
24016
+ return { error: contained.error };
24017
+ const dir = contained.path;
24018
+ mkdirSync6(dir, { recursive: true });
24019
+ const base7 = base_sha.slice(0, 7);
24020
+ const head7 = head_sha.slice(0, 7);
24021
+ const diffPath = path19.join(dir, `review-${base7}..${head7}.diff`);
24022
+ try {
24023
+ const diff = execFileSync4("git", ["diff", base_sha, head_sha], {
24024
+ cwd: contained.base,
24025
+ encoding: "utf8",
24026
+ stdio: ["pipe", "pipe", "pipe"]
24027
+ });
24028
+ writeFileSync5(diffPath, diff, "utf8");
24029
+ const rel = posix2(path19.relative(contained.base, diffPath));
24030
+ return { diff_path: rel, base_sha, head_sha, base7, head7 };
24031
+ } catch (error2) {
24032
+ return { error: error2 instanceof Error ? error2.message : "git diff failed" };
24033
+ }
24034
+ }
24035
+ function sddAppendProgress({
24036
+ progress_path,
24037
+ line,
24038
+ workspace_root
24039
+ }) {
24040
+ const contained = resolveDocsPath({ workspace_root, path: progress_path });
24041
+ if (!contained.ok)
24042
+ return { error: contained.error };
24043
+ const path_ = contained.path;
24044
+ const trimmed = line.trim();
24045
+ if (!PROGRESS_RE.test(trimmed)) {
24046
+ return { error: "invalid progress line format" };
24047
+ }
24048
+ if (existsSync11(path_) && statSync6(path_).isDirectory()) {
24049
+ return { error: `progress path is a directory: ${progress_path}` };
24050
+ }
24051
+ mkdirSync6(path19.dirname(path_), { recursive: true });
24052
+ appendFileSync2(path_, trimmed + `
24053
+ `, "utf8");
24054
+ const rel = posix2(path19.relative(contained.base, path_));
24055
+ return { ok: true, line: trimmed, progress_path: rel };
24056
+ }
24057
+ var posix2 = (p) => p.split(path19.sep).join("/"), PROGRESS_RE;
24058
+ var init_sdd = __esm(() => {
24059
+ init_docs_validate();
24060
+ init_docs_validate();
24061
+ init_flow_state();
24062
+ init_docs_layout();
24063
+ PROGRESS_RE = /^Task\s+\d+:\s+complete\s+\(commits\s+[0-9a-f]{7,40}\.\.[0-9a-f]{7,40},/i;
24064
+ });
24065
+
24066
+ // packages/workit-core/src/core/menu.ts
24067
+ var init_menu = () => {};
24068
+
24069
+ // packages/workit-core/src/core/flow-state.ts
24070
+ import {
24071
+ closeSync,
24072
+ existsSync as existsSync12,
24073
+ fstatSync,
24074
+ fsyncSync,
24075
+ mkdirSync as mkdirSync7,
24076
+ openSync,
24077
+ readFileSync as readFileSync13,
24078
+ renameSync,
24079
+ rmSync,
24080
+ statSync as statSync7,
24081
+ unlinkSync as unlinkSync3,
24082
+ writeFileSync as writeFileSync6
24083
+ } from "node:fs";
24084
+ import { createHash } from "node:crypto";
24085
+ import path20 from "node:path";
23810
24086
 
23811
24087
  class HostReceiptStore {
23812
24088
  #bySession = new Map;
@@ -23861,10 +24137,15 @@ class HostReceiptStore {
23861
24137
  return { ok: true, receipt };
23862
24138
  }
23863
24139
  }
23864
- var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, err2 = (code, error2) => ({ ok: false, code, error: error2 }), SLUG_RE2, flowPath = (root, slug) => {
24140
+ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, err2 = (code, error2, details) => ({
24141
+ ok: false,
24142
+ code,
24143
+ error: error2,
24144
+ ...details ? { details } : {}
24145
+ }), SLUG_RE2, flowPath = (root, slug) => {
23865
24146
  if (!SLUG_RE2.test(slug))
23866
24147
  throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
23867
- return path19.join(root, "docs", slug, "sdd", "flow.json");
24148
+ return path20.join(root, "docs", slug, "sdd", "flow.json");
23868
24149
  }, resolveDoc = (root, slug, docPath, kind) => {
23869
24150
  const resolved = resolveCanonicalLayout({
23870
24151
  workspace_root: root,
@@ -23879,60 +24160,233 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23879
24160
  const spec = p.spec ?? {};
23880
24161
  const plan = p.plan ?? {};
23881
24162
  const menu = p.menu ?? {};
24163
+ const execution = p.execution ?? {};
23882
24164
  return {
23883
24165
  slug: p.slug ?? slug,
23884
24166
  activated: p.activated ?? true,
23885
24167
  spec: {
23886
24168
  path: spec.path ?? "",
23887
24169
  status: spec.status ?? "draft",
23888
- evidence: spec.evidence ?? null
24170
+ evidence: spec.evidence ?? null,
24171
+ approved_digest: spec.approved_digest ?? null
23889
24172
  },
23890
24173
  plan: {
23891
24174
  path: plan.path ?? "",
23892
24175
  status: plan.status ?? "draft",
23893
- evidence: plan.evidence ?? null
24176
+ evidence: plan.evidence ?? null,
24177
+ approved_digest: plan.approved_digest ?? null
23894
24178
  },
23895
24179
  menu: {
23896
24180
  presented: Boolean(menu.presented),
23897
24181
  chosen: menu.chosen ?? "",
23898
24182
  evidence: menu.evidence ?? null
23899
24183
  },
24184
+ execution: {
24185
+ status: execution.status ?? "pending",
24186
+ mode: execution.mode ?? null,
24187
+ evidence: execution.evidence ?? null
24188
+ },
24189
+ handoff_destination: p.handoff_destination ?? false,
23900
24190
  updated_at: p.updated_at ?? Date.now()
23901
24191
  };
23902
24192
  }, emptyState = (slug) => ({
23903
24193
  slug,
23904
24194
  activated: false,
23905
- spec: { path: "", status: "draft", evidence: null },
23906
- plan: { path: "", status: "draft", evidence: null },
24195
+ spec: { path: "", status: "draft", evidence: null, approved_digest: null },
24196
+ plan: { path: "", status: "draft", evidence: null, approved_digest: null },
23907
24197
  menu: { presented: false, chosen: "", evidence: null },
24198
+ execution: { status: "pending", mode: null, evidence: null },
24199
+ handoff_destination: false,
23908
24200
  updated_at: Date.now()
23909
24201
  }), readFlowState = (root, slug) => {
23910
24202
  const file = flowPath(root, slug);
23911
- if (!existsSync11(file))
24203
+ if (!existsSync12(file))
23912
24204
  return emptyState(slug);
23913
24205
  try {
23914
- return normalizeState(JSON.parse(readFileSync12(file, "utf8")), slug);
24206
+ return normalizeState(JSON.parse(readFileSync13(file, "utf8")), slug);
23915
24207
  } catch {
23916
24208
  return emptyState(slug);
23917
24209
  }
24210
+ }, HEX64_RE, FLOW_STATUSES, EXECUTION_STATUSES, isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v), validateEvidenceValue = (v, allowCli) => {
24211
+ if (v === null)
24212
+ return true;
24213
+ if (!isRecord(v))
24214
+ return false;
24215
+ if (v.host === "opencode") {
24216
+ return v.attested === true && typeof v.callID === "string" && typeof v.selectedLabel === "string" && typeof v.recordedAt === "number";
24217
+ }
24218
+ if (v.host === "cursor")
24219
+ return v.attested === false && v.confirmation === "contract";
24220
+ if (allowCli && v.host === "cli") {
24221
+ return v.attested === false && (v.confirmation === "flag" || v.confirmation === "tty");
24222
+ }
24223
+ return false;
24224
+ }, validateState = (parsed, slug) => {
24225
+ if (!isRecord(parsed))
24226
+ return { ok: false, error: "flow state must be a JSON object" };
24227
+ if (parsed.slug !== undefined && (typeof parsed.slug !== "string" || parsed.slug !== slug)) {
24228
+ return { ok: false, error: `flow state slug must be ${JSON.stringify(slug)}` };
24229
+ }
24230
+ if (parsed.activated !== undefined && typeof parsed.activated !== "boolean") {
24231
+ return { ok: false, error: "flow state activated must be a boolean" };
24232
+ }
24233
+ if (parsed.handoff_destination !== undefined && typeof parsed.handoff_destination !== "boolean") {
24234
+ return { ok: false, error: "flow state handoff_destination must be a boolean" };
24235
+ }
24236
+ if (parsed.updated_at !== undefined && (typeof parsed.updated_at !== "number" || !Number.isFinite(parsed.updated_at))) {
24237
+ return { ok: false, error: "flow state updated_at must be a finite number" };
24238
+ }
24239
+ const doc2 = (value, name) => {
24240
+ const p = isRecord(value) ? value : {};
24241
+ if (!isRecord(value) && value !== undefined) {
24242
+ return `flow state ${name} must be an object`;
24243
+ }
24244
+ if (p.status !== undefined && !FLOW_STATUSES.includes(p.status)) {
24245
+ return `flow state ${name}.status must be draft, self_reviewed, or approved`;
24246
+ }
24247
+ if (p.path !== undefined && typeof p.path !== "string") {
24248
+ return `flow state ${name}.path must be a string`;
24249
+ }
24250
+ if (p.approved_digest !== undefined && p.approved_digest !== null && (typeof p.approved_digest !== "string" || !HEX64_RE.test(p.approved_digest))) {
24251
+ return `flow state ${name}.approved_digest must be 64-char lowercase hex or null`;
24252
+ }
24253
+ if (p.evidence !== undefined && !validateEvidenceValue(p.evidence, false)) {
24254
+ return `flow state ${name}.evidence has an unsupported shape`;
24255
+ }
24256
+ return {
24257
+ path: p.path ?? "",
24258
+ status: p.status ?? "draft",
24259
+ evidence: p.evidence ?? null,
24260
+ approved_digest: p.approved_digest ?? null
24261
+ };
24262
+ };
24263
+ const spec = doc2(parsed.spec, "spec");
24264
+ if (typeof spec === "string")
24265
+ return { ok: false, error: spec };
24266
+ const plan = doc2(parsed.plan, "plan");
24267
+ if (typeof plan === "string")
24268
+ return { ok: false, error: plan };
24269
+ const menuRaw = isRecord(parsed.menu) ? parsed.menu : undefined;
24270
+ if (parsed.menu !== undefined && !isRecord(parsed.menu)) {
24271
+ return { ok: false, error: "flow state menu must be an object" };
24272
+ }
24273
+ if (menuRaw?.presented !== undefined && typeof menuRaw.presented !== "boolean") {
24274
+ return { ok: false, error: "flow state menu.presented must be a boolean" };
24275
+ }
24276
+ if (menuRaw?.chosen !== undefined && typeof menuRaw.chosen !== "string") {
24277
+ return { ok: false, error: "flow state menu.chosen must be a string" };
24278
+ }
24279
+ if (menuRaw?.chosen !== undefined && menuRaw.chosen !== "" && !MENU_CHOICES.includes(menuRaw.chosen)) {
24280
+ return {
24281
+ ok: false,
24282
+ error: `flow state menu.chosen must be one of: ${MENU_CHOICES.join(", ")} (or an empty string when the menu is unpresented)`
24283
+ };
24284
+ }
24285
+ if (menuRaw?.evidence !== undefined && !validateEvidenceValue(menuRaw.evidence, false)) {
24286
+ return { ok: false, error: "flow state menu.evidence has an unsupported shape" };
24287
+ }
24288
+ const execRaw = isRecord(parsed.execution) ? parsed.execution : undefined;
24289
+ if (parsed.execution !== undefined && !isRecord(parsed.execution)) {
24290
+ return { ok: false, error: "flow state execution must be an object" };
24291
+ }
24292
+ if (execRaw?.status !== undefined && !EXECUTION_STATUSES.includes(execRaw.status)) {
24293
+ return {
24294
+ ok: false,
24295
+ error: "flow state execution.status must be pending, active, paused, or completed"
24296
+ };
24297
+ }
24298
+ if (execRaw?.mode !== undefined && execRaw.mode !== null && execRaw.mode !== "subagent-driven" && execRaw.mode !== "inline") {
24299
+ return {
24300
+ ok: false,
24301
+ error: "flow state execution.mode must be subagent-driven, inline, or null"
24302
+ };
24303
+ }
24304
+ if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
24305
+ return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
24306
+ }
24307
+ return {
24308
+ ok: true,
24309
+ state: {
24310
+ slug,
24311
+ activated: parsed.activated ?? true,
24312
+ spec,
24313
+ plan,
24314
+ menu: {
24315
+ presented: menuRaw?.presented ?? false,
24316
+ chosen: menuRaw?.chosen ?? "",
24317
+ evidence: menuRaw?.evidence ?? null
24318
+ },
24319
+ execution: {
24320
+ status: execRaw?.status ?? "pending",
24321
+ mode: execRaw?.mode ?? null,
24322
+ evidence: execRaw?.evidence ?? null
24323
+ },
24324
+ handoff_destination: parsed.handoff_destination ?? false,
24325
+ updated_at: parsed.updated_at ?? Date.now()
24326
+ }
24327
+ };
23918
24328
  }, readFlowStrict = (root, slug) => {
23919
24329
  const file = flowPath(root, slug);
23920
- if (!existsSync11(file)) {
24330
+ const rel = path20.posix.join("docs", slug, "sdd", "flow.json");
24331
+ if (!existsSync12(file)) {
23921
24332
  return err2("flow_not_activated", `flow not activated for ${slug} — run workflow_flow_status first`);
23922
24333
  }
24334
+ let text;
23923
24335
  try {
23924
- return { ok: true, state: normalizeState(JSON.parse(readFileSync12(file, "utf8")), slug) };
24336
+ text = readFileSync13(file, "utf8");
23925
24337
  } catch (error2) {
23926
- return err2("flow_corrupt", `corrupt flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24338
+ return err2("flow_io_error", `cannot read flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24339
+ }
24340
+ let parsed;
24341
+ try {
24342
+ parsed = JSON.parse(text);
24343
+ } catch (error2) {
24344
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: rel, original_bytes_preserved: true });
24345
+ }
24346
+ const validated = validateState(parsed, slug);
24347
+ if (!validated.ok) {
24348
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${validated.error}`, {
24349
+ path: rel,
24350
+ original_bytes_preserved: true
24351
+ });
23927
24352
  }
23928
- }, uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`, writeFlowState = (root, state) => {
23929
- const file = flowPath(root, state.slug);
23930
- mkdirSync6(path19.dirname(file), { recursive: true });
24353
+ return { ok: true, state: validated.state, raw: parsed };
24354
+ }, uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`, writeFlowFileAtomic = (file, state) => {
24355
+ const text = JSON.stringify(state, null, 2) + `
24356
+ `;
23931
24357
  const tmp = uniqueTempPath(file);
23932
- writeFileSync5(tmp, JSON.stringify(state, null, 2) + `
23933
- `, "utf8");
23934
- renameSync(tmp, file);
23935
- }, MAX_WRITE_ATTEMPTS = 5, writeFlowStateIfCurrent = (root, expected, next) => {
24358
+ mkdirSync7(path20.dirname(file), { recursive: true });
24359
+ let fd = null;
24360
+ try {
24361
+ fd = openSync(tmp, "w");
24362
+ writeFileSync6(fd, text, "utf8");
24363
+ fsyncSync(fd);
24364
+ closeSync(fd);
24365
+ fd = null;
24366
+ renameSync(tmp, file);
24367
+ } finally {
24368
+ try {
24369
+ if (fd !== null)
24370
+ closeSync(fd);
24371
+ } catch {}
24372
+ try {
24373
+ if (existsSync12(tmp))
24374
+ rmSync(tmp, { force: true });
24375
+ } catch {}
24376
+ }
24377
+ }, MAX_WRITE_ATTEMPTS = 5, STALE_LOCK_MS = 1000, lockMtimeMs = (lock) => {
24378
+ try {
24379
+ return statSync7(lock).mtimeMs;
24380
+ } catch {
24381
+ return null;
24382
+ }
24383
+ }, lockOwnedBy = (fd, lock) => {
24384
+ try {
24385
+ return fstatSync(fd).ino === statSync7(lock).ino;
24386
+ } catch {
24387
+ return false;
24388
+ }
24389
+ }, writeFlowStateIfCurrent = (root, expected, next) => {
23936
24390
  const file = flowPath(root, next.slug);
23937
24391
  const expectedText = JSON.stringify(expected, null, 2) + `
23938
24392
  `;
@@ -23941,13 +24395,18 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23941
24395
  if (expectedText === nextText)
23942
24396
  return { ok: true };
23943
24397
  const tmp = uniqueTempPath(file);
24398
+ let fd = null;
23944
24399
  try {
23945
- const currentText = existsSync11(file) ? readFileSync12(file, "utf8") : null;
24400
+ const currentText = existsSync12(file) ? readFileSync13(file, "utf8") : null;
23946
24401
  if (currentText !== expectedText)
23947
24402
  return { ok: false, conflict: true };
23948
- mkdirSync6(path19.dirname(file), { recursive: true });
23949
- writeFileSync5(tmp, nextText, "utf8");
23950
- const reRead = existsSync11(file) ? readFileSync12(file, "utf8") : null;
24403
+ mkdirSync7(path20.dirname(file), { recursive: true });
24404
+ fd = openSync(tmp, "w");
24405
+ writeFileSync6(fd, nextText, "utf8");
24406
+ fsyncSync(fd);
24407
+ closeSync(fd);
24408
+ fd = null;
24409
+ const reRead = existsSync12(file) ? readFileSync13(file, "utf8") : null;
23951
24410
  if (reRead !== expectedText)
23952
24411
  return { ok: false, conflict: true };
23953
24412
  renameSync(tmp, file);
@@ -23956,45 +24415,226 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23956
24415
  return { ok: false, io_error: error2 instanceof Error ? error2.message : String(error2) };
23957
24416
  } finally {
23958
24417
  try {
23959
- if (existsSync11(tmp))
24418
+ if (fd !== null)
24419
+ closeSync(fd);
24420
+ } catch {}
24421
+ try {
24422
+ if (existsSync12(tmp))
23960
24423
  rmSync(tmp, { force: true });
23961
24424
  } catch {}
23962
24425
  }
23963
- }, readModifyWrite = (root, slug, mutate) => {
24426
+ }, readCanonicalDigest = (root, rel) => {
24427
+ const abs = path20.join(root, ...rel.split("/"));
24428
+ let bytes;
24429
+ try {
24430
+ bytes = readFileSync13(abs);
24431
+ } catch (error2) {
24432
+ if (error2.code === "ENOENT") {
24433
+ return { ok: false, code: "document_missing" };
24434
+ }
24435
+ return { ok: false, code: "document_unreadable" };
24436
+ }
24437
+ let text;
24438
+ try {
24439
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
24440
+ } catch {
24441
+ return { ok: false, code: "document_unreadable" };
24442
+ }
24443
+ return { ok: true, text, digest: createHash("sha256").update(bytes).digest("hex") };
24444
+ }, resetForSpecDrift = (state) => ({
24445
+ ...state,
24446
+ spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
24447
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24448
+ menu: { presented: false, chosen: "", evidence: null },
24449
+ execution: { status: "pending", mode: null, evidence: null },
24450
+ handoff_destination: false,
24451
+ updated_at: Date.now()
24452
+ }), resetForPlanDrift = (state) => ({
24453
+ ...state,
24454
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24455
+ menu: { presented: false, chosen: "", evidence: null },
24456
+ execution: { status: "pending", mode: null, evidence: null },
24457
+ handoff_destination: false,
24458
+ updated_at: Date.now()
24459
+ }), driftCodeFor = (root, relPath, storedDigest) => {
24460
+ if (storedDigest === null)
24461
+ return "digest_missing";
24462
+ const current = readCanonicalDigest(root, relPath);
24463
+ if (!current.ok)
24464
+ return current.code;
24465
+ return current.digest !== storedDigest ? "digest_mismatch" : null;
24466
+ }, reconcileState = (root, slug, state) => {
24467
+ const specPath = path20.posix.join("docs", slug, "spec.md");
24468
+ const planPath = path20.posix.join("docs", slug, "plan.md");
24469
+ if (state.spec.status === "approved") {
24470
+ const code = driftCodeFor(root, specPath, state.spec.approved_digest);
24471
+ if (code) {
24472
+ return {
24473
+ state: resetForSpecDrift(state),
24474
+ drift: [{ document: "spec", code, path: specPath }]
24475
+ };
24476
+ }
24477
+ }
24478
+ if (state.plan.status === "approved") {
24479
+ const code = driftCodeFor(root, planPath, state.plan.approved_digest);
24480
+ if (code) {
24481
+ return {
24482
+ state: resetForPlanDrift(state),
24483
+ drift: [{ document: "plan", code, path: planPath }]
24484
+ };
24485
+ }
24486
+ }
24487
+ return { state, drift: [] };
24488
+ }, deriveLegacyExecution = (root, slug, state) => {
24489
+ const ledger = ledgerCompletion(root, slug);
24490
+ if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
24491
+ return { status: "active", mode: "subagent-driven", evidence: null };
24492
+ }
24493
+ return { status: "pending", mode: null, evidence: null };
24494
+ }, normalizeCompatibility = (root, slug, parsed, state) => {
24495
+ if (!isRecord(parsed) || !("execution" in parsed)) {
24496
+ const derived = deriveLegacyExecution(root, slug, state);
24497
+ const current = state.execution;
24498
+ if (derived.status !== current.status || derived.mode !== current.mode) {
24499
+ return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
24500
+ }
24501
+ }
24502
+ return { state, changed: false };
24503
+ }, withFlowLock = (file, fn) => {
24504
+ const lock = `${file}.lock`;
24505
+ if (!existsSync12(path20.dirname(file)))
24506
+ return { locked: true, value: fn() };
24507
+ try {
24508
+ if (existsSync12(`${lock}.stale`))
24509
+ rmSync(`${lock}.stale`, { force: true });
24510
+ } catch {}
24511
+ const wait = new Int32Array(new SharedArrayBuffer(4));
24512
+ let fd = null;
23964
24513
  for (let attempt = 0;attempt < MAX_WRITE_ATTEMPTS; attempt++) {
24514
+ try {
24515
+ fd = openSync(lock, "wx");
24516
+ break;
24517
+ } catch (error2) {
24518
+ const code = error2.code;
24519
+ if (code !== "EEXIST") {
24520
+ return {
24521
+ locked: false,
24522
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`)
24523
+ };
24524
+ }
24525
+ const mtime = lockMtimeMs(lock);
24526
+ if (mtime !== null && Date.now() - mtime > STALE_LOCK_MS) {
24527
+ try {
24528
+ renameSync(lock, `${lock}.stale`);
24529
+ unlinkSync3(`${lock}.stale`);
24530
+ } catch {}
24531
+ try {
24532
+ fd = openSync(lock, "wx");
24533
+ break;
24534
+ } catch (innerError) {
24535
+ const innerCode = innerError.code;
24536
+ if (innerCode !== "EEXIST") {
24537
+ return {
24538
+ locked: false,
24539
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${innerError instanceof Error ? innerError.message : String(innerError)}`)
24540
+ };
24541
+ }
24542
+ }
24543
+ }
24544
+ if (attempt === MAX_WRITE_ATTEMPTS - 1) {
24545
+ return {
24546
+ locked: false,
24547
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path20.dirname(file)}: re-read the flow state and retry the transition`)
24548
+ };
24549
+ }
24550
+ Atomics.wait(wait, 0, 0, 10);
24551
+ }
24552
+ }
24553
+ if (fd === null) {
24554
+ return {
24555
+ locked: false,
24556
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path20.dirname(file)}: re-read the flow state and retry the transition`)
24557
+ };
24558
+ }
24559
+ try {
24560
+ return { locked: true, value: fn() };
24561
+ } finally {
24562
+ try {
24563
+ if (fd !== null && lockOwnedBy(fd, lock))
24564
+ rmSync(lock, { force: true });
24565
+ } catch {}
24566
+ try {
24567
+ if (fd !== null)
24568
+ closeSync(fd);
24569
+ } catch {}
24570
+ }
24571
+ }, readEffectiveFlowState = (root, slug) => {
24572
+ const file = flowPath(root, slug);
24573
+ const rel = path20.posix.join("docs", slug, "sdd", "flow.json");
24574
+ const locked = withFlowLock(file, () => {
23965
24575
  const strict = readFlowStrict(root, slug);
23966
24576
  if (!strict.ok)
23967
24577
  return strict;
23968
- const result = mutate(strict.state);
23969
- if (!result.ok)
23970
- return result;
23971
- const commit = writeFlowStateIfCurrent(root, strict.state, result.next);
23972
- if (commit.ok)
23973
- return { ok: true };
23974
- if ("io_error" in commit) {
23975
- return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24578
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
24579
+ const { state, drift } = reconcileState(root, slug, normalized.state);
24580
+ if (normalized.changed || drift.length > 0) {
24581
+ try {
24582
+ writeFlowFileAtomic(file, state);
24583
+ } catch (error2) {
24584
+ return err2("flow_io_error", `cannot persist reconciled flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: rel, original_bytes_preserved: true });
24585
+ }
23976
24586
  }
23977
- }
23978
- return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
24587
+ return { ok: true, state, drift };
24588
+ });
24589
+ if (!locked.locked)
24590
+ return locked.error;
24591
+ return locked.value;
24592
+ }, readModifyWrite = (root, slug, mutate) => {
24593
+ const file = flowPath(root, slug);
24594
+ const locked = withFlowLock(file, () => {
24595
+ for (let attempt = 0;attempt < MAX_WRITE_ATTEMPTS; attempt++) {
24596
+ const strict = readFlowStrict(root, slug);
24597
+ if (!strict.ok)
24598
+ return strict;
24599
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
24600
+ const reconciled = reconcileState(root, slug, normalized.state);
24601
+ const result = mutate(reconciled.state);
24602
+ if (!result.ok)
24603
+ return result;
24604
+ let baseline = strict.state;
24605
+ if (normalized.changed) {
24606
+ try {
24607
+ writeFlowFileAtomic(file, normalized.state);
24608
+ } catch (error2) {
24609
+ return err2("flow_io_error", `cannot persist normalized flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24610
+ }
24611
+ baseline = normalized.state;
24612
+ }
24613
+ const commit = writeFlowStateIfCurrent(root, baseline, result.next);
24614
+ if (commit.ok)
24615
+ return { ok: true };
24616
+ if ("io_error" in commit) {
24617
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24618
+ }
24619
+ }
24620
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
24621
+ });
24622
+ if (!locked.locked)
24623
+ return locked.error;
24624
+ return locked.value;
23979
24625
  }, assertMutationWorkspace = (root, ctx) => {
23980
24626
  if (ctx && ctx.hostWorkspace !== root) {
23981
24627
  return err2("workspace_mismatch", `mutation context workspace ${JSON.stringify(ctx.hostWorkspace)} does not match flow workspace ${JSON.stringify(root)}`);
23982
24628
  }
23983
24629
  return { ok: true };
23984
- }, assertCoordinatorBoundary = (ctx, menu) => {
23985
- if (ctx?.role === "coordinator" && menu.chosen === "subagent-driven") {
24630
+ }, assertCoordinatorBoundary = (ctx, state) => {
24631
+ if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
23986
24632
  return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
23987
24633
  }
23988
24634
  if (ctx?.role === "delegated" && !ctx.taskIdentity) {
23989
24635
  return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
23990
24636
  }
23991
24637
  return { ok: true };
23992
- }, nextFlowStatus = (current) => {
23993
- if (current === "draft")
23994
- return { ok: true, next: "approved" };
23995
- if (current === "self_reviewed")
23996
- return { ok: true, next: "approved" };
23997
- return err2("flow_already_approved", "already approved; no further transitions");
23998
24638
  }, MAX_CLOCK_SKEW_MS = 60000, MAX_RECEIPTS_PER_SESSION = 10, RECEIPT_FRESHNESS_MS, EVIDENCE_WINDOW_MS, NEGATIVE_ANSWER_LABELS, isNegativeLabel = (label) => {
23999
24639
  const normalized = label.trim().toLowerCase();
24000
24640
  return NEGATIVE_ANSWER_LABELS.some((entry) => {
@@ -24098,24 +24738,38 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24098
24738
  });
24099
24739
  if (!resolved.ok)
24100
24740
  return err2("flow_prepare_failed", resolved.error);
24101
- const specPath = path19.posix.join("docs", slug, "spec.md");
24102
- const planPath = path19.posix.join("docs", slug, "plan.md");
24103
- const current = readFlowState(root, slug);
24104
- const state = current.activated ? {
24105
- ...current,
24106
- spec: { ...current.spec, path: specPath },
24107
- plan: { ...current.plan, path: planPath },
24108
- updated_at: Date.now()
24109
- } : {
24110
- slug,
24111
- activated: true,
24112
- spec: { path: specPath, status: "draft", evidence: null },
24113
- plan: { path: planPath, status: "draft", evidence: null },
24114
- menu: { presented: false, chosen: "", evidence: null },
24115
- updated_at: Date.now()
24116
- };
24117
- writeFlowState(root, state);
24118
- return { ok: true };
24741
+ const specPath = path20.posix.join("docs", slug, "spec.md");
24742
+ const planPath = path20.posix.join("docs", slug, "plan.md");
24743
+ const file = flowPath(root, slug);
24744
+ const locked = withFlowLock(file, () => {
24745
+ if (!existsSync12(file)) {
24746
+ writeFlowFileAtomic(file, {
24747
+ slug,
24748
+ activated: true,
24749
+ spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
24750
+ plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
24751
+ menu: { presented: false, chosen: "", evidence: null },
24752
+ execution: { status: "pending", mode: null, evidence: null },
24753
+ handoff_destination: false,
24754
+ updated_at: Date.now()
24755
+ });
24756
+ return { ok: true };
24757
+ }
24758
+ const strict = readFlowStrict(root, slug);
24759
+ if (!strict.ok)
24760
+ return strict;
24761
+ const reconciled = reconcileState(root, slug, strict.state);
24762
+ writeFlowFileAtomic(file, {
24763
+ ...reconciled.state,
24764
+ spec: { ...reconciled.state.spec, path: specPath },
24765
+ plan: { ...reconciled.state.plan, path: planPath },
24766
+ updated_at: Date.now()
24767
+ });
24768
+ return { ok: true };
24769
+ });
24770
+ if (!locked.locked)
24771
+ return locked.error;
24772
+ return locked.value;
24119
24773
  }, transitionSpec = (root, slug, specPath, evidence, ctx) => {
24120
24774
  const bound = assertMutationWorkspace(root, ctx);
24121
24775
  if (!bound.ok)
@@ -24126,39 +24780,39 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24126
24780
  const doc2 = resolveDoc(root, slug, specPath, "spec");
24127
24781
  if (!doc2.ok)
24128
24782
  return err2("path_invalid", doc2.error);
24783
+ const relPath = path20.posix.join("docs", slug, "spec.md");
24129
24784
  return readModifyWrite(root, slug, (state) => {
24130
- if (!existsSync11(doc2.path))
24785
+ if (!existsSync12(doc2.path))
24131
24786
  return err2("spec_missing", `spec not found: ${specPath}`);
24132
- if (state.spec.status === "draft") {
24133
- let text;
24134
- try {
24135
- text = readFileSync12(doc2.path, "utf8");
24136
- } catch (error2) {
24137
- return err2("spec_self_review_failed", `spec self-review failed: unreadable spec: ${error2 instanceof Error ? error2.message : String(error2)}`);
24138
- }
24139
- const hard = qualitySpec(text).filter((f) => f.severity === "hard");
24140
- const missing = [];
24141
- if (!/^\s*\*+Branch:\*+/im.test(stripFences(text)))
24142
- missing.push("**Branch:** header missing");
24143
- if (hard.length > 0 || missing.length > 0) {
24144
- return err2("spec_self_review_failed", "spec self-review failed: " + hard.map((f) => `${f.code} — ${f.message}`).concat(missing).join("; ") + " — see templates/spec-template.md for the required structure");
24787
+ if (state.spec.status === "draft" || state.spec.status === "self_reviewed") {
24788
+ const digest = readCanonicalDigest(root, relPath);
24789
+ if (!digest.ok) {
24790
+ return err2("spec_self_review_failed", `spec self-review failed: unreadable or invalid UTF-8 canonical spec: ${specPath}`);
24791
+ }
24792
+ if (state.spec.status === "draft") {
24793
+ const hard = qualitySpec(digest.text).filter((f) => f.severity === "hard");
24794
+ const missing = [];
24795
+ if (!/^\s*\*+Branch:\*+/im.test(stripFences(digest.text)))
24796
+ missing.push("**Branch:** header missing");
24797
+ if (hard.length > 0 || missing.length > 0) {
24798
+ return err2("spec_self_review_failed", "spec self-review failed: " + hard.map((f) => `${f.code} ${f.message}`).concat(missing).join("; ") + " — see templates/spec-template.md for the required structure");
24799
+ }
24145
24800
  }
24801
+ return {
24802
+ ok: true,
24803
+ next: {
24804
+ ...state,
24805
+ spec: {
24806
+ path: relPath,
24807
+ status: "approved",
24808
+ evidence: recorded.evidence,
24809
+ approved_digest: digest.digest
24810
+ },
24811
+ updated_at: Date.now()
24812
+ }
24813
+ };
24146
24814
  }
24147
- const step = nextFlowStatus(state.spec.status);
24148
- if (!step.ok)
24149
- return step;
24150
- return {
24151
- ok: true,
24152
- next: {
24153
- ...state,
24154
- spec: {
24155
- path: path19.posix.join("docs", slug, "spec.md"),
24156
- status: step.next,
24157
- evidence: recorded.evidence
24158
- },
24159
- updated_at: Date.now()
24160
- }
24161
- };
24815
+ return err2("flow_already_approved", "already approved; no further transitions");
24162
24816
  });
24163
24817
  }, transitionPlan = (root, slug, planPath, evidence, ctx) => {
24164
24818
  const bound = assertMutationWorkspace(root, ctx);
@@ -24170,91 +24824,236 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24170
24824
  const doc2 = resolveDoc(root, slug, planPath, "plan");
24171
24825
  if (!doc2.ok)
24172
24826
  return err2("path_invalid", doc2.error);
24827
+ const relPath = path20.posix.join("docs", slug, "plan.md");
24173
24828
  return readModifyWrite(root, slug, (state) => {
24174
- if (!existsSync11(doc2.path))
24829
+ if (!existsSync12(doc2.path))
24175
24830
  return err2("plan_missing", `plan not found: ${planPath}`);
24176
24831
  if (state.spec.status !== "approved") {
24177
24832
  return err2("spec_not_approved", "spec must be approved before the plan can be approved");
24178
24833
  }
24179
- if (state.plan.status === "draft") {
24180
- let text;
24181
- try {
24182
- text = readFileSync12(doc2.path, "utf8");
24183
- } catch (error2) {
24184
- return err2("plan_self_review_failed", `plan self-review failed: unreadable plan: ${error2 instanceof Error ? error2.message : String(error2)}`);
24185
- }
24186
- const missing = [];
24187
- const stripped = stripFences(text);
24188
- if (parseTasksFromPlan(text).length === 0)
24189
- missing.push("no ### Task N: sections outside fences");
24190
- if (!/^\s*\*+Spec:\*+/im.test(stripped))
24191
- missing.push("**Spec:** header missing");
24192
- if (!/^\s*\*+Branch:\*+/im.test(stripped))
24193
- missing.push("**Branch:** header missing");
24194
- if (missing.length > 0)
24195
- return err2("plan_self_review_failed", "plan self-review failed: " + missing.join("; "));
24196
- }
24197
- const step = nextFlowStatus(state.plan.status);
24198
- if (!step.ok)
24199
- return step;
24834
+ if (state.plan.status === "draft" || state.plan.status === "self_reviewed") {
24835
+ const digest = readCanonicalDigest(root, relPath);
24836
+ if (!digest.ok) {
24837
+ return err2("plan_self_review_failed", `plan self-review failed: unreadable or invalid UTF-8 canonical plan: ${planPath}`);
24838
+ }
24839
+ if (state.plan.status === "draft") {
24840
+ const missing = [];
24841
+ const stripped = stripFences(digest.text);
24842
+ if (parseTasksFromPlan(digest.text).length === 0)
24843
+ missing.push("no ### Task N: sections outside fences");
24844
+ if (!/^\s*\*+Spec:\*+/im.test(stripped))
24845
+ missing.push("**Spec:** header missing");
24846
+ if (!/^\s*\*+Branch:\*+/im.test(stripped))
24847
+ missing.push("**Branch:** header missing");
24848
+ if (missing.length > 0)
24849
+ return err2("plan_self_review_failed", "plan self-review failed: " + missing.join("; "));
24850
+ }
24851
+ return {
24852
+ ok: true,
24853
+ next: {
24854
+ ...state,
24855
+ plan: {
24856
+ path: relPath,
24857
+ status: "approved",
24858
+ evidence: recorded.evidence,
24859
+ approved_digest: digest.digest
24860
+ },
24861
+ updated_at: Date.now()
24862
+ }
24863
+ };
24864
+ }
24865
+ return err2("flow_already_approved", "already approved; no further transitions");
24866
+ });
24867
+ }, recordMenuChoice = (root, slug, planPath, choice, evidence, ctx) => {
24868
+ const bound = assertMutationWorkspace(root, ctx);
24869
+ if (!bound.ok)
24870
+ return bound;
24871
+ const recorded = assertEvidenceShape(evidence);
24872
+ if (!recorded.ok)
24873
+ return err2("evidence_invalid", recorded.error);
24874
+ if (typeof choice !== "string" || !MENU_CHOICES.includes(choice)) {
24875
+ return err2("menu_choice_invalid", `invalid menu choice: ${JSON.stringify(choice)}`);
24876
+ }
24877
+ if (recorded.evidence.host === "cursor" && choice === "subagent-driven") {
24878
+ return err2("unsupported_mode", CURSOR_SUBAGENT_UNSUPPORTED_TEXT);
24879
+ }
24880
+ if (recorded.evidence.host === "opencode" && !sameChoiceLabel(recorded.evidence.selectedLabel, choice)) {
24881
+ return err2("evidence_mismatch", `evidence selectedLabel ${JSON.stringify(recorded.evidence.selectedLabel)} does not match choice ${JSON.stringify(choice)}`);
24882
+ }
24883
+ const doc2 = resolveDoc(root, slug, planPath, "plan");
24884
+ if (!doc2.ok)
24885
+ return err2("path_invalid", doc2.error);
24886
+ return readModifyWrite(root, slug, (state) => {
24887
+ if (state.spec.status !== "approved")
24888
+ return err2("spec_not_approved", "spec must be approved before the execution menu");
24889
+ if (state.plan.status !== "approved")
24890
+ return err2("plan_not_approved", "plan must be approved before the execution menu");
24891
+ if (state.handoff_destination && choice === "handoff") {
24892
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
24893
+ }
24894
+ const executing = choice === "subagent-driven" || choice === "inline";
24200
24895
  return {
24201
24896
  ok: true,
24202
24897
  next: {
24203
24898
  ...state,
24204
- plan: {
24205
- path: path19.posix.join("docs", slug, "plan.md"),
24206
- status: step.next,
24207
- evidence: recorded.evidence
24208
- },
24899
+ plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
24900
+ menu: { presented: true, chosen: choice, evidence: recorded.evidence },
24901
+ execution: executing ? { status: "active", mode: choice, evidence: recorded.evidence } : { status: "pending", mode: null, evidence: recorded.evidence },
24902
+ updated_at: Date.now()
24903
+ }
24904
+ };
24905
+ });
24906
+ }, markHandoffDestination = (root, slug, planPath) => {
24907
+ const doc2 = resolveDoc(root, slug, planPath, "plan");
24908
+ if (!doc2.ok)
24909
+ return err2("path_invalid", doc2.error);
24910
+ return readModifyWrite(root, slug, (state) => {
24911
+ if (state.spec.status !== "approved")
24912
+ return err2("spec_not_approved", "spec must be approved before marking a handoff destination");
24913
+ if (state.plan.status !== "approved")
24914
+ return err2("plan_not_approved", "plan must be approved before marking a handoff destination");
24915
+ if (state.handoff_destination) {
24916
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
24917
+ }
24918
+ if (state.menu.chosen !== "handoff") {
24919
+ return err2("handoff_not_chosen", `source menu choice must be "handoff" to mark a handoff destination (chosen: ${JSON.stringify(state.menu.chosen)})`);
24920
+ }
24921
+ return {
24922
+ ok: true,
24923
+ next: {
24924
+ ...state,
24925
+ handoff_destination: true,
24926
+ menu: { presented: false, chosen: "", evidence: null },
24209
24927
  updated_at: Date.now()
24210
24928
  }
24211
24929
  };
24212
24930
  });
24213
- }, recordMenuChoice = (root, slug, planPath, choice, evidence, ctx) => {
24931
+ }, CLI_CONFIRMATION_KEYS, validateLifecycleEvidence = (input) => {
24932
+ if (typeof input !== "object" || input === null) {
24933
+ return {
24934
+ ok: false,
24935
+ error: "lifecycle evidence required — native choice evidence or an exact CLI confirmation"
24936
+ };
24937
+ }
24938
+ const record3 = input;
24939
+ if (record3.host === "cli") {
24940
+ const validValue = record3.attested === false && (record3.confirmation === "flag" || record3.confirmation === "tty");
24941
+ const keys = Object.keys(record3).sort();
24942
+ const exactShape = keys.length === CLI_CONFIRMATION_KEYS.length && CLI_CONFIRMATION_KEYS.every((key) => keys.includes(key));
24943
+ if (validValue && exactShape) {
24944
+ return {
24945
+ ok: true,
24946
+ evidence: {
24947
+ host: "cli",
24948
+ attested: false,
24949
+ confirmation: record3.confirmation
24950
+ }
24951
+ };
24952
+ }
24953
+ return {
24954
+ ok: false,
24955
+ error: 'cli confirmations accept only the exact { host: "cli", attested: false, confirmation: "flag" | "tty" } shape'
24956
+ };
24957
+ }
24958
+ return assertEvidenceShape(input);
24959
+ }, errPendingFlow = (action) => err2("flow_not_active", `cannot ${action} a pending flow — the execution menu has not started it`), errCompletedFlow = (action) => err2("flow_already_completed", `cannot ${action} a completed flow`), completeExecution = (root, slug, deps) => {
24960
+ const file = flowPath(root, slug);
24961
+ const captured = readEffectiveFlowState(root, slug);
24962
+ if (!captured.ok)
24963
+ return captured;
24964
+ const exec = captured.state.execution;
24965
+ if (exec.status === "pending")
24966
+ return errPendingFlow("complete");
24967
+ if (exec.status === "completed")
24968
+ return errCompletedFlow("complete");
24969
+ const ledger = ledgerCompletion(root, slug);
24970
+ if (!ledger.complete) {
24971
+ return err2("execution_incomplete", `execution ledger incomplete for ${slug}: missing tasks ${ledger.missing.join(", ")}`, { required: ledger.required, completed: ledger.completed, missing: ledger.missing });
24972
+ }
24973
+ const verifier = deps?.verifyProject ?? runVerifyProject;
24974
+ const verify = verifier(root, false);
24975
+ if (verify.exitCode !== 0) {
24976
+ return err2("verification_failed", `repository verification failed for ${slug} (exit ${verify.exitCode}) — see the verification output`, { exitCode: verify.exitCode });
24977
+ }
24978
+ const locked = withFlowLock(file, () => {
24979
+ const strict = readFlowStrict(root, slug);
24980
+ if (!strict.ok)
24981
+ return strict;
24982
+ const reconciled = reconcileState(root, slug, strict.state);
24983
+ const currentExec = reconciled.state.execution;
24984
+ if (currentExec.status !== exec.status || currentExec.mode !== exec.mode) {
24985
+ return err2("flow_concurrent_conflict", `concurrent execution state change detected for ${slug}: re-read the flow state and retry completion`);
24986
+ }
24987
+ const next = {
24988
+ ...reconciled.state,
24989
+ execution: { ...exec, status: "completed" },
24990
+ handoff_destination: false,
24991
+ updated_at: Date.now()
24992
+ };
24993
+ const commit = writeFlowStateIfCurrent(root, captured.state, next);
24994
+ if (commit.ok)
24995
+ return { ok: true };
24996
+ if ("io_error" in commit) {
24997
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24998
+ }
24999
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry completion`);
25000
+ });
25001
+ if (!locked.locked)
25002
+ return locked.error;
25003
+ return locked.value;
25004
+ }, transitionExecution = (root, slug, planPath, action, evidence, ctx, deps) => {
24214
25005
  const bound = assertMutationWorkspace(root, ctx);
24215
25006
  if (!bound.ok)
24216
25007
  return bound;
24217
- const recorded = assertEvidenceShape(evidence);
24218
- if (!recorded.ok)
24219
- return err2("evidence_invalid", recorded.error);
24220
- if (typeof choice !== "string" || !MENU_CHOICES.includes(choice)) {
24221
- return err2("menu_choice_invalid", `invalid menu choice: ${JSON.stringify(choice)}`);
24222
- }
24223
- if (recorded.evidence.host === "cursor" && choice === "subagent-driven") {
24224
- return err2("unsupported_mode", CURSOR_SUBAGENT_UNSUPPORTED_TEXT);
24225
- }
24226
- if (recorded.evidence.host === "opencode" && !sameChoiceLabel(recorded.evidence.selectedLabel, choice)) {
24227
- return err2("evidence_mismatch", `evidence selectedLabel ${JSON.stringify(recorded.evidence.selectedLabel)} does not match choice ${JSON.stringify(choice)}`);
24228
- }
25008
+ const validated = validateLifecycleEvidence(evidence);
25009
+ if (!validated.ok)
25010
+ return err2("evidence_invalid", validated.error);
24229
25011
  const doc2 = resolveDoc(root, slug, planPath, "plan");
24230
25012
  if (!doc2.ok)
24231
25013
  return err2("path_invalid", doc2.error);
25014
+ if (action === "complete")
25015
+ return completeExecution(root, slug, deps);
25016
+ if (action === "pause") {
25017
+ return readModifyWrite(root, slug, (state) => {
25018
+ const exec = state.execution;
25019
+ if (exec.status === "pending")
25020
+ return errPendingFlow("pause");
25021
+ if (exec.status === "completed")
25022
+ return errCompletedFlow("pause");
25023
+ if (exec.status === "paused")
25024
+ return err2("flow_already_paused", "flow is already paused");
25025
+ return {
25026
+ ok: true,
25027
+ next: { ...state, execution: { ...exec, status: "paused" }, updated_at: Date.now() }
25028
+ };
25029
+ });
25030
+ }
24232
25031
  return readModifyWrite(root, slug, (state) => {
24233
- if (state.spec.status !== "approved")
24234
- return err2("spec_not_approved", "spec must be approved before the execution menu");
24235
- if (state.plan.status !== "approved")
24236
- return err2("plan_not_approved", "plan must be approved before the execution menu");
25032
+ const exec = state.execution;
25033
+ if (exec.status === "completed")
25034
+ return errCompletedFlow("resume");
25035
+ if (exec.status !== "paused") {
25036
+ return err2("flow_not_paused", exec.status === "active" ? "flow is already active — cannot resume" : "cannot resume a pending flow — the execution menu has not started it");
25037
+ }
24237
25038
  return {
24238
25039
  ok: true,
24239
- next: {
24240
- ...state,
24241
- plan: state.plan.path ? state.plan : { path: planPath, status: state.plan.status },
24242
- menu: { presented: true, chosen: choice, evidence: recorded.evidence },
24243
- updated_at: Date.now()
24244
- }
25040
+ next: { ...state, execution: { ...exec, status: "active" }, updated_at: Date.now() }
24245
25041
  };
24246
25042
  });
25043
+ }, slugFromPath = (p) => {
25044
+ const dirName = path20.basename(path20.dirname(p));
25045
+ return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
24247
25046
  }, slugFromSddPath = (p) => {
24248
- const match = p.split(path19.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
25047
+ const match = p.split(path20.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
24249
25048
  return match?.[1] ?? "";
24250
25049
  }, assertProductGates = (root, slug, opts = {}, ctx) => {
24251
25050
  const bound = assertMutationWorkspace(root, ctx);
24252
25051
  if (!bound.ok)
24253
25052
  return bound;
24254
- const strict = readFlowStrict(root, slug);
24255
- if (!strict.ok)
24256
- return strict;
24257
- const state = strict.state;
25053
+ const effective = readEffectiveFlowState(root, slug);
25054
+ if (!effective.ok)
25055
+ return effective;
25056
+ const state = effective.state;
24258
25057
  if (state.spec.status !== "approved") {
24259
25058
  return err2("spec_not_approved", `spec not approved (status: ${state.spec.status}). Run workflow_spec_approve after the user's approval.`);
24260
25059
  }
@@ -24266,18 +25065,21 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24266
25065
  }
24267
25066
  if (opts.requireDocs) {
24268
25067
  const validated = docsValidate({
24269
- spec_path: path19.posix.join("docs", slug, "spec.md"),
24270
- plan_path: path19.posix.join("docs", slug, "plan.md"),
25068
+ spec_path: path20.posix.join("docs", slug, "spec.md"),
25069
+ plan_path: path20.posix.join("docs", slug, "plan.md"),
24271
25070
  workspace_root: root
24272
25071
  });
24273
25072
  if (validated.ok === false)
24274
25073
  return err2("docs_invalid", validated.error);
24275
25074
  }
24276
- return assertCoordinatorBoundary(ctx, state.menu);
25075
+ return assertCoordinatorBoundary(ctx, state);
24277
25076
  }, BASH_READ_TOKENS, BASH_GIT_READ_SUBCOMMANDS, BASH_GIT_MUTABLE_SUBCOMMANDS, BASH_GIT_READ_FLAGS, BASH_FIND_DENIED_FLAGS, BASH_TEST_VERBS, BASH_DENIED_HEADS, BASH_OUTPUT_FLAG_VERBS, BASH_PAREN_EXEMPT_HEADS, BASH_GIT_VALUE_FLAGS, COORDINATOR_SHELL_DENIED_TEXT;
24278
25077
  var init_flow_state = __esm(() => {
24279
25078
  init_docs_validate();
24280
25079
  init_docs_layout();
25080
+ init_sdd();
25081
+ init_verify_project();
25082
+ init_menu();
24281
25083
  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.";
24282
25084
  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`.";
24283
25085
  MENU_CHOICES = [
@@ -24288,6 +25090,9 @@ var init_flow_state = __esm(() => {
24288
25090
  "review-plan"
24289
25091
  ];
24290
25092
  SLUG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
25093
+ HEX64_RE = /^[0-9a-f]{64}$/;
25094
+ FLOW_STATUSES = ["draft", "self_reviewed", "approved"];
25095
+ EXECUTION_STATUSES = ["pending", "active", "paused", "completed"];
24291
25096
  RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
24292
25097
  EVIDENCE_WINDOW_MS = 24 * 60 * 60 * 1000;
24293
25098
  NEGATIVE_ANSWER_LABELS = [
@@ -24304,6 +25109,7 @@ var init_flow_state = __esm(() => {
24304
25109
  "deny"
24305
25110
  ];
24306
25111
  CURSOR_KEYS = ["attested", "confirmation", "host"];
25112
+ CLI_CONFIRMATION_KEYS = ["attested", "confirmation", "host"];
24307
25113
  BASH_READ_TOKENS = new Set([
24308
25114
  "cat",
24309
25115
  "head",
@@ -24440,34 +25246,34 @@ var init_flow_evidence = __esm(() => {
24440
25246
  });
24441
25247
 
24442
25248
  // packages/workit-core/src/core/docs-migration.ts
24443
- import { execFileSync as execFileSync4 } from "node:child_process";
25249
+ import { execFileSync as execFileSync5 } from "node:child_process";
24444
25250
  import {
24445
- existsSync as existsSync12,
24446
- lstatSync,
24447
- mkdirSync as mkdirSync7,
25251
+ existsSync as existsSync13,
25252
+ lstatSync as lstatSync2,
25253
+ mkdirSync as mkdirSync8,
24448
25254
  readdirSync as readdirSync6,
24449
- readFileSync as readFileSync13,
25255
+ readFileSync as readFileSync14,
24450
25256
  realpathSync as realpathSync3,
24451
25257
  renameSync as renameSync2,
24452
25258
  rmSync as rmSync2,
24453
- statSync as statSync6,
24454
- writeFileSync as writeFileSync6
25259
+ statSync as statSync8,
25260
+ writeFileSync as writeFileSync7
24455
25261
  } from "node:fs";
24456
- import path20 from "node:path";
24457
- var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG = "superpowers", SPEC_LINK_RE2, posix2 = (p) => p.split(path20.sep).join("/"), legacyRoot = (workspace) => path20.join(workspace, "docs", "superpowers"), readFileSafe = (p) => {
25262
+ import path21 from "node:path";
25263
+ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG = "superpowers", SPEC_LINK_RE2, posix3 = (p) => p.split(path21.sep).join("/"), legacyRoot = (workspace) => path21.join(workspace, "docs", "superpowers"), readFileSafe = (p) => {
24458
25264
  try {
24459
- return readFileSync13(p, "utf8");
25265
+ return readFileSync14(p, "utf8");
24460
25266
  } catch {
24461
25267
  return null;
24462
25268
  }
24463
25269
  }, sddIgnoreActive = (cwd, slug) => {
24464
25270
  try {
24465
- execFileSync4("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
25271
+ execFileSync5("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
24466
25272
  } catch {
24467
25273
  return false;
24468
25274
  }
24469
25275
  try {
24470
- execFileSync4("git", ["-C", cwd, "check-ignore", path20.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
25276
+ execFileSync5("git", ["-C", cwd, "check-ignore", path21.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
24471
25277
  return true;
24472
25278
  } catch {
24473
25279
  return false;
@@ -24489,22 +25295,22 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24489
25295
  }, detectLegacyDocs = (workspace_root) => {
24490
25296
  const root = legacyRoot(workspace_root);
24491
25297
  const raw = [];
24492
- if (existsSync12(root)) {
25298
+ if (existsSync13(root)) {
24493
25299
  for (const entry of readdirSync6(root, { withFileTypes: true })) {
24494
- const abs = path20.join(root, entry.name);
24495
- const rel = posix2(path20.relative(workspace_root, abs));
25300
+ const abs = path21.join(root, entry.name);
25301
+ const rel = posix3(path21.relative(workspace_root, abs));
24496
25302
  if (entry.isDirectory()) {
24497
- const hasSpec = existsSync12(path20.join(abs, "spec.md"));
24498
- const hasPlan = existsSync12(path20.join(abs, "plan.md"));
24499
- if (!hasSpec && !hasPlan && !existsSync12(path20.join(abs, "sdd")))
25303
+ const hasSpec = existsSync13(path21.join(abs, "spec.md"));
25304
+ const hasPlan = existsSync13(path21.join(abs, "plan.md"));
25305
+ if (!hasSpec && !hasPlan && !existsSync13(path21.join(abs, "sdd")))
24500
25306
  continue;
24501
- const explicit = explicitTargetSlug(hasPlan ? readFileSafe(path20.join(abs, "plan.md")) : null);
25307
+ const explicit = explicitTargetSlug(hasPlan ? readFileSafe(path21.join(abs, "plan.md")) : null);
24502
25308
  raw.push({
24503
25309
  slug: explicit ?? entry.name,
24504
25310
  legacy_dir: rel,
24505
- spec: hasSpec ? posix2(path20.join(rel, "spec.md")) : null,
24506
- plan: hasPlan ? posix2(path20.join(rel, "plan.md")) : null,
24507
- sdd: existsSync12(path20.join(abs, "sdd")),
25311
+ spec: hasSpec ? posix3(path21.join(rel, "spec.md")) : null,
25312
+ plan: hasPlan ? posix3(path21.join(rel, "plan.md")) : null,
25313
+ sdd: existsSync13(path21.join(abs, "sdd")),
24508
25314
  explicit: explicit !== null
24509
25315
  });
24510
25316
  } else if (entry.name === "spec.md" || entry.name === "plan.md") {
@@ -24572,7 +25378,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24572
25378
  return { ok: true, out, changed: out !== text };
24573
25379
  }, isPlanMalformed = (text) => !/^\s*\*+Spec:\*+/im.test(text), readBufferSafe = (p) => {
24574
25380
  try {
24575
- return readFileSync13(p);
25381
+ return readFileSync14(p);
24576
25382
  } catch {
24577
25383
  return null;
24578
25384
  }
@@ -24595,7 +25401,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24595
25401
  const raw = readBufferSafe(fromAbs);
24596
25402
  if (raw === null)
24597
25403
  return { ok: false, error: `unreadable source ${fromAbs}` };
24598
- if (path20.basename(toRel) !== "flow.json")
25404
+ if (path21.basename(toRel) !== "flow.json")
24599
25405
  return { ok: true, bytes: raw, status: "copied" };
24600
25406
  const flow = flowRewrite(raw.toString("utf8"), legacyName, slug);
24601
25407
  if (!flow.ok)
@@ -24618,18 +25424,18 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24618
25424
  } catch {
24619
25425
  return { ok: false, error: `dangling symlink refused: ${legacyRel}` };
24620
25426
  }
24621
- if (real !== workspaceReal && !real.startsWith(workspaceReal + path20.sep)) {
25427
+ if (real !== workspaceReal && !real.startsWith(workspaceReal + path21.sep)) {
24622
25428
  return { ok: false, error: `symlink escape refused: ${legacyRel}` };
24623
25429
  }
24624
25430
  if (visits.has(real))
24625
25431
  return { ok: true };
24626
25432
  visits.add(real);
24627
25433
  for (const entry of readdirSync6(real, { withFileTypes: true })) {
24628
- const fromAbs = path20.join(real, entry.name);
24629
- const fromRel = posix2(path20.join(legacyRel, entry.name));
24630
- const toRel = posix2(path20.join(destRelRoot, entry.name));
25434
+ const fromAbs = path21.join(real, entry.name);
25435
+ const fromRel = posix3(path21.join(legacyRel, entry.name));
25436
+ const toRel = posix3(path21.join(destRelRoot, entry.name));
24631
25437
  if (entry.isDirectory()) {
24632
- const sub = planTree(workspace, fromAbs, fromRel, toRel, path20.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
25438
+ const sub = planTree(workspace, fromAbs, fromRel, toRel, path21.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
24633
25439
  if (!sub.ok)
24634
25440
  return sub;
24635
25441
  } else if (entry.isFile() || entry.isSymbolicLink()) {
@@ -24641,11 +25447,11 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24641
25447
  } catch {
24642
25448
  return { ok: false, error: `dangling symlink refused: ${fromRel}` };
24643
25449
  }
24644
- if (target !== workspaceReal && !target.startsWith(workspaceReal + path20.sep)) {
25450
+ if (target !== workspaceReal && !target.startsWith(workspaceReal + path21.sep)) {
24645
25451
  return { ok: false, error: `symlink escape refused: ${fromRel}` };
24646
25452
  }
24647
- if (statSync6(target).isDirectory()) {
24648
- const sub = planTree(workspace, fromAbs, fromRel, toRel, path20.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
25453
+ if (statSync8(target).isDirectory()) {
25454
+ const sub = planTree(workspace, fromAbs, fromRel, toRel, path21.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
24649
25455
  if (!sub.ok)
24650
25456
  return sub;
24651
25457
  continue;
@@ -24662,7 +25468,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24662
25468
  fromAbs,
24663
25469
  fromRel,
24664
25470
  toRel,
24665
- destAbs: path20.join(destAbsRoot, entry.name),
25471
+ destAbs: path21.join(destAbsRoot, entry.name),
24666
25472
  bytes: classified.bytes,
24667
25473
  status: classified.status
24668
25474
  });
@@ -24675,36 +25481,36 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24675
25481
  kind,
24676
25482
  legacyName,
24677
25483
  slug: entry.slug,
24678
- toRel: posix2(path20.join("docs", entry.slug, `${kind}.md`)),
24679
- destAbs: path20.join(destDirAbs, `${kind}.md`)
25484
+ toRel: posix3(path21.join("docs", entry.slug, `${kind}.md`)),
25485
+ destAbs: path21.join(destDirAbs, `${kind}.md`)
24680
25486
  });
24681
25487
  if (entry.spec) {
24682
- const classified = classifyDoc("spec", legacyName, entry.slug, path20.join(workspace, entry.spec));
25488
+ const classified = classifyDoc("spec", legacyName, entry.slug, path21.join(workspace, entry.spec));
24683
25489
  if (!classified.ok)
24684
25490
  return classified;
24685
25491
  plan.push({
24686
25492
  ...base("spec"),
24687
- fromAbs: path20.join(workspace, entry.spec),
25493
+ fromAbs: path21.join(workspace, entry.spec),
24688
25494
  fromRel: entry.spec,
24689
25495
  bytes: classified.bytes,
24690
25496
  status: classified.status
24691
25497
  });
24692
25498
  }
24693
25499
  if (entry.plan) {
24694
- const classified = classifyDoc("plan", legacyName, entry.slug, path20.join(workspace, entry.plan));
25500
+ const classified = classifyDoc("plan", legacyName, entry.slug, path21.join(workspace, entry.plan));
24695
25501
  if (!classified.ok)
24696
25502
  return classified;
24697
25503
  plan.push({
24698
25504
  ...base("plan"),
24699
- fromAbs: path20.join(workspace, entry.plan),
25505
+ fromAbs: path21.join(workspace, entry.plan),
24700
25506
  fromRel: entry.plan,
24701
25507
  bytes: classified.bytes,
24702
25508
  status: classified.status
24703
25509
  });
24704
25510
  }
24705
25511
  if (entry.sdd) {
24706
- const legacySdd = posix2(path20.join(entry.legacy_dir, "sdd"));
24707
- return planTree(workspace, path20.join(workspace, legacySdd), legacySdd, posix2(path20.join("docs", entry.slug, "sdd")), path20.join(destDirAbs, "sdd"), legacyName, entry.slug, plan, visits);
25512
+ const legacySdd = posix3(path21.join(entry.legacy_dir, "sdd"));
25513
+ return planTree(workspace, path21.join(workspace, legacySdd), legacySdd, posix3(path21.join("docs", entry.slug, "sdd")), path21.join(destDirAbs, "sdd"), legacyName, entry.slug, plan, visits);
24708
25514
  }
24709
25515
  return { ok: true };
24710
25516
  }, abort = (error2, collisions) => ({
@@ -24747,11 +25553,11 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24747
25553
  }
24748
25554
  const collisions = [];
24749
25555
  for (const item of plan) {
24750
- if (!existsSync12(item.destAbs))
25556
+ if (!existsSync13(item.destAbs))
24751
25557
  continue;
24752
25558
  let identical = false;
24753
25559
  try {
24754
- identical = !lstatSync(item.destAbs).isDirectory() && readFileSync13(item.destAbs).equals(item.bytes);
25560
+ identical = !lstatSync2(item.destAbs).isDirectory() && readFileSync14(item.destAbs).equals(item.bytes);
24755
25561
  } catch {
24756
25562
  identical = false;
24757
25563
  }
@@ -24786,8 +25592,8 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24786
25592
  }
24787
25593
  const tmp = `${item.destAbs}.workit-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
24788
25594
  try {
24789
- mkdirSync7(path20.dirname(item.destAbs), { recursive: true });
24790
- writeFileSync6(tmp, item.bytes);
25595
+ mkdirSync8(path21.dirname(item.destAbs), { recursive: true });
25596
+ writeFileSync7(tmp, item.bytes);
24791
25597
  renameSync2(tmp, item.destAbs);
24792
25598
  } catch (error2) {
24793
25599
  try {
@@ -24813,32 +25619,32 @@ var init_docs_migration = __esm(() => {
24813
25619
  });
24814
25620
 
24815
25621
  // packages/workit-core/src/core/docs-repo.ts
24816
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7, readdirSync as readdirSync7 } from "node:fs";
24817
- import { execFileSync as execFileSync5 } from "node:child_process";
24818
- import path21 from "node:path";
24819
- var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(configDir(), "docs-repo.json"), readDocsRepoConfig = () => {
25622
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8, readdirSync as readdirSync7 } from "node:fs";
25623
+ import { execFileSync as execFileSync6 } from "node:child_process";
25624
+ import path22 from "node:path";
25625
+ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(configDir(), "docs-repo.json"), readDocsRepoConfig = () => {
24820
25626
  try {
24821
- const parsed = JSON.parse(readFileSync14(configPath(), "utf8"));
25627
+ const parsed = JSON.parse(readFileSync15(configPath(), "utf8"));
24822
25628
  return parsed.path ? { path: parsed.path } : null;
24823
25629
  } catch {
24824
25630
  return null;
24825
25631
  }
24826
25632
  }, writeDocsRepoConfig = (docsPath) => {
24827
25633
  const file = configPath();
24828
- mkdirSync8(path21.dirname(file), { recursive: true });
24829
- writeFileSync7(file, JSON.stringify({ path: docsPath }, null, 2) + `
25634
+ mkdirSync9(path22.dirname(file), { recursive: true });
25635
+ writeFileSync8(file, JSON.stringify({ path: docsPath }, null, 2) + `
24830
25636
  `, "utf8");
24831
25637
  }, docsRepoPath = () => readDocsRepoConfig()?.path ?? null, validateDocsRepo = (docsPath) => {
24832
- if (!existsSync13(docsPath))
25638
+ if (!existsSync14(docsPath))
24833
25639
  return { ok: false, error: `docs repo path does not exist: ${docsPath}` };
24834
25640
  try {
24835
- execFileSync5("git", ["-C", docsPath, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
25641
+ execFileSync6("git", ["-C", docsPath, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
24836
25642
  } catch {
24837
25643
  return { ok: false, error: `docs repo is not a git repository: ${docsPath}` };
24838
25644
  }
24839
- const featuresDir = path21.join(docsPath, "features");
24840
- if (!existsSync13(featuresDir))
24841
- mkdirSync8(featuresDir, { recursive: true });
25645
+ const featuresDir = path22.join(docsPath, "features");
25646
+ if (!existsSync14(featuresDir))
25647
+ mkdirSync9(featuresDir, { recursive: true });
24842
25648
  return { ok: true };
24843
25649
  }, linkDocsRepo = (docsPath, confirmed) => {
24844
25650
  if (!confirmed)
@@ -24851,23 +25657,23 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24851
25657
  }, listSpecs = (workspaceRoot) => {
24852
25658
  const repoPath = docsRepoPath();
24853
25659
  const specs = [];
24854
- const docsDir = path21.join(workspaceRoot, "docs");
24855
- if (existsSync13(docsDir)) {
25660
+ const docsDir = path22.join(workspaceRoot, "docs");
25661
+ if (existsSync14(docsDir)) {
24856
25662
  for (const slug of readdirSync7(docsDir)) {
24857
25663
  if (slug.startsWith("."))
24858
25664
  continue;
24859
- const spec = path21.posix.join("docs", slug, "spec.md");
24860
- if (!existsSync13(path21.join(workspaceRoot, spec)))
25665
+ const spec = path22.posix.join("docs", slug, "spec.md");
25666
+ if (!existsSync14(path22.join(workspaceRoot, spec)))
24861
25667
  continue;
24862
25668
  let promoted = false;
24863
25669
  let target = null;
24864
25670
  if (repoPath) {
24865
- const featuresDir = path21.join(repoPath, "features");
24866
- if (existsSync13(featuresDir)) {
25671
+ const featuresDir = path22.join(repoPath, "features");
25672
+ if (existsSync14(featuresDir)) {
24867
25673
  const match = readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
24868
25674
  if (match) {
24869
25675
  promoted = true;
24870
- target = path21.join(repoPath, "features", match);
25676
+ target = path22.join(repoPath, "features", match);
24871
25677
  }
24872
25678
  }
24873
25679
  }
@@ -24880,7 +25686,7 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24880
25686
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
24881
25687
  }, readSafe4 = (p) => {
24882
25688
  try {
24883
- return readFileSync14(p, "utf8");
25689
+ return readFileSync15(p, "utf8");
24884
25690
  } catch {
24885
25691
  return null;
24886
25692
  }
@@ -24907,8 +25713,8 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24907
25713
  const repoValid = validateDocsRepo(repoPath);
24908
25714
  if (!repoValid.ok)
24909
25715
  return { ok: false, error: repoValid.error };
24910
- const specRel = path21.posix.join("docs", slug, "spec.md");
24911
- const planRel = path21.posix.join("docs", slug, "plan.md");
25716
+ const specRel = path22.posix.join("docs", slug, "spec.md");
25717
+ const planRel = path22.posix.join("docs", slug, "plan.md");
24912
25718
  const specText = readSafe4(resolved.layout.spec);
24913
25719
  if (specText === null)
24914
25720
  return { ok: false, error: `docs/${slug}/spec.md not found` };
@@ -24932,14 +25738,14 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24932
25738
  };
24933
25739
  }
24934
25740
  if (!opts.force) {
24935
- const sddDir = path21.join(workspaceRootCanonical, "docs", slug, "sdd");
24936
- if (existsSync13(sddDir)) {
25741
+ const sddDir = path22.join(workspaceRootCanonical, "docs", slug, "sdd");
25742
+ if (existsSync14(sddDir)) {
24937
25743
  try {
24938
- execFileSync5("git", [
25744
+ execFileSync6("git", [
24939
25745
  "-C",
24940
25746
  workspaceRootCanonical,
24941
25747
  "check-ignore",
24942
- path21.posix.join("docs", slug, "sdd", "progress.md")
25748
+ path22.posix.join("docs", slug, "sdd", "progress.md")
24943
25749
  ], { stdio: "pipe" });
24944
25750
  } catch {
24945
25751
  return {
@@ -24950,14 +25756,14 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24950
25756
  }
24951
25757
  }
24952
25758
  const prefix = monthPrefix();
24953
- const featuresDir = path21.join(repoPath, "features");
24954
- const existing = existsSync13(featuresDir) ? readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
24955
- const targetDir = path21.join(featuresDir, existing ?? `${prefix}-${slug}`);
24956
- mkdirSync8(targetDir, { recursive: true });
25759
+ const featuresDir = path22.join(repoPath, "features");
25760
+ const existing = existsSync14(featuresDir) ? readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
25761
+ const targetDir = path22.join(featuresDir, existing ?? `${prefix}-${slug}`);
25762
+ mkdirSync9(targetDir, { recursive: true });
24957
25763
  const files = ["spec.md"];
24958
- writeFileSync7(path21.join(targetDir, "spec.md"), specText, "utf8");
25764
+ writeFileSync8(path22.join(targetDir, "spec.md"), specText, "utf8");
24959
25765
  if (planText !== null) {
24960
- writeFileSync7(path21.join(targetDir, "plan.md"), planText, "utf8");
25766
+ writeFileSync8(path22.join(targetDir, "plan.md"), planText, "utf8");
24961
25767
  files.push("plan.md");
24962
25768
  }
24963
25769
  const title = specText.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? slug;
@@ -24978,9 +25784,9 @@ ${specSummary(specText)}
24978
25784
  | [spec.md](./spec.md) | Especificación completa |
24979
25785
  ${planText !== null ? `| [plan.md](./plan.md) | Plan de implementación |
24980
25786
  ` : ""}`;
24981
- writeFileSync7(path21.join(targetDir, "README.md"), readme, "utf8");
25787
+ writeFileSync8(path22.join(targetDir, "README.md"), readme, "utf8");
24982
25788
  files.push("README.md");
24983
- const indexPath = path21.join(repoPath, "features", "README.md");
25789
+ const indexPath = path22.join(repoPath, "features", "README.md");
24984
25790
  const indexText = readSafe4(indexPath) ?? `# Features
24985
25791
 
24986
25792
  Especificaciones y planes por feature.
@@ -25003,7 +25809,7 @@ Especificaciones y planes por feature.
25003
25809
  ${row}
25004
25810
  `;
25005
25811
  }
25006
- writeFileSync7(indexPath, newIndex, "utf8");
25812
+ writeFileSync8(indexPath, newIndex, "utf8");
25007
25813
  return { ok: true, target_dir: targetDir, files, index_updated: true };
25008
25814
  };
25009
25815
  var init_docs_repo = __esm(() => {
@@ -25013,13 +25819,13 @@ var init_docs_repo = __esm(() => {
25013
25819
  });
25014
25820
 
25015
25821
  // packages/workit-core/src/core/gitignore.ts
25016
- import { existsSync as existsSync14, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
25017
- import path22 from "node:path";
25822
+ import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
25823
+ import path23 from "node:path";
25018
25824
  var GITIGNORE_ENTRIES, ensureProjectGitignore = (workspaceRoot, confirmed) => {
25019
25825
  if (!confirmed)
25020
25826
  return { ok: false, error: "confirmed: true required" };
25021
- const file = path22.join(workspaceRoot, ".gitignore");
25022
- const existing = existsSync14(file) ? readFileSync15(file, "utf8") : "";
25827
+ const file = path23.join(workspaceRoot, ".gitignore");
25828
+ const existing = existsSync15(file) ? readFileSync16(file, "utf8") : "";
25023
25829
  const existingLines = new Set(existing.split(`
25024
25830
  `).map((l) => l.trim()).filter(Boolean));
25025
25831
  const added = [];
@@ -25034,12 +25840,12 @@ var GITIGNORE_ENTRIES, ensureProjectGitignore = (workspaceRoot, confirmed) => {
25034
25840
  const separator = existing && !existing.endsWith(`
25035
25841
  `) ? `
25036
25842
  ` : "";
25037
- writeFileSync8(file, existing + separator + (existing ? `
25843
+ writeFileSync9(file, existing + separator + (existing ? `
25038
25844
  ` : "") + append.join(`
25039
25845
  `) + `
25040
25846
  `, "utf8");
25041
- } else if (!existsSync14(file)) {
25042
- writeFileSync8(file, "", "utf8");
25847
+ } else if (!existsSync15(file)) {
25848
+ writeFileSync9(file, "", "utf8");
25043
25849
  }
25044
25850
  return { ok: true, path: file, added };
25045
25851
  };
@@ -25063,29 +25869,29 @@ var init_gitignore = __esm(() => {
25063
25869
  });
25064
25870
 
25065
25871
  // packages/workit-core/src/core/templates.ts
25066
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
25067
- import path23 from "node:path";
25068
- var repoRoot4, templatePath = (name) => path23.join(configDir(), "templates", `${name}.md`), readTemplate = (name) => {
25872
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
25873
+ import path24 from "node:path";
25874
+ var repoRoot4, templatePath = (name) => path24.join(configDir(), "templates", `${name}.md`), readTemplate = (name) => {
25069
25875
  const cfg = templatePath(name);
25070
- if (existsSync15(cfg))
25071
- return { source: "config", content: readFileSync16(cfg, "utf8") };
25876
+ if (existsSync16(cfg))
25877
+ return { source: "config", content: readFileSync17(cfg, "utf8") };
25072
25878
  return {
25073
25879
  source: "repo",
25074
- content: readFileSync16(path23.join(repoRoot4, "templates", `${name}.md`), "utf8")
25880
+ content: readFileSync17(path24.join(repoRoot4, "templates", `${name}.md`), "utf8")
25075
25881
  };
25076
25882
  }, writeTemplate = (name, content, confirmed) => {
25077
25883
  if (!confirmed)
25078
25884
  return { ok: false, error: "confirmed: true required" };
25079
25885
  const file = templatePath(name);
25080
- mkdirSync9(path23.dirname(file), { recursive: true });
25081
- writeFileSync9(file, content, "utf8");
25886
+ mkdirSync10(path24.dirname(file), { recursive: true });
25887
+ writeFileSync10(file, content, "utf8");
25082
25888
  return { ok: true, path: file };
25083
25889
  }, listTemplates = () => ["issue-update", "greeting", "headers"].map((name) => {
25084
25890
  const cfg = templatePath(name);
25085
- const repoFile = path23.join(repoRoot4, "templates", `${name}.md`);
25086
- if (existsSync15(cfg))
25891
+ const repoFile = path24.join(repoRoot4, "templates", `${name}.md`);
25892
+ if (existsSync16(cfg))
25087
25893
  return { name, source: "config", path: cfg };
25088
- if (existsSync15(repoFile))
25894
+ if (existsSync16(repoFile))
25089
25895
  return { name, source: "repo", path: repoFile };
25090
25896
  return { name, source: "missing", path: cfg };
25091
25897
  });
@@ -25096,9 +25902,9 @@ var init_templates = __esm(() => {
25096
25902
  });
25097
25903
 
25098
25904
  // packages/workit-core/src/core/rules.ts
25099
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync17, readdirSync as readdirSync8, writeFileSync as writeFileSync10 } from "node:fs";
25100
- import path24 from "node:path";
25101
- var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) => {
25905
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
25906
+ import path25 from "node:path";
25907
+ var rulesDir = () => path25.join(configDir(), "rules"), parseRule = (markdown) => {
25102
25908
  const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
25103
25909
  if (!fm)
25104
25910
  return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
@@ -25125,12 +25931,12 @@ var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) =
25125
25931
  }, listRules = () => {
25126
25932
  const result = [];
25127
25933
  const dir = rulesDir();
25128
- if (existsSync16(dir)) {
25934
+ if (existsSync17(dir)) {
25129
25935
  for (const entry of readdirSync8(dir)) {
25130
- const file = path24.join(dir, entry, "rule.md");
25131
- if (!existsSync16(file))
25936
+ const file = path25.join(dir, entry, "rule.md");
25937
+ if (!existsSync17(file))
25132
25938
  continue;
25133
- const parsed = parseRule(readFileSync17(file, "utf8"));
25939
+ const parsed = parseRule(readFileSync18(file, "utf8"));
25134
25940
  if ("error" in parsed)
25135
25941
  continue;
25136
25942
  result.push({ name: parsed.name, platforms: parsed.platforms, source: "config" });
@@ -25142,16 +25948,16 @@ var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) =
25142
25948
  return { ok: false, error: "confirmed: true required" };
25143
25949
  if (!RULE_NAME_RE.test(rule.name))
25144
25950
  return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
25145
- const dir = path24.join(rulesDir(), rule.name);
25146
- mkdirSync10(dir, { recursive: true });
25147
- const file = path24.join(dir, "rule.md");
25951
+ const dir = path25.join(rulesDir(), rule.name);
25952
+ mkdirSync11(dir, { recursive: true });
25953
+ const file = path25.join(dir, "rule.md");
25148
25954
  const md = `---
25149
25955
  name: ${rule.name}
25150
25956
  description: ${rule.description}
25151
25957
  platforms: [${rule.platforms.join(", ")}]
25152
25958
  ---
25153
25959
  ${rule.body}`;
25154
- writeFileSync10(file, md, "utf8");
25960
+ writeFileSync11(file, md, "utf8");
25155
25961
  return { ok: true, path: file };
25156
25962
  };
25157
25963
  var init_rules = __esm(() => {
@@ -25160,10 +25966,10 @@ var init_rules = __esm(() => {
25160
25966
  });
25161
25967
 
25162
25968
  // packages/workit-core/src/core/safe-write.ts
25163
- import { writeFileSync as writeFileSync11 } from "node:fs";
25969
+ import { writeFileSync as writeFileSync12 } from "node:fs";
25164
25970
  function writeFileExclusive(file, content, mode) {
25165
25971
  try {
25166
- writeFileSync11(file, content, { encoding: "utf8", flag: "wx", mode });
25972
+ writeFileSync12(file, content, { encoding: "utf8", flag: "wx", mode });
25167
25973
  return "created";
25168
25974
  } catch (err3) {
25169
25975
  if (err3.code === "EEXIST")
@@ -25245,26 +26051,26 @@ var init_setup_state = __esm(() => {
25245
26051
  import {
25246
26052
  copyFileSync as copyFileSync2,
25247
26053
  cpSync as cpSync2,
25248
- existsSync as existsSync17,
25249
- mkdirSync as mkdirSync11,
26054
+ existsSync as existsSync18,
26055
+ mkdirSync as mkdirSync12,
25250
26056
  mkdtempSync,
25251
- readFileSync as readFileSync18,
26057
+ readFileSync as readFileSync19,
25252
26058
  readdirSync as readdirSync9,
25253
26059
  renameSync as renameSync3,
25254
26060
  rmSync as rmSync3,
25255
- statSync as statSync7,
25256
- writeFileSync as writeFileSync12
26061
+ statSync as statSync9,
26062
+ writeFileSync as writeFileSync13
25257
26063
  } from "node:fs";
25258
- import path25 from "node:path";
26064
+ import path26 from "node:path";
25259
26065
  import { isDeepStrictEqual } from "node:util";
25260
26066
  function applyWorkspaceBranchPolicy(opts) {
25261
26067
  const { workspace_root, env = process.env } = opts;
25262
- const dir = path25.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
26068
+ const dir = path26.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
25263
26069
  const { status, path: wsPath, entries } = readWorkspacesResult(dir);
25264
26070
  if (status === "malformed")
25265
26071
  return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
25266
26072
  const detection = detectBranchPolicy(workspace_root);
25267
- const name = String(env.WORKFLOW_BP_NAME ?? path25.basename(workspace_root));
26073
+ const name = String(env.WORKFLOW_BP_NAME ?? path26.basename(workspace_root));
25268
26074
  const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
25269
26075
  const policy2 = {
25270
26076
  preset: detection.preset,
@@ -25289,8 +26095,8 @@ function applyWorkspaceBranchPolicy(opts) {
25289
26095
  };
25290
26096
  }
25291
26097
  const next = existing ? entries.map((w, i) => i === idx ? { ...w, branchPolicy: policy2 } : w) : [...entries, { name, glob, branchPolicy: policy2 }];
25292
- mkdirSync11(path25.dirname(wsPath), { recursive: true });
25293
- writeFileSync12(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
26098
+ mkdirSync12(path26.dirname(wsPath), { recursive: true });
26099
+ writeFileSync13(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
25294
26100
  `, "utf8");
25295
26101
  return {
25296
26102
  ok: true,
@@ -25332,7 +26138,7 @@ var init_setup = __esm(() => {
25332
26138
 
25333
26139
  // packages/workit-core/src/core/youtrack.ts
25334
26140
  import fs4 from "node:fs";
25335
- import path26 from "node:path";
26141
+ import path27 from "node:path";
25336
26142
  function readYouTrackConfig(required2) {
25337
26143
  const cfgPath = youTrackConfigPath();
25338
26144
  if (!fs4.existsSync(cfgPath)) {
@@ -25356,7 +26162,7 @@ function youTrackConfigLoad() {
25356
26162
  }
25357
26163
  const cfgPath = loaded.path;
25358
26164
  const tokenFile = String(loaded.config.tokenFile ?? "");
25359
- const tokenPath = tokenFile ? path26.isAbsolute(tokenFile) ? path26.resolve(tokenFile) : path26.resolve(process.cwd(), tokenFile) : "";
26165
+ const tokenPath = tokenFile ? path27.isAbsolute(tokenFile) ? path27.resolve(tokenFile) : path27.resolve(process.cwd(), tokenFile) : "";
25360
26166
  if (!tokenPath || !fs4.existsSync(tokenPath)) {
25361
26167
  return { error: "missing youtrack.token" };
25362
26168
  }
@@ -25370,8 +26176,8 @@ function youTrackConfigLoad() {
25370
26176
  const redacted = { ...loaded.config };
25371
26177
  delete redacted.tokenFile;
25372
26178
  redacted.tokenPresent = true;
25373
- redacted.configPath = path26.resolve(cfgPath);
25374
- redacted.tokenPath = path26.resolve(tokenPath);
26179
+ redacted.configPath = path27.resolve(cfgPath);
26180
+ redacted.tokenPath = path27.resolve(tokenPath);
25375
26181
  return { data: redacted };
25376
26182
  }
25377
26183
  function tzParts(date4, tz) {
@@ -25616,7 +26422,7 @@ function youTrackTokenCreateUrl() {
25616
26422
  const desc = String(defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings");
25617
26423
  const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
25618
26424
  const base = String(config2.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
25619
- const tokenFile = String(config2.tokenFile ?? path26.join(path26.dirname(loaded.path), "youtrack.token"));
26425
+ const tokenFile = String(config2.tokenFile ?? path27.join(path27.dirname(loaded.path), "youtrack.token"));
25620
26426
  const tab = String(defaults.profileTab ?? "account-security");
25621
26427
  const createUrl = `${base}/users/me?${new URLSearchParams({ tab })}`;
25622
26428
  const docsUrl = "https://www.jetbrains.com/help/youtrack/cloud/manage-permanent-token.html";
@@ -25625,7 +26431,7 @@ function youTrackTokenCreateUrl() {
25625
26431
  tokenName: name,
25626
26432
  tokenDescription: desc,
25627
26433
  scopes,
25628
- tokenFile: path26.resolve(tokenFile),
26434
+ tokenFile: path27.resolve(tokenFile),
25629
26435
  createUrl,
25630
26436
  docsUrl,
25631
26437
  prefillSupported: false,
@@ -25662,7 +26468,7 @@ function verifyYouTrackToken(scripts = defaultScripts) {
25662
26468
  function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25663
26469
  const root = resolveWorkspaceRoot(workspace_root);
25664
26470
  for (const rel of [spec_path, plan_path].filter(Boolean)) {
25665
- const full = path26.isAbsolute(rel) ? rel : path26.join(root, rel);
26471
+ const full = path27.isAbsolute(rel) ? rel : path27.join(root, rel);
25666
26472
  if (!fs4.existsSync(full))
25667
26473
  continue;
25668
26474
  const text = fs4.readFileSync(full, "utf8");
@@ -25841,7 +26647,7 @@ async function postUpdate({
25841
26647
  }
25842
26648
  return { ok: true, issueId, postedComment: true };
25843
26649
  }
25844
- var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path26.join(configDir(), "youtrack.json"), youTrackTokenModeOk = (p) => {
26650
+ var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path27.join(configDir(), "youtrack.json"), youTrackTokenModeOk = (p) => {
25845
26651
  if (process.platform === "win32")
25846
26652
  return true;
25847
26653
  const mode = fs4.statSync(p).mode & 511;
@@ -25851,7 +26657,7 @@ var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () =>
25851
26657
  if ("error" in loaded)
25852
26658
  return loaded;
25853
26659
  const tokenFile = String(loaded.config.tokenFile ?? "");
25854
- const tokenPath = tokenFile ? path26.isAbsolute(tokenFile) ? path26.resolve(tokenFile) : path26.resolve(process.cwd(), tokenFile) : "";
26660
+ const tokenPath = tokenFile ? path27.isAbsolute(tokenFile) ? path27.resolve(tokenFile) : path27.resolve(process.cwd(), tokenFile) : "";
25855
26661
  if (!tokenPath || !fs4.existsSync(tokenPath))
25856
26662
  return { error: "missing youtrack.token" };
25857
26663
  if (!youTrackTokenModeOk(tokenPath))
@@ -25884,10 +26690,10 @@ var init_youtrack = __esm(() => {
25884
26690
 
25885
26691
  // packages/workit-core/src/core/init.ts
25886
26692
  import fs5 from "node:fs";
25887
- import path27 from "node:path";
26693
+ import path28 from "node:path";
25888
26694
  function initStatusData(configDirPath = configDir()) {
25889
- const ytJson = path27.join(configDirPath, "youtrack.json");
25890
- const vcsJson = path27.join(configDirPath, "vcs.json");
26695
+ const ytJson = path28.join(configDirPath, "youtrack.json");
26696
+ const vcsJson = path28.join(configDirPath, "vcs.json");
25891
26697
  const items = [];
25892
26698
  let youtrackConfig = null;
25893
26699
  let youtrackTokenCreate = null;
@@ -25910,8 +26716,8 @@ function initStatusData(configDirPath = configDir()) {
25910
26716
  });
25911
26717
  }
25912
26718
  }
25913
- const expanded = tokenFile ? path27.resolve(tokenFile) : null;
25914
- const resolvedTokenFile = expanded && fs5.existsSync(expanded) ? resolvePath(expanded) : expanded ? path27.isAbsolute(expanded) ? expanded : path27.resolve(configDirPath, expanded) : null;
26719
+ const expanded = tokenFile ? path28.resolve(tokenFile) : null;
26720
+ const resolvedTokenFile = expanded && fs5.existsSync(expanded) ? resolvePath(expanded) : expanded ? path28.isAbsolute(expanded) ? expanded : path28.resolve(configDirPath, expanded) : null;
25915
26721
  youtrackConfig = {
25916
26722
  config_edit_path: resolvePath(ytJson),
25917
26723
  baseUrl: base,
@@ -25958,11 +26764,11 @@ function initStatusData(configDirPath = configDir()) {
25958
26764
  id: "youtrack_json",
25959
26765
  label: "YouTrack config",
25960
26766
  ok: fs5.existsSync(ytJson) && youtrackConfig !== null && !("error" in youtrackConfig),
25961
- path: fs5.existsSync(ytJson) ? resolvePath(ytJson) : path27.resolve(ytJson),
26767
+ path: fs5.existsSync(ytJson) ? resolvePath(ytJson) : path28.resolve(ytJson),
25962
26768
  config_edit_path: resolvePath(ytJson),
25963
26769
  fix: "workflow_toolkit_init_apply action=youtrack_scaffold"
25964
26770
  });
25965
- const ytTokenPath = youtrackConfig?.tokenFile ?? path27.join(configDirPath, "youtrack.token");
26771
+ const ytTokenPath = youtrackConfig?.tokenFile ?? path28.join(configDirPath, "youtrack.token");
25966
26772
  const tokenText = fs5.existsSync(ytTokenPath) ? fs5.readFileSync(ytTokenPath, "utf8").trim() : "";
25967
26773
  const placeholder = fs5.existsSync(ytTokenPath) && isPlaceholder2(tokenText);
25968
26774
  const tokenOk = fs5.existsSync(ytTokenPath) && modeOk(ytTokenPath) && Boolean(tokenText) && !isPlaceholder2(tokenText);
@@ -25970,8 +26776,8 @@ function initStatusData(configDirPath = configDir()) {
25970
26776
  id: "youtrack_token",
25971
26777
  label: "YouTrack API token (mode 600, not placeholder)",
25972
26778
  ok: tokenOk,
25973
- path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path27.resolve(ytTokenPath),
25974
- token_edit_path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path27.resolve(ytTokenPath),
26779
+ path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path28.resolve(ytTokenPath),
26780
+ token_edit_path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path28.resolve(ytTokenPath),
25975
26781
  placeholder,
25976
26782
  fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER4} with your permanent token, save, then /wk-status`
25977
26783
  };
@@ -25996,7 +26802,7 @@ function initStatusData(configDirPath = configDir()) {
25996
26802
  const provider = String(vcsParsed.provider ?? "gitlab").toLowerCase();
25997
26803
  const tokenFiles = {};
25998
26804
  for (const k of ["gitlab", "github"]) {
25999
- tokenFiles[k] = String(vcsParsed[k]?.tokenFile ?? path27.join(configDirPath, `${k}.token`));
26805
+ tokenFiles[k] = String(vcsParsed[k]?.tokenFile ?? path28.join(configDirPath, `${k}.token`));
26000
26806
  }
26001
26807
  vcsCfg = {
26002
26808
  config_edit_path: resolvePath(vcsJson),
@@ -26021,14 +26827,14 @@ function initStatusData(configDirPath = configDir()) {
26021
26827
  id: "vcs_json",
26022
26828
  label: "VCS config (GitLab / GitHub)",
26023
26829
  ok: fs5.existsSync(vcsJson) && vcsCfg !== null && !("error" in vcsCfg),
26024
- path: fs5.existsSync(vcsJson) ? resolvePath(vcsJson) : path27.resolve(vcsJson),
26830
+ path: fs5.existsSync(vcsJson) ? resolvePath(vcsJson) : path28.resolve(vcsJson),
26025
26831
  config_edit_path: resolvePath(vcsJson),
26026
26832
  fix: "workflow_toolkit_init_apply action=vcs_scaffold"
26027
26833
  });
26028
26834
  const provActive = vcsCfg && !("error" in vcsCfg) ? vcsCfg.provider : null;
26029
26835
  const tokenItem = (tid, label, rawPath, providerKey) => {
26030
- const t = path27.isAbsolute(rawPath) ? rawPath : path27.resolve(configDirPath, rawPath);
26031
- const abs = fs5.existsSync(t) ? resolvePath(t) : path27.resolve(t);
26836
+ const t = path28.isAbsolute(rawPath) ? rawPath : path28.resolve(configDirPath, rawPath);
26837
+ const abs = fs5.existsSync(t) ? resolvePath(t) : path28.resolve(t);
26032
26838
  const text = fs5.existsSync(t) ? fs5.readFileSync(t, "utf8").trim() : "";
26033
26839
  const ph = isPlaceholder2(text);
26034
26840
  const ok2 = fs5.existsSync(t) && modeOk(t) && Boolean(text) && !ph;
@@ -26059,7 +26865,7 @@ function initStatusData(configDirPath = configDir()) {
26059
26865
  };
26060
26866
  const vcsTokenFiles = {};
26061
26867
  for (const k of ["gitlab", "github"]) {
26062
- vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path27.join(configDirPath, `${k}.token`));
26868
+ vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path28.join(configDirPath, `${k}.token`));
26063
26869
  }
26064
26870
  items.push(tokenItem("gitlab_token", "GitLab token (mode 600, not placeholder)", vcsTokenFiles.gitlab, "gitlab"));
26065
26871
  items.push(tokenItem("github_token", "GitHub token (mode 600, not placeholder)", vcsTokenFiles.github, "github"));
@@ -26116,16 +26922,16 @@ function initApplyData(action, env = process.env) {
26116
26922
  fs5.mkdirSync(dir, { recursive: true });
26117
26923
  switch (action) {
26118
26924
  case "youtrack_json": {
26119
- const out = path27.join(dir, "youtrack.json");
26925
+ const out = path28.join(dir, "youtrack.json");
26120
26926
  fs5.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
26121
26927
  `, "utf8");
26122
26928
  return { action, ok: true, path: out };
26123
26929
  }
26124
26930
  case "youtrack_token_placeholder": {
26125
- const p = path27.join(dir, "youtrack.token");
26931
+ const p = path28.join(dir, "youtrack.token");
26126
26932
  const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER4 + `
26127
26933
  `, 384) === "preserved";
26128
- const abs = path27.resolve(p);
26934
+ const abs = path28.resolve(p);
26129
26935
  return {
26130
26936
  action,
26131
26937
  ok: true,
@@ -26137,14 +26943,14 @@ function initApplyData(action, env = process.env) {
26137
26943
  };
26138
26944
  }
26139
26945
  case "youtrack_scaffold": {
26140
- const jsonOut = path27.join(dir, "youtrack.json");
26141
- const tokenOut = path27.join(dir, "youtrack.token");
26946
+ const jsonOut = path28.join(dir, "youtrack.json");
26947
+ const tokenOut = path28.join(dir, "youtrack.token");
26142
26948
  fs5.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
26143
26949
  `, "utf8");
26144
26950
  const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER4 + `
26145
26951
  `, 384) === "preserved";
26146
- const configPath2 = path27.resolve(jsonOut);
26147
- const tokenPath = path27.resolve(tokenOut);
26952
+ const configPath2 = path28.resolve(jsonOut);
26953
+ const tokenPath = path28.resolve(tokenOut);
26148
26954
  const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
26149
26955
  process.env.WORKFLOW_YOUTRACK_CONFIG = configPath2;
26150
26956
  let tokenCreate = {};
@@ -26198,19 +27004,19 @@ function initApplyData(action, env = process.env) {
26198
27004
  }
26199
27005
  }
26200
27006
  case "vcs_scaffold": {
26201
- const jsonOut = path27.join(dir, "vcs.json");
27007
+ const jsonOut = path28.join(dir, "vcs.json");
26202
27008
  fs5.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(dir), null, 2) + `
26203
27009
  `, "utf8");
26204
- const glPath = path27.join(dir, "gitlab.token");
26205
- const ghPath = path27.join(dir, "github.token");
27010
+ const glPath = path28.join(dir, "gitlab.token");
27011
+ const ghPath = path28.join(dir, "github.token");
26206
27012
  const preservedTokens = [];
26207
27013
  for (const p of [glPath, ghPath]) {
26208
27014
  if (writeFileExclusive(p, TOKEN_PLACEHOLDER4 + `
26209
27015
  `, 384) === "preserved") {
26210
- preservedTokens.push(path27.resolve(p));
27016
+ preservedTokens.push(path28.resolve(p));
26211
27017
  }
26212
27018
  }
26213
- const configPath2 = path27.resolve(jsonOut);
27019
+ const configPath2 = path28.resolve(jsonOut);
26214
27020
  const prev = process.env.WORKFLOW_VCS_CONFIG;
26215
27021
  process.env.WORKFLOW_VCS_CONFIG = configPath2;
26216
27022
  try {
@@ -26218,14 +27024,14 @@ function initApplyData(action, env = process.env) {
26218
27024
  const provider = String(cfg.provider ?? "gitlab");
26219
27025
  const tokenUrls = vcsTokenCreateUrls();
26220
27026
  const active = tokenUrls.active ?? {};
26221
- const activePath = provider === "gitlab" ? path27.resolve(glPath) : path27.resolve(ghPath);
27027
+ const activePath = provider === "gitlab" ? path28.resolve(glPath) : path28.resolve(ghPath);
26222
27028
  return {
26223
27029
  action,
26224
27030
  ok: true,
26225
27031
  vcs_json: configPath2,
26226
27032
  config_edit_path: configPath2,
26227
- gitlab_token: path27.resolve(glPath),
26228
- github_token: path27.resolve(ghPath),
27033
+ gitlab_token: path28.resolve(glPath),
27034
+ github_token: path28.resolve(ghPath),
26229
27035
  token_edit_path: activePath,
26230
27036
  token_create_url: active.createUrl,
26231
27037
  token_create_urls: tokenUrls,
@@ -26290,11 +27096,11 @@ var TOKEN_PLACEHOLDER4 = "YOUR_TOKEN_HERE", readJson2 = (p) => {
26290
27096
  try {
26291
27097
  return fs5.realpathSync(p);
26292
27098
  } catch {
26293
- return path27.resolve(p);
27099
+ return path28.resolve(p);
26294
27100
  }
26295
27101
  }, youtrackJsonContent = (dir) => ({
26296
27102
  baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
26297
- tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path27.join(dir, "youtrack.token"),
27103
+ tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path28.join(dir, "youtrack.token"),
26298
27104
  timezone: process.env.WORKFLOW_YT_TIMEZONE ?? "America/Santiago",
26299
27105
  locale: "es-CL",
26300
27106
  defaultMention: process.env.WORKFLOW_YT_MENTION ?? "Alejandra.Flores",
@@ -26330,11 +27136,11 @@ var TOKEN_PLACEHOLDER4 = "YOUR_TOKEN_HERE", readJson2 = (p) => {
26330
27136
  gitlab: {
26331
27137
  host: process.env.WORKFLOW_GITLAB_HOST ?? "gitlab.com",
26332
27138
  apiUrl: process.env.WORKFLOW_GITLAB_API_URL ?? "https://gitlab.com/api/v4",
26333
- tokenFile: path27.join(dir, "gitlab.token")
27139
+ tokenFile: path28.join(dir, "gitlab.token")
26334
27140
  },
26335
27141
  github: {
26336
27142
  host: process.env.WORKFLOW_GITHUB_HOST ?? "github.com",
26337
- tokenFile: path27.join(dir, "github.token")
27143
+ tokenFile: path28.join(dir, "github.token")
26338
27144
  },
26339
27145
  pr: { squashOnMerge: true, removeSourceBranch: true, pushBranch: true, confirmSkip: true },
26340
27146
  tokenDefaults: {
@@ -26355,185 +27161,6 @@ var init_init = __esm(() => {
26355
27161
  init_youtrack();
26356
27162
  });
26357
27163
 
26358
- // packages/workit-core/src/core/sdd.ts
26359
- import {
26360
- appendFileSync as appendFileSync2,
26361
- existsSync as existsSync18,
26362
- mkdirSync as mkdirSync12,
26363
- readFileSync as readFileSync19,
26364
- statSync as statSync8,
26365
- writeFileSync as writeFileSync13
26366
- } from "node:fs";
26367
- import { execFileSync as execFileSync6 } from "node:child_process";
26368
- import path28 from "node:path";
26369
- function todosFromTasks(tasks, completedTaskIds = []) {
26370
- const done = new Set((completedTaskIds ?? []).map((id) => Number(id)));
26371
- const todos = (tasks ?? []).map((t) => {
26372
- const id = Number(t.id);
26373
- return {
26374
- id: `task-${id}`,
26375
- content: `Task ${id}: ${t.title ?? ""}`.trim(),
26376
- status: done.has(id) ? "completed" : "pending"
26377
- };
26378
- });
26379
- const firstPending = todos.find((t) => t.status === "pending");
26380
- if (firstPending)
26381
- firstPending.status = "in_progress";
26382
- return todos;
26383
- }
26384
- function sddContext({
26385
- slug,
26386
- plan_path,
26387
- workspace_root
26388
- }) {
26389
- const resolved = resolveCanonicalLayout({ workspace_root, slug, plan_path });
26390
- if (!resolved.ok)
26391
- return { error: resolved.error };
26392
- const cwd = resolved.layout.workspace;
26393
- const resolvedSlug = resolved.layout.slug;
26394
- if (!resolvedSlug)
26395
- return { error: "slug or plan_path required" };
26396
- const sdd_dir = path28.posix.join("docs", resolvedSlug, "sdd");
26397
- const progress_path = path28.posix.join(sdd_dir, "progress.md");
26398
- const manifest_path = path28.posix.join(sdd_dir, "manifest.json");
26399
- let progress_lines = [];
26400
- let completed_task_ids = [];
26401
- const absProgress = path28.join(resolved.layout.sdd, "progress.md");
26402
- if (existsSync18(absProgress)) {
26403
- progress_lines = readFileSync19(absProgress, "utf8").split(`
26404
- `).map((ln) => ln.trim()).filter(Boolean);
26405
- const pat = /^Task\s+(\d+):\s+complete\b/i;
26406
- completed_task_ids = progress_lines.map((ln) => pat.exec(ln)?.[1]).filter(Boolean).map(Number);
26407
- }
26408
- let manifest = {};
26409
- const absManifest = path28.join(resolved.layout.sdd, "manifest.json");
26410
- if (existsSync18(absManifest)) {
26411
- try {
26412
- manifest = JSON.parse(readFileSync19(absManifest, "utf8"));
26413
- } catch {
26414
- manifest = {};
26415
- }
26416
- }
26417
- const legacy_path = path28.join(cwd, ".superpowers/sdd");
26418
- const legacy_exists = existsSync18(legacy_path);
26419
- let todos = [];
26420
- let task_count = 0;
26421
- if (plan_path) {
26422
- const planText = readFileSync19(resolved.layout.plan, "utf8");
26423
- const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
26424
- const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
26425
- if (spec_path) {
26426
- const validated = docsValidate({
26427
- spec_path,
26428
- plan_path: path28.relative(cwd, resolved.layout.plan),
26429
- workspace_root: cwd
26430
- });
26431
- if (validated.ok === false)
26432
- return { ok: false, errors: validated.errors, error: validated.error };
26433
- }
26434
- const tasks = parseTasksFromPlan(planText);
26435
- if (tasks.length > 0) {
26436
- task_count = tasks.length;
26437
- todos = todosFromTasks(tasks, completed_task_ids);
26438
- }
26439
- }
26440
- const flow = readFlowState(cwd, resolvedSlug);
26441
- return {
26442
- slug: resolvedSlug,
26443
- sdd_dir,
26444
- progress_path,
26445
- manifest_path,
26446
- progress_lines,
26447
- completed_task_ids,
26448
- manifest,
26449
- created: existsSync18(path28.join(cwd, sdd_dir)),
26450
- forbidden_legacy_path: ".superpowers/sdd",
26451
- legacy_sdd_exists: legacy_exists,
26452
- warning: legacy_exists ? "Ignore .superpowers/sdd — use sdd_dir from this tool only" : undefined,
26453
- todos,
26454
- task_count,
26455
- flow: { spec: flow.spec, plan: flow.plan, menu: flow.menu },
26456
- todowrite_required: true,
26457
- todowrite_hint: "REQUIRED: Call OpenCode todowrite with todos from this result so the native task list shows progress. Before each task set status in_progress; after workflow_sdd_append_progress set it completed."
26458
- };
26459
- }
26460
- function sddTaskBrief({
26461
- sdd_dir,
26462
- task_id,
26463
- section_text,
26464
- workspace_root
26465
- }) {
26466
- const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
26467
- if (!contained.ok)
26468
- return { error: contained.error };
26469
- const dir = contained.path;
26470
- mkdirSync12(dir, { recursive: true });
26471
- const out = path28.join(dir, `task-${task_id}-brief.md`);
26472
- writeFileSync13(out, `# Task ${task_id} brief
26473
-
26474
- ${section_text}
26475
- `, "utf8");
26476
- const rel = posix3(path28.relative(contained.base, out));
26477
- return { brief_path: rel, task_id };
26478
- }
26479
- function sddReviewPackage({
26480
- sdd_dir,
26481
- base_sha,
26482
- head_sha,
26483
- workspace_root
26484
- }) {
26485
- const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
26486
- if (!contained.ok)
26487
- return { error: contained.error };
26488
- const dir = contained.path;
26489
- mkdirSync12(dir, { recursive: true });
26490
- const base7 = base_sha.slice(0, 7);
26491
- const head7 = head_sha.slice(0, 7);
26492
- const diffPath = path28.join(dir, `review-${base7}..${head7}.diff`);
26493
- try {
26494
- const diff = execFileSync6("git", ["diff", base_sha, head_sha], {
26495
- cwd: contained.base,
26496
- encoding: "utf8",
26497
- stdio: ["pipe", "pipe", "pipe"]
26498
- });
26499
- writeFileSync13(diffPath, diff, "utf8");
26500
- const rel = posix3(path28.relative(contained.base, diffPath));
26501
- return { diff_path: rel, base_sha, head_sha, base7, head7 };
26502
- } catch (error2) {
26503
- return { error: error2 instanceof Error ? error2.message : "git diff failed" };
26504
- }
26505
- }
26506
- function sddAppendProgress({
26507
- progress_path,
26508
- line,
26509
- workspace_root
26510
- }) {
26511
- const contained = resolveDocsPath({ workspace_root, path: progress_path });
26512
- if (!contained.ok)
26513
- return { error: contained.error };
26514
- const path_ = contained.path;
26515
- const trimmed = line.trim();
26516
- if (!PROGRESS_RE.test(trimmed)) {
26517
- return { error: "invalid progress line format" };
26518
- }
26519
- if (existsSync18(path_) && statSync8(path_).isDirectory()) {
26520
- return { error: `progress path is a directory: ${progress_path}` };
26521
- }
26522
- mkdirSync12(path28.dirname(path_), { recursive: true });
26523
- appendFileSync2(path_, trimmed + `
26524
- `, "utf8");
26525
- const rel = posix3(path28.relative(contained.base, path_));
26526
- return { ok: true, line: trimmed, progress_path: rel };
26527
- }
26528
- var posix3 = (p) => p.split(path28.sep).join("/"), PROGRESS_RE;
26529
- var init_sdd = __esm(() => {
26530
- init_docs_validate();
26531
- init_docs_validate();
26532
- init_flow_state();
26533
- init_docs_layout();
26534
- PROGRESS_RE = /^Task\s+\d+:\s+complete\s+\(commits\s+[0-9a-f]{7,40}\.\.[0-9a-f]{7,40},/i;
26535
- });
26536
-
26537
27164
  // packages/workit-core/src/core/present.ts
26538
27165
  function boxLine(text, width) {
26539
27166
  const inner = width - 4;
@@ -26686,7 +27313,38 @@ var logger, readServerVersion = () => {
26686
27313
  };
26687
27314
  }
26688
27315
  });
26689
- }, changelogCategorySchema, transport;
27316
+ }, changelogCategorySchema, lifecycleTool = (action, description) => {
27317
+ registerTool(`workflow_plan_${action}`, {
27318
+ description,
27319
+ inputSchema: {
27320
+ plan_path: exports_external.string(),
27321
+ workspace_root: workspaceRootSchema
27322
+ }
27323
+ }, async ({ plan_path, workspace_root }) => {
27324
+ const resolved = resolveCanonicalLayout({ workspace_root, plan_path });
27325
+ if (!resolved.ok) {
27326
+ return jsonResult(withWorkspace(workspace_root, { error: resolved.error }));
27327
+ }
27328
+ const { workspace, slug } = resolved.layout;
27329
+ const result = transitionExecution(workspace, slug, plan_path, action, cursorConfirmation(), cursorMutationContext(workspace));
27330
+ if (result.ok === false) {
27331
+ return jsonResult(withWorkspace(workspace_root, {
27332
+ error: result.error,
27333
+ code: result.code,
27334
+ ...result.details ? { details: result.details } : {}
27335
+ }));
27336
+ }
27337
+ const effective = readEffectiveFlowState(workspace, slug);
27338
+ if (!effective.ok) {
27339
+ return jsonResult(withWorkspace(workspace_root, { error: effective.error, code: effective.code }));
27340
+ }
27341
+ return jsonResult(withWorkspace(workspace_root, {
27342
+ plan: plan_path,
27343
+ execution: effective.state.execution,
27344
+ drift: effective.drift
27345
+ }));
27346
+ });
27347
+ }, transport;
26690
27348
  var init_server3 = __esm(async () => {
26691
27349
  init_mcp();
26692
27350
  init_stdio2();
@@ -27190,40 +27848,54 @@ var init_server3 = __esm(async () => {
27190
27848
  return jsonResult(withWorkspace(workspace_root, { error: built.error }));
27191
27849
  }
27192
27850
  const { prompt, spec: specPath, plan: planPath } = built;
27193
- if (planPath) {
27194
- const tasksData = parsePlanTasks(planPath, root);
27195
- if ("error" in tasksData) {
27196
- return jsonResult(withWorkspace(workspace_root, {
27197
- prompt,
27198
- error: tasksData.error
27199
- }));
27200
- }
27201
- const payload = {
27851
+ if (!planPath) {
27852
+ return jsonResult(withWorkspace(workspace_root, {
27853
+ error: "Could not resolve spec and plan for handoff"
27854
+ }));
27855
+ }
27856
+ const tasksData = parsePlanTasks(planPath, root);
27857
+ if ("error" in tasksData) {
27858
+ return jsonResult(withWorkspace(workspace_root, {
27202
27859
  prompt,
27203
- tasks: tasksData.tasks,
27204
- task_count: tasksData.task_count,
27205
- workspace_root: root
27206
- };
27207
- if (specPath) {
27208
- const branchData = resolveHandoffBranch(specPath, planPath, root);
27209
- if (!("error" in branchData)) {
27210
- payload.branch = branchData.branch;
27211
- }
27212
- }
27213
- const sdd = sddContext({ plan_path: planPath, workspace_root: root });
27214
- if (!sdd.error) {
27215
- payload.slug = sdd.slug;
27216
- payload.sdd_dir = sdd.sdd_dir;
27217
- payload.progress_path = sdd.progress_path;
27218
- payload.completed_task_ids = sdd.completed_task_ids;
27219
- payload.todos = sdd.todos;
27220
- payload.todo_write_required = true;
27860
+ error: tasksData.error
27861
+ }));
27862
+ }
27863
+ const slug = slugFromPath(planPath);
27864
+ const marked = markHandoffDestination(root, slug, planPath);
27865
+ if (marked.ok === false) {
27866
+ return jsonResult(withWorkspace(workspace_root, {
27867
+ error: marked.error,
27868
+ code: marked.code,
27869
+ ...marked.details ? { details: marked.details } : {}
27870
+ }));
27871
+ }
27872
+ const payload = {
27873
+ prompt,
27874
+ tasks: tasksData.tasks,
27875
+ task_count: tasksData.task_count,
27876
+ workspace_root: root
27877
+ };
27878
+ if (specPath) {
27879
+ const branchData = resolveHandoffBranch(specPath, planPath, root);
27880
+ if (!("error" in branchData)) {
27881
+ payload.branch = branchData.branch;
27221
27882
  }
27222
- return jsonResult(withWorkspace(workspace_root, payload));
27223
27883
  }
27224
- return jsonResult(withWorkspace(workspace_root, {
27225
- error: "Could not resolve spec and plan for handoff"
27226
- }));
27884
+ const sdd = sddContext({ plan_path: planPath, workspace_root: root });
27885
+ if (!sdd.error) {
27886
+ payload.slug = sdd.slug;
27887
+ payload.sdd_dir = sdd.sdd_dir;
27888
+ payload.progress_path = sdd.progress_path;
27889
+ payload.completed_task_ids = sdd.completed_task_ids;
27890
+ payload.todos = sdd.todos;
27891
+ payload.todo_write_required = true;
27892
+ }
27893
+ const effective = readEffectiveFlowState(root, slug);
27894
+ if (effective.ok) {
27895
+ payload.handoff_destination = effective.state.handoff_destination;
27896
+ payload.menu = effective.state.menu;
27897
+ }
27898
+ return jsonResult(withWorkspace(workspace_root, payload));
27227
27899
  });
27228
27900
  registerTool("workflow_toolkit_init_status", {
27229
27901
  description: "Check workit setup (MCP deps, YouTrack config, token)",
@@ -27520,18 +28192,23 @@ var init_server3 = __esm(async () => {
27520
28192
  if (!resolved.ok)
27521
28193
  return jsonResult(withWorkspace(workspace_root, { error: resolved.error }));
27522
28194
  const { workspace, slug } = resolved.layout;
27523
- let state = readFlowState(workspace, slug);
27524
- if (!state.activated) {
28195
+ let effective = readEffectiveFlowState(workspace, slug);
28196
+ if (!effective.ok && effective.code === "flow_not_activated") {
27525
28197
  const prepared = prepareFlowState(workspace, slug, { spec_path, plan_path }, cursorMutationContext(workspace));
27526
28198
  if (!prepared.ok)
27527
28199
  return jsonResult({ error: prepared.error, code: prepared.code });
27528
- state = readFlowState(workspace, slug);
28200
+ effective = readEffectiveFlowState(workspace, slug);
27529
28201
  }
28202
+ if (!effective.ok)
28203
+ return jsonResult({ error: effective.error, code: effective.code });
28204
+ const { state, drift } = effective;
27530
28205
  return jsonResult({
27531
28206
  slug,
27532
28207
  spec: state.spec,
27533
28208
  plan: state.plan,
27534
28209
  menu: state.menu,
28210
+ execution: state.execution,
28211
+ drift,
27535
28212
  flow_path: `docs/${slug}/sdd/flow.json`
27536
28213
  });
27537
28214
  });
@@ -27584,6 +28261,9 @@ var init_server3 = __esm(async () => {
27584
28261
  return jsonResult({ error: result.error, code: result.code });
27585
28262
  return jsonResult({ menu: { presented: true, chosen: choice } });
27586
28263
  });
28264
+ lifecycleTool("pause", "Pause a running plan with the Cursor policy-only confirmation: active -> paused. The MCP cannot observe AskQuestion results, so it records attested: false; there is no evidence argument (CA-42). Requires plan_path and workspace_root.");
28265
+ lifecycleTool("resume", "Resume a paused plan with the Cursor policy-only confirmation: paused -> active. The MCP cannot observe AskQuestion results, so it records attested: false; there is no evidence argument (CA-42). Requires plan_path and workspace_root.");
28266
+ lifecycleTool("complete", "Complete a running plan with the Cursor policy-only confirmation: active/paused -> completed, after the SDD ledger is complete and repository verification passes. The MCP cannot observe AskQuestion results, so it records attested: false; there is no evidence argument (CA-42). Requires plan_path and workspace_root.");
27587
28267
  registerTool("workflow_docs_repo_link", {
27588
28268
  description: "Link the component docs repo in the toolkit config",
27589
28269
  inputSchema: {