@brainervirus/workit-cursor 0.8.1 → 0.8.3

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.
@@ -20945,7 +20945,10 @@ function vcsConfig(mode, cwd) {
20945
20945
  const root = vcsCwd(cwd);
20946
20946
  const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
20947
20947
  const provider = String(wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? "gitlab").toLowerCase();
20948
- const defaultTarget = String(wsVcs.defaultTargetBranch ?? cfg.defaultTargetBranch ?? resolveBranchPolicyFor(root).defaultTargetBranch ?? "develop");
20948
+ const wp = ws?.branchPolicy ?? {};
20949
+ const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
20950
+ const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
20951
+ const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
20949
20952
  const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
20950
20953
  const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
20951
20954
  let issuesProvider = null;
@@ -22640,8 +22643,9 @@ function prCreate(env, cwd) {
22640
22643
  if (provider !== "gitlab" && provider !== "github")
22641
22644
  return { error: `unsupported provider: ${provider}` };
22642
22645
  const targetOverride = env.WF_PR_TARGET;
22643
- const target = targetOverride || String(cfg.defaultTargetBranch ?? policy2.defaultTargetBranch ?? "develop");
22644
- if (targetOverride) {
22646
+ const resolvedDefault = String(cfg.defaultTargetBranch ?? policy2.defaultTargetBranch ?? "develop");
22647
+ const target = targetOverride || resolvedDefault;
22648
+ if (targetOverride && targetOverride !== resolvedDefault) {
22645
22649
  const { allowed, protected: protectedTargets } = policy2;
22646
22650
  if (protectedTargets.has(targetOverride.toLowerCase()))
22647
22651
  return {
@@ -22749,6 +22753,29 @@ function prCreate(env, cwd) {
22749
22753
  cmd.push("--yes");
22750
22754
  cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GITLAB_TOKEN: token };
22751
22755
  } else {
22756
+ if (push) {
22757
+ if (!branch) {
22758
+ return {
22759
+ error: "push failed",
22760
+ provider,
22761
+ mode: "push",
22762
+ targetBranch: target,
22763
+ stderr: "empty current branch (detached HEAD or unborn HEAD)"
22764
+ };
22765
+ }
22766
+ const pushRes = spawnSync5("git", ["push", "-u", "origin", branch], {
22767
+ cwd: root,
22768
+ encoding: "utf8"
22769
+ });
22770
+ if (pushRes.status !== 0) {
22771
+ return {
22772
+ error: "push failed",
22773
+ provider,
22774
+ targetBranch: target,
22775
+ stderr: (pushRes.stderr ?? "").slice(0, 800)
22776
+ };
22777
+ }
22778
+ }
22752
22779
  cmd = ["gh", "pr", "create", "--title", title, "--base", target];
22753
22780
  if (finalBody)
22754
22781
  cmd.push("--body", finalBody);
@@ -23209,14 +23236,23 @@ var init_hygiene = __esm(() => {
23209
23236
  });
23210
23237
 
23211
23238
  // packages/workit-core/src/core/docs-layout.ts
23212
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
23239
+ import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
23213
23240
  import path14 from "node:path";
23214
23241
  var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).join("/"), canonicalize = (base, candidate) => {
23215
23242
  const abs = path14.resolve(base, candidate);
23216
23243
  let ancestor = abs;
23217
23244
  while (!existsSync8(ancestor))
23218
23245
  ancestor = path14.dirname(ancestor);
23219
- const real = realpathSync2(ancestor);
23246
+ let real;
23247
+ try {
23248
+ real = realpathSync2(ancestor);
23249
+ } catch (error2) {
23250
+ if (error2.code === "EACCES" && !lstatSync(ancestor).isSymbolicLink()) {
23251
+ real = path14.join(realpathSync2(path14.dirname(ancestor)), path14.basename(ancestor));
23252
+ } else {
23253
+ throw error2;
23254
+ }
23255
+ }
23220
23256
  if (real !== base && !real.startsWith(base + path14.sep)) {
23221
23257
  throw new Error(`path must stay inside repository root: ${candidate}`);
23222
23258
  }
@@ -23256,14 +23292,8 @@ var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).joi
23256
23292
  if (path14.isAbsolute(candidate)) {
23257
23293
  return { ok: false, error: `absolute path not allowed: ${candidate}` };
23258
23294
  }
23259
- let abs;
23260
- try {
23261
- abs = canonicalize(workspace, candidate);
23262
- } catch (error2) {
23263
- return { ok: false, error: error2 instanceof Error ? error2.message : String(error2) };
23264
- }
23265
- const rel = posix(path14.relative(workspace, abs));
23266
- const match = rel.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
23295
+ const spelling = posix(candidate);
23296
+ const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
23267
23297
  if (!match) {
23268
23298
  return {
23269
23299
  ok: false,
@@ -23290,6 +23320,18 @@ var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).joi
23290
23320
  };
23291
23321
  }
23292
23322
  derived = pathSlug;
23323
+ let abs;
23324
+ try {
23325
+ abs = canonicalize(workspace, candidate);
23326
+ } catch (error2) {
23327
+ return { ok: false, error: error2 instanceof Error ? error2.message : String(error2) };
23328
+ }
23329
+ if (posix(path14.relative(workspace, abs)) !== spelling) {
23330
+ return {
23331
+ ok: false,
23332
+ error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`
23333
+ };
23334
+ }
23293
23335
  }
23294
23336
  let resolvedSlug = slug;
23295
23337
  if (resolvedSlug !== undefined) {
@@ -23798,6 +23840,9 @@ ${JSON.stringify({ ok: false, errors: validated.errors })}`
23798
23840
  return { error: "missing template templates/execution-contract.md" };
23799
23841
  }
23800
23842
  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);
23843
+ if (!/^<workflow-handoff-destination>true<\/workflow-handoff-destination>$/m.test(contract)) {
23844
+ return { error: "handoff destination contract missing its destination marker" };
23845
+ }
23801
23846
  return { prompt: contract };
23802
23847
  };
23803
23848
  var init_handoff_context = __esm(() => {
@@ -23831,9 +23876,240 @@ var init_handoff_tools = __esm(() => {
23831
23876
  init_package_root();
23832
23877
  });
23833
23878
 
23834
- // packages/workit-core/src/core/flow-state.ts
23835
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, renameSync, rmSync, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
23879
+ // packages/workit-core/src/core/sdd.ts
23880
+ import {
23881
+ appendFileSync as appendFileSync2,
23882
+ existsSync as existsSync11,
23883
+ mkdirSync as mkdirSync6,
23884
+ readFileSync as readFileSync12,
23885
+ statSync as statSync6,
23886
+ writeFileSync as writeFileSync5
23887
+ } from "node:fs";
23888
+ import { execFileSync as execFileSync4 } from "node:child_process";
23836
23889
  import path19 from "node:path";
23890
+ function todosFromTasks(tasks, completedTaskIds = []) {
23891
+ const done = new Set((completedTaskIds ?? []).map((id) => Number(id)));
23892
+ const todos = (tasks ?? []).map((t) => {
23893
+ const id = Number(t.id);
23894
+ return {
23895
+ id: `task-${id}`,
23896
+ content: `Task ${id}: ${t.title ?? ""}`.trim(),
23897
+ status: done.has(id) ? "completed" : "pending"
23898
+ };
23899
+ });
23900
+ const firstPending = todos.find((t) => t.status === "pending");
23901
+ if (firstPending)
23902
+ firstPending.status = "in_progress";
23903
+ return todos;
23904
+ }
23905
+ function ledgerCompletion(root, slug) {
23906
+ let started = false;
23907
+ const completed = [];
23908
+ const absProgress = path19.join(root, "docs", slug, "sdd", "progress.md");
23909
+ if (existsSync11(absProgress)) {
23910
+ try {
23911
+ for (const line of readFileSync12(absProgress, "utf8").split(`
23912
+ `)) {
23913
+ const match = /^Task\s+(\d+):/i.exec(line);
23914
+ if (match) {
23915
+ started = true;
23916
+ if (/^Task\s+\d+:\s*complete\b/i.test(line))
23917
+ completed.push(Number(match[1]));
23918
+ }
23919
+ }
23920
+ } catch {}
23921
+ }
23922
+ const required2 = [];
23923
+ try {
23924
+ const absPlan = path19.join(root, "docs", slug, "plan.md");
23925
+ if (existsSync11(absPlan)) {
23926
+ for (const task of parseTasksFromPlan(readFileSync12(absPlan, "utf8")))
23927
+ required2.push(task.id);
23928
+ }
23929
+ } catch {}
23930
+ const completedSet = new Set(completed);
23931
+ const missing = required2.filter((id) => !completedSet.has(id));
23932
+ return {
23933
+ started,
23934
+ complete: required2.length > 0 && missing.length === 0,
23935
+ required: required2,
23936
+ completed,
23937
+ missing
23938
+ };
23939
+ }
23940
+ function sddContext({
23941
+ slug,
23942
+ plan_path,
23943
+ workspace_root
23944
+ }) {
23945
+ const resolved = resolveCanonicalLayout({ workspace_root, slug, plan_path });
23946
+ if (!resolved.ok)
23947
+ return { error: resolved.error };
23948
+ const cwd = resolved.layout.workspace;
23949
+ const resolvedSlug = resolved.layout.slug;
23950
+ if (!resolvedSlug)
23951
+ return { error: "slug or plan_path required" };
23952
+ const sdd_dir = path19.posix.join("docs", resolvedSlug, "sdd");
23953
+ const progress_path = path19.posix.join(sdd_dir, "progress.md");
23954
+ const manifest_path = path19.posix.join(sdd_dir, "manifest.json");
23955
+ let progress_lines = [];
23956
+ let completed_task_ids = [];
23957
+ const absProgress = path19.join(resolved.layout.sdd, "progress.md");
23958
+ if (existsSync11(absProgress)) {
23959
+ progress_lines = readFileSync12(absProgress, "utf8").split(`
23960
+ `).map((ln) => ln.trim()).filter(Boolean);
23961
+ const pat = /^Task\s+(\d+):\s+complete\b/i;
23962
+ completed_task_ids = progress_lines.map((ln) => pat.exec(ln)?.[1]).filter(Boolean).map(Number);
23963
+ }
23964
+ let manifest = {};
23965
+ const absManifest = path19.join(resolved.layout.sdd, "manifest.json");
23966
+ if (existsSync11(absManifest)) {
23967
+ try {
23968
+ manifest = JSON.parse(readFileSync12(absManifest, "utf8"));
23969
+ } catch {
23970
+ manifest = {};
23971
+ }
23972
+ }
23973
+ const legacy_path = path19.join(cwd, ".superpowers/sdd");
23974
+ const legacy_exists = existsSync11(legacy_path);
23975
+ let todos = [];
23976
+ let task_count = 0;
23977
+ if (plan_path) {
23978
+ const planText = readFileSync12(resolved.layout.plan, "utf8");
23979
+ const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
23980
+ const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
23981
+ if (spec_path) {
23982
+ const validated = docsValidate({
23983
+ spec_path,
23984
+ plan_path: path19.relative(cwd, resolved.layout.plan),
23985
+ workspace_root: cwd
23986
+ });
23987
+ if (validated.ok === false)
23988
+ return { ok: false, errors: validated.errors, error: validated.error };
23989
+ }
23990
+ const tasks = parseTasksFromPlan(planText);
23991
+ if (tasks.length > 0) {
23992
+ task_count = tasks.length;
23993
+ todos = todosFromTasks(tasks, completed_task_ids);
23994
+ }
23995
+ }
23996
+ const flow = readFlowState(cwd, resolvedSlug);
23997
+ return {
23998
+ slug: resolvedSlug,
23999
+ sdd_dir,
24000
+ progress_path,
24001
+ manifest_path,
24002
+ progress_lines,
24003
+ completed_task_ids,
24004
+ manifest,
24005
+ created: existsSync11(path19.join(cwd, sdd_dir)),
24006
+ forbidden_legacy_path: ".superpowers/sdd",
24007
+ legacy_sdd_exists: legacy_exists,
24008
+ warning: legacy_exists ? "Ignore .superpowers/sdd — use sdd_dir from this tool only" : undefined,
24009
+ todos,
24010
+ task_count,
24011
+ flow: { spec: flow.spec, plan: flow.plan, menu: flow.menu },
24012
+ todowrite_required: true,
24013
+ 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."
24014
+ };
24015
+ }
24016
+ function sddTaskBrief({
24017
+ sdd_dir,
24018
+ task_id,
24019
+ section_text,
24020
+ workspace_root
24021
+ }) {
24022
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
24023
+ if (!contained.ok)
24024
+ return { error: contained.error };
24025
+ const dir = contained.path;
24026
+ mkdirSync6(dir, { recursive: true });
24027
+ const out = path19.join(dir, `task-${task_id}-brief.md`);
24028
+ writeFileSync5(out, `# Task ${task_id} brief
24029
+
24030
+ ${section_text}
24031
+ `, "utf8");
24032
+ const rel = posix2(path19.relative(contained.base, out));
24033
+ return { brief_path: rel, task_id };
24034
+ }
24035
+ function sddReviewPackage({
24036
+ sdd_dir,
24037
+ base_sha,
24038
+ head_sha,
24039
+ workspace_root
24040
+ }) {
24041
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
24042
+ if (!contained.ok)
24043
+ return { error: contained.error };
24044
+ const dir = contained.path;
24045
+ mkdirSync6(dir, { recursive: true });
24046
+ const base7 = base_sha.slice(0, 7);
24047
+ const head7 = head_sha.slice(0, 7);
24048
+ const diffPath = path19.join(dir, `review-${base7}..${head7}.diff`);
24049
+ try {
24050
+ const diff = execFileSync4("git", ["diff", base_sha, head_sha], {
24051
+ cwd: contained.base,
24052
+ encoding: "utf8",
24053
+ stdio: ["pipe", "pipe", "pipe"]
24054
+ });
24055
+ writeFileSync5(diffPath, diff, "utf8");
24056
+ const rel = posix2(path19.relative(contained.base, diffPath));
24057
+ return { diff_path: rel, base_sha, head_sha, base7, head7 };
24058
+ } catch (error2) {
24059
+ return { error: error2 instanceof Error ? error2.message : "git diff failed" };
24060
+ }
24061
+ }
24062
+ function sddAppendProgress({
24063
+ progress_path,
24064
+ line,
24065
+ workspace_root
24066
+ }) {
24067
+ const contained = resolveDocsPath({ workspace_root, path: progress_path });
24068
+ if (!contained.ok)
24069
+ return { error: contained.error };
24070
+ const path_ = contained.path;
24071
+ const trimmed = line.trim();
24072
+ if (!PROGRESS_RE.test(trimmed)) {
24073
+ return { error: "invalid progress line format" };
24074
+ }
24075
+ if (existsSync11(path_) && statSync6(path_).isDirectory()) {
24076
+ return { error: `progress path is a directory: ${progress_path}` };
24077
+ }
24078
+ mkdirSync6(path19.dirname(path_), { recursive: true });
24079
+ appendFileSync2(path_, trimmed + `
24080
+ `, "utf8");
24081
+ const rel = posix2(path19.relative(contained.base, path_));
24082
+ return { ok: true, line: trimmed, progress_path: rel };
24083
+ }
24084
+ var posix2 = (p) => p.split(path19.sep).join("/"), PROGRESS_RE;
24085
+ var init_sdd = __esm(() => {
24086
+ init_docs_validate();
24087
+ init_docs_validate();
24088
+ init_flow_state();
24089
+ init_docs_layout();
24090
+ PROGRESS_RE = /^Task\s+\d+:\s+complete\s+\(commits\s+[0-9a-f]{7,40}\.\.[0-9a-f]{7,40},/i;
24091
+ });
24092
+
24093
+ // packages/workit-core/src/core/menu.ts
24094
+ var init_menu = () => {};
24095
+
24096
+ // packages/workit-core/src/core/flow-state.ts
24097
+ import {
24098
+ closeSync,
24099
+ existsSync as existsSync12,
24100
+ fstatSync,
24101
+ fsyncSync,
24102
+ mkdirSync as mkdirSync7,
24103
+ openSync,
24104
+ readFileSync as readFileSync13,
24105
+ renameSync,
24106
+ rmSync,
24107
+ statSync as statSync7,
24108
+ unlinkSync as unlinkSync3,
24109
+ writeFileSync as writeFileSync6
24110
+ } from "node:fs";
24111
+ import { createHash } from "node:crypto";
24112
+ import path20 from "node:path";
23837
24113
 
23838
24114
  class HostReceiptStore {
23839
24115
  #bySession = new Map;
@@ -23888,10 +24164,15 @@ class HostReceiptStore {
23888
24164
  return { ok: true, receipt };
23889
24165
  }
23890
24166
  }
23891
- var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, err2 = (code, error2) => ({ ok: false, code, error: error2 }), SLUG_RE2, flowPath = (root, slug) => {
24167
+ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, err2 = (code, error2, details) => ({
24168
+ ok: false,
24169
+ code,
24170
+ error: error2,
24171
+ ...details ? { details } : {}
24172
+ }), SLUG_RE2, flowPath = (root, slug) => {
23892
24173
  if (!SLUG_RE2.test(slug))
23893
24174
  throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
23894
- return path19.join(root, "docs", slug, "sdd", "flow.json");
24175
+ return path20.join(root, "docs", slug, "sdd", "flow.json");
23895
24176
  }, resolveDoc = (root, slug, docPath, kind) => {
23896
24177
  const resolved = resolveCanonicalLayout({
23897
24178
  workspace_root: root,
@@ -23906,60 +24187,233 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23906
24187
  const spec = p.spec ?? {};
23907
24188
  const plan = p.plan ?? {};
23908
24189
  const menu = p.menu ?? {};
24190
+ const execution = p.execution ?? {};
23909
24191
  return {
23910
24192
  slug: p.slug ?? slug,
23911
24193
  activated: p.activated ?? true,
23912
24194
  spec: {
23913
24195
  path: spec.path ?? "",
23914
24196
  status: spec.status ?? "draft",
23915
- evidence: spec.evidence ?? null
24197
+ evidence: spec.evidence ?? null,
24198
+ approved_digest: spec.approved_digest ?? null
23916
24199
  },
23917
24200
  plan: {
23918
24201
  path: plan.path ?? "",
23919
24202
  status: plan.status ?? "draft",
23920
- evidence: plan.evidence ?? null
24203
+ evidence: plan.evidence ?? null,
24204
+ approved_digest: plan.approved_digest ?? null
23921
24205
  },
23922
24206
  menu: {
23923
24207
  presented: Boolean(menu.presented),
23924
24208
  chosen: menu.chosen ?? "",
23925
24209
  evidence: menu.evidence ?? null
23926
24210
  },
24211
+ execution: {
24212
+ status: execution.status ?? "pending",
24213
+ mode: execution.mode ?? null,
24214
+ evidence: execution.evidence ?? null
24215
+ },
24216
+ handoff_destination: p.handoff_destination ?? false,
23927
24217
  updated_at: p.updated_at ?? Date.now()
23928
24218
  };
23929
24219
  }, emptyState = (slug) => ({
23930
24220
  slug,
23931
24221
  activated: false,
23932
- spec: { path: "", status: "draft", evidence: null },
23933
- plan: { path: "", status: "draft", evidence: null },
24222
+ spec: { path: "", status: "draft", evidence: null, approved_digest: null },
24223
+ plan: { path: "", status: "draft", evidence: null, approved_digest: null },
23934
24224
  menu: { presented: false, chosen: "", evidence: null },
24225
+ execution: { status: "pending", mode: null, evidence: null },
24226
+ handoff_destination: false,
23935
24227
  updated_at: Date.now()
23936
24228
  }), readFlowState = (root, slug) => {
23937
24229
  const file = flowPath(root, slug);
23938
- if (!existsSync11(file))
24230
+ if (!existsSync12(file))
23939
24231
  return emptyState(slug);
23940
24232
  try {
23941
- return normalizeState(JSON.parse(readFileSync12(file, "utf8")), slug);
24233
+ return normalizeState(JSON.parse(readFileSync13(file, "utf8")), slug);
23942
24234
  } catch {
23943
24235
  return emptyState(slug);
23944
24236
  }
24237
+ }, HEX64_RE, FLOW_STATUSES, EXECUTION_STATUSES, isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v), validateEvidenceValue = (v, allowCli) => {
24238
+ if (v === null)
24239
+ return true;
24240
+ if (!isRecord(v))
24241
+ return false;
24242
+ if (v.host === "opencode") {
24243
+ return v.attested === true && typeof v.callID === "string" && typeof v.selectedLabel === "string" && typeof v.recordedAt === "number";
24244
+ }
24245
+ if (v.host === "cursor")
24246
+ return v.attested === false && v.confirmation === "contract";
24247
+ if (allowCli && v.host === "cli") {
24248
+ return v.attested === false && (v.confirmation === "flag" || v.confirmation === "tty");
24249
+ }
24250
+ return false;
24251
+ }, validateState = (parsed, slug) => {
24252
+ if (!isRecord(parsed))
24253
+ return { ok: false, error: "flow state must be a JSON object" };
24254
+ if (parsed.slug !== undefined && (typeof parsed.slug !== "string" || parsed.slug !== slug)) {
24255
+ return { ok: false, error: `flow state slug must be ${JSON.stringify(slug)}` };
24256
+ }
24257
+ if (parsed.activated !== undefined && typeof parsed.activated !== "boolean") {
24258
+ return { ok: false, error: "flow state activated must be a boolean" };
24259
+ }
24260
+ if (parsed.handoff_destination !== undefined && typeof parsed.handoff_destination !== "boolean") {
24261
+ return { ok: false, error: "flow state handoff_destination must be a boolean" };
24262
+ }
24263
+ if (parsed.updated_at !== undefined && (typeof parsed.updated_at !== "number" || !Number.isFinite(parsed.updated_at))) {
24264
+ return { ok: false, error: "flow state updated_at must be a finite number" };
24265
+ }
24266
+ const doc2 = (value, name) => {
24267
+ const p = isRecord(value) ? value : {};
24268
+ if (!isRecord(value) && value !== undefined) {
24269
+ return `flow state ${name} must be an object`;
24270
+ }
24271
+ if (p.status !== undefined && !FLOW_STATUSES.includes(p.status)) {
24272
+ return `flow state ${name}.status must be draft, self_reviewed, or approved`;
24273
+ }
24274
+ if (p.path !== undefined && typeof p.path !== "string") {
24275
+ return `flow state ${name}.path must be a string`;
24276
+ }
24277
+ if (p.approved_digest !== undefined && p.approved_digest !== null && (typeof p.approved_digest !== "string" || !HEX64_RE.test(p.approved_digest))) {
24278
+ return `flow state ${name}.approved_digest must be 64-char lowercase hex or null`;
24279
+ }
24280
+ if (p.evidence !== undefined && !validateEvidenceValue(p.evidence, false)) {
24281
+ return `flow state ${name}.evidence has an unsupported shape`;
24282
+ }
24283
+ return {
24284
+ path: p.path ?? "",
24285
+ status: p.status ?? "draft",
24286
+ evidence: p.evidence ?? null,
24287
+ approved_digest: p.approved_digest ?? null
24288
+ };
24289
+ };
24290
+ const spec = doc2(parsed.spec, "spec");
24291
+ if (typeof spec === "string")
24292
+ return { ok: false, error: spec };
24293
+ const plan = doc2(parsed.plan, "plan");
24294
+ if (typeof plan === "string")
24295
+ return { ok: false, error: plan };
24296
+ const menuRaw = isRecord(parsed.menu) ? parsed.menu : undefined;
24297
+ if (parsed.menu !== undefined && !isRecord(parsed.menu)) {
24298
+ return { ok: false, error: "flow state menu must be an object" };
24299
+ }
24300
+ if (menuRaw?.presented !== undefined && typeof menuRaw.presented !== "boolean") {
24301
+ return { ok: false, error: "flow state menu.presented must be a boolean" };
24302
+ }
24303
+ if (menuRaw?.chosen !== undefined && typeof menuRaw.chosen !== "string") {
24304
+ return { ok: false, error: "flow state menu.chosen must be a string" };
24305
+ }
24306
+ if (menuRaw?.chosen !== undefined && menuRaw.chosen !== "" && !MENU_CHOICES.includes(menuRaw.chosen)) {
24307
+ return {
24308
+ ok: false,
24309
+ error: `flow state menu.chosen must be one of: ${MENU_CHOICES.join(", ")} (or an empty string when the menu is unpresented)`
24310
+ };
24311
+ }
24312
+ if (menuRaw?.evidence !== undefined && !validateEvidenceValue(menuRaw.evidence, false)) {
24313
+ return { ok: false, error: "flow state menu.evidence has an unsupported shape" };
24314
+ }
24315
+ const execRaw = isRecord(parsed.execution) ? parsed.execution : undefined;
24316
+ if (parsed.execution !== undefined && !isRecord(parsed.execution)) {
24317
+ return { ok: false, error: "flow state execution must be an object" };
24318
+ }
24319
+ if (execRaw?.status !== undefined && !EXECUTION_STATUSES.includes(execRaw.status)) {
24320
+ return {
24321
+ ok: false,
24322
+ error: "flow state execution.status must be pending, active, paused, or completed"
24323
+ };
24324
+ }
24325
+ if (execRaw?.mode !== undefined && execRaw.mode !== null && execRaw.mode !== "subagent-driven" && execRaw.mode !== "inline") {
24326
+ return {
24327
+ ok: false,
24328
+ error: "flow state execution.mode must be subagent-driven, inline, or null"
24329
+ };
24330
+ }
24331
+ if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
24332
+ return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
24333
+ }
24334
+ return {
24335
+ ok: true,
24336
+ state: {
24337
+ slug,
24338
+ activated: parsed.activated ?? true,
24339
+ spec,
24340
+ plan,
24341
+ menu: {
24342
+ presented: menuRaw?.presented ?? false,
24343
+ chosen: menuRaw?.chosen ?? "",
24344
+ evidence: menuRaw?.evidence ?? null
24345
+ },
24346
+ execution: {
24347
+ status: execRaw?.status ?? "pending",
24348
+ mode: execRaw?.mode ?? null,
24349
+ evidence: execRaw?.evidence ?? null
24350
+ },
24351
+ handoff_destination: parsed.handoff_destination ?? false,
24352
+ updated_at: parsed.updated_at ?? Date.now()
24353
+ }
24354
+ };
23945
24355
  }, readFlowStrict = (root, slug) => {
23946
24356
  const file = flowPath(root, slug);
23947
- if (!existsSync11(file)) {
24357
+ const rel = path20.posix.join("docs", slug, "sdd", "flow.json");
24358
+ if (!existsSync12(file)) {
23948
24359
  return err2("flow_not_activated", `flow not activated for ${slug} — run workflow_flow_status first`);
23949
24360
  }
24361
+ let text;
23950
24362
  try {
23951
- return { ok: true, state: normalizeState(JSON.parse(readFileSync12(file, "utf8")), slug) };
24363
+ text = readFileSync13(file, "utf8");
23952
24364
  } catch (error2) {
23953
- return err2("flow_corrupt", `corrupt flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24365
+ return err2("flow_io_error", `cannot read flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
23954
24366
  }
23955
- }, uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`, writeFlowState = (root, state) => {
23956
- const file = flowPath(root, state.slug);
23957
- mkdirSync6(path19.dirname(file), { recursive: true });
24367
+ let parsed;
24368
+ try {
24369
+ parsed = JSON.parse(text);
24370
+ } catch (error2) {
24371
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: rel, original_bytes_preserved: true });
24372
+ }
24373
+ const validated = validateState(parsed, slug);
24374
+ if (!validated.ok) {
24375
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${validated.error}`, {
24376
+ path: rel,
24377
+ original_bytes_preserved: true
24378
+ });
24379
+ }
24380
+ return { ok: true, state: validated.state, raw: parsed };
24381
+ }, uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`, writeFlowFileAtomic = (file, state) => {
24382
+ const text = JSON.stringify(state, null, 2) + `
24383
+ `;
23958
24384
  const tmp = uniqueTempPath(file);
23959
- writeFileSync5(tmp, JSON.stringify(state, null, 2) + `
23960
- `, "utf8");
23961
- renameSync(tmp, file);
23962
- }, MAX_WRITE_ATTEMPTS = 5, writeFlowStateIfCurrent = (root, expected, next) => {
24385
+ mkdirSync7(path20.dirname(file), { recursive: true });
24386
+ let fd = null;
24387
+ try {
24388
+ fd = openSync(tmp, "w");
24389
+ writeFileSync6(fd, text, "utf8");
24390
+ fsyncSync(fd);
24391
+ closeSync(fd);
24392
+ fd = null;
24393
+ renameSync(tmp, file);
24394
+ } finally {
24395
+ try {
24396
+ if (fd !== null)
24397
+ closeSync(fd);
24398
+ } catch {}
24399
+ try {
24400
+ if (existsSync12(tmp))
24401
+ rmSync(tmp, { force: true });
24402
+ } catch {}
24403
+ }
24404
+ }, MAX_WRITE_ATTEMPTS = 5, STALE_LOCK_MS = 1000, lockMtimeMs = (lock) => {
24405
+ try {
24406
+ return statSync7(lock).mtimeMs;
24407
+ } catch {
24408
+ return null;
24409
+ }
24410
+ }, lockOwnedBy = (fd, lock) => {
24411
+ try {
24412
+ return fstatSync(fd).ino === statSync7(lock).ino;
24413
+ } catch {
24414
+ return false;
24415
+ }
24416
+ }, writeFlowStateIfCurrent = (root, expected, next) => {
23963
24417
  const file = flowPath(root, next.slug);
23964
24418
  const expectedText = JSON.stringify(expected, null, 2) + `
23965
24419
  `;
@@ -23968,13 +24422,18 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23968
24422
  if (expectedText === nextText)
23969
24423
  return { ok: true };
23970
24424
  const tmp = uniqueTempPath(file);
24425
+ let fd = null;
23971
24426
  try {
23972
- const currentText = existsSync11(file) ? readFileSync12(file, "utf8") : null;
24427
+ const currentText = existsSync12(file) ? readFileSync13(file, "utf8") : null;
23973
24428
  if (currentText !== expectedText)
23974
24429
  return { ok: false, conflict: true };
23975
- mkdirSync6(path19.dirname(file), { recursive: true });
23976
- writeFileSync5(tmp, nextText, "utf8");
23977
- const reRead = existsSync11(file) ? readFileSync12(file, "utf8") : null;
24430
+ mkdirSync7(path20.dirname(file), { recursive: true });
24431
+ fd = openSync(tmp, "w");
24432
+ writeFileSync6(fd, nextText, "utf8");
24433
+ fsyncSync(fd);
24434
+ closeSync(fd);
24435
+ fd = null;
24436
+ const reRead = existsSync12(file) ? readFileSync13(file, "utf8") : null;
23978
24437
  if (reRead !== expectedText)
23979
24438
  return { ok: false, conflict: true };
23980
24439
  renameSync(tmp, file);
@@ -23983,45 +24442,226 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23983
24442
  return { ok: false, io_error: error2 instanceof Error ? error2.message : String(error2) };
23984
24443
  } finally {
23985
24444
  try {
23986
- if (existsSync11(tmp))
24445
+ if (fd !== null)
24446
+ closeSync(fd);
24447
+ } catch {}
24448
+ try {
24449
+ if (existsSync12(tmp))
23987
24450
  rmSync(tmp, { force: true });
23988
24451
  } catch {}
23989
24452
  }
23990
- }, readModifyWrite = (root, slug, mutate) => {
24453
+ }, readCanonicalDigest = (root, rel) => {
24454
+ const abs = path20.join(root, ...rel.split("/"));
24455
+ let bytes;
24456
+ try {
24457
+ bytes = readFileSync13(abs);
24458
+ } catch (error2) {
24459
+ if (error2.code === "ENOENT") {
24460
+ return { ok: false, code: "document_missing" };
24461
+ }
24462
+ return { ok: false, code: "document_unreadable" };
24463
+ }
24464
+ let text;
24465
+ try {
24466
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
24467
+ } catch {
24468
+ return { ok: false, code: "document_unreadable" };
24469
+ }
24470
+ return { ok: true, text, digest: createHash("sha256").update(bytes).digest("hex") };
24471
+ }, resetForSpecDrift = (state) => ({
24472
+ ...state,
24473
+ spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
24474
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24475
+ menu: { presented: false, chosen: "", evidence: null },
24476
+ execution: { status: "pending", mode: null, evidence: null },
24477
+ handoff_destination: false,
24478
+ updated_at: Date.now()
24479
+ }), resetForPlanDrift = (state) => ({
24480
+ ...state,
24481
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24482
+ menu: { presented: false, chosen: "", evidence: null },
24483
+ execution: { status: "pending", mode: null, evidence: null },
24484
+ handoff_destination: false,
24485
+ updated_at: Date.now()
24486
+ }), driftCodeFor = (root, relPath, storedDigest) => {
24487
+ if (storedDigest === null)
24488
+ return "digest_missing";
24489
+ const current = readCanonicalDigest(root, relPath);
24490
+ if (!current.ok)
24491
+ return current.code;
24492
+ return current.digest !== storedDigest ? "digest_mismatch" : null;
24493
+ }, reconcileState = (root, slug, state) => {
24494
+ const specPath = path20.posix.join("docs", slug, "spec.md");
24495
+ const planPath = path20.posix.join("docs", slug, "plan.md");
24496
+ if (state.spec.status === "approved") {
24497
+ const code = driftCodeFor(root, specPath, state.spec.approved_digest);
24498
+ if (code) {
24499
+ return {
24500
+ state: resetForSpecDrift(state),
24501
+ drift: [{ document: "spec", code, path: specPath }]
24502
+ };
24503
+ }
24504
+ }
24505
+ if (state.plan.status === "approved") {
24506
+ const code = driftCodeFor(root, planPath, state.plan.approved_digest);
24507
+ if (code) {
24508
+ return {
24509
+ state: resetForPlanDrift(state),
24510
+ drift: [{ document: "plan", code, path: planPath }]
24511
+ };
24512
+ }
24513
+ }
24514
+ return { state, drift: [] };
24515
+ }, deriveLegacyExecution = (root, slug, state) => {
24516
+ const ledger = ledgerCompletion(root, slug);
24517
+ if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
24518
+ return { status: "active", mode: "subagent-driven", evidence: null };
24519
+ }
24520
+ return { status: "pending", mode: null, evidence: null };
24521
+ }, normalizeCompatibility = (root, slug, parsed, state) => {
24522
+ if (!isRecord(parsed) || !("execution" in parsed)) {
24523
+ const derived = deriveLegacyExecution(root, slug, state);
24524
+ const current = state.execution;
24525
+ if (derived.status !== current.status || derived.mode !== current.mode) {
24526
+ return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
24527
+ }
24528
+ }
24529
+ return { state, changed: false };
24530
+ }, withFlowLock = (file, fn) => {
24531
+ const lock = `${file}.lock`;
24532
+ if (!existsSync12(path20.dirname(file)))
24533
+ return { locked: true, value: fn() };
24534
+ try {
24535
+ if (existsSync12(`${lock}.stale`))
24536
+ rmSync(`${lock}.stale`, { force: true });
24537
+ } catch {}
24538
+ const wait = new Int32Array(new SharedArrayBuffer(4));
24539
+ let fd = null;
23991
24540
  for (let attempt = 0;attempt < MAX_WRITE_ATTEMPTS; attempt++) {
24541
+ try {
24542
+ fd = openSync(lock, "wx");
24543
+ break;
24544
+ } catch (error2) {
24545
+ const code = error2.code;
24546
+ if (code !== "EEXIST") {
24547
+ return {
24548
+ locked: false,
24549
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`)
24550
+ };
24551
+ }
24552
+ const mtime = lockMtimeMs(lock);
24553
+ if (mtime !== null && Date.now() - mtime > STALE_LOCK_MS) {
24554
+ try {
24555
+ renameSync(lock, `${lock}.stale`);
24556
+ unlinkSync3(`${lock}.stale`);
24557
+ } catch {}
24558
+ try {
24559
+ fd = openSync(lock, "wx");
24560
+ break;
24561
+ } catch (innerError) {
24562
+ const innerCode = innerError.code;
24563
+ if (innerCode !== "EEXIST") {
24564
+ return {
24565
+ locked: false,
24566
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${innerError instanceof Error ? innerError.message : String(innerError)}`)
24567
+ };
24568
+ }
24569
+ }
24570
+ }
24571
+ if (attempt === MAX_WRITE_ATTEMPTS - 1) {
24572
+ return {
24573
+ locked: false,
24574
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path20.dirname(file)}: re-read the flow state and retry the transition`)
24575
+ };
24576
+ }
24577
+ Atomics.wait(wait, 0, 0, 10);
24578
+ }
24579
+ }
24580
+ if (fd === null) {
24581
+ return {
24582
+ locked: false,
24583
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path20.dirname(file)}: re-read the flow state and retry the transition`)
24584
+ };
24585
+ }
24586
+ try {
24587
+ return { locked: true, value: fn() };
24588
+ } finally {
24589
+ try {
24590
+ if (fd !== null && lockOwnedBy(fd, lock))
24591
+ rmSync(lock, { force: true });
24592
+ } catch {}
24593
+ try {
24594
+ if (fd !== null)
24595
+ closeSync(fd);
24596
+ } catch {}
24597
+ }
24598
+ }, readEffectiveFlowState = (root, slug) => {
24599
+ const file = flowPath(root, slug);
24600
+ const rel = path20.posix.join("docs", slug, "sdd", "flow.json");
24601
+ const locked = withFlowLock(file, () => {
23992
24602
  const strict = readFlowStrict(root, slug);
23993
24603
  if (!strict.ok)
23994
24604
  return strict;
23995
- const result = mutate(strict.state);
23996
- if (!result.ok)
23997
- return result;
23998
- const commit = writeFlowStateIfCurrent(root, strict.state, result.next);
23999
- if (commit.ok)
24000
- return { ok: true };
24001
- if ("io_error" in commit) {
24002
- return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24605
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
24606
+ const { state, drift } = reconcileState(root, slug, normalized.state);
24607
+ if (normalized.changed || drift.length > 0) {
24608
+ try {
24609
+ writeFlowFileAtomic(file, state);
24610
+ } catch (error2) {
24611
+ 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 });
24612
+ }
24003
24613
  }
24004
- }
24005
- return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
24614
+ return { ok: true, state, drift };
24615
+ });
24616
+ if (!locked.locked)
24617
+ return locked.error;
24618
+ return locked.value;
24619
+ }, readModifyWrite = (root, slug, mutate) => {
24620
+ const file = flowPath(root, slug);
24621
+ const locked = withFlowLock(file, () => {
24622
+ for (let attempt = 0;attempt < MAX_WRITE_ATTEMPTS; attempt++) {
24623
+ const strict = readFlowStrict(root, slug);
24624
+ if (!strict.ok)
24625
+ return strict;
24626
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
24627
+ const reconciled = reconcileState(root, slug, normalized.state);
24628
+ const result = mutate(reconciled.state);
24629
+ if (!result.ok)
24630
+ return result;
24631
+ let baseline = strict.state;
24632
+ if (normalized.changed) {
24633
+ try {
24634
+ writeFlowFileAtomic(file, normalized.state);
24635
+ } catch (error2) {
24636
+ return err2("flow_io_error", `cannot persist normalized flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24637
+ }
24638
+ baseline = normalized.state;
24639
+ }
24640
+ const commit = writeFlowStateIfCurrent(root, baseline, result.next);
24641
+ if (commit.ok)
24642
+ return { ok: true };
24643
+ if ("io_error" in commit) {
24644
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24645
+ }
24646
+ }
24647
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
24648
+ });
24649
+ if (!locked.locked)
24650
+ return locked.error;
24651
+ return locked.value;
24006
24652
  }, assertMutationWorkspace = (root, ctx) => {
24007
24653
  if (ctx && ctx.hostWorkspace !== root) {
24008
24654
  return err2("workspace_mismatch", `mutation context workspace ${JSON.stringify(ctx.hostWorkspace)} does not match flow workspace ${JSON.stringify(root)}`);
24009
24655
  }
24010
24656
  return { ok: true };
24011
- }, assertCoordinatorBoundary = (ctx, menu) => {
24012
- if (ctx?.role === "coordinator" && menu.chosen === "subagent-driven") {
24657
+ }, assertCoordinatorBoundary = (ctx, state) => {
24658
+ if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
24013
24659
  return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
24014
24660
  }
24015
24661
  if (ctx?.role === "delegated" && !ctx.taskIdentity) {
24016
24662
  return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
24017
24663
  }
24018
24664
  return { ok: true };
24019
- }, nextFlowStatus = (current) => {
24020
- if (current === "draft")
24021
- return { ok: true, next: "approved" };
24022
- if (current === "self_reviewed")
24023
- return { ok: true, next: "approved" };
24024
- return err2("flow_already_approved", "already approved; no further transitions");
24025
24665
  }, MAX_CLOCK_SKEW_MS = 60000, MAX_RECEIPTS_PER_SESSION = 10, RECEIPT_FRESHNESS_MS, EVIDENCE_WINDOW_MS, NEGATIVE_ANSWER_LABELS, isNegativeLabel = (label) => {
24026
24666
  const normalized = label.trim().toLowerCase();
24027
24667
  return NEGATIVE_ANSWER_LABELS.some((entry) => {
@@ -24125,24 +24765,38 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24125
24765
  });
24126
24766
  if (!resolved.ok)
24127
24767
  return err2("flow_prepare_failed", resolved.error);
24128
- const specPath = path19.posix.join("docs", slug, "spec.md");
24129
- const planPath = path19.posix.join("docs", slug, "plan.md");
24130
- const current = readFlowState(root, slug);
24131
- const state = current.activated ? {
24132
- ...current,
24133
- spec: { ...current.spec, path: specPath },
24134
- plan: { ...current.plan, path: planPath },
24135
- updated_at: Date.now()
24136
- } : {
24137
- slug,
24138
- activated: true,
24139
- spec: { path: specPath, status: "draft", evidence: null },
24140
- plan: { path: planPath, status: "draft", evidence: null },
24141
- menu: { presented: false, chosen: "", evidence: null },
24142
- updated_at: Date.now()
24143
- };
24144
- writeFlowState(root, state);
24145
- return { ok: true };
24768
+ const specPath = path20.posix.join("docs", slug, "spec.md");
24769
+ const planPath = path20.posix.join("docs", slug, "plan.md");
24770
+ const file = flowPath(root, slug);
24771
+ const locked = withFlowLock(file, () => {
24772
+ if (!existsSync12(file)) {
24773
+ writeFlowFileAtomic(file, {
24774
+ slug,
24775
+ activated: true,
24776
+ spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
24777
+ plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
24778
+ menu: { presented: false, chosen: "", evidence: null },
24779
+ execution: { status: "pending", mode: null, evidence: null },
24780
+ handoff_destination: false,
24781
+ updated_at: Date.now()
24782
+ });
24783
+ return { ok: true };
24784
+ }
24785
+ const strict = readFlowStrict(root, slug);
24786
+ if (!strict.ok)
24787
+ return strict;
24788
+ const reconciled = reconcileState(root, slug, strict.state);
24789
+ writeFlowFileAtomic(file, {
24790
+ ...reconciled.state,
24791
+ spec: { ...reconciled.state.spec, path: specPath },
24792
+ plan: { ...reconciled.state.plan, path: planPath },
24793
+ updated_at: Date.now()
24794
+ });
24795
+ return { ok: true };
24796
+ });
24797
+ if (!locked.locked)
24798
+ return locked.error;
24799
+ return locked.value;
24146
24800
  }, transitionSpec = (root, slug, specPath, evidence, ctx) => {
24147
24801
  const bound = assertMutationWorkspace(root, ctx);
24148
24802
  if (!bound.ok)
@@ -24153,39 +24807,39 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24153
24807
  const doc2 = resolveDoc(root, slug, specPath, "spec");
24154
24808
  if (!doc2.ok)
24155
24809
  return err2("path_invalid", doc2.error);
24810
+ const relPath = path20.posix.join("docs", slug, "spec.md");
24156
24811
  return readModifyWrite(root, slug, (state) => {
24157
- if (!existsSync11(doc2.path))
24812
+ if (!existsSync12(doc2.path))
24158
24813
  return err2("spec_missing", `spec not found: ${specPath}`);
24159
- if (state.spec.status === "draft") {
24160
- let text;
24161
- try {
24162
- text = readFileSync12(doc2.path, "utf8");
24163
- } catch (error2) {
24164
- return err2("spec_self_review_failed", `spec self-review failed: unreadable spec: ${error2 instanceof Error ? error2.message : String(error2)}`);
24165
- }
24166
- const hard = qualitySpec(text).filter((f) => f.severity === "hard");
24167
- const missing = [];
24168
- if (!/^\s*\*+Branch:\*+/im.test(stripFences(text)))
24169
- missing.push("**Branch:** header missing");
24170
- if (hard.length > 0 || missing.length > 0) {
24171
- 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");
24814
+ if (state.spec.status === "draft" || state.spec.status === "self_reviewed") {
24815
+ const digest = readCanonicalDigest(root, relPath);
24816
+ if (!digest.ok) {
24817
+ return err2("spec_self_review_failed", `spec self-review failed: unreadable or invalid UTF-8 canonical spec: ${specPath}`);
24818
+ }
24819
+ if (state.spec.status === "draft") {
24820
+ const hard = qualitySpec(digest.text).filter((f) => f.severity === "hard");
24821
+ const missing = [];
24822
+ if (!/^\s*\*+Branch:\*+/im.test(stripFences(digest.text)))
24823
+ missing.push("**Branch:** header missing");
24824
+ if (hard.length > 0 || missing.length > 0) {
24825
+ 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");
24826
+ }
24172
24827
  }
24828
+ return {
24829
+ ok: true,
24830
+ next: {
24831
+ ...state,
24832
+ spec: {
24833
+ path: relPath,
24834
+ status: "approved",
24835
+ evidence: recorded.evidence,
24836
+ approved_digest: digest.digest
24837
+ },
24838
+ updated_at: Date.now()
24839
+ }
24840
+ };
24173
24841
  }
24174
- const step = nextFlowStatus(state.spec.status);
24175
- if (!step.ok)
24176
- return step;
24177
- return {
24178
- ok: true,
24179
- next: {
24180
- ...state,
24181
- spec: {
24182
- path: path19.posix.join("docs", slug, "spec.md"),
24183
- status: step.next,
24184
- evidence: recorded.evidence
24185
- },
24186
- updated_at: Date.now()
24187
- }
24188
- };
24842
+ return err2("flow_already_approved", "already approved; no further transitions");
24189
24843
  });
24190
24844
  }, transitionPlan = (root, slug, planPath, evidence, ctx) => {
24191
24845
  const bound = assertMutationWorkspace(root, ctx);
@@ -24197,91 +24851,236 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24197
24851
  const doc2 = resolveDoc(root, slug, planPath, "plan");
24198
24852
  if (!doc2.ok)
24199
24853
  return err2("path_invalid", doc2.error);
24854
+ const relPath = path20.posix.join("docs", slug, "plan.md");
24200
24855
  return readModifyWrite(root, slug, (state) => {
24201
- if (!existsSync11(doc2.path))
24856
+ if (!existsSync12(doc2.path))
24202
24857
  return err2("plan_missing", `plan not found: ${planPath}`);
24203
24858
  if (state.spec.status !== "approved") {
24204
24859
  return err2("spec_not_approved", "spec must be approved before the plan can be approved");
24205
24860
  }
24206
- if (state.plan.status === "draft") {
24207
- let text;
24208
- try {
24209
- text = readFileSync12(doc2.path, "utf8");
24210
- } catch (error2) {
24211
- return err2("plan_self_review_failed", `plan self-review failed: unreadable plan: ${error2 instanceof Error ? error2.message : String(error2)}`);
24212
- }
24213
- const missing = [];
24214
- const stripped = stripFences(text);
24215
- if (parseTasksFromPlan(text).length === 0)
24216
- missing.push("no ### Task N: sections outside fences");
24217
- if (!/^\s*\*+Spec:\*+/im.test(stripped))
24218
- missing.push("**Spec:** header missing");
24219
- if (!/^\s*\*+Branch:\*+/im.test(stripped))
24220
- missing.push("**Branch:** header missing");
24221
- if (missing.length > 0)
24222
- return err2("plan_self_review_failed", "plan self-review failed: " + missing.join("; "));
24223
- }
24224
- const step = nextFlowStatus(state.plan.status);
24225
- if (!step.ok)
24226
- return step;
24861
+ if (state.plan.status === "draft" || state.plan.status === "self_reviewed") {
24862
+ const digest = readCanonicalDigest(root, relPath);
24863
+ if (!digest.ok) {
24864
+ return err2("plan_self_review_failed", `plan self-review failed: unreadable or invalid UTF-8 canonical plan: ${planPath}`);
24865
+ }
24866
+ if (state.plan.status === "draft") {
24867
+ const missing = [];
24868
+ const stripped = stripFences(digest.text);
24869
+ if (parseTasksFromPlan(digest.text).length === 0)
24870
+ missing.push("no ### Task N: sections outside fences");
24871
+ if (!/^\s*\*+Spec:\*+/im.test(stripped))
24872
+ missing.push("**Spec:** header missing");
24873
+ if (!/^\s*\*+Branch:\*+/im.test(stripped))
24874
+ missing.push("**Branch:** header missing");
24875
+ if (missing.length > 0)
24876
+ return err2("plan_self_review_failed", "plan self-review failed: " + missing.join("; "));
24877
+ }
24878
+ return {
24879
+ ok: true,
24880
+ next: {
24881
+ ...state,
24882
+ plan: {
24883
+ path: relPath,
24884
+ status: "approved",
24885
+ evidence: recorded.evidence,
24886
+ approved_digest: digest.digest
24887
+ },
24888
+ updated_at: Date.now()
24889
+ }
24890
+ };
24891
+ }
24892
+ return err2("flow_already_approved", "already approved; no further transitions");
24893
+ });
24894
+ }, recordMenuChoice = (root, slug, planPath, choice, evidence, ctx) => {
24895
+ const bound = assertMutationWorkspace(root, ctx);
24896
+ if (!bound.ok)
24897
+ return bound;
24898
+ const recorded = assertEvidenceShape(evidence);
24899
+ if (!recorded.ok)
24900
+ return err2("evidence_invalid", recorded.error);
24901
+ if (typeof choice !== "string" || !MENU_CHOICES.includes(choice)) {
24902
+ return err2("menu_choice_invalid", `invalid menu choice: ${JSON.stringify(choice)}`);
24903
+ }
24904
+ if (recorded.evidence.host === "cursor" && choice === "subagent-driven") {
24905
+ return err2("unsupported_mode", CURSOR_SUBAGENT_UNSUPPORTED_TEXT);
24906
+ }
24907
+ if (recorded.evidence.host === "opencode" && !sameChoiceLabel(recorded.evidence.selectedLabel, choice)) {
24908
+ return err2("evidence_mismatch", `evidence selectedLabel ${JSON.stringify(recorded.evidence.selectedLabel)} does not match choice ${JSON.stringify(choice)}`);
24909
+ }
24910
+ const doc2 = resolveDoc(root, slug, planPath, "plan");
24911
+ if (!doc2.ok)
24912
+ return err2("path_invalid", doc2.error);
24913
+ return readModifyWrite(root, slug, (state) => {
24914
+ if (state.spec.status !== "approved")
24915
+ return err2("spec_not_approved", "spec must be approved before the execution menu");
24916
+ if (state.plan.status !== "approved")
24917
+ return err2("plan_not_approved", "plan must be approved before the execution menu");
24918
+ if (state.handoff_destination && choice === "handoff") {
24919
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
24920
+ }
24921
+ const executing = choice === "subagent-driven" || choice === "inline";
24227
24922
  return {
24228
24923
  ok: true,
24229
24924
  next: {
24230
24925
  ...state,
24231
- plan: {
24232
- path: path19.posix.join("docs", slug, "plan.md"),
24233
- status: step.next,
24234
- evidence: recorded.evidence
24235
- },
24926
+ plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
24927
+ menu: { presented: true, chosen: choice, evidence: recorded.evidence },
24928
+ execution: executing ? { status: "active", mode: choice, evidence: recorded.evidence } : { status: "pending", mode: null, evidence: recorded.evidence },
24929
+ updated_at: Date.now()
24930
+ }
24931
+ };
24932
+ });
24933
+ }, markHandoffDestination = (root, slug, planPath) => {
24934
+ const doc2 = resolveDoc(root, slug, planPath, "plan");
24935
+ if (!doc2.ok)
24936
+ return err2("path_invalid", doc2.error);
24937
+ return readModifyWrite(root, slug, (state) => {
24938
+ if (state.spec.status !== "approved")
24939
+ return err2("spec_not_approved", "spec must be approved before marking a handoff destination");
24940
+ if (state.plan.status !== "approved")
24941
+ return err2("plan_not_approved", "plan must be approved before marking a handoff destination");
24942
+ if (state.handoff_destination) {
24943
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
24944
+ }
24945
+ if (state.menu.chosen !== "handoff") {
24946
+ return err2("handoff_not_chosen", `source menu choice must be "handoff" to mark a handoff destination (chosen: ${JSON.stringify(state.menu.chosen)})`);
24947
+ }
24948
+ return {
24949
+ ok: true,
24950
+ next: {
24951
+ ...state,
24952
+ handoff_destination: true,
24953
+ menu: { presented: false, chosen: "", evidence: null },
24236
24954
  updated_at: Date.now()
24237
24955
  }
24238
24956
  };
24239
24957
  });
24240
- }, recordMenuChoice = (root, slug, planPath, choice, evidence, ctx) => {
24958
+ }, CLI_CONFIRMATION_KEYS, validateLifecycleEvidence = (input) => {
24959
+ if (typeof input !== "object" || input === null) {
24960
+ return {
24961
+ ok: false,
24962
+ error: "lifecycle evidence required — native choice evidence or an exact CLI confirmation"
24963
+ };
24964
+ }
24965
+ const record3 = input;
24966
+ if (record3.host === "cli") {
24967
+ const validValue = record3.attested === false && (record3.confirmation === "flag" || record3.confirmation === "tty");
24968
+ const keys = Object.keys(record3).sort();
24969
+ const exactShape = keys.length === CLI_CONFIRMATION_KEYS.length && CLI_CONFIRMATION_KEYS.every((key) => keys.includes(key));
24970
+ if (validValue && exactShape) {
24971
+ return {
24972
+ ok: true,
24973
+ evidence: {
24974
+ host: "cli",
24975
+ attested: false,
24976
+ confirmation: record3.confirmation
24977
+ }
24978
+ };
24979
+ }
24980
+ return {
24981
+ ok: false,
24982
+ error: 'cli confirmations accept only the exact { host: "cli", attested: false, confirmation: "flag" | "tty" } shape'
24983
+ };
24984
+ }
24985
+ return assertEvidenceShape(input);
24986
+ }, 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) => {
24987
+ const file = flowPath(root, slug);
24988
+ const captured = readEffectiveFlowState(root, slug);
24989
+ if (!captured.ok)
24990
+ return captured;
24991
+ const exec = captured.state.execution;
24992
+ if (exec.status === "pending")
24993
+ return errPendingFlow("complete");
24994
+ if (exec.status === "completed")
24995
+ return errCompletedFlow("complete");
24996
+ const ledger = ledgerCompletion(root, slug);
24997
+ if (!ledger.complete) {
24998
+ return err2("execution_incomplete", `execution ledger incomplete for ${slug}: missing tasks ${ledger.missing.join(", ")}`, { required: ledger.required, completed: ledger.completed, missing: ledger.missing });
24999
+ }
25000
+ const verifier = deps?.verifyProject ?? runVerifyProject;
25001
+ const verify = verifier(root, false);
25002
+ if (verify.exitCode !== 0) {
25003
+ return err2("verification_failed", `repository verification failed for ${slug} (exit ${verify.exitCode}) — see the verification output`, { exitCode: verify.exitCode });
25004
+ }
25005
+ const locked = withFlowLock(file, () => {
25006
+ const strict = readFlowStrict(root, slug);
25007
+ if (!strict.ok)
25008
+ return strict;
25009
+ const reconciled = reconcileState(root, slug, strict.state);
25010
+ const currentExec = reconciled.state.execution;
25011
+ if (currentExec.status !== exec.status || currentExec.mode !== exec.mode) {
25012
+ return err2("flow_concurrent_conflict", `concurrent execution state change detected for ${slug}: re-read the flow state and retry completion`);
25013
+ }
25014
+ const next = {
25015
+ ...reconciled.state,
25016
+ execution: { ...exec, status: "completed" },
25017
+ handoff_destination: false,
25018
+ updated_at: Date.now()
25019
+ };
25020
+ const commit = writeFlowStateIfCurrent(root, captured.state, next);
25021
+ if (commit.ok)
25022
+ return { ok: true };
25023
+ if ("io_error" in commit) {
25024
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
25025
+ }
25026
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry completion`);
25027
+ });
25028
+ if (!locked.locked)
25029
+ return locked.error;
25030
+ return locked.value;
25031
+ }, transitionExecution = (root, slug, planPath, action, evidence, ctx, deps) => {
24241
25032
  const bound = assertMutationWorkspace(root, ctx);
24242
25033
  if (!bound.ok)
24243
25034
  return bound;
24244
- const recorded = assertEvidenceShape(evidence);
24245
- if (!recorded.ok)
24246
- return err2("evidence_invalid", recorded.error);
24247
- if (typeof choice !== "string" || !MENU_CHOICES.includes(choice)) {
24248
- return err2("menu_choice_invalid", `invalid menu choice: ${JSON.stringify(choice)}`);
24249
- }
24250
- if (recorded.evidence.host === "cursor" && choice === "subagent-driven") {
24251
- return err2("unsupported_mode", CURSOR_SUBAGENT_UNSUPPORTED_TEXT);
24252
- }
24253
- if (recorded.evidence.host === "opencode" && !sameChoiceLabel(recorded.evidence.selectedLabel, choice)) {
24254
- return err2("evidence_mismatch", `evidence selectedLabel ${JSON.stringify(recorded.evidence.selectedLabel)} does not match choice ${JSON.stringify(choice)}`);
24255
- }
25035
+ const validated = validateLifecycleEvidence(evidence);
25036
+ if (!validated.ok)
25037
+ return err2("evidence_invalid", validated.error);
24256
25038
  const doc2 = resolveDoc(root, slug, planPath, "plan");
24257
25039
  if (!doc2.ok)
24258
25040
  return err2("path_invalid", doc2.error);
25041
+ if (action === "complete")
25042
+ return completeExecution(root, slug, deps);
25043
+ if (action === "pause") {
25044
+ return readModifyWrite(root, slug, (state) => {
25045
+ const exec = state.execution;
25046
+ if (exec.status === "pending")
25047
+ return errPendingFlow("pause");
25048
+ if (exec.status === "completed")
25049
+ return errCompletedFlow("pause");
25050
+ if (exec.status === "paused")
25051
+ return err2("flow_already_paused", "flow is already paused");
25052
+ return {
25053
+ ok: true,
25054
+ next: { ...state, execution: { ...exec, status: "paused" }, updated_at: Date.now() }
25055
+ };
25056
+ });
25057
+ }
24259
25058
  return readModifyWrite(root, slug, (state) => {
24260
- if (state.spec.status !== "approved")
24261
- return err2("spec_not_approved", "spec must be approved before the execution menu");
24262
- if (state.plan.status !== "approved")
24263
- return err2("plan_not_approved", "plan must be approved before the execution menu");
25059
+ const exec = state.execution;
25060
+ if (exec.status === "completed")
25061
+ return errCompletedFlow("resume");
25062
+ if (exec.status !== "paused") {
25063
+ 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");
25064
+ }
24264
25065
  return {
24265
25066
  ok: true,
24266
- next: {
24267
- ...state,
24268
- plan: state.plan.path ? state.plan : { path: planPath, status: state.plan.status },
24269
- menu: { presented: true, chosen: choice, evidence: recorded.evidence },
24270
- updated_at: Date.now()
24271
- }
25067
+ next: { ...state, execution: { ...exec, status: "active" }, updated_at: Date.now() }
24272
25068
  };
24273
25069
  });
25070
+ }, slugFromPath = (p) => {
25071
+ const dirName = path20.basename(path20.dirname(p));
25072
+ return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
24274
25073
  }, slugFromSddPath = (p) => {
24275
- const match = p.split(path19.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
25074
+ const match = p.split(path20.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
24276
25075
  return match?.[1] ?? "";
24277
25076
  }, assertProductGates = (root, slug, opts = {}, ctx) => {
24278
25077
  const bound = assertMutationWorkspace(root, ctx);
24279
25078
  if (!bound.ok)
24280
25079
  return bound;
24281
- const strict = readFlowStrict(root, slug);
24282
- if (!strict.ok)
24283
- return strict;
24284
- const state = strict.state;
25080
+ const effective = readEffectiveFlowState(root, slug);
25081
+ if (!effective.ok)
25082
+ return effective;
25083
+ const state = effective.state;
24285
25084
  if (state.spec.status !== "approved") {
24286
25085
  return err2("spec_not_approved", `spec not approved (status: ${state.spec.status}). Run workflow_spec_approve after the user's approval.`);
24287
25086
  }
@@ -24293,18 +25092,21 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24293
25092
  }
24294
25093
  if (opts.requireDocs) {
24295
25094
  const validated = docsValidate({
24296
- spec_path: path19.posix.join("docs", slug, "spec.md"),
24297
- plan_path: path19.posix.join("docs", slug, "plan.md"),
25095
+ spec_path: path20.posix.join("docs", slug, "spec.md"),
25096
+ plan_path: path20.posix.join("docs", slug, "plan.md"),
24298
25097
  workspace_root: root
24299
25098
  });
24300
25099
  if (validated.ok === false)
24301
25100
  return err2("docs_invalid", validated.error);
24302
25101
  }
24303
- return assertCoordinatorBoundary(ctx, state.menu);
25102
+ return assertCoordinatorBoundary(ctx, state);
24304
25103
  }, 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;
24305
25104
  var init_flow_state = __esm(() => {
24306
25105
  init_docs_validate();
24307
25106
  init_docs_layout();
25107
+ init_sdd();
25108
+ init_verify_project();
25109
+ init_menu();
24308
25110
  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.";
24309
25111
  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`.";
24310
25112
  MENU_CHOICES = [
@@ -24315,6 +25117,9 @@ var init_flow_state = __esm(() => {
24315
25117
  "review-plan"
24316
25118
  ];
24317
25119
  SLUG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
25120
+ HEX64_RE = /^[0-9a-f]{64}$/;
25121
+ FLOW_STATUSES = ["draft", "self_reviewed", "approved"];
25122
+ EXECUTION_STATUSES = ["pending", "active", "paused", "completed"];
24318
25123
  RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
24319
25124
  EVIDENCE_WINDOW_MS = 24 * 60 * 60 * 1000;
24320
25125
  NEGATIVE_ANSWER_LABELS = [
@@ -24331,6 +25136,7 @@ var init_flow_state = __esm(() => {
24331
25136
  "deny"
24332
25137
  ];
24333
25138
  CURSOR_KEYS = ["attested", "confirmation", "host"];
25139
+ CLI_CONFIRMATION_KEYS = ["attested", "confirmation", "host"];
24334
25140
  BASH_READ_TOKENS = new Set([
24335
25141
  "cat",
24336
25142
  "head",
@@ -24467,34 +25273,34 @@ var init_flow_evidence = __esm(() => {
24467
25273
  });
24468
25274
 
24469
25275
  // packages/workit-core/src/core/docs-migration.ts
24470
- import { execFileSync as execFileSync4 } from "node:child_process";
25276
+ import { execFileSync as execFileSync5 } from "node:child_process";
24471
25277
  import {
24472
- existsSync as existsSync12,
24473
- lstatSync,
24474
- mkdirSync as mkdirSync7,
25278
+ existsSync as existsSync13,
25279
+ lstatSync as lstatSync2,
25280
+ mkdirSync as mkdirSync8,
24475
25281
  readdirSync as readdirSync6,
24476
- readFileSync as readFileSync13,
25282
+ readFileSync as readFileSync14,
24477
25283
  realpathSync as realpathSync3,
24478
25284
  renameSync as renameSync2,
24479
25285
  rmSync as rmSync2,
24480
- statSync as statSync6,
24481
- writeFileSync as writeFileSync6
25286
+ statSync as statSync8,
25287
+ writeFileSync as writeFileSync7
24482
25288
  } from "node:fs";
24483
- import path20 from "node:path";
24484
- 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) => {
25289
+ import path21 from "node:path";
25290
+ 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) => {
24485
25291
  try {
24486
- return readFileSync13(p, "utf8");
25292
+ return readFileSync14(p, "utf8");
24487
25293
  } catch {
24488
25294
  return null;
24489
25295
  }
24490
25296
  }, sddIgnoreActive = (cwd, slug) => {
24491
25297
  try {
24492
- execFileSync4("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
25298
+ execFileSync5("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
24493
25299
  } catch {
24494
25300
  return false;
24495
25301
  }
24496
25302
  try {
24497
- execFileSync4("git", ["-C", cwd, "check-ignore", path20.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
25303
+ execFileSync5("git", ["-C", cwd, "check-ignore", path21.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
24498
25304
  return true;
24499
25305
  } catch {
24500
25306
  return false;
@@ -24516,22 +25322,22 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24516
25322
  }, detectLegacyDocs = (workspace_root) => {
24517
25323
  const root = legacyRoot(workspace_root);
24518
25324
  const raw = [];
24519
- if (existsSync12(root)) {
25325
+ if (existsSync13(root)) {
24520
25326
  for (const entry of readdirSync6(root, { withFileTypes: true })) {
24521
- const abs = path20.join(root, entry.name);
24522
- const rel = posix2(path20.relative(workspace_root, abs));
25327
+ const abs = path21.join(root, entry.name);
25328
+ const rel = posix3(path21.relative(workspace_root, abs));
24523
25329
  if (entry.isDirectory()) {
24524
- const hasSpec = existsSync12(path20.join(abs, "spec.md"));
24525
- const hasPlan = existsSync12(path20.join(abs, "plan.md"));
24526
- if (!hasSpec && !hasPlan && !existsSync12(path20.join(abs, "sdd")))
25330
+ const hasSpec = existsSync13(path21.join(abs, "spec.md"));
25331
+ const hasPlan = existsSync13(path21.join(abs, "plan.md"));
25332
+ if (!hasSpec && !hasPlan && !existsSync13(path21.join(abs, "sdd")))
24527
25333
  continue;
24528
- const explicit = explicitTargetSlug(hasPlan ? readFileSafe(path20.join(abs, "plan.md")) : null);
25334
+ const explicit = explicitTargetSlug(hasPlan ? readFileSafe(path21.join(abs, "plan.md")) : null);
24529
25335
  raw.push({
24530
25336
  slug: explicit ?? entry.name,
24531
25337
  legacy_dir: rel,
24532
- spec: hasSpec ? posix2(path20.join(rel, "spec.md")) : null,
24533
- plan: hasPlan ? posix2(path20.join(rel, "plan.md")) : null,
24534
- sdd: existsSync12(path20.join(abs, "sdd")),
25338
+ spec: hasSpec ? posix3(path21.join(rel, "spec.md")) : null,
25339
+ plan: hasPlan ? posix3(path21.join(rel, "plan.md")) : null,
25340
+ sdd: existsSync13(path21.join(abs, "sdd")),
24535
25341
  explicit: explicit !== null
24536
25342
  });
24537
25343
  } else if (entry.name === "spec.md" || entry.name === "plan.md") {
@@ -24599,7 +25405,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24599
25405
  return { ok: true, out, changed: out !== text };
24600
25406
  }, isPlanMalformed = (text) => !/^\s*\*+Spec:\*+/im.test(text), readBufferSafe = (p) => {
24601
25407
  try {
24602
- return readFileSync13(p);
25408
+ return readFileSync14(p);
24603
25409
  } catch {
24604
25410
  return null;
24605
25411
  }
@@ -24622,7 +25428,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24622
25428
  const raw = readBufferSafe(fromAbs);
24623
25429
  if (raw === null)
24624
25430
  return { ok: false, error: `unreadable source ${fromAbs}` };
24625
- if (path20.basename(toRel) !== "flow.json")
25431
+ if (path21.basename(toRel) !== "flow.json")
24626
25432
  return { ok: true, bytes: raw, status: "copied" };
24627
25433
  const flow = flowRewrite(raw.toString("utf8"), legacyName, slug);
24628
25434
  if (!flow.ok)
@@ -24645,18 +25451,18 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24645
25451
  } catch {
24646
25452
  return { ok: false, error: `dangling symlink refused: ${legacyRel}` };
24647
25453
  }
24648
- if (real !== workspaceReal && !real.startsWith(workspaceReal + path20.sep)) {
25454
+ if (real !== workspaceReal && !real.startsWith(workspaceReal + path21.sep)) {
24649
25455
  return { ok: false, error: `symlink escape refused: ${legacyRel}` };
24650
25456
  }
24651
25457
  if (visits.has(real))
24652
25458
  return { ok: true };
24653
25459
  visits.add(real);
24654
25460
  for (const entry of readdirSync6(real, { withFileTypes: true })) {
24655
- const fromAbs = path20.join(real, entry.name);
24656
- const fromRel = posix2(path20.join(legacyRel, entry.name));
24657
- const toRel = posix2(path20.join(destRelRoot, entry.name));
25461
+ const fromAbs = path21.join(real, entry.name);
25462
+ const fromRel = posix3(path21.join(legacyRel, entry.name));
25463
+ const toRel = posix3(path21.join(destRelRoot, entry.name));
24658
25464
  if (entry.isDirectory()) {
24659
- const sub = planTree(workspace, fromAbs, fromRel, toRel, path20.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
25465
+ const sub = planTree(workspace, fromAbs, fromRel, toRel, path21.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
24660
25466
  if (!sub.ok)
24661
25467
  return sub;
24662
25468
  } else if (entry.isFile() || entry.isSymbolicLink()) {
@@ -24668,11 +25474,11 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24668
25474
  } catch {
24669
25475
  return { ok: false, error: `dangling symlink refused: ${fromRel}` };
24670
25476
  }
24671
- if (target !== workspaceReal && !target.startsWith(workspaceReal + path20.sep)) {
25477
+ if (target !== workspaceReal && !target.startsWith(workspaceReal + path21.sep)) {
24672
25478
  return { ok: false, error: `symlink escape refused: ${fromRel}` };
24673
25479
  }
24674
- if (statSync6(target).isDirectory()) {
24675
- const sub = planTree(workspace, fromAbs, fromRel, toRel, path20.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
25480
+ if (statSync8(target).isDirectory()) {
25481
+ const sub = planTree(workspace, fromAbs, fromRel, toRel, path21.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
24676
25482
  if (!sub.ok)
24677
25483
  return sub;
24678
25484
  continue;
@@ -24689,7 +25495,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24689
25495
  fromAbs,
24690
25496
  fromRel,
24691
25497
  toRel,
24692
- destAbs: path20.join(destAbsRoot, entry.name),
25498
+ destAbs: path21.join(destAbsRoot, entry.name),
24693
25499
  bytes: classified.bytes,
24694
25500
  status: classified.status
24695
25501
  });
@@ -24702,36 +25508,36 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24702
25508
  kind,
24703
25509
  legacyName,
24704
25510
  slug: entry.slug,
24705
- toRel: posix2(path20.join("docs", entry.slug, `${kind}.md`)),
24706
- destAbs: path20.join(destDirAbs, `${kind}.md`)
25511
+ toRel: posix3(path21.join("docs", entry.slug, `${kind}.md`)),
25512
+ destAbs: path21.join(destDirAbs, `${kind}.md`)
24707
25513
  });
24708
25514
  if (entry.spec) {
24709
- const classified = classifyDoc("spec", legacyName, entry.slug, path20.join(workspace, entry.spec));
25515
+ const classified = classifyDoc("spec", legacyName, entry.slug, path21.join(workspace, entry.spec));
24710
25516
  if (!classified.ok)
24711
25517
  return classified;
24712
25518
  plan.push({
24713
25519
  ...base("spec"),
24714
- fromAbs: path20.join(workspace, entry.spec),
25520
+ fromAbs: path21.join(workspace, entry.spec),
24715
25521
  fromRel: entry.spec,
24716
25522
  bytes: classified.bytes,
24717
25523
  status: classified.status
24718
25524
  });
24719
25525
  }
24720
25526
  if (entry.plan) {
24721
- const classified = classifyDoc("plan", legacyName, entry.slug, path20.join(workspace, entry.plan));
25527
+ const classified = classifyDoc("plan", legacyName, entry.slug, path21.join(workspace, entry.plan));
24722
25528
  if (!classified.ok)
24723
25529
  return classified;
24724
25530
  plan.push({
24725
25531
  ...base("plan"),
24726
- fromAbs: path20.join(workspace, entry.plan),
25532
+ fromAbs: path21.join(workspace, entry.plan),
24727
25533
  fromRel: entry.plan,
24728
25534
  bytes: classified.bytes,
24729
25535
  status: classified.status
24730
25536
  });
24731
25537
  }
24732
25538
  if (entry.sdd) {
24733
- const legacySdd = posix2(path20.join(entry.legacy_dir, "sdd"));
24734
- return planTree(workspace, path20.join(workspace, legacySdd), legacySdd, posix2(path20.join("docs", entry.slug, "sdd")), path20.join(destDirAbs, "sdd"), legacyName, entry.slug, plan, visits);
25539
+ const legacySdd = posix3(path21.join(entry.legacy_dir, "sdd"));
25540
+ return planTree(workspace, path21.join(workspace, legacySdd), legacySdd, posix3(path21.join("docs", entry.slug, "sdd")), path21.join(destDirAbs, "sdd"), legacyName, entry.slug, plan, visits);
24735
25541
  }
24736
25542
  return { ok: true };
24737
25543
  }, abort = (error2, collisions) => ({
@@ -24774,11 +25580,11 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24774
25580
  }
24775
25581
  const collisions = [];
24776
25582
  for (const item of plan) {
24777
- if (!existsSync12(item.destAbs))
25583
+ if (!existsSync13(item.destAbs))
24778
25584
  continue;
24779
25585
  let identical = false;
24780
25586
  try {
24781
- identical = !lstatSync(item.destAbs).isDirectory() && readFileSync13(item.destAbs).equals(item.bytes);
25587
+ identical = !lstatSync2(item.destAbs).isDirectory() && readFileSync14(item.destAbs).equals(item.bytes);
24782
25588
  } catch {
24783
25589
  identical = false;
24784
25590
  }
@@ -24813,8 +25619,8 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24813
25619
  }
24814
25620
  const tmp = `${item.destAbs}.workit-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
24815
25621
  try {
24816
- mkdirSync7(path20.dirname(item.destAbs), { recursive: true });
24817
- writeFileSync6(tmp, item.bytes);
25622
+ mkdirSync8(path21.dirname(item.destAbs), { recursive: true });
25623
+ writeFileSync7(tmp, item.bytes);
24818
25624
  renameSync2(tmp, item.destAbs);
24819
25625
  } catch (error2) {
24820
25626
  try {
@@ -24840,32 +25646,32 @@ var init_docs_migration = __esm(() => {
24840
25646
  });
24841
25647
 
24842
25648
  // packages/workit-core/src/core/docs-repo.ts
24843
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7, readdirSync as readdirSync7 } from "node:fs";
24844
- import { execFileSync as execFileSync5 } from "node:child_process";
24845
- import path21 from "node:path";
24846
- var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(configDir(), "docs-repo.json"), readDocsRepoConfig = () => {
25649
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8, readdirSync as readdirSync7 } from "node:fs";
25650
+ import { execFileSync as execFileSync6 } from "node:child_process";
25651
+ import path22 from "node:path";
25652
+ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(configDir(), "docs-repo.json"), readDocsRepoConfig = () => {
24847
25653
  try {
24848
- const parsed = JSON.parse(readFileSync14(configPath(), "utf8"));
25654
+ const parsed = JSON.parse(readFileSync15(configPath(), "utf8"));
24849
25655
  return parsed.path ? { path: parsed.path } : null;
24850
25656
  } catch {
24851
25657
  return null;
24852
25658
  }
24853
25659
  }, writeDocsRepoConfig = (docsPath) => {
24854
25660
  const file = configPath();
24855
- mkdirSync8(path21.dirname(file), { recursive: true });
24856
- writeFileSync7(file, JSON.stringify({ path: docsPath }, null, 2) + `
25661
+ mkdirSync9(path22.dirname(file), { recursive: true });
25662
+ writeFileSync8(file, JSON.stringify({ path: docsPath }, null, 2) + `
24857
25663
  `, "utf8");
24858
25664
  }, docsRepoPath = () => readDocsRepoConfig()?.path ?? null, validateDocsRepo = (docsPath) => {
24859
- if (!existsSync13(docsPath))
25665
+ if (!existsSync14(docsPath))
24860
25666
  return { ok: false, error: `docs repo path does not exist: ${docsPath}` };
24861
25667
  try {
24862
- execFileSync5("git", ["-C", docsPath, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
25668
+ execFileSync6("git", ["-C", docsPath, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
24863
25669
  } catch {
24864
25670
  return { ok: false, error: `docs repo is not a git repository: ${docsPath}` };
24865
25671
  }
24866
- const featuresDir = path21.join(docsPath, "features");
24867
- if (!existsSync13(featuresDir))
24868
- mkdirSync8(featuresDir, { recursive: true });
25672
+ const featuresDir = path22.join(docsPath, "features");
25673
+ if (!existsSync14(featuresDir))
25674
+ mkdirSync9(featuresDir, { recursive: true });
24869
25675
  return { ok: true };
24870
25676
  }, linkDocsRepo = (docsPath, confirmed) => {
24871
25677
  if (!confirmed)
@@ -24878,23 +25684,23 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24878
25684
  }, listSpecs = (workspaceRoot) => {
24879
25685
  const repoPath = docsRepoPath();
24880
25686
  const specs = [];
24881
- const docsDir = path21.join(workspaceRoot, "docs");
24882
- if (existsSync13(docsDir)) {
25687
+ const docsDir = path22.join(workspaceRoot, "docs");
25688
+ if (existsSync14(docsDir)) {
24883
25689
  for (const slug of readdirSync7(docsDir)) {
24884
25690
  if (slug.startsWith("."))
24885
25691
  continue;
24886
- const spec = path21.posix.join("docs", slug, "spec.md");
24887
- if (!existsSync13(path21.join(workspaceRoot, spec)))
25692
+ const spec = path22.posix.join("docs", slug, "spec.md");
25693
+ if (!existsSync14(path22.join(workspaceRoot, spec)))
24888
25694
  continue;
24889
25695
  let promoted = false;
24890
25696
  let target = null;
24891
25697
  if (repoPath) {
24892
- const featuresDir = path21.join(repoPath, "features");
24893
- if (existsSync13(featuresDir)) {
25698
+ const featuresDir = path22.join(repoPath, "features");
25699
+ if (existsSync14(featuresDir)) {
24894
25700
  const match = readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
24895
25701
  if (match) {
24896
25702
  promoted = true;
24897
- target = path21.join(repoPath, "features", match);
25703
+ target = path22.join(repoPath, "features", match);
24898
25704
  }
24899
25705
  }
24900
25706
  }
@@ -24907,7 +25713,7 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24907
25713
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
24908
25714
  }, readSafe4 = (p) => {
24909
25715
  try {
24910
- return readFileSync14(p, "utf8");
25716
+ return readFileSync15(p, "utf8");
24911
25717
  } catch {
24912
25718
  return null;
24913
25719
  }
@@ -24934,8 +25740,8 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24934
25740
  const repoValid = validateDocsRepo(repoPath);
24935
25741
  if (!repoValid.ok)
24936
25742
  return { ok: false, error: repoValid.error };
24937
- const specRel = path21.posix.join("docs", slug, "spec.md");
24938
- const planRel = path21.posix.join("docs", slug, "plan.md");
25743
+ const specRel = path22.posix.join("docs", slug, "spec.md");
25744
+ const planRel = path22.posix.join("docs", slug, "plan.md");
24939
25745
  const specText = readSafe4(resolved.layout.spec);
24940
25746
  if (specText === null)
24941
25747
  return { ok: false, error: `docs/${slug}/spec.md not found` };
@@ -24959,14 +25765,14 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24959
25765
  };
24960
25766
  }
24961
25767
  if (!opts.force) {
24962
- const sddDir = path21.join(workspaceRootCanonical, "docs", slug, "sdd");
24963
- if (existsSync13(sddDir)) {
25768
+ const sddDir = path22.join(workspaceRootCanonical, "docs", slug, "sdd");
25769
+ if (existsSync14(sddDir)) {
24964
25770
  try {
24965
- execFileSync5("git", [
25771
+ execFileSync6("git", [
24966
25772
  "-C",
24967
25773
  workspaceRootCanonical,
24968
25774
  "check-ignore",
24969
- path21.posix.join("docs", slug, "sdd", "progress.md")
25775
+ path22.posix.join("docs", slug, "sdd", "progress.md")
24970
25776
  ], { stdio: "pipe" });
24971
25777
  } catch {
24972
25778
  return {
@@ -24977,14 +25783,14 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24977
25783
  }
24978
25784
  }
24979
25785
  const prefix = monthPrefix();
24980
- const featuresDir = path21.join(repoPath, "features");
24981
- const existing = existsSync13(featuresDir) ? readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
24982
- const targetDir = path21.join(featuresDir, existing ?? `${prefix}-${slug}`);
24983
- mkdirSync8(targetDir, { recursive: true });
25786
+ const featuresDir = path22.join(repoPath, "features");
25787
+ const existing = existsSync14(featuresDir) ? readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
25788
+ const targetDir = path22.join(featuresDir, existing ?? `${prefix}-${slug}`);
25789
+ mkdirSync9(targetDir, { recursive: true });
24984
25790
  const files = ["spec.md"];
24985
- writeFileSync7(path21.join(targetDir, "spec.md"), specText, "utf8");
25791
+ writeFileSync8(path22.join(targetDir, "spec.md"), specText, "utf8");
24986
25792
  if (planText !== null) {
24987
- writeFileSync7(path21.join(targetDir, "plan.md"), planText, "utf8");
25793
+ writeFileSync8(path22.join(targetDir, "plan.md"), planText, "utf8");
24988
25794
  files.push("plan.md");
24989
25795
  }
24990
25796
  const title = specText.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? slug;
@@ -25005,9 +25811,9 @@ ${specSummary(specText)}
25005
25811
  | [spec.md](./spec.md) | Especificación completa |
25006
25812
  ${planText !== null ? `| [plan.md](./plan.md) | Plan de implementación |
25007
25813
  ` : ""}`;
25008
- writeFileSync7(path21.join(targetDir, "README.md"), readme, "utf8");
25814
+ writeFileSync8(path22.join(targetDir, "README.md"), readme, "utf8");
25009
25815
  files.push("README.md");
25010
- const indexPath = path21.join(repoPath, "features", "README.md");
25816
+ const indexPath = path22.join(repoPath, "features", "README.md");
25011
25817
  const indexText = readSafe4(indexPath) ?? `# Features
25012
25818
 
25013
25819
  Especificaciones y planes por feature.
@@ -25030,7 +25836,7 @@ Especificaciones y planes por feature.
25030
25836
  ${row}
25031
25837
  `;
25032
25838
  }
25033
- writeFileSync7(indexPath, newIndex, "utf8");
25839
+ writeFileSync8(indexPath, newIndex, "utf8");
25034
25840
  return { ok: true, target_dir: targetDir, files, index_updated: true };
25035
25841
  };
25036
25842
  var init_docs_repo = __esm(() => {
@@ -25040,13 +25846,13 @@ var init_docs_repo = __esm(() => {
25040
25846
  });
25041
25847
 
25042
25848
  // packages/workit-core/src/core/gitignore.ts
25043
- import { existsSync as existsSync14, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
25044
- import path22 from "node:path";
25849
+ import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
25850
+ import path23 from "node:path";
25045
25851
  var GITIGNORE_ENTRIES, ensureProjectGitignore = (workspaceRoot, confirmed) => {
25046
25852
  if (!confirmed)
25047
25853
  return { ok: false, error: "confirmed: true required" };
25048
- const file = path22.join(workspaceRoot, ".gitignore");
25049
- const existing = existsSync14(file) ? readFileSync15(file, "utf8") : "";
25854
+ const file = path23.join(workspaceRoot, ".gitignore");
25855
+ const existing = existsSync15(file) ? readFileSync16(file, "utf8") : "";
25050
25856
  const existingLines = new Set(existing.split(`
25051
25857
  `).map((l) => l.trim()).filter(Boolean));
25052
25858
  const added = [];
@@ -25061,12 +25867,12 @@ var GITIGNORE_ENTRIES, ensureProjectGitignore = (workspaceRoot, confirmed) => {
25061
25867
  const separator = existing && !existing.endsWith(`
25062
25868
  `) ? `
25063
25869
  ` : "";
25064
- writeFileSync8(file, existing + separator + (existing ? `
25870
+ writeFileSync9(file, existing + separator + (existing ? `
25065
25871
  ` : "") + append.join(`
25066
25872
  `) + `
25067
25873
  `, "utf8");
25068
- } else if (!existsSync14(file)) {
25069
- writeFileSync8(file, "", "utf8");
25874
+ } else if (!existsSync15(file)) {
25875
+ writeFileSync9(file, "", "utf8");
25070
25876
  }
25071
25877
  return { ok: true, path: file, added };
25072
25878
  };
@@ -25090,29 +25896,29 @@ var init_gitignore = __esm(() => {
25090
25896
  });
25091
25897
 
25092
25898
  // packages/workit-core/src/core/templates.ts
25093
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
25094
- import path23 from "node:path";
25095
- var repoRoot4, templatePath = (name) => path23.join(configDir(), "templates", `${name}.md`), readTemplate = (name) => {
25899
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
25900
+ import path24 from "node:path";
25901
+ var repoRoot4, templatePath = (name) => path24.join(configDir(), "templates", `${name}.md`), readTemplate = (name) => {
25096
25902
  const cfg = templatePath(name);
25097
- if (existsSync15(cfg))
25098
- return { source: "config", content: readFileSync16(cfg, "utf8") };
25903
+ if (existsSync16(cfg))
25904
+ return { source: "config", content: readFileSync17(cfg, "utf8") };
25099
25905
  return {
25100
25906
  source: "repo",
25101
- content: readFileSync16(path23.join(repoRoot4, "templates", `${name}.md`), "utf8")
25907
+ content: readFileSync17(path24.join(repoRoot4, "templates", `${name}.md`), "utf8")
25102
25908
  };
25103
25909
  }, writeTemplate = (name, content, confirmed) => {
25104
25910
  if (!confirmed)
25105
25911
  return { ok: false, error: "confirmed: true required" };
25106
25912
  const file = templatePath(name);
25107
- mkdirSync9(path23.dirname(file), { recursive: true });
25108
- writeFileSync9(file, content, "utf8");
25913
+ mkdirSync10(path24.dirname(file), { recursive: true });
25914
+ writeFileSync10(file, content, "utf8");
25109
25915
  return { ok: true, path: file };
25110
25916
  }, listTemplates = () => ["issue-update", "greeting", "headers"].map((name) => {
25111
25917
  const cfg = templatePath(name);
25112
- const repoFile = path23.join(repoRoot4, "templates", `${name}.md`);
25113
- if (existsSync15(cfg))
25918
+ const repoFile = path24.join(repoRoot4, "templates", `${name}.md`);
25919
+ if (existsSync16(cfg))
25114
25920
  return { name, source: "config", path: cfg };
25115
- if (existsSync15(repoFile))
25921
+ if (existsSync16(repoFile))
25116
25922
  return { name, source: "repo", path: repoFile };
25117
25923
  return { name, source: "missing", path: cfg };
25118
25924
  });
@@ -25123,9 +25929,9 @@ var init_templates = __esm(() => {
25123
25929
  });
25124
25930
 
25125
25931
  // packages/workit-core/src/core/rules.ts
25126
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync17, readdirSync as readdirSync8, writeFileSync as writeFileSync10 } from "node:fs";
25127
- import path24 from "node:path";
25128
- var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) => {
25932
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
25933
+ import path25 from "node:path";
25934
+ var rulesDir = () => path25.join(configDir(), "rules"), parseRule = (markdown) => {
25129
25935
  const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
25130
25936
  if (!fm)
25131
25937
  return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
@@ -25152,12 +25958,12 @@ var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) =
25152
25958
  }, listRules = () => {
25153
25959
  const result = [];
25154
25960
  const dir = rulesDir();
25155
- if (existsSync16(dir)) {
25961
+ if (existsSync17(dir)) {
25156
25962
  for (const entry of readdirSync8(dir)) {
25157
- const file = path24.join(dir, entry, "rule.md");
25158
- if (!existsSync16(file))
25963
+ const file = path25.join(dir, entry, "rule.md");
25964
+ if (!existsSync17(file))
25159
25965
  continue;
25160
- const parsed = parseRule(readFileSync17(file, "utf8"));
25966
+ const parsed = parseRule(readFileSync18(file, "utf8"));
25161
25967
  if ("error" in parsed)
25162
25968
  continue;
25163
25969
  result.push({ name: parsed.name, platforms: parsed.platforms, source: "config" });
@@ -25169,16 +25975,16 @@ var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) =
25169
25975
  return { ok: false, error: "confirmed: true required" };
25170
25976
  if (!RULE_NAME_RE.test(rule.name))
25171
25977
  return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
25172
- const dir = path24.join(rulesDir(), rule.name);
25173
- mkdirSync10(dir, { recursive: true });
25174
- const file = path24.join(dir, "rule.md");
25978
+ const dir = path25.join(rulesDir(), rule.name);
25979
+ mkdirSync11(dir, { recursive: true });
25980
+ const file = path25.join(dir, "rule.md");
25175
25981
  const md = `---
25176
25982
  name: ${rule.name}
25177
25983
  description: ${rule.description}
25178
25984
  platforms: [${rule.platforms.join(", ")}]
25179
25985
  ---
25180
25986
  ${rule.body}`;
25181
- writeFileSync10(file, md, "utf8");
25987
+ writeFileSync11(file, md, "utf8");
25182
25988
  return { ok: true, path: file };
25183
25989
  };
25184
25990
  var init_rules = __esm(() => {
@@ -25187,10 +25993,10 @@ var init_rules = __esm(() => {
25187
25993
  });
25188
25994
 
25189
25995
  // packages/workit-core/src/core/safe-write.ts
25190
- import { writeFileSync as writeFileSync11 } from "node:fs";
25996
+ import { writeFileSync as writeFileSync12 } from "node:fs";
25191
25997
  function writeFileExclusive(file, content, mode) {
25192
25998
  try {
25193
- writeFileSync11(file, content, { encoding: "utf8", flag: "wx", mode });
25999
+ writeFileSync12(file, content, { encoding: "utf8", flag: "wx", mode });
25194
26000
  return "created";
25195
26001
  } catch (err3) {
25196
26002
  if (err3.code === "EEXIST")
@@ -25272,26 +26078,26 @@ var init_setup_state = __esm(() => {
25272
26078
  import {
25273
26079
  copyFileSync as copyFileSync2,
25274
26080
  cpSync as cpSync2,
25275
- existsSync as existsSync17,
25276
- mkdirSync as mkdirSync11,
26081
+ existsSync as existsSync18,
26082
+ mkdirSync as mkdirSync12,
25277
26083
  mkdtempSync,
25278
- readFileSync as readFileSync18,
26084
+ readFileSync as readFileSync19,
25279
26085
  readdirSync as readdirSync9,
25280
26086
  renameSync as renameSync3,
25281
26087
  rmSync as rmSync3,
25282
- statSync as statSync7,
25283
- writeFileSync as writeFileSync12
26088
+ statSync as statSync9,
26089
+ writeFileSync as writeFileSync13
25284
26090
  } from "node:fs";
25285
- import path25 from "node:path";
26091
+ import path26 from "node:path";
25286
26092
  import { isDeepStrictEqual } from "node:util";
25287
26093
  function applyWorkspaceBranchPolicy(opts) {
25288
26094
  const { workspace_root, env = process.env } = opts;
25289
- const dir = path25.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
26095
+ const dir = path26.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
25290
26096
  const { status, path: wsPath, entries } = readWorkspacesResult(dir);
25291
26097
  if (status === "malformed")
25292
26098
  return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
25293
26099
  const detection = detectBranchPolicy(workspace_root);
25294
- const name = String(env.WORKFLOW_BP_NAME ?? path25.basename(workspace_root));
26100
+ const name = String(env.WORKFLOW_BP_NAME ?? path26.basename(workspace_root));
25295
26101
  const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
25296
26102
  const policy2 = {
25297
26103
  preset: detection.preset,
@@ -25316,8 +26122,8 @@ function applyWorkspaceBranchPolicy(opts) {
25316
26122
  };
25317
26123
  }
25318
26124
  const next = existing ? entries.map((w, i) => i === idx ? { ...w, branchPolicy: policy2 } : w) : [...entries, { name, glob, branchPolicy: policy2 }];
25319
- mkdirSync11(path25.dirname(wsPath), { recursive: true });
25320
- writeFileSync12(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
26125
+ mkdirSync12(path26.dirname(wsPath), { recursive: true });
26126
+ writeFileSync13(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
25321
26127
  `, "utf8");
25322
26128
  return {
25323
26129
  ok: true,
@@ -25359,7 +26165,7 @@ var init_setup = __esm(() => {
25359
26165
 
25360
26166
  // packages/workit-core/src/core/youtrack.ts
25361
26167
  import fs4 from "node:fs";
25362
- import path26 from "node:path";
26168
+ import path27 from "node:path";
25363
26169
  function readYouTrackConfig(required2) {
25364
26170
  const cfgPath = youTrackConfigPath();
25365
26171
  if (!fs4.existsSync(cfgPath)) {
@@ -25383,7 +26189,7 @@ function youTrackConfigLoad() {
25383
26189
  }
25384
26190
  const cfgPath = loaded.path;
25385
26191
  const tokenFile = String(loaded.config.tokenFile ?? "");
25386
- const tokenPath = tokenFile ? path26.isAbsolute(tokenFile) ? path26.resolve(tokenFile) : path26.resolve(process.cwd(), tokenFile) : "";
26192
+ const tokenPath = tokenFile ? path27.isAbsolute(tokenFile) ? path27.resolve(tokenFile) : path27.resolve(process.cwd(), tokenFile) : "";
25387
26193
  if (!tokenPath || !fs4.existsSync(tokenPath)) {
25388
26194
  return { error: "missing youtrack.token" };
25389
26195
  }
@@ -25397,8 +26203,8 @@ function youTrackConfigLoad() {
25397
26203
  const redacted = { ...loaded.config };
25398
26204
  delete redacted.tokenFile;
25399
26205
  redacted.tokenPresent = true;
25400
- redacted.configPath = path26.resolve(cfgPath);
25401
- redacted.tokenPath = path26.resolve(tokenPath);
26206
+ redacted.configPath = path27.resolve(cfgPath);
26207
+ redacted.tokenPath = path27.resolve(tokenPath);
25402
26208
  return { data: redacted };
25403
26209
  }
25404
26210
  function tzParts(date4, tz) {
@@ -25643,7 +26449,7 @@ function youTrackTokenCreateUrl() {
25643
26449
  const desc = String(defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings");
25644
26450
  const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
25645
26451
  const base = String(config2.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
25646
- const tokenFile = String(config2.tokenFile ?? path26.join(path26.dirname(loaded.path), "youtrack.token"));
26452
+ const tokenFile = String(config2.tokenFile ?? path27.join(path27.dirname(loaded.path), "youtrack.token"));
25647
26453
  const tab = String(defaults.profileTab ?? "account-security");
25648
26454
  const createUrl = `${base}/users/me?${new URLSearchParams({ tab })}`;
25649
26455
  const docsUrl = "https://www.jetbrains.com/help/youtrack/cloud/manage-permanent-token.html";
@@ -25652,7 +26458,7 @@ function youTrackTokenCreateUrl() {
25652
26458
  tokenName: name,
25653
26459
  tokenDescription: desc,
25654
26460
  scopes,
25655
- tokenFile: path26.resolve(tokenFile),
26461
+ tokenFile: path27.resolve(tokenFile),
25656
26462
  createUrl,
25657
26463
  docsUrl,
25658
26464
  prefillSupported: false,
@@ -25689,7 +26495,7 @@ function verifyYouTrackToken(scripts = defaultScripts) {
25689
26495
  function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25690
26496
  const root = resolveWorkspaceRoot(workspace_root);
25691
26497
  for (const rel of [spec_path, plan_path].filter(Boolean)) {
25692
- const full = path26.isAbsolute(rel) ? rel : path26.join(root, rel);
26498
+ const full = path27.isAbsolute(rel) ? rel : path27.join(root, rel);
25693
26499
  if (!fs4.existsSync(full))
25694
26500
  continue;
25695
26501
  const text = fs4.readFileSync(full, "utf8");
@@ -25868,7 +26674,7 @@ async function postUpdate({
25868
26674
  }
25869
26675
  return { ok: true, issueId, postedComment: true };
25870
26676
  }
25871
- var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path26.join(configDir(), "youtrack.json"), youTrackTokenModeOk = (p) => {
26677
+ var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path27.join(configDir(), "youtrack.json"), youTrackTokenModeOk = (p) => {
25872
26678
  if (process.platform === "win32")
25873
26679
  return true;
25874
26680
  const mode = fs4.statSync(p).mode & 511;
@@ -25878,7 +26684,7 @@ var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () =>
25878
26684
  if ("error" in loaded)
25879
26685
  return loaded;
25880
26686
  const tokenFile = String(loaded.config.tokenFile ?? "");
25881
- const tokenPath = tokenFile ? path26.isAbsolute(tokenFile) ? path26.resolve(tokenFile) : path26.resolve(process.cwd(), tokenFile) : "";
26687
+ const tokenPath = tokenFile ? path27.isAbsolute(tokenFile) ? path27.resolve(tokenFile) : path27.resolve(process.cwd(), tokenFile) : "";
25882
26688
  if (!tokenPath || !fs4.existsSync(tokenPath))
25883
26689
  return { error: "missing youtrack.token" };
25884
26690
  if (!youTrackTokenModeOk(tokenPath))
@@ -25911,10 +26717,10 @@ var init_youtrack = __esm(() => {
25911
26717
 
25912
26718
  // packages/workit-core/src/core/init.ts
25913
26719
  import fs5 from "node:fs";
25914
- import path27 from "node:path";
26720
+ import path28 from "node:path";
25915
26721
  function initStatusData(configDirPath = configDir()) {
25916
- const ytJson = path27.join(configDirPath, "youtrack.json");
25917
- const vcsJson = path27.join(configDirPath, "vcs.json");
26722
+ const ytJson = path28.join(configDirPath, "youtrack.json");
26723
+ const vcsJson = path28.join(configDirPath, "vcs.json");
25918
26724
  const items = [];
25919
26725
  let youtrackConfig = null;
25920
26726
  let youtrackTokenCreate = null;
@@ -25937,8 +26743,8 @@ function initStatusData(configDirPath = configDir()) {
25937
26743
  });
25938
26744
  }
25939
26745
  }
25940
- const expanded = tokenFile ? path27.resolve(tokenFile) : null;
25941
- const resolvedTokenFile = expanded && fs5.existsSync(expanded) ? resolvePath(expanded) : expanded ? path27.isAbsolute(expanded) ? expanded : path27.resolve(configDirPath, expanded) : null;
26746
+ const expanded = tokenFile ? path28.resolve(tokenFile) : null;
26747
+ const resolvedTokenFile = expanded && fs5.existsSync(expanded) ? resolvePath(expanded) : expanded ? path28.isAbsolute(expanded) ? expanded : path28.resolve(configDirPath, expanded) : null;
25942
26748
  youtrackConfig = {
25943
26749
  config_edit_path: resolvePath(ytJson),
25944
26750
  baseUrl: base,
@@ -25985,11 +26791,11 @@ function initStatusData(configDirPath = configDir()) {
25985
26791
  id: "youtrack_json",
25986
26792
  label: "YouTrack config",
25987
26793
  ok: fs5.existsSync(ytJson) && youtrackConfig !== null && !("error" in youtrackConfig),
25988
- path: fs5.existsSync(ytJson) ? resolvePath(ytJson) : path27.resolve(ytJson),
26794
+ path: fs5.existsSync(ytJson) ? resolvePath(ytJson) : path28.resolve(ytJson),
25989
26795
  config_edit_path: resolvePath(ytJson),
25990
26796
  fix: "workflow_toolkit_init_apply action=youtrack_scaffold"
25991
26797
  });
25992
- const ytTokenPath = youtrackConfig?.tokenFile ?? path27.join(configDirPath, "youtrack.token");
26798
+ const ytTokenPath = youtrackConfig?.tokenFile ?? path28.join(configDirPath, "youtrack.token");
25993
26799
  const tokenText = fs5.existsSync(ytTokenPath) ? fs5.readFileSync(ytTokenPath, "utf8").trim() : "";
25994
26800
  const placeholder = fs5.existsSync(ytTokenPath) && isPlaceholder2(tokenText);
25995
26801
  const tokenOk = fs5.existsSync(ytTokenPath) && modeOk(ytTokenPath) && Boolean(tokenText) && !isPlaceholder2(tokenText);
@@ -25997,8 +26803,8 @@ function initStatusData(configDirPath = configDir()) {
25997
26803
  id: "youtrack_token",
25998
26804
  label: "YouTrack API token (mode 600, not placeholder)",
25999
26805
  ok: tokenOk,
26000
- path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path27.resolve(ytTokenPath),
26001
- token_edit_path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path27.resolve(ytTokenPath),
26806
+ path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path28.resolve(ytTokenPath),
26807
+ token_edit_path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path28.resolve(ytTokenPath),
26002
26808
  placeholder,
26003
26809
  fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER4} with your permanent token, save, then /wk-status`
26004
26810
  };
@@ -26023,7 +26829,7 @@ function initStatusData(configDirPath = configDir()) {
26023
26829
  const provider = String(vcsParsed.provider ?? "gitlab").toLowerCase();
26024
26830
  const tokenFiles = {};
26025
26831
  for (const k of ["gitlab", "github"]) {
26026
- tokenFiles[k] = String(vcsParsed[k]?.tokenFile ?? path27.join(configDirPath, `${k}.token`));
26832
+ tokenFiles[k] = String(vcsParsed[k]?.tokenFile ?? path28.join(configDirPath, `${k}.token`));
26027
26833
  }
26028
26834
  vcsCfg = {
26029
26835
  config_edit_path: resolvePath(vcsJson),
@@ -26048,14 +26854,14 @@ function initStatusData(configDirPath = configDir()) {
26048
26854
  id: "vcs_json",
26049
26855
  label: "VCS config (GitLab / GitHub)",
26050
26856
  ok: fs5.existsSync(vcsJson) && vcsCfg !== null && !("error" in vcsCfg),
26051
- path: fs5.existsSync(vcsJson) ? resolvePath(vcsJson) : path27.resolve(vcsJson),
26857
+ path: fs5.existsSync(vcsJson) ? resolvePath(vcsJson) : path28.resolve(vcsJson),
26052
26858
  config_edit_path: resolvePath(vcsJson),
26053
26859
  fix: "workflow_toolkit_init_apply action=vcs_scaffold"
26054
26860
  });
26055
26861
  const provActive = vcsCfg && !("error" in vcsCfg) ? vcsCfg.provider : null;
26056
26862
  const tokenItem = (tid, label, rawPath, providerKey) => {
26057
- const t = path27.isAbsolute(rawPath) ? rawPath : path27.resolve(configDirPath, rawPath);
26058
- const abs = fs5.existsSync(t) ? resolvePath(t) : path27.resolve(t);
26863
+ const t = path28.isAbsolute(rawPath) ? rawPath : path28.resolve(configDirPath, rawPath);
26864
+ const abs = fs5.existsSync(t) ? resolvePath(t) : path28.resolve(t);
26059
26865
  const text = fs5.existsSync(t) ? fs5.readFileSync(t, "utf8").trim() : "";
26060
26866
  const ph = isPlaceholder2(text);
26061
26867
  const ok2 = fs5.existsSync(t) && modeOk(t) && Boolean(text) && !ph;
@@ -26086,7 +26892,7 @@ function initStatusData(configDirPath = configDir()) {
26086
26892
  };
26087
26893
  const vcsTokenFiles = {};
26088
26894
  for (const k of ["gitlab", "github"]) {
26089
- vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path27.join(configDirPath, `${k}.token`));
26895
+ vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path28.join(configDirPath, `${k}.token`));
26090
26896
  }
26091
26897
  items.push(tokenItem("gitlab_token", "GitLab token (mode 600, not placeholder)", vcsTokenFiles.gitlab, "gitlab"));
26092
26898
  items.push(tokenItem("github_token", "GitHub token (mode 600, not placeholder)", vcsTokenFiles.github, "github"));
@@ -26143,16 +26949,16 @@ function initApplyData(action, env = process.env) {
26143
26949
  fs5.mkdirSync(dir, { recursive: true });
26144
26950
  switch (action) {
26145
26951
  case "youtrack_json": {
26146
- const out = path27.join(dir, "youtrack.json");
26952
+ const out = path28.join(dir, "youtrack.json");
26147
26953
  fs5.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
26148
26954
  `, "utf8");
26149
26955
  return { action, ok: true, path: out };
26150
26956
  }
26151
26957
  case "youtrack_token_placeholder": {
26152
- const p = path27.join(dir, "youtrack.token");
26958
+ const p = path28.join(dir, "youtrack.token");
26153
26959
  const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER4 + `
26154
26960
  `, 384) === "preserved";
26155
- const abs = path27.resolve(p);
26961
+ const abs = path28.resolve(p);
26156
26962
  return {
26157
26963
  action,
26158
26964
  ok: true,
@@ -26164,14 +26970,14 @@ function initApplyData(action, env = process.env) {
26164
26970
  };
26165
26971
  }
26166
26972
  case "youtrack_scaffold": {
26167
- const jsonOut = path27.join(dir, "youtrack.json");
26168
- const tokenOut = path27.join(dir, "youtrack.token");
26973
+ const jsonOut = path28.join(dir, "youtrack.json");
26974
+ const tokenOut = path28.join(dir, "youtrack.token");
26169
26975
  fs5.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
26170
26976
  `, "utf8");
26171
26977
  const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER4 + `
26172
26978
  `, 384) === "preserved";
26173
- const configPath2 = path27.resolve(jsonOut);
26174
- const tokenPath = path27.resolve(tokenOut);
26979
+ const configPath2 = path28.resolve(jsonOut);
26980
+ const tokenPath = path28.resolve(tokenOut);
26175
26981
  const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
26176
26982
  process.env.WORKFLOW_YOUTRACK_CONFIG = configPath2;
26177
26983
  let tokenCreate = {};
@@ -26225,19 +27031,19 @@ function initApplyData(action, env = process.env) {
26225
27031
  }
26226
27032
  }
26227
27033
  case "vcs_scaffold": {
26228
- const jsonOut = path27.join(dir, "vcs.json");
27034
+ const jsonOut = path28.join(dir, "vcs.json");
26229
27035
  fs5.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(dir), null, 2) + `
26230
27036
  `, "utf8");
26231
- const glPath = path27.join(dir, "gitlab.token");
26232
- const ghPath = path27.join(dir, "github.token");
27037
+ const glPath = path28.join(dir, "gitlab.token");
27038
+ const ghPath = path28.join(dir, "github.token");
26233
27039
  const preservedTokens = [];
26234
27040
  for (const p of [glPath, ghPath]) {
26235
27041
  if (writeFileExclusive(p, TOKEN_PLACEHOLDER4 + `
26236
27042
  `, 384) === "preserved") {
26237
- preservedTokens.push(path27.resolve(p));
27043
+ preservedTokens.push(path28.resolve(p));
26238
27044
  }
26239
27045
  }
26240
- const configPath2 = path27.resolve(jsonOut);
27046
+ const configPath2 = path28.resolve(jsonOut);
26241
27047
  const prev = process.env.WORKFLOW_VCS_CONFIG;
26242
27048
  process.env.WORKFLOW_VCS_CONFIG = configPath2;
26243
27049
  try {
@@ -26245,14 +27051,14 @@ function initApplyData(action, env = process.env) {
26245
27051
  const provider = String(cfg.provider ?? "gitlab");
26246
27052
  const tokenUrls = vcsTokenCreateUrls();
26247
27053
  const active = tokenUrls.active ?? {};
26248
- const activePath = provider === "gitlab" ? path27.resolve(glPath) : path27.resolve(ghPath);
27054
+ const activePath = provider === "gitlab" ? path28.resolve(glPath) : path28.resolve(ghPath);
26249
27055
  return {
26250
27056
  action,
26251
27057
  ok: true,
26252
27058
  vcs_json: configPath2,
26253
27059
  config_edit_path: configPath2,
26254
- gitlab_token: path27.resolve(glPath),
26255
- github_token: path27.resolve(ghPath),
27060
+ gitlab_token: path28.resolve(glPath),
27061
+ github_token: path28.resolve(ghPath),
26256
27062
  token_edit_path: activePath,
26257
27063
  token_create_url: active.createUrl,
26258
27064
  token_create_urls: tokenUrls,
@@ -26317,11 +27123,11 @@ var TOKEN_PLACEHOLDER4 = "YOUR_TOKEN_HERE", readJson2 = (p) => {
26317
27123
  try {
26318
27124
  return fs5.realpathSync(p);
26319
27125
  } catch {
26320
- return path27.resolve(p);
27126
+ return path28.resolve(p);
26321
27127
  }
26322
27128
  }, youtrackJsonContent = (dir) => ({
26323
27129
  baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
26324
- tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path27.join(dir, "youtrack.token"),
27130
+ tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path28.join(dir, "youtrack.token"),
26325
27131
  timezone: process.env.WORKFLOW_YT_TIMEZONE ?? "America/Santiago",
26326
27132
  locale: "es-CL",
26327
27133
  defaultMention: process.env.WORKFLOW_YT_MENTION ?? "Alejandra.Flores",
@@ -26357,11 +27163,11 @@ var TOKEN_PLACEHOLDER4 = "YOUR_TOKEN_HERE", readJson2 = (p) => {
26357
27163
  gitlab: {
26358
27164
  host: process.env.WORKFLOW_GITLAB_HOST ?? "gitlab.com",
26359
27165
  apiUrl: process.env.WORKFLOW_GITLAB_API_URL ?? "https://gitlab.com/api/v4",
26360
- tokenFile: path27.join(dir, "gitlab.token")
27166
+ tokenFile: path28.join(dir, "gitlab.token")
26361
27167
  },
26362
27168
  github: {
26363
27169
  host: process.env.WORKFLOW_GITHUB_HOST ?? "github.com",
26364
- tokenFile: path27.join(dir, "github.token")
27170
+ tokenFile: path28.join(dir, "github.token")
26365
27171
  },
26366
27172
  pr: { squashOnMerge: true, removeSourceBranch: true, pushBranch: true, confirmSkip: true },
26367
27173
  tokenDefaults: {
@@ -26382,185 +27188,6 @@ var init_init = __esm(() => {
26382
27188
  init_youtrack();
26383
27189
  });
26384
27190
 
26385
- // packages/workit-core/src/core/sdd.ts
26386
- import {
26387
- appendFileSync as appendFileSync2,
26388
- existsSync as existsSync18,
26389
- mkdirSync as mkdirSync12,
26390
- readFileSync as readFileSync19,
26391
- statSync as statSync8,
26392
- writeFileSync as writeFileSync13
26393
- } from "node:fs";
26394
- import { execFileSync as execFileSync6 } from "node:child_process";
26395
- import path28 from "node:path";
26396
- function todosFromTasks(tasks, completedTaskIds = []) {
26397
- const done = new Set((completedTaskIds ?? []).map((id) => Number(id)));
26398
- const todos = (tasks ?? []).map((t) => {
26399
- const id = Number(t.id);
26400
- return {
26401
- id: `task-${id}`,
26402
- content: `Task ${id}: ${t.title ?? ""}`.trim(),
26403
- status: done.has(id) ? "completed" : "pending"
26404
- };
26405
- });
26406
- const firstPending = todos.find((t) => t.status === "pending");
26407
- if (firstPending)
26408
- firstPending.status = "in_progress";
26409
- return todos;
26410
- }
26411
- function sddContext({
26412
- slug,
26413
- plan_path,
26414
- workspace_root
26415
- }) {
26416
- const resolved = resolveCanonicalLayout({ workspace_root, slug, plan_path });
26417
- if (!resolved.ok)
26418
- return { error: resolved.error };
26419
- const cwd = resolved.layout.workspace;
26420
- const resolvedSlug = resolved.layout.slug;
26421
- if (!resolvedSlug)
26422
- return { error: "slug or plan_path required" };
26423
- const sdd_dir = path28.posix.join("docs", resolvedSlug, "sdd");
26424
- const progress_path = path28.posix.join(sdd_dir, "progress.md");
26425
- const manifest_path = path28.posix.join(sdd_dir, "manifest.json");
26426
- let progress_lines = [];
26427
- let completed_task_ids = [];
26428
- const absProgress = path28.join(resolved.layout.sdd, "progress.md");
26429
- if (existsSync18(absProgress)) {
26430
- progress_lines = readFileSync19(absProgress, "utf8").split(`
26431
- `).map((ln) => ln.trim()).filter(Boolean);
26432
- const pat = /^Task\s+(\d+):\s+complete\b/i;
26433
- completed_task_ids = progress_lines.map((ln) => pat.exec(ln)?.[1]).filter(Boolean).map(Number);
26434
- }
26435
- let manifest = {};
26436
- const absManifest = path28.join(resolved.layout.sdd, "manifest.json");
26437
- if (existsSync18(absManifest)) {
26438
- try {
26439
- manifest = JSON.parse(readFileSync19(absManifest, "utf8"));
26440
- } catch {
26441
- manifest = {};
26442
- }
26443
- }
26444
- const legacy_path = path28.join(cwd, ".superpowers/sdd");
26445
- const legacy_exists = existsSync18(legacy_path);
26446
- let todos = [];
26447
- let task_count = 0;
26448
- if (plan_path) {
26449
- const planText = readFileSync19(resolved.layout.plan, "utf8");
26450
- const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
26451
- const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
26452
- if (spec_path) {
26453
- const validated = docsValidate({
26454
- spec_path,
26455
- plan_path: path28.relative(cwd, resolved.layout.plan),
26456
- workspace_root: cwd
26457
- });
26458
- if (validated.ok === false)
26459
- return { ok: false, errors: validated.errors, error: validated.error };
26460
- }
26461
- const tasks = parseTasksFromPlan(planText);
26462
- if (tasks.length > 0) {
26463
- task_count = tasks.length;
26464
- todos = todosFromTasks(tasks, completed_task_ids);
26465
- }
26466
- }
26467
- const flow = readFlowState(cwd, resolvedSlug);
26468
- return {
26469
- slug: resolvedSlug,
26470
- sdd_dir,
26471
- progress_path,
26472
- manifest_path,
26473
- progress_lines,
26474
- completed_task_ids,
26475
- manifest,
26476
- created: existsSync18(path28.join(cwd, sdd_dir)),
26477
- forbidden_legacy_path: ".superpowers/sdd",
26478
- legacy_sdd_exists: legacy_exists,
26479
- warning: legacy_exists ? "Ignore .superpowers/sdd — use sdd_dir from this tool only" : undefined,
26480
- todos,
26481
- task_count,
26482
- flow: { spec: flow.spec, plan: flow.plan, menu: flow.menu },
26483
- todowrite_required: true,
26484
- 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."
26485
- };
26486
- }
26487
- function sddTaskBrief({
26488
- sdd_dir,
26489
- task_id,
26490
- section_text,
26491
- workspace_root
26492
- }) {
26493
- const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
26494
- if (!contained.ok)
26495
- return { error: contained.error };
26496
- const dir = contained.path;
26497
- mkdirSync12(dir, { recursive: true });
26498
- const out = path28.join(dir, `task-${task_id}-brief.md`);
26499
- writeFileSync13(out, `# Task ${task_id} brief
26500
-
26501
- ${section_text}
26502
- `, "utf8");
26503
- const rel = posix3(path28.relative(contained.base, out));
26504
- return { brief_path: rel, task_id };
26505
- }
26506
- function sddReviewPackage({
26507
- sdd_dir,
26508
- base_sha,
26509
- head_sha,
26510
- workspace_root
26511
- }) {
26512
- const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
26513
- if (!contained.ok)
26514
- return { error: contained.error };
26515
- const dir = contained.path;
26516
- mkdirSync12(dir, { recursive: true });
26517
- const base7 = base_sha.slice(0, 7);
26518
- const head7 = head_sha.slice(0, 7);
26519
- const diffPath = path28.join(dir, `review-${base7}..${head7}.diff`);
26520
- try {
26521
- const diff = execFileSync6("git", ["diff", base_sha, head_sha], {
26522
- cwd: contained.base,
26523
- encoding: "utf8",
26524
- stdio: ["pipe", "pipe", "pipe"]
26525
- });
26526
- writeFileSync13(diffPath, diff, "utf8");
26527
- const rel = posix3(path28.relative(contained.base, diffPath));
26528
- return { diff_path: rel, base_sha, head_sha, base7, head7 };
26529
- } catch (error2) {
26530
- return { error: error2 instanceof Error ? error2.message : "git diff failed" };
26531
- }
26532
- }
26533
- function sddAppendProgress({
26534
- progress_path,
26535
- line,
26536
- workspace_root
26537
- }) {
26538
- const contained = resolveDocsPath({ workspace_root, path: progress_path });
26539
- if (!contained.ok)
26540
- return { error: contained.error };
26541
- const path_ = contained.path;
26542
- const trimmed = line.trim();
26543
- if (!PROGRESS_RE.test(trimmed)) {
26544
- return { error: "invalid progress line format" };
26545
- }
26546
- if (existsSync18(path_) && statSync8(path_).isDirectory()) {
26547
- return { error: `progress path is a directory: ${progress_path}` };
26548
- }
26549
- mkdirSync12(path28.dirname(path_), { recursive: true });
26550
- appendFileSync2(path_, trimmed + `
26551
- `, "utf8");
26552
- const rel = posix3(path28.relative(contained.base, path_));
26553
- return { ok: true, line: trimmed, progress_path: rel };
26554
- }
26555
- var posix3 = (p) => p.split(path28.sep).join("/"), PROGRESS_RE;
26556
- var init_sdd = __esm(() => {
26557
- init_docs_validate();
26558
- init_docs_validate();
26559
- init_flow_state();
26560
- init_docs_layout();
26561
- PROGRESS_RE = /^Task\s+\d+:\s+complete\s+\(commits\s+[0-9a-f]{7,40}\.\.[0-9a-f]{7,40},/i;
26562
- });
26563
-
26564
27191
  // packages/workit-core/src/core/present.ts
26565
27192
  function boxLine(text, width) {
26566
27193
  const inner = width - 4;
@@ -26713,7 +27340,38 @@ var logger, readServerVersion = () => {
26713
27340
  };
26714
27341
  }
26715
27342
  });
26716
- }, changelogCategorySchema, transport;
27343
+ }, changelogCategorySchema, lifecycleTool = (action, description) => {
27344
+ registerTool(`workflow_plan_${action}`, {
27345
+ description,
27346
+ inputSchema: {
27347
+ plan_path: exports_external.string(),
27348
+ workspace_root: workspaceRootSchema
27349
+ }
27350
+ }, async ({ plan_path, workspace_root }) => {
27351
+ const resolved = resolveCanonicalLayout({ workspace_root, plan_path });
27352
+ if (!resolved.ok) {
27353
+ return jsonResult(withWorkspace(workspace_root, { error: resolved.error }));
27354
+ }
27355
+ const { workspace, slug } = resolved.layout;
27356
+ const result = transitionExecution(workspace, slug, plan_path, action, cursorConfirmation(), cursorMutationContext(workspace));
27357
+ if (result.ok === false) {
27358
+ return jsonResult(withWorkspace(workspace_root, {
27359
+ error: result.error,
27360
+ code: result.code,
27361
+ ...result.details ? { details: result.details } : {}
27362
+ }));
27363
+ }
27364
+ const effective = readEffectiveFlowState(workspace, slug);
27365
+ if (!effective.ok) {
27366
+ return jsonResult(withWorkspace(workspace_root, { error: effective.error, code: effective.code }));
27367
+ }
27368
+ return jsonResult(withWorkspace(workspace_root, {
27369
+ plan: plan_path,
27370
+ execution: effective.state.execution,
27371
+ drift: effective.drift
27372
+ }));
27373
+ });
27374
+ }, transport;
26717
27375
  var init_server3 = __esm(async () => {
26718
27376
  init_mcp();
26719
27377
  init_stdio2();
@@ -27217,40 +27875,54 @@ var init_server3 = __esm(async () => {
27217
27875
  return jsonResult(withWorkspace(workspace_root, { error: built.error }));
27218
27876
  }
27219
27877
  const { prompt, spec: specPath, plan: planPath } = built;
27220
- if (planPath) {
27221
- const tasksData = parsePlanTasks(planPath, root);
27222
- if ("error" in tasksData) {
27223
- return jsonResult(withWorkspace(workspace_root, {
27224
- prompt,
27225
- error: tasksData.error
27226
- }));
27227
- }
27228
- const payload = {
27878
+ if (!planPath) {
27879
+ return jsonResult(withWorkspace(workspace_root, {
27880
+ error: "Could not resolve spec and plan for handoff"
27881
+ }));
27882
+ }
27883
+ const tasksData = parsePlanTasks(planPath, root);
27884
+ if ("error" in tasksData) {
27885
+ return jsonResult(withWorkspace(workspace_root, {
27229
27886
  prompt,
27230
- tasks: tasksData.tasks,
27231
- task_count: tasksData.task_count,
27232
- workspace_root: root
27233
- };
27234
- if (specPath) {
27235
- const branchData = resolveHandoffBranch(specPath, planPath, root);
27236
- if (!("error" in branchData)) {
27237
- payload.branch = branchData.branch;
27238
- }
27239
- }
27240
- const sdd = sddContext({ plan_path: planPath, workspace_root: root });
27241
- if (!sdd.error) {
27242
- payload.slug = sdd.slug;
27243
- payload.sdd_dir = sdd.sdd_dir;
27244
- payload.progress_path = sdd.progress_path;
27245
- payload.completed_task_ids = sdd.completed_task_ids;
27246
- payload.todos = sdd.todos;
27247
- payload.todo_write_required = true;
27887
+ error: tasksData.error
27888
+ }));
27889
+ }
27890
+ const slug = slugFromPath(planPath);
27891
+ const marked = markHandoffDestination(root, slug, planPath);
27892
+ if (marked.ok === false) {
27893
+ return jsonResult(withWorkspace(workspace_root, {
27894
+ error: marked.error,
27895
+ code: marked.code,
27896
+ ...marked.details ? { details: marked.details } : {}
27897
+ }));
27898
+ }
27899
+ const payload = {
27900
+ prompt,
27901
+ tasks: tasksData.tasks,
27902
+ task_count: tasksData.task_count,
27903
+ workspace_root: root
27904
+ };
27905
+ if (specPath) {
27906
+ const branchData = resolveHandoffBranch(specPath, planPath, root);
27907
+ if (!("error" in branchData)) {
27908
+ payload.branch = branchData.branch;
27248
27909
  }
27249
- return jsonResult(withWorkspace(workspace_root, payload));
27250
27910
  }
27251
- return jsonResult(withWorkspace(workspace_root, {
27252
- error: "Could not resolve spec and plan for handoff"
27253
- }));
27911
+ const sdd = sddContext({ plan_path: planPath, workspace_root: root });
27912
+ if (!sdd.error) {
27913
+ payload.slug = sdd.slug;
27914
+ payload.sdd_dir = sdd.sdd_dir;
27915
+ payload.progress_path = sdd.progress_path;
27916
+ payload.completed_task_ids = sdd.completed_task_ids;
27917
+ payload.todos = sdd.todos;
27918
+ payload.todo_write_required = true;
27919
+ }
27920
+ const effective = readEffectiveFlowState(root, slug);
27921
+ if (effective.ok) {
27922
+ payload.handoff_destination = effective.state.handoff_destination;
27923
+ payload.menu = effective.state.menu;
27924
+ }
27925
+ return jsonResult(withWorkspace(workspace_root, payload));
27254
27926
  });
27255
27927
  registerTool("workflow_toolkit_init_status", {
27256
27928
  description: "Check workit setup (MCP deps, YouTrack config, token)",
@@ -27547,18 +28219,23 @@ var init_server3 = __esm(async () => {
27547
28219
  if (!resolved.ok)
27548
28220
  return jsonResult(withWorkspace(workspace_root, { error: resolved.error }));
27549
28221
  const { workspace, slug } = resolved.layout;
27550
- let state = readFlowState(workspace, slug);
27551
- if (!state.activated) {
28222
+ let effective = readEffectiveFlowState(workspace, slug);
28223
+ if (!effective.ok && effective.code === "flow_not_activated") {
27552
28224
  const prepared = prepareFlowState(workspace, slug, { spec_path, plan_path }, cursorMutationContext(workspace));
27553
28225
  if (!prepared.ok)
27554
28226
  return jsonResult({ error: prepared.error, code: prepared.code });
27555
- state = readFlowState(workspace, slug);
28227
+ effective = readEffectiveFlowState(workspace, slug);
27556
28228
  }
28229
+ if (!effective.ok)
28230
+ return jsonResult({ error: effective.error, code: effective.code });
28231
+ const { state, drift } = effective;
27557
28232
  return jsonResult({
27558
28233
  slug,
27559
28234
  spec: state.spec,
27560
28235
  plan: state.plan,
27561
28236
  menu: state.menu,
28237
+ execution: state.execution,
28238
+ drift,
27562
28239
  flow_path: `docs/${slug}/sdd/flow.json`
27563
28240
  });
27564
28241
  });
@@ -27611,6 +28288,9 @@ var init_server3 = __esm(async () => {
27611
28288
  return jsonResult({ error: result.error, code: result.code });
27612
28289
  return jsonResult({ menu: { presented: true, chosen: choice } });
27613
28290
  });
28291
+ 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.");
28292
+ 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.");
28293
+ 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.");
27614
28294
  registerTool("workflow_docs_repo_link", {
27615
28295
  description: "Link the component docs repo in the toolkit config",
27616
28296
  inputSchema: {