@brainervirus/workit-cursor 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23209,14 +23209,23 @@ var init_hygiene = __esm(() => {
23209
23209
  });
23210
23210
 
23211
23211
  // 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";
23212
+ import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
23213
23213
  import path14 from "node:path";
23214
23214
  var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).join("/"), canonicalize = (base, candidate) => {
23215
23215
  const abs = path14.resolve(base, candidate);
23216
23216
  let ancestor = abs;
23217
23217
  while (!existsSync8(ancestor))
23218
23218
  ancestor = path14.dirname(ancestor);
23219
- const real = realpathSync2(ancestor);
23219
+ let real;
23220
+ try {
23221
+ real = realpathSync2(ancestor);
23222
+ } catch (error2) {
23223
+ if (error2.code === "EACCES" && !lstatSync(ancestor).isSymbolicLink()) {
23224
+ real = path14.join(realpathSync2(path14.dirname(ancestor)), path14.basename(ancestor));
23225
+ } else {
23226
+ throw error2;
23227
+ }
23228
+ }
23220
23229
  if (real !== base && !real.startsWith(base + path14.sep)) {
23221
23230
  throw new Error(`path must stay inside repository root: ${candidate}`);
23222
23231
  }
@@ -23256,14 +23265,8 @@ var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).joi
23256
23265
  if (path14.isAbsolute(candidate)) {
23257
23266
  return { ok: false, error: `absolute path not allowed: ${candidate}` };
23258
23267
  }
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$/);
23268
+ const spelling = posix(candidate);
23269
+ const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
23267
23270
  if (!match) {
23268
23271
  return {
23269
23272
  ok: false,
@@ -23290,6 +23293,18 @@ var SLUG_RE, LEGACY_SLUG = "superpowers", posix = (p) => p.split(path14.sep).joi
23290
23293
  };
23291
23294
  }
23292
23295
  derived = pathSlug;
23296
+ let abs;
23297
+ try {
23298
+ abs = canonicalize(workspace, candidate);
23299
+ } catch (error2) {
23300
+ return { ok: false, error: error2 instanceof Error ? error2.message : String(error2) };
23301
+ }
23302
+ if (posix(path14.relative(workspace, abs)) !== spelling) {
23303
+ return {
23304
+ ok: false,
23305
+ error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`
23306
+ };
23307
+ }
23293
23308
  }
23294
23309
  let resolvedSlug = slug;
23295
23310
  if (resolvedSlug !== undefined) {
@@ -23798,6 +23813,9 @@ ${JSON.stringify({ ok: false, errors: validated.errors })}`
23798
23813
  return { error: "missing template templates/execution-contract.md" };
23799
23814
  }
23800
23815
  contract = contract.replace(/<SPEC_PATH>/g, spec).replace(/<PLAN_PATH>/g, plan).replace(/<BRANCH>/g, branch).replace(/<SLUG>/g, slug).replace(/<SDD_DIR>/g, sddDir).replace(/<TASK_LIST>/g, taskList);
23816
+ if (!/^<workflow-handoff-destination>true<\/workflow-handoff-destination>$/m.test(contract)) {
23817
+ return { error: "handoff destination contract missing its destination marker" };
23818
+ }
23801
23819
  return { prompt: contract };
23802
23820
  };
23803
23821
  var init_handoff_context = __esm(() => {
@@ -23831,9 +23849,240 @@ var init_handoff_tools = __esm(() => {
23831
23849
  init_package_root();
23832
23850
  });
23833
23851
 
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";
23852
+ // packages/workit-core/src/core/sdd.ts
23853
+ import {
23854
+ appendFileSync as appendFileSync2,
23855
+ existsSync as existsSync11,
23856
+ mkdirSync as mkdirSync6,
23857
+ readFileSync as readFileSync12,
23858
+ statSync as statSync6,
23859
+ writeFileSync as writeFileSync5
23860
+ } from "node:fs";
23861
+ import { execFileSync as execFileSync4 } from "node:child_process";
23836
23862
  import path19 from "node:path";
23863
+ function todosFromTasks(tasks, completedTaskIds = []) {
23864
+ const done = new Set((completedTaskIds ?? []).map((id) => Number(id)));
23865
+ const todos = (tasks ?? []).map((t) => {
23866
+ const id = Number(t.id);
23867
+ return {
23868
+ id: `task-${id}`,
23869
+ content: `Task ${id}: ${t.title ?? ""}`.trim(),
23870
+ status: done.has(id) ? "completed" : "pending"
23871
+ };
23872
+ });
23873
+ const firstPending = todos.find((t) => t.status === "pending");
23874
+ if (firstPending)
23875
+ firstPending.status = "in_progress";
23876
+ return todos;
23877
+ }
23878
+ function ledgerCompletion(root, slug) {
23879
+ let started = false;
23880
+ const completed = [];
23881
+ const absProgress = path19.join(root, "docs", slug, "sdd", "progress.md");
23882
+ if (existsSync11(absProgress)) {
23883
+ try {
23884
+ for (const line of readFileSync12(absProgress, "utf8").split(`
23885
+ `)) {
23886
+ const match = /^Task\s+(\d+):/i.exec(line);
23887
+ if (match) {
23888
+ started = true;
23889
+ if (/^Task\s+\d+:\s*complete\b/i.test(line))
23890
+ completed.push(Number(match[1]));
23891
+ }
23892
+ }
23893
+ } catch {}
23894
+ }
23895
+ const required2 = [];
23896
+ try {
23897
+ const absPlan = path19.join(root, "docs", slug, "plan.md");
23898
+ if (existsSync11(absPlan)) {
23899
+ for (const task of parseTasksFromPlan(readFileSync12(absPlan, "utf8")))
23900
+ required2.push(task.id);
23901
+ }
23902
+ } catch {}
23903
+ const completedSet = new Set(completed);
23904
+ const missing = required2.filter((id) => !completedSet.has(id));
23905
+ return {
23906
+ started,
23907
+ complete: required2.length > 0 && missing.length === 0,
23908
+ required: required2,
23909
+ completed,
23910
+ missing
23911
+ };
23912
+ }
23913
+ function sddContext({
23914
+ slug,
23915
+ plan_path,
23916
+ workspace_root
23917
+ }) {
23918
+ const resolved = resolveCanonicalLayout({ workspace_root, slug, plan_path });
23919
+ if (!resolved.ok)
23920
+ return { error: resolved.error };
23921
+ const cwd = resolved.layout.workspace;
23922
+ const resolvedSlug = resolved.layout.slug;
23923
+ if (!resolvedSlug)
23924
+ return { error: "slug or plan_path required" };
23925
+ const sdd_dir = path19.posix.join("docs", resolvedSlug, "sdd");
23926
+ const progress_path = path19.posix.join(sdd_dir, "progress.md");
23927
+ const manifest_path = path19.posix.join(sdd_dir, "manifest.json");
23928
+ let progress_lines = [];
23929
+ let completed_task_ids = [];
23930
+ const absProgress = path19.join(resolved.layout.sdd, "progress.md");
23931
+ if (existsSync11(absProgress)) {
23932
+ progress_lines = readFileSync12(absProgress, "utf8").split(`
23933
+ `).map((ln) => ln.trim()).filter(Boolean);
23934
+ const pat = /^Task\s+(\d+):\s+complete\b/i;
23935
+ completed_task_ids = progress_lines.map((ln) => pat.exec(ln)?.[1]).filter(Boolean).map(Number);
23936
+ }
23937
+ let manifest = {};
23938
+ const absManifest = path19.join(resolved.layout.sdd, "manifest.json");
23939
+ if (existsSync11(absManifest)) {
23940
+ try {
23941
+ manifest = JSON.parse(readFileSync12(absManifest, "utf8"));
23942
+ } catch {
23943
+ manifest = {};
23944
+ }
23945
+ }
23946
+ const legacy_path = path19.join(cwd, ".superpowers/sdd");
23947
+ const legacy_exists = existsSync11(legacy_path);
23948
+ let todos = [];
23949
+ let task_count = 0;
23950
+ if (plan_path) {
23951
+ const planText = readFileSync12(resolved.layout.plan, "utf8");
23952
+ const specMatch = planText.match(/^\*\*Spec:\*\*\s*(?:`([^`]+)`|(\S+))/m);
23953
+ const spec_path = specMatch?.[1] ?? specMatch?.[2] ?? "";
23954
+ if (spec_path) {
23955
+ const validated = docsValidate({
23956
+ spec_path,
23957
+ plan_path: path19.relative(cwd, resolved.layout.plan),
23958
+ workspace_root: cwd
23959
+ });
23960
+ if (validated.ok === false)
23961
+ return { ok: false, errors: validated.errors, error: validated.error };
23962
+ }
23963
+ const tasks = parseTasksFromPlan(planText);
23964
+ if (tasks.length > 0) {
23965
+ task_count = tasks.length;
23966
+ todos = todosFromTasks(tasks, completed_task_ids);
23967
+ }
23968
+ }
23969
+ const flow = readFlowState(cwd, resolvedSlug);
23970
+ return {
23971
+ slug: resolvedSlug,
23972
+ sdd_dir,
23973
+ progress_path,
23974
+ manifest_path,
23975
+ progress_lines,
23976
+ completed_task_ids,
23977
+ manifest,
23978
+ created: existsSync11(path19.join(cwd, sdd_dir)),
23979
+ forbidden_legacy_path: ".superpowers/sdd",
23980
+ legacy_sdd_exists: legacy_exists,
23981
+ warning: legacy_exists ? "Ignore .superpowers/sdd — use sdd_dir from this tool only" : undefined,
23982
+ todos,
23983
+ task_count,
23984
+ flow: { spec: flow.spec, plan: flow.plan, menu: flow.menu },
23985
+ todowrite_required: true,
23986
+ todowrite_hint: "REQUIRED: Call OpenCode todowrite with todos from this result so the native task list shows progress. Before each task set status in_progress; after workflow_sdd_append_progress set it completed."
23987
+ };
23988
+ }
23989
+ function sddTaskBrief({
23990
+ sdd_dir,
23991
+ task_id,
23992
+ section_text,
23993
+ workspace_root
23994
+ }) {
23995
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
23996
+ if (!contained.ok)
23997
+ return { error: contained.error };
23998
+ const dir = contained.path;
23999
+ mkdirSync6(dir, { recursive: true });
24000
+ const out = path19.join(dir, `task-${task_id}-brief.md`);
24001
+ writeFileSync5(out, `# Task ${task_id} brief
24002
+
24003
+ ${section_text}
24004
+ `, "utf8");
24005
+ const rel = posix2(path19.relative(contained.base, out));
24006
+ return { brief_path: rel, task_id };
24007
+ }
24008
+ function sddReviewPackage({
24009
+ sdd_dir,
24010
+ base_sha,
24011
+ head_sha,
24012
+ workspace_root
24013
+ }) {
24014
+ const contained = resolveDocsPath({ workspace_root, path: sdd_dir });
24015
+ if (!contained.ok)
24016
+ return { error: contained.error };
24017
+ const dir = contained.path;
24018
+ mkdirSync6(dir, { recursive: true });
24019
+ const base7 = base_sha.slice(0, 7);
24020
+ const head7 = head_sha.slice(0, 7);
24021
+ const diffPath = path19.join(dir, `review-${base7}..${head7}.diff`);
24022
+ try {
24023
+ const diff = execFileSync4("git", ["diff", base_sha, head_sha], {
24024
+ cwd: contained.base,
24025
+ encoding: "utf8",
24026
+ stdio: ["pipe", "pipe", "pipe"]
24027
+ });
24028
+ writeFileSync5(diffPath, diff, "utf8");
24029
+ const rel = posix2(path19.relative(contained.base, diffPath));
24030
+ return { diff_path: rel, base_sha, head_sha, base7, head7 };
24031
+ } catch (error2) {
24032
+ return { error: error2 instanceof Error ? error2.message : "git diff failed" };
24033
+ }
24034
+ }
24035
+ function sddAppendProgress({
24036
+ progress_path,
24037
+ line,
24038
+ workspace_root
24039
+ }) {
24040
+ const contained = resolveDocsPath({ workspace_root, path: progress_path });
24041
+ if (!contained.ok)
24042
+ return { error: contained.error };
24043
+ const path_ = contained.path;
24044
+ const trimmed = line.trim();
24045
+ if (!PROGRESS_RE.test(trimmed)) {
24046
+ return { error: "invalid progress line format" };
24047
+ }
24048
+ if (existsSync11(path_) && statSync6(path_).isDirectory()) {
24049
+ return { error: `progress path is a directory: ${progress_path}` };
24050
+ }
24051
+ mkdirSync6(path19.dirname(path_), { recursive: true });
24052
+ appendFileSync2(path_, trimmed + `
24053
+ `, "utf8");
24054
+ const rel = posix2(path19.relative(contained.base, path_));
24055
+ return { ok: true, line: trimmed, progress_path: rel };
24056
+ }
24057
+ var posix2 = (p) => p.split(path19.sep).join("/"), PROGRESS_RE;
24058
+ var init_sdd = __esm(() => {
24059
+ init_docs_validate();
24060
+ init_docs_validate();
24061
+ init_flow_state();
24062
+ init_docs_layout();
24063
+ PROGRESS_RE = /^Task\s+\d+:\s+complete\s+\(commits\s+[0-9a-f]{7,40}\.\.[0-9a-f]{7,40},/i;
24064
+ });
24065
+
24066
+ // packages/workit-core/src/core/menu.ts
24067
+ var init_menu = () => {};
24068
+
24069
+ // packages/workit-core/src/core/flow-state.ts
24070
+ import {
24071
+ closeSync,
24072
+ existsSync as existsSync12,
24073
+ fstatSync,
24074
+ fsyncSync,
24075
+ mkdirSync as mkdirSync7,
24076
+ openSync,
24077
+ readFileSync as readFileSync13,
24078
+ renameSync,
24079
+ rmSync,
24080
+ statSync as statSync7,
24081
+ unlinkSync as unlinkSync3,
24082
+ writeFileSync as writeFileSync6
24083
+ } from "node:fs";
24084
+ import { createHash } from "node:crypto";
24085
+ import path20 from "node:path";
23837
24086
 
23838
24087
  class HostReceiptStore {
23839
24088
  #bySession = new Map;
@@ -23888,10 +24137,15 @@ class HostReceiptStore {
23888
24137
  return { ok: true, receipt };
23889
24138
  }
23890
24139
  }
23891
- var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, err2 = (code, error2) => ({ ok: false, code, error: error2 }), SLUG_RE2, flowPath = (root, slug) => {
24140
+ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, err2 = (code, error2, details) => ({
24141
+ ok: false,
24142
+ code,
24143
+ error: error2,
24144
+ ...details ? { details } : {}
24145
+ }), SLUG_RE2, flowPath = (root, slug) => {
23892
24146
  if (!SLUG_RE2.test(slug))
23893
24147
  throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
23894
- return path19.join(root, "docs", slug, "sdd", "flow.json");
24148
+ return path20.join(root, "docs", slug, "sdd", "flow.json");
23895
24149
  }, resolveDoc = (root, slug, docPath, kind) => {
23896
24150
  const resolved = resolveCanonicalLayout({
23897
24151
  workspace_root: root,
@@ -23906,60 +24160,233 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23906
24160
  const spec = p.spec ?? {};
23907
24161
  const plan = p.plan ?? {};
23908
24162
  const menu = p.menu ?? {};
24163
+ const execution = p.execution ?? {};
23909
24164
  return {
23910
24165
  slug: p.slug ?? slug,
23911
24166
  activated: p.activated ?? true,
23912
24167
  spec: {
23913
24168
  path: spec.path ?? "",
23914
24169
  status: spec.status ?? "draft",
23915
- evidence: spec.evidence ?? null
24170
+ evidence: spec.evidence ?? null,
24171
+ approved_digest: spec.approved_digest ?? null
23916
24172
  },
23917
24173
  plan: {
23918
24174
  path: plan.path ?? "",
23919
24175
  status: plan.status ?? "draft",
23920
- evidence: plan.evidence ?? null
24176
+ evidence: plan.evidence ?? null,
24177
+ approved_digest: plan.approved_digest ?? null
23921
24178
  },
23922
24179
  menu: {
23923
24180
  presented: Boolean(menu.presented),
23924
24181
  chosen: menu.chosen ?? "",
23925
24182
  evidence: menu.evidence ?? null
23926
24183
  },
24184
+ execution: {
24185
+ status: execution.status ?? "pending",
24186
+ mode: execution.mode ?? null,
24187
+ evidence: execution.evidence ?? null
24188
+ },
24189
+ handoff_destination: p.handoff_destination ?? false,
23927
24190
  updated_at: p.updated_at ?? Date.now()
23928
24191
  };
23929
24192
  }, emptyState = (slug) => ({
23930
24193
  slug,
23931
24194
  activated: false,
23932
- spec: { path: "", status: "draft", evidence: null },
23933
- plan: { path: "", status: "draft", evidence: null },
24195
+ spec: { path: "", status: "draft", evidence: null, approved_digest: null },
24196
+ plan: { path: "", status: "draft", evidence: null, approved_digest: null },
23934
24197
  menu: { presented: false, chosen: "", evidence: null },
24198
+ execution: { status: "pending", mode: null, evidence: null },
24199
+ handoff_destination: false,
23935
24200
  updated_at: Date.now()
23936
24201
  }), readFlowState = (root, slug) => {
23937
24202
  const file = flowPath(root, slug);
23938
- if (!existsSync11(file))
24203
+ if (!existsSync12(file))
23939
24204
  return emptyState(slug);
23940
24205
  try {
23941
- return normalizeState(JSON.parse(readFileSync12(file, "utf8")), slug);
24206
+ return normalizeState(JSON.parse(readFileSync13(file, "utf8")), slug);
23942
24207
  } catch {
23943
24208
  return emptyState(slug);
23944
24209
  }
24210
+ }, HEX64_RE, FLOW_STATUSES, EXECUTION_STATUSES, isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v), validateEvidenceValue = (v, allowCli) => {
24211
+ if (v === null)
24212
+ return true;
24213
+ if (!isRecord(v))
24214
+ return false;
24215
+ if (v.host === "opencode") {
24216
+ return v.attested === true && typeof v.callID === "string" && typeof v.selectedLabel === "string" && typeof v.recordedAt === "number";
24217
+ }
24218
+ if (v.host === "cursor")
24219
+ return v.attested === false && v.confirmation === "contract";
24220
+ if (allowCli && v.host === "cli") {
24221
+ return v.attested === false && (v.confirmation === "flag" || v.confirmation === "tty");
24222
+ }
24223
+ return false;
24224
+ }, validateState = (parsed, slug) => {
24225
+ if (!isRecord(parsed))
24226
+ return { ok: false, error: "flow state must be a JSON object" };
24227
+ if (parsed.slug !== undefined && (typeof parsed.slug !== "string" || parsed.slug !== slug)) {
24228
+ return { ok: false, error: `flow state slug must be ${JSON.stringify(slug)}` };
24229
+ }
24230
+ if (parsed.activated !== undefined && typeof parsed.activated !== "boolean") {
24231
+ return { ok: false, error: "flow state activated must be a boolean" };
24232
+ }
24233
+ if (parsed.handoff_destination !== undefined && typeof parsed.handoff_destination !== "boolean") {
24234
+ return { ok: false, error: "flow state handoff_destination must be a boolean" };
24235
+ }
24236
+ if (parsed.updated_at !== undefined && (typeof parsed.updated_at !== "number" || !Number.isFinite(parsed.updated_at))) {
24237
+ return { ok: false, error: "flow state updated_at must be a finite number" };
24238
+ }
24239
+ const doc2 = (value, name) => {
24240
+ const p = isRecord(value) ? value : {};
24241
+ if (!isRecord(value) && value !== undefined) {
24242
+ return `flow state ${name} must be an object`;
24243
+ }
24244
+ if (p.status !== undefined && !FLOW_STATUSES.includes(p.status)) {
24245
+ return `flow state ${name}.status must be draft, self_reviewed, or approved`;
24246
+ }
24247
+ if (p.path !== undefined && typeof p.path !== "string") {
24248
+ return `flow state ${name}.path must be a string`;
24249
+ }
24250
+ if (p.approved_digest !== undefined && p.approved_digest !== null && (typeof p.approved_digest !== "string" || !HEX64_RE.test(p.approved_digest))) {
24251
+ return `flow state ${name}.approved_digest must be 64-char lowercase hex or null`;
24252
+ }
24253
+ if (p.evidence !== undefined && !validateEvidenceValue(p.evidence, false)) {
24254
+ return `flow state ${name}.evidence has an unsupported shape`;
24255
+ }
24256
+ return {
24257
+ path: p.path ?? "",
24258
+ status: p.status ?? "draft",
24259
+ evidence: p.evidence ?? null,
24260
+ approved_digest: p.approved_digest ?? null
24261
+ };
24262
+ };
24263
+ const spec = doc2(parsed.spec, "spec");
24264
+ if (typeof spec === "string")
24265
+ return { ok: false, error: spec };
24266
+ const plan = doc2(parsed.plan, "plan");
24267
+ if (typeof plan === "string")
24268
+ return { ok: false, error: plan };
24269
+ const menuRaw = isRecord(parsed.menu) ? parsed.menu : undefined;
24270
+ if (parsed.menu !== undefined && !isRecord(parsed.menu)) {
24271
+ return { ok: false, error: "flow state menu must be an object" };
24272
+ }
24273
+ if (menuRaw?.presented !== undefined && typeof menuRaw.presented !== "boolean") {
24274
+ return { ok: false, error: "flow state menu.presented must be a boolean" };
24275
+ }
24276
+ if (menuRaw?.chosen !== undefined && typeof menuRaw.chosen !== "string") {
24277
+ return { ok: false, error: "flow state menu.chosen must be a string" };
24278
+ }
24279
+ if (menuRaw?.chosen !== undefined && menuRaw.chosen !== "" && !MENU_CHOICES.includes(menuRaw.chosen)) {
24280
+ return {
24281
+ ok: false,
24282
+ error: `flow state menu.chosen must be one of: ${MENU_CHOICES.join(", ")} (or an empty string when the menu is unpresented)`
24283
+ };
24284
+ }
24285
+ if (menuRaw?.evidence !== undefined && !validateEvidenceValue(menuRaw.evidence, false)) {
24286
+ return { ok: false, error: "flow state menu.evidence has an unsupported shape" };
24287
+ }
24288
+ const execRaw = isRecord(parsed.execution) ? parsed.execution : undefined;
24289
+ if (parsed.execution !== undefined && !isRecord(parsed.execution)) {
24290
+ return { ok: false, error: "flow state execution must be an object" };
24291
+ }
24292
+ if (execRaw?.status !== undefined && !EXECUTION_STATUSES.includes(execRaw.status)) {
24293
+ return {
24294
+ ok: false,
24295
+ error: "flow state execution.status must be pending, active, paused, or completed"
24296
+ };
24297
+ }
24298
+ if (execRaw?.mode !== undefined && execRaw.mode !== null && execRaw.mode !== "subagent-driven" && execRaw.mode !== "inline") {
24299
+ return {
24300
+ ok: false,
24301
+ error: "flow state execution.mode must be subagent-driven, inline, or null"
24302
+ };
24303
+ }
24304
+ if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
24305
+ return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
24306
+ }
24307
+ return {
24308
+ ok: true,
24309
+ state: {
24310
+ slug,
24311
+ activated: parsed.activated ?? true,
24312
+ spec,
24313
+ plan,
24314
+ menu: {
24315
+ presented: menuRaw?.presented ?? false,
24316
+ chosen: menuRaw?.chosen ?? "",
24317
+ evidence: menuRaw?.evidence ?? null
24318
+ },
24319
+ execution: {
24320
+ status: execRaw?.status ?? "pending",
24321
+ mode: execRaw?.mode ?? null,
24322
+ evidence: execRaw?.evidence ?? null
24323
+ },
24324
+ handoff_destination: parsed.handoff_destination ?? false,
24325
+ updated_at: parsed.updated_at ?? Date.now()
24326
+ }
24327
+ };
23945
24328
  }, readFlowStrict = (root, slug) => {
23946
24329
  const file = flowPath(root, slug);
23947
- if (!existsSync11(file)) {
24330
+ const rel = path20.posix.join("docs", slug, "sdd", "flow.json");
24331
+ if (!existsSync12(file)) {
23948
24332
  return err2("flow_not_activated", `flow not activated for ${slug} — run workflow_flow_status first`);
23949
24333
  }
24334
+ let text;
23950
24335
  try {
23951
- return { ok: true, state: normalizeState(JSON.parse(readFileSync12(file, "utf8")), slug) };
24336
+ text = readFileSync13(file, "utf8");
23952
24337
  } catch (error2) {
23953
- return err2("flow_corrupt", `corrupt flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24338
+ return err2("flow_io_error", `cannot read flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
23954
24339
  }
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 });
24340
+ let parsed;
24341
+ try {
24342
+ parsed = JSON.parse(text);
24343
+ } catch (error2) {
24344
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: rel, original_bytes_preserved: true });
24345
+ }
24346
+ const validated = validateState(parsed, slug);
24347
+ if (!validated.ok) {
24348
+ return err2("flow_state_invalid", `invalid flow state at ${file}: ${validated.error}`, {
24349
+ path: rel,
24350
+ original_bytes_preserved: true
24351
+ });
24352
+ }
24353
+ return { ok: true, state: validated.state, raw: parsed };
24354
+ }, uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`, writeFlowFileAtomic = (file, state) => {
24355
+ const text = JSON.stringify(state, null, 2) + `
24356
+ `;
23958
24357
  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) => {
24358
+ mkdirSync7(path20.dirname(file), { recursive: true });
24359
+ let fd = null;
24360
+ try {
24361
+ fd = openSync(tmp, "w");
24362
+ writeFileSync6(fd, text, "utf8");
24363
+ fsyncSync(fd);
24364
+ closeSync(fd);
24365
+ fd = null;
24366
+ renameSync(tmp, file);
24367
+ } finally {
24368
+ try {
24369
+ if (fd !== null)
24370
+ closeSync(fd);
24371
+ } catch {}
24372
+ try {
24373
+ if (existsSync12(tmp))
24374
+ rmSync(tmp, { force: true });
24375
+ } catch {}
24376
+ }
24377
+ }, MAX_WRITE_ATTEMPTS = 5, STALE_LOCK_MS = 1000, lockMtimeMs = (lock) => {
24378
+ try {
24379
+ return statSync7(lock).mtimeMs;
24380
+ } catch {
24381
+ return null;
24382
+ }
24383
+ }, lockOwnedBy = (fd, lock) => {
24384
+ try {
24385
+ return fstatSync(fd).ino === statSync7(lock).ino;
24386
+ } catch {
24387
+ return false;
24388
+ }
24389
+ }, writeFlowStateIfCurrent = (root, expected, next) => {
23963
24390
  const file = flowPath(root, next.slug);
23964
24391
  const expectedText = JSON.stringify(expected, null, 2) + `
23965
24392
  `;
@@ -23968,13 +24395,18 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23968
24395
  if (expectedText === nextText)
23969
24396
  return { ok: true };
23970
24397
  const tmp = uniqueTempPath(file);
24398
+ let fd = null;
23971
24399
  try {
23972
- const currentText = existsSync11(file) ? readFileSync12(file, "utf8") : null;
24400
+ const currentText = existsSync12(file) ? readFileSync13(file, "utf8") : null;
23973
24401
  if (currentText !== expectedText)
23974
24402
  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;
24403
+ mkdirSync7(path20.dirname(file), { recursive: true });
24404
+ fd = openSync(tmp, "w");
24405
+ writeFileSync6(fd, nextText, "utf8");
24406
+ fsyncSync(fd);
24407
+ closeSync(fd);
24408
+ fd = null;
24409
+ const reRead = existsSync12(file) ? readFileSync13(file, "utf8") : null;
23978
24410
  if (reRead !== expectedText)
23979
24411
  return { ok: false, conflict: true };
23980
24412
  renameSync(tmp, file);
@@ -23983,45 +24415,226 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
23983
24415
  return { ok: false, io_error: error2 instanceof Error ? error2.message : String(error2) };
23984
24416
  } finally {
23985
24417
  try {
23986
- if (existsSync11(tmp))
24418
+ if (fd !== null)
24419
+ closeSync(fd);
24420
+ } catch {}
24421
+ try {
24422
+ if (existsSync12(tmp))
23987
24423
  rmSync(tmp, { force: true });
23988
24424
  } catch {}
23989
24425
  }
23990
- }, readModifyWrite = (root, slug, mutate) => {
24426
+ }, readCanonicalDigest = (root, rel) => {
24427
+ const abs = path20.join(root, ...rel.split("/"));
24428
+ let bytes;
24429
+ try {
24430
+ bytes = readFileSync13(abs);
24431
+ } catch (error2) {
24432
+ if (error2.code === "ENOENT") {
24433
+ return { ok: false, code: "document_missing" };
24434
+ }
24435
+ return { ok: false, code: "document_unreadable" };
24436
+ }
24437
+ let text;
24438
+ try {
24439
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
24440
+ } catch {
24441
+ return { ok: false, code: "document_unreadable" };
24442
+ }
24443
+ return { ok: true, text, digest: createHash("sha256").update(bytes).digest("hex") };
24444
+ }, resetForSpecDrift = (state) => ({
24445
+ ...state,
24446
+ spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
24447
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24448
+ menu: { presented: false, chosen: "", evidence: null },
24449
+ execution: { status: "pending", mode: null, evidence: null },
24450
+ handoff_destination: false,
24451
+ updated_at: Date.now()
24452
+ }), resetForPlanDrift = (state) => ({
24453
+ ...state,
24454
+ plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
24455
+ menu: { presented: false, chosen: "", evidence: null },
24456
+ execution: { status: "pending", mode: null, evidence: null },
24457
+ handoff_destination: false,
24458
+ updated_at: Date.now()
24459
+ }), driftCodeFor = (root, relPath, storedDigest) => {
24460
+ if (storedDigest === null)
24461
+ return "digest_missing";
24462
+ const current = readCanonicalDigest(root, relPath);
24463
+ if (!current.ok)
24464
+ return current.code;
24465
+ return current.digest !== storedDigest ? "digest_mismatch" : null;
24466
+ }, reconcileState = (root, slug, state) => {
24467
+ const specPath = path20.posix.join("docs", slug, "spec.md");
24468
+ const planPath = path20.posix.join("docs", slug, "plan.md");
24469
+ if (state.spec.status === "approved") {
24470
+ const code = driftCodeFor(root, specPath, state.spec.approved_digest);
24471
+ if (code) {
24472
+ return {
24473
+ state: resetForSpecDrift(state),
24474
+ drift: [{ document: "spec", code, path: specPath }]
24475
+ };
24476
+ }
24477
+ }
24478
+ if (state.plan.status === "approved") {
24479
+ const code = driftCodeFor(root, planPath, state.plan.approved_digest);
24480
+ if (code) {
24481
+ return {
24482
+ state: resetForPlanDrift(state),
24483
+ drift: [{ document: "plan", code, path: planPath }]
24484
+ };
24485
+ }
24486
+ }
24487
+ return { state, drift: [] };
24488
+ }, deriveLegacyExecution = (root, slug, state) => {
24489
+ const ledger = ledgerCompletion(root, slug);
24490
+ if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
24491
+ return { status: "active", mode: "subagent-driven", evidence: null };
24492
+ }
24493
+ return { status: "pending", mode: null, evidence: null };
24494
+ }, normalizeCompatibility = (root, slug, parsed, state) => {
24495
+ if (!isRecord(parsed) || !("execution" in parsed)) {
24496
+ const derived = deriveLegacyExecution(root, slug, state);
24497
+ const current = state.execution;
24498
+ if (derived.status !== current.status || derived.mode !== current.mode) {
24499
+ return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
24500
+ }
24501
+ }
24502
+ return { state, changed: false };
24503
+ }, withFlowLock = (file, fn) => {
24504
+ const lock = `${file}.lock`;
24505
+ if (!existsSync12(path20.dirname(file)))
24506
+ return { locked: true, value: fn() };
24507
+ try {
24508
+ if (existsSync12(`${lock}.stale`))
24509
+ rmSync(`${lock}.stale`, { force: true });
24510
+ } catch {}
24511
+ const wait = new Int32Array(new SharedArrayBuffer(4));
24512
+ let fd = null;
23991
24513
  for (let attempt = 0;attempt < MAX_WRITE_ATTEMPTS; attempt++) {
24514
+ try {
24515
+ fd = openSync(lock, "wx");
24516
+ break;
24517
+ } catch (error2) {
24518
+ const code = error2.code;
24519
+ if (code !== "EEXIST") {
24520
+ return {
24521
+ locked: false,
24522
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`)
24523
+ };
24524
+ }
24525
+ const mtime = lockMtimeMs(lock);
24526
+ if (mtime !== null && Date.now() - mtime > STALE_LOCK_MS) {
24527
+ try {
24528
+ renameSync(lock, `${lock}.stale`);
24529
+ unlinkSync3(`${lock}.stale`);
24530
+ } catch {}
24531
+ try {
24532
+ fd = openSync(lock, "wx");
24533
+ break;
24534
+ } catch (innerError) {
24535
+ const innerCode = innerError.code;
24536
+ if (innerCode !== "EEXIST") {
24537
+ return {
24538
+ locked: false,
24539
+ error: err2("flow_io_error", `flow lock failed for ${file}: ${innerError instanceof Error ? innerError.message : String(innerError)}`)
24540
+ };
24541
+ }
24542
+ }
24543
+ }
24544
+ if (attempt === MAX_WRITE_ATTEMPTS - 1) {
24545
+ return {
24546
+ locked: false,
24547
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path20.dirname(file)}: re-read the flow state and retry the transition`)
24548
+ };
24549
+ }
24550
+ Atomics.wait(wait, 0, 0, 10);
24551
+ }
24552
+ }
24553
+ if (fd === null) {
24554
+ return {
24555
+ locked: false,
24556
+ error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path20.dirname(file)}: re-read the flow state and retry the transition`)
24557
+ };
24558
+ }
24559
+ try {
24560
+ return { locked: true, value: fn() };
24561
+ } finally {
24562
+ try {
24563
+ if (fd !== null && lockOwnedBy(fd, lock))
24564
+ rmSync(lock, { force: true });
24565
+ } catch {}
24566
+ try {
24567
+ if (fd !== null)
24568
+ closeSync(fd);
24569
+ } catch {}
24570
+ }
24571
+ }, readEffectiveFlowState = (root, slug) => {
24572
+ const file = flowPath(root, slug);
24573
+ const rel = path20.posix.join("docs", slug, "sdd", "flow.json");
24574
+ const locked = withFlowLock(file, () => {
23992
24575
  const strict = readFlowStrict(root, slug);
23993
24576
  if (!strict.ok)
23994
24577
  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}`);
24578
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
24579
+ const { state, drift } = reconcileState(root, slug, normalized.state);
24580
+ if (normalized.changed || drift.length > 0) {
24581
+ try {
24582
+ writeFlowFileAtomic(file, state);
24583
+ } catch (error2) {
24584
+ return err2("flow_io_error", `cannot persist reconciled flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: rel, original_bytes_preserved: true });
24585
+ }
24003
24586
  }
24004
- }
24005
- return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
24587
+ return { ok: true, state, drift };
24588
+ });
24589
+ if (!locked.locked)
24590
+ return locked.error;
24591
+ return locked.value;
24592
+ }, readModifyWrite = (root, slug, mutate) => {
24593
+ const file = flowPath(root, slug);
24594
+ const locked = withFlowLock(file, () => {
24595
+ for (let attempt = 0;attempt < MAX_WRITE_ATTEMPTS; attempt++) {
24596
+ const strict = readFlowStrict(root, slug);
24597
+ if (!strict.ok)
24598
+ return strict;
24599
+ const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
24600
+ const reconciled = reconcileState(root, slug, normalized.state);
24601
+ const result = mutate(reconciled.state);
24602
+ if (!result.ok)
24603
+ return result;
24604
+ let baseline = strict.state;
24605
+ if (normalized.changed) {
24606
+ try {
24607
+ writeFlowFileAtomic(file, normalized.state);
24608
+ } catch (error2) {
24609
+ return err2("flow_io_error", `cannot persist normalized flow state at ${file}: ${error2 instanceof Error ? error2.message : String(error2)}`);
24610
+ }
24611
+ baseline = normalized.state;
24612
+ }
24613
+ const commit = writeFlowStateIfCurrent(root, baseline, result.next);
24614
+ if (commit.ok)
24615
+ return { ok: true };
24616
+ if ("io_error" in commit) {
24617
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24618
+ }
24619
+ }
24620
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
24621
+ });
24622
+ if (!locked.locked)
24623
+ return locked.error;
24624
+ return locked.value;
24006
24625
  }, assertMutationWorkspace = (root, ctx) => {
24007
24626
  if (ctx && ctx.hostWorkspace !== root) {
24008
24627
  return err2("workspace_mismatch", `mutation context workspace ${JSON.stringify(ctx.hostWorkspace)} does not match flow workspace ${JSON.stringify(root)}`);
24009
24628
  }
24010
24629
  return { ok: true };
24011
- }, assertCoordinatorBoundary = (ctx, menu) => {
24012
- if (ctx?.role === "coordinator" && menu.chosen === "subagent-driven") {
24630
+ }, assertCoordinatorBoundary = (ctx, state) => {
24631
+ if (ctx?.role === "coordinator" && state.execution.status === "active" && state.execution.mode === "subagent-driven") {
24013
24632
  return err2("coordinator_blocked", COORDINATOR_RECOVERY_TEXT);
24014
24633
  }
24015
24634
  if (ctx?.role === "delegated" && !ctx.taskIdentity) {
24016
24635
  return err2("delegated_unauthenticated", "delegated mutations require an authenticated task identity (taskIdentity) — re-run inside the delegated worker session");
24017
24636
  }
24018
24637
  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
24638
  }, MAX_CLOCK_SKEW_MS = 60000, MAX_RECEIPTS_PER_SESSION = 10, RECEIPT_FRESHNESS_MS, EVIDENCE_WINDOW_MS, NEGATIVE_ANSWER_LABELS, isNegativeLabel = (label) => {
24026
24639
  const normalized = label.trim().toLowerCase();
24027
24640
  return NEGATIVE_ANSWER_LABELS.some((entry) => {
@@ -24125,24 +24738,38 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24125
24738
  });
24126
24739
  if (!resolved.ok)
24127
24740
  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 };
24741
+ const specPath = path20.posix.join("docs", slug, "spec.md");
24742
+ const planPath = path20.posix.join("docs", slug, "plan.md");
24743
+ const file = flowPath(root, slug);
24744
+ const locked = withFlowLock(file, () => {
24745
+ if (!existsSync12(file)) {
24746
+ writeFlowFileAtomic(file, {
24747
+ slug,
24748
+ activated: true,
24749
+ spec: { path: specPath, status: "draft", evidence: null, approved_digest: null },
24750
+ plan: { path: planPath, status: "draft", evidence: null, approved_digest: null },
24751
+ menu: { presented: false, chosen: "", evidence: null },
24752
+ execution: { status: "pending", mode: null, evidence: null },
24753
+ handoff_destination: false,
24754
+ updated_at: Date.now()
24755
+ });
24756
+ return { ok: true };
24757
+ }
24758
+ const strict = readFlowStrict(root, slug);
24759
+ if (!strict.ok)
24760
+ return strict;
24761
+ const reconciled = reconcileState(root, slug, strict.state);
24762
+ writeFlowFileAtomic(file, {
24763
+ ...reconciled.state,
24764
+ spec: { ...reconciled.state.spec, path: specPath },
24765
+ plan: { ...reconciled.state.plan, path: planPath },
24766
+ updated_at: Date.now()
24767
+ });
24768
+ return { ok: true };
24769
+ });
24770
+ if (!locked.locked)
24771
+ return locked.error;
24772
+ return locked.value;
24146
24773
  }, transitionSpec = (root, slug, specPath, evidence, ctx) => {
24147
24774
  const bound = assertMutationWorkspace(root, ctx);
24148
24775
  if (!bound.ok)
@@ -24153,39 +24780,39 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24153
24780
  const doc2 = resolveDoc(root, slug, specPath, "spec");
24154
24781
  if (!doc2.ok)
24155
24782
  return err2("path_invalid", doc2.error);
24783
+ const relPath = path20.posix.join("docs", slug, "spec.md");
24156
24784
  return readModifyWrite(root, slug, (state) => {
24157
- if (!existsSync11(doc2.path))
24785
+ if (!existsSync12(doc2.path))
24158
24786
  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");
24787
+ if (state.spec.status === "draft" || state.spec.status === "self_reviewed") {
24788
+ const digest = readCanonicalDigest(root, relPath);
24789
+ if (!digest.ok) {
24790
+ return err2("spec_self_review_failed", `spec self-review failed: unreadable or invalid UTF-8 canonical spec: ${specPath}`);
24791
+ }
24792
+ if (state.spec.status === "draft") {
24793
+ const hard = qualitySpec(digest.text).filter((f) => f.severity === "hard");
24794
+ const missing = [];
24795
+ if (!/^\s*\*+Branch:\*+/im.test(stripFences(digest.text)))
24796
+ missing.push("**Branch:** header missing");
24797
+ if (hard.length > 0 || missing.length > 0) {
24798
+ return err2("spec_self_review_failed", "spec self-review failed: " + hard.map((f) => `${f.code} ${f.message}`).concat(missing).join("; ") + " — see templates/spec-template.md for the required structure");
24799
+ }
24172
24800
  }
24801
+ return {
24802
+ ok: true,
24803
+ next: {
24804
+ ...state,
24805
+ spec: {
24806
+ path: relPath,
24807
+ status: "approved",
24808
+ evidence: recorded.evidence,
24809
+ approved_digest: digest.digest
24810
+ },
24811
+ updated_at: Date.now()
24812
+ }
24813
+ };
24173
24814
  }
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
- };
24815
+ return err2("flow_already_approved", "already approved; no further transitions");
24189
24816
  });
24190
24817
  }, transitionPlan = (root, slug, planPath, evidence, ctx) => {
24191
24818
  const bound = assertMutationWorkspace(root, ctx);
@@ -24197,91 +24824,236 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24197
24824
  const doc2 = resolveDoc(root, slug, planPath, "plan");
24198
24825
  if (!doc2.ok)
24199
24826
  return err2("path_invalid", doc2.error);
24827
+ const relPath = path20.posix.join("docs", slug, "plan.md");
24200
24828
  return readModifyWrite(root, slug, (state) => {
24201
- if (!existsSync11(doc2.path))
24829
+ if (!existsSync12(doc2.path))
24202
24830
  return err2("plan_missing", `plan not found: ${planPath}`);
24203
24831
  if (state.spec.status !== "approved") {
24204
24832
  return err2("spec_not_approved", "spec must be approved before the plan can be approved");
24205
24833
  }
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;
24834
+ if (state.plan.status === "draft" || state.plan.status === "self_reviewed") {
24835
+ const digest = readCanonicalDigest(root, relPath);
24836
+ if (!digest.ok) {
24837
+ return err2("plan_self_review_failed", `plan self-review failed: unreadable or invalid UTF-8 canonical plan: ${planPath}`);
24838
+ }
24839
+ if (state.plan.status === "draft") {
24840
+ const missing = [];
24841
+ const stripped = stripFences(digest.text);
24842
+ if (parseTasksFromPlan(digest.text).length === 0)
24843
+ missing.push("no ### Task N: sections outside fences");
24844
+ if (!/^\s*\*+Spec:\*+/im.test(stripped))
24845
+ missing.push("**Spec:** header missing");
24846
+ if (!/^\s*\*+Branch:\*+/im.test(stripped))
24847
+ missing.push("**Branch:** header missing");
24848
+ if (missing.length > 0)
24849
+ return err2("plan_self_review_failed", "plan self-review failed: " + missing.join("; "));
24850
+ }
24851
+ return {
24852
+ ok: true,
24853
+ next: {
24854
+ ...state,
24855
+ plan: {
24856
+ path: relPath,
24857
+ status: "approved",
24858
+ evidence: recorded.evidence,
24859
+ approved_digest: digest.digest
24860
+ },
24861
+ updated_at: Date.now()
24862
+ }
24863
+ };
24864
+ }
24865
+ return err2("flow_already_approved", "already approved; no further transitions");
24866
+ });
24867
+ }, recordMenuChoice = (root, slug, planPath, choice, evidence, ctx) => {
24868
+ const bound = assertMutationWorkspace(root, ctx);
24869
+ if (!bound.ok)
24870
+ return bound;
24871
+ const recorded = assertEvidenceShape(evidence);
24872
+ if (!recorded.ok)
24873
+ return err2("evidence_invalid", recorded.error);
24874
+ if (typeof choice !== "string" || !MENU_CHOICES.includes(choice)) {
24875
+ return err2("menu_choice_invalid", `invalid menu choice: ${JSON.stringify(choice)}`);
24876
+ }
24877
+ if (recorded.evidence.host === "cursor" && choice === "subagent-driven") {
24878
+ return err2("unsupported_mode", CURSOR_SUBAGENT_UNSUPPORTED_TEXT);
24879
+ }
24880
+ if (recorded.evidence.host === "opencode" && !sameChoiceLabel(recorded.evidence.selectedLabel, choice)) {
24881
+ return err2("evidence_mismatch", `evidence selectedLabel ${JSON.stringify(recorded.evidence.selectedLabel)} does not match choice ${JSON.stringify(choice)}`);
24882
+ }
24883
+ const doc2 = resolveDoc(root, slug, planPath, "plan");
24884
+ if (!doc2.ok)
24885
+ return err2("path_invalid", doc2.error);
24886
+ return readModifyWrite(root, slug, (state) => {
24887
+ if (state.spec.status !== "approved")
24888
+ return err2("spec_not_approved", "spec must be approved before the execution menu");
24889
+ if (state.plan.status !== "approved")
24890
+ return err2("plan_not_approved", "plan must be approved before the execution menu");
24891
+ if (state.handoff_destination && choice === "handoff") {
24892
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
24893
+ }
24894
+ const executing = choice === "subagent-driven" || choice === "inline";
24227
24895
  return {
24228
24896
  ok: true,
24229
24897
  next: {
24230
24898
  ...state,
24231
- plan: {
24232
- path: path19.posix.join("docs", slug, "plan.md"),
24233
- status: step.next,
24234
- evidence: recorded.evidence
24235
- },
24899
+ plan: { ...state.plan, path: state.plan.path || `docs/${slug}/plan.md` },
24900
+ menu: { presented: true, chosen: choice, evidence: recorded.evidence },
24901
+ execution: executing ? { status: "active", mode: choice, evidence: recorded.evidence } : { status: "pending", mode: null, evidence: recorded.evidence },
24902
+ updated_at: Date.now()
24903
+ }
24904
+ };
24905
+ });
24906
+ }, markHandoffDestination = (root, slug, planPath) => {
24907
+ const doc2 = resolveDoc(root, slug, planPath, "plan");
24908
+ if (!doc2.ok)
24909
+ return err2("path_invalid", doc2.error);
24910
+ return readModifyWrite(root, slug, (state) => {
24911
+ if (state.spec.status !== "approved")
24912
+ return err2("spec_not_approved", "spec must be approved before marking a handoff destination");
24913
+ if (state.plan.status !== "approved")
24914
+ return err2("plan_not_approved", "plan must be approved before marking a handoff destination");
24915
+ if (state.handoff_destination) {
24916
+ return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
24917
+ }
24918
+ if (state.menu.chosen !== "handoff") {
24919
+ return err2("handoff_not_chosen", `source menu choice must be "handoff" to mark a handoff destination (chosen: ${JSON.stringify(state.menu.chosen)})`);
24920
+ }
24921
+ return {
24922
+ ok: true,
24923
+ next: {
24924
+ ...state,
24925
+ handoff_destination: true,
24926
+ menu: { presented: false, chosen: "", evidence: null },
24236
24927
  updated_at: Date.now()
24237
24928
  }
24238
24929
  };
24239
24930
  });
24240
- }, recordMenuChoice = (root, slug, planPath, choice, evidence, ctx) => {
24931
+ }, CLI_CONFIRMATION_KEYS, validateLifecycleEvidence = (input) => {
24932
+ if (typeof input !== "object" || input === null) {
24933
+ return {
24934
+ ok: false,
24935
+ error: "lifecycle evidence required — native choice evidence or an exact CLI confirmation"
24936
+ };
24937
+ }
24938
+ const record3 = input;
24939
+ if (record3.host === "cli") {
24940
+ const validValue = record3.attested === false && (record3.confirmation === "flag" || record3.confirmation === "tty");
24941
+ const keys = Object.keys(record3).sort();
24942
+ const exactShape = keys.length === CLI_CONFIRMATION_KEYS.length && CLI_CONFIRMATION_KEYS.every((key) => keys.includes(key));
24943
+ if (validValue && exactShape) {
24944
+ return {
24945
+ ok: true,
24946
+ evidence: {
24947
+ host: "cli",
24948
+ attested: false,
24949
+ confirmation: record3.confirmation
24950
+ }
24951
+ };
24952
+ }
24953
+ return {
24954
+ ok: false,
24955
+ error: 'cli confirmations accept only the exact { host: "cli", attested: false, confirmation: "flag" | "tty" } shape'
24956
+ };
24957
+ }
24958
+ return assertEvidenceShape(input);
24959
+ }, errPendingFlow = (action) => err2("flow_not_active", `cannot ${action} a pending flow — the execution menu has not started it`), errCompletedFlow = (action) => err2("flow_already_completed", `cannot ${action} a completed flow`), completeExecution = (root, slug, deps) => {
24960
+ const file = flowPath(root, slug);
24961
+ const captured = readEffectiveFlowState(root, slug);
24962
+ if (!captured.ok)
24963
+ return captured;
24964
+ const exec = captured.state.execution;
24965
+ if (exec.status === "pending")
24966
+ return errPendingFlow("complete");
24967
+ if (exec.status === "completed")
24968
+ return errCompletedFlow("complete");
24969
+ const ledger = ledgerCompletion(root, slug);
24970
+ if (!ledger.complete) {
24971
+ return err2("execution_incomplete", `execution ledger incomplete for ${slug}: missing tasks ${ledger.missing.join(", ")}`, { required: ledger.required, completed: ledger.completed, missing: ledger.missing });
24972
+ }
24973
+ const verifier = deps?.verifyProject ?? runVerifyProject;
24974
+ const verify = verifier(root, false);
24975
+ if (verify.exitCode !== 0) {
24976
+ return err2("verification_failed", `repository verification failed for ${slug} (exit ${verify.exitCode}) — see the verification output`, { exitCode: verify.exitCode });
24977
+ }
24978
+ const locked = withFlowLock(file, () => {
24979
+ const strict = readFlowStrict(root, slug);
24980
+ if (!strict.ok)
24981
+ return strict;
24982
+ const reconciled = reconcileState(root, slug, strict.state);
24983
+ const currentExec = reconciled.state.execution;
24984
+ if (currentExec.status !== exec.status || currentExec.mode !== exec.mode) {
24985
+ return err2("flow_concurrent_conflict", `concurrent execution state change detected for ${slug}: re-read the flow state and retry completion`);
24986
+ }
24987
+ const next = {
24988
+ ...reconciled.state,
24989
+ execution: { ...exec, status: "completed" },
24990
+ handoff_destination: false,
24991
+ updated_at: Date.now()
24992
+ };
24993
+ const commit = writeFlowStateIfCurrent(root, captured.state, next);
24994
+ if (commit.ok)
24995
+ return { ok: true };
24996
+ if ("io_error" in commit) {
24997
+ return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
24998
+ }
24999
+ return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry completion`);
25000
+ });
25001
+ if (!locked.locked)
25002
+ return locked.error;
25003
+ return locked.value;
25004
+ }, transitionExecution = (root, slug, planPath, action, evidence, ctx, deps) => {
24241
25005
  const bound = assertMutationWorkspace(root, ctx);
24242
25006
  if (!bound.ok)
24243
25007
  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
- }
25008
+ const validated = validateLifecycleEvidence(evidence);
25009
+ if (!validated.ok)
25010
+ return err2("evidence_invalid", validated.error);
24256
25011
  const doc2 = resolveDoc(root, slug, planPath, "plan");
24257
25012
  if (!doc2.ok)
24258
25013
  return err2("path_invalid", doc2.error);
25014
+ if (action === "complete")
25015
+ return completeExecution(root, slug, deps);
25016
+ if (action === "pause") {
25017
+ return readModifyWrite(root, slug, (state) => {
25018
+ const exec = state.execution;
25019
+ if (exec.status === "pending")
25020
+ return errPendingFlow("pause");
25021
+ if (exec.status === "completed")
25022
+ return errCompletedFlow("pause");
25023
+ if (exec.status === "paused")
25024
+ return err2("flow_already_paused", "flow is already paused");
25025
+ return {
25026
+ ok: true,
25027
+ next: { ...state, execution: { ...exec, status: "paused" }, updated_at: Date.now() }
25028
+ };
25029
+ });
25030
+ }
24259
25031
  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");
25032
+ const exec = state.execution;
25033
+ if (exec.status === "completed")
25034
+ return errCompletedFlow("resume");
25035
+ if (exec.status !== "paused") {
25036
+ return err2("flow_not_paused", exec.status === "active" ? "flow is already active — cannot resume" : "cannot resume a pending flow — the execution menu has not started it");
25037
+ }
24264
25038
  return {
24265
25039
  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
- }
25040
+ next: { ...state, execution: { ...exec, status: "active" }, updated_at: Date.now() }
24272
25041
  };
24273
25042
  });
25043
+ }, slugFromPath = (p) => {
25044
+ const dirName = path20.basename(path20.dirname(p));
25045
+ return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
24274
25046
  }, slugFromSddPath = (p) => {
24275
- const match = p.split(path19.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
25047
+ const match = p.split(path20.sep).join("/").match(/^docs\/([^/]+)\/sdd(\/|$|['"])/);
24276
25048
  return match?.[1] ?? "";
24277
25049
  }, assertProductGates = (root, slug, opts = {}, ctx) => {
24278
25050
  const bound = assertMutationWorkspace(root, ctx);
24279
25051
  if (!bound.ok)
24280
25052
  return bound;
24281
- const strict = readFlowStrict(root, slug);
24282
- if (!strict.ok)
24283
- return strict;
24284
- const state = strict.state;
25053
+ const effective = readEffectiveFlowState(root, slug);
25054
+ if (!effective.ok)
25055
+ return effective;
25056
+ const state = effective.state;
24285
25057
  if (state.spec.status !== "approved") {
24286
25058
  return err2("spec_not_approved", `spec not approved (status: ${state.spec.status}). Run workflow_spec_approve after the user's approval.`);
24287
25059
  }
@@ -24293,18 +25065,21 @@ var COORDINATOR_RECOVERY_TEXT, CURSOR_SUBAGENT_UNSUPPORTED_TEXT, MENU_CHOICES, e
24293
25065
  }
24294
25066
  if (opts.requireDocs) {
24295
25067
  const validated = docsValidate({
24296
- spec_path: path19.posix.join("docs", slug, "spec.md"),
24297
- plan_path: path19.posix.join("docs", slug, "plan.md"),
25068
+ spec_path: path20.posix.join("docs", slug, "spec.md"),
25069
+ plan_path: path20.posix.join("docs", slug, "plan.md"),
24298
25070
  workspace_root: root
24299
25071
  });
24300
25072
  if (validated.ok === false)
24301
25073
  return err2("docs_invalid", validated.error);
24302
25074
  }
24303
- return assertCoordinatorBoundary(ctx, state.menu);
25075
+ return assertCoordinatorBoundary(ctx, state);
24304
25076
  }, BASH_READ_TOKENS, BASH_GIT_READ_SUBCOMMANDS, BASH_GIT_MUTABLE_SUBCOMMANDS, BASH_GIT_READ_FLAGS, BASH_FIND_DENIED_FLAGS, BASH_TEST_VERBS, BASH_DENIED_HEADS, BASH_OUTPUT_FLAG_VERBS, BASH_PAREN_EXEMPT_HEADS, BASH_GIT_VALUE_FLAGS, COORDINATOR_SHELL_DENIED_TEXT;
24305
25077
  var init_flow_state = __esm(() => {
24306
25078
  init_docs_validate();
24307
25079
  init_docs_layout();
25080
+ init_sdd();
25081
+ init_verify_project();
25082
+ init_menu();
24308
25083
  COORDINATOR_RECOVERY_TEXT = "A subagent-driven plan is active: coordinator product edits are blocked. " + "Delegate product mutations (task briefs, progress, review packages) to an " + "authenticated delegated worker via `task` / `wk-implement` instead of " + "editing in the coordinator session.";
24309
25084
  CURSOR_SUBAGENT_UNSUPPORTED_TEXT = "Cursor cannot execute subagent-driven plans: the MCP has no child-session " + "support. Choose Inline, Handoff, or a review option in this session, or " + "run the plan in OpenCode with `wk-implement`.";
24310
25085
  MENU_CHOICES = [
@@ -24315,6 +25090,9 @@ var init_flow_state = __esm(() => {
24315
25090
  "review-plan"
24316
25091
  ];
24317
25092
  SLUG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
25093
+ HEX64_RE = /^[0-9a-f]{64}$/;
25094
+ FLOW_STATUSES = ["draft", "self_reviewed", "approved"];
25095
+ EXECUTION_STATUSES = ["pending", "active", "paused", "completed"];
24318
25096
  RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
24319
25097
  EVIDENCE_WINDOW_MS = 24 * 60 * 60 * 1000;
24320
25098
  NEGATIVE_ANSWER_LABELS = [
@@ -24331,6 +25109,7 @@ var init_flow_state = __esm(() => {
24331
25109
  "deny"
24332
25110
  ];
24333
25111
  CURSOR_KEYS = ["attested", "confirmation", "host"];
25112
+ CLI_CONFIRMATION_KEYS = ["attested", "confirmation", "host"];
24334
25113
  BASH_READ_TOKENS = new Set([
24335
25114
  "cat",
24336
25115
  "head",
@@ -24467,34 +25246,34 @@ var init_flow_evidence = __esm(() => {
24467
25246
  });
24468
25247
 
24469
25248
  // packages/workit-core/src/core/docs-migration.ts
24470
- import { execFileSync as execFileSync4 } from "node:child_process";
25249
+ import { execFileSync as execFileSync5 } from "node:child_process";
24471
25250
  import {
24472
- existsSync as existsSync12,
24473
- lstatSync,
24474
- mkdirSync as mkdirSync7,
25251
+ existsSync as existsSync13,
25252
+ lstatSync as lstatSync2,
25253
+ mkdirSync as mkdirSync8,
24475
25254
  readdirSync as readdirSync6,
24476
- readFileSync as readFileSync13,
25255
+ readFileSync as readFileSync14,
24477
25256
  realpathSync as realpathSync3,
24478
25257
  renameSync as renameSync2,
24479
25258
  rmSync as rmSync2,
24480
- statSync as statSync6,
24481
- writeFileSync as writeFileSync6
25259
+ statSync as statSync8,
25260
+ writeFileSync as writeFileSync7
24482
25261
  } 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) => {
25262
+ import path21 from "node:path";
25263
+ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG = "superpowers", SPEC_LINK_RE2, posix3 = (p) => p.split(path21.sep).join("/"), legacyRoot = (workspace) => path21.join(workspace, "docs", "superpowers"), readFileSafe = (p) => {
24485
25264
  try {
24486
- return readFileSync13(p, "utf8");
25265
+ return readFileSync14(p, "utf8");
24487
25266
  } catch {
24488
25267
  return null;
24489
25268
  }
24490
25269
  }, sddIgnoreActive = (cwd, slug) => {
24491
25270
  try {
24492
- execFileSync4("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
25271
+ execFileSync5("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
24493
25272
  } catch {
24494
25273
  return false;
24495
25274
  }
24496
25275
  try {
24497
- execFileSync4("git", ["-C", cwd, "check-ignore", path20.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
25276
+ execFileSync5("git", ["-C", cwd, "check-ignore", path21.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
24498
25277
  return true;
24499
25278
  } catch {
24500
25279
  return false;
@@ -24516,22 +25295,22 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24516
25295
  }, detectLegacyDocs = (workspace_root) => {
24517
25296
  const root = legacyRoot(workspace_root);
24518
25297
  const raw = [];
24519
- if (existsSync12(root)) {
25298
+ if (existsSync13(root)) {
24520
25299
  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));
25300
+ const abs = path21.join(root, entry.name);
25301
+ const rel = posix3(path21.relative(workspace_root, abs));
24523
25302
  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")))
25303
+ const hasSpec = existsSync13(path21.join(abs, "spec.md"));
25304
+ const hasPlan = existsSync13(path21.join(abs, "plan.md"));
25305
+ if (!hasSpec && !hasPlan && !existsSync13(path21.join(abs, "sdd")))
24527
25306
  continue;
24528
- const explicit = explicitTargetSlug(hasPlan ? readFileSafe(path20.join(abs, "plan.md")) : null);
25307
+ const explicit = explicitTargetSlug(hasPlan ? readFileSafe(path21.join(abs, "plan.md")) : null);
24529
25308
  raw.push({
24530
25309
  slug: explicit ?? entry.name,
24531
25310
  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")),
25311
+ spec: hasSpec ? posix3(path21.join(rel, "spec.md")) : null,
25312
+ plan: hasPlan ? posix3(path21.join(rel, "plan.md")) : null,
25313
+ sdd: existsSync13(path21.join(abs, "sdd")),
24535
25314
  explicit: explicit !== null
24536
25315
  });
24537
25316
  } else if (entry.name === "spec.md" || entry.name === "plan.md") {
@@ -24599,7 +25378,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24599
25378
  return { ok: true, out, changed: out !== text };
24600
25379
  }, isPlanMalformed = (text) => !/^\s*\*+Spec:\*+/im.test(text), readBufferSafe = (p) => {
24601
25380
  try {
24602
- return readFileSync13(p);
25381
+ return readFileSync14(p);
24603
25382
  } catch {
24604
25383
  return null;
24605
25384
  }
@@ -24622,7 +25401,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24622
25401
  const raw = readBufferSafe(fromAbs);
24623
25402
  if (raw === null)
24624
25403
  return { ok: false, error: `unreadable source ${fromAbs}` };
24625
- if (path20.basename(toRel) !== "flow.json")
25404
+ if (path21.basename(toRel) !== "flow.json")
24626
25405
  return { ok: true, bytes: raw, status: "copied" };
24627
25406
  const flow = flowRewrite(raw.toString("utf8"), legacyName, slug);
24628
25407
  if (!flow.ok)
@@ -24645,18 +25424,18 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24645
25424
  } catch {
24646
25425
  return { ok: false, error: `dangling symlink refused: ${legacyRel}` };
24647
25426
  }
24648
- if (real !== workspaceReal && !real.startsWith(workspaceReal + path20.sep)) {
25427
+ if (real !== workspaceReal && !real.startsWith(workspaceReal + path21.sep)) {
24649
25428
  return { ok: false, error: `symlink escape refused: ${legacyRel}` };
24650
25429
  }
24651
25430
  if (visits.has(real))
24652
25431
  return { ok: true };
24653
25432
  visits.add(real);
24654
25433
  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));
25434
+ const fromAbs = path21.join(real, entry.name);
25435
+ const fromRel = posix3(path21.join(legacyRel, entry.name));
25436
+ const toRel = posix3(path21.join(destRelRoot, entry.name));
24658
25437
  if (entry.isDirectory()) {
24659
- const sub = planTree(workspace, fromAbs, fromRel, toRel, path20.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
25438
+ const sub = planTree(workspace, fromAbs, fromRel, toRel, path21.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
24660
25439
  if (!sub.ok)
24661
25440
  return sub;
24662
25441
  } else if (entry.isFile() || entry.isSymbolicLink()) {
@@ -24668,11 +25447,11 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24668
25447
  } catch {
24669
25448
  return { ok: false, error: `dangling symlink refused: ${fromRel}` };
24670
25449
  }
24671
- if (target !== workspaceReal && !target.startsWith(workspaceReal + path20.sep)) {
25450
+ if (target !== workspaceReal && !target.startsWith(workspaceReal + path21.sep)) {
24672
25451
  return { ok: false, error: `symlink escape refused: ${fromRel}` };
24673
25452
  }
24674
- if (statSync6(target).isDirectory()) {
24675
- const sub = planTree(workspace, fromAbs, fromRel, toRel, path20.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
25453
+ if (statSync8(target).isDirectory()) {
25454
+ const sub = planTree(workspace, fromAbs, fromRel, toRel, path21.join(destAbsRoot, entry.name), legacyName, slug, plan, visits);
24676
25455
  if (!sub.ok)
24677
25456
  return sub;
24678
25457
  continue;
@@ -24689,7 +25468,7 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24689
25468
  fromAbs,
24690
25469
  fromRel,
24691
25470
  toRel,
24692
- destAbs: path20.join(destAbsRoot, entry.name),
25471
+ destAbs: path21.join(destAbsRoot, entry.name),
24693
25472
  bytes: classified.bytes,
24694
25473
  status: classified.status
24695
25474
  });
@@ -24702,36 +25481,36 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24702
25481
  kind,
24703
25482
  legacyName,
24704
25483
  slug: entry.slug,
24705
- toRel: posix2(path20.join("docs", entry.slug, `${kind}.md`)),
24706
- destAbs: path20.join(destDirAbs, `${kind}.md`)
25484
+ toRel: posix3(path21.join("docs", entry.slug, `${kind}.md`)),
25485
+ destAbs: path21.join(destDirAbs, `${kind}.md`)
24707
25486
  });
24708
25487
  if (entry.spec) {
24709
- const classified = classifyDoc("spec", legacyName, entry.slug, path20.join(workspace, entry.spec));
25488
+ const classified = classifyDoc("spec", legacyName, entry.slug, path21.join(workspace, entry.spec));
24710
25489
  if (!classified.ok)
24711
25490
  return classified;
24712
25491
  plan.push({
24713
25492
  ...base("spec"),
24714
- fromAbs: path20.join(workspace, entry.spec),
25493
+ fromAbs: path21.join(workspace, entry.spec),
24715
25494
  fromRel: entry.spec,
24716
25495
  bytes: classified.bytes,
24717
25496
  status: classified.status
24718
25497
  });
24719
25498
  }
24720
25499
  if (entry.plan) {
24721
- const classified = classifyDoc("plan", legacyName, entry.slug, path20.join(workspace, entry.plan));
25500
+ const classified = classifyDoc("plan", legacyName, entry.slug, path21.join(workspace, entry.plan));
24722
25501
  if (!classified.ok)
24723
25502
  return classified;
24724
25503
  plan.push({
24725
25504
  ...base("plan"),
24726
- fromAbs: path20.join(workspace, entry.plan),
25505
+ fromAbs: path21.join(workspace, entry.plan),
24727
25506
  fromRel: entry.plan,
24728
25507
  bytes: classified.bytes,
24729
25508
  status: classified.status
24730
25509
  });
24731
25510
  }
24732
25511
  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);
25512
+ const legacySdd = posix3(path21.join(entry.legacy_dir, "sdd"));
25513
+ return planTree(workspace, path21.join(workspace, legacySdd), legacySdd, posix3(path21.join("docs", entry.slug, "sdd")), path21.join(destDirAbs, "sdd"), legacyName, entry.slug, plan, visits);
24735
25514
  }
24736
25515
  return { ok: true };
24737
25516
  }, abort = (error2, collisions) => ({
@@ -24774,11 +25553,11 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24774
25553
  }
24775
25554
  const collisions = [];
24776
25555
  for (const item of plan) {
24777
- if (!existsSync12(item.destAbs))
25556
+ if (!existsSync13(item.destAbs))
24778
25557
  continue;
24779
25558
  let identical = false;
24780
25559
  try {
24781
- identical = !lstatSync(item.destAbs).isDirectory() && readFileSync13(item.destAbs).equals(item.bytes);
25560
+ identical = !lstatSync2(item.destAbs).isDirectory() && readFileSync14(item.destAbs).equals(item.bytes);
24782
25561
  } catch {
24783
25562
  identical = false;
24784
25563
  }
@@ -24813,8 +25592,8 @@ var LEGACY_DIR = "docs/superpowers", MIGRATION_CHOICES, SLUG_RE3, RESERVED_SLUG
24813
25592
  }
24814
25593
  const tmp = `${item.destAbs}.workit-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
24815
25594
  try {
24816
- mkdirSync7(path20.dirname(item.destAbs), { recursive: true });
24817
- writeFileSync6(tmp, item.bytes);
25595
+ mkdirSync8(path21.dirname(item.destAbs), { recursive: true });
25596
+ writeFileSync7(tmp, item.bytes);
24818
25597
  renameSync2(tmp, item.destAbs);
24819
25598
  } catch (error2) {
24820
25599
  try {
@@ -24840,32 +25619,32 @@ var init_docs_migration = __esm(() => {
24840
25619
  });
24841
25620
 
24842
25621
  // 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 = () => {
25622
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8, readdirSync as readdirSync7 } from "node:fs";
25623
+ import { execFileSync as execFileSync6 } from "node:child_process";
25624
+ import path22 from "node:path";
25625
+ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path22.join(configDir(), "docs-repo.json"), readDocsRepoConfig = () => {
24847
25626
  try {
24848
- const parsed = JSON.parse(readFileSync14(configPath(), "utf8"));
25627
+ const parsed = JSON.parse(readFileSync15(configPath(), "utf8"));
24849
25628
  return parsed.path ? { path: parsed.path } : null;
24850
25629
  } catch {
24851
25630
  return null;
24852
25631
  }
24853
25632
  }, writeDocsRepoConfig = (docsPath) => {
24854
25633
  const file = configPath();
24855
- mkdirSync8(path21.dirname(file), { recursive: true });
24856
- writeFileSync7(file, JSON.stringify({ path: docsPath }, null, 2) + `
25634
+ mkdirSync9(path22.dirname(file), { recursive: true });
25635
+ writeFileSync8(file, JSON.stringify({ path: docsPath }, null, 2) + `
24857
25636
  `, "utf8");
24858
25637
  }, docsRepoPath = () => readDocsRepoConfig()?.path ?? null, validateDocsRepo = (docsPath) => {
24859
- if (!existsSync13(docsPath))
25638
+ if (!existsSync14(docsPath))
24860
25639
  return { ok: false, error: `docs repo path does not exist: ${docsPath}` };
24861
25640
  try {
24862
- execFileSync5("git", ["-C", docsPath, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
25641
+ execFileSync6("git", ["-C", docsPath, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
24863
25642
  } catch {
24864
25643
  return { ok: false, error: `docs repo is not a git repository: ${docsPath}` };
24865
25644
  }
24866
- const featuresDir = path21.join(docsPath, "features");
24867
- if (!existsSync13(featuresDir))
24868
- mkdirSync8(featuresDir, { recursive: true });
25645
+ const featuresDir = path22.join(docsPath, "features");
25646
+ if (!existsSync14(featuresDir))
25647
+ mkdirSync9(featuresDir, { recursive: true });
24869
25648
  return { ok: true };
24870
25649
  }, linkDocsRepo = (docsPath, confirmed) => {
24871
25650
  if (!confirmed)
@@ -24878,23 +25657,23 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24878
25657
  }, listSpecs = (workspaceRoot) => {
24879
25658
  const repoPath = docsRepoPath();
24880
25659
  const specs = [];
24881
- const docsDir = path21.join(workspaceRoot, "docs");
24882
- if (existsSync13(docsDir)) {
25660
+ const docsDir = path22.join(workspaceRoot, "docs");
25661
+ if (existsSync14(docsDir)) {
24883
25662
  for (const slug of readdirSync7(docsDir)) {
24884
25663
  if (slug.startsWith("."))
24885
25664
  continue;
24886
- const spec = path21.posix.join("docs", slug, "spec.md");
24887
- if (!existsSync13(path21.join(workspaceRoot, spec)))
25665
+ const spec = path22.posix.join("docs", slug, "spec.md");
25666
+ if (!existsSync14(path22.join(workspaceRoot, spec)))
24888
25667
  continue;
24889
25668
  let promoted = false;
24890
25669
  let target = null;
24891
25670
  if (repoPath) {
24892
- const featuresDir = path21.join(repoPath, "features");
24893
- if (existsSync13(featuresDir)) {
25671
+ const featuresDir = path22.join(repoPath, "features");
25672
+ if (existsSync14(featuresDir)) {
24894
25673
  const match = readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d));
24895
25674
  if (match) {
24896
25675
  promoted = true;
24897
- target = path21.join(repoPath, "features", match);
25676
+ target = path22.join(repoPath, "features", match);
24898
25677
  }
24899
25678
  }
24900
25679
  }
@@ -24907,7 +25686,7 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24907
25686
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
24908
25687
  }, readSafe4 = (p) => {
24909
25688
  try {
24910
- return readFileSync14(p, "utf8");
25689
+ return readFileSync15(p, "utf8");
24911
25690
  } catch {
24912
25691
  return null;
24913
25692
  }
@@ -24934,8 +25713,8 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24934
25713
  const repoValid = validateDocsRepo(repoPath);
24935
25714
  if (!repoValid.ok)
24936
25715
  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");
25716
+ const specRel = path22.posix.join("docs", slug, "spec.md");
25717
+ const planRel = path22.posix.join("docs", slug, "plan.md");
24939
25718
  const specText = readSafe4(resolved.layout.spec);
24940
25719
  if (specText === null)
24941
25720
  return { ok: false, error: `docs/${slug}/spec.md not found` };
@@ -24959,14 +25738,14 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24959
25738
  };
24960
25739
  }
24961
25740
  if (!opts.force) {
24962
- const sddDir = path21.join(workspaceRootCanonical, "docs", slug, "sdd");
24963
- if (existsSync13(sddDir)) {
25741
+ const sddDir = path22.join(workspaceRootCanonical, "docs", slug, "sdd");
25742
+ if (existsSync14(sddDir)) {
24964
25743
  try {
24965
- execFileSync5("git", [
25744
+ execFileSync6("git", [
24966
25745
  "-C",
24967
25746
  workspaceRootCanonical,
24968
25747
  "check-ignore",
24969
- path21.posix.join("docs", slug, "sdd", "progress.md")
25748
+ path22.posix.join("docs", slug, "sdd", "progress.md")
24970
25749
  ], { stdio: "pipe" });
24971
25750
  } catch {
24972
25751
  return {
@@ -24977,14 +25756,14 @@ var configPath = () => process.env.WORKFLOW_DOCS_REPO_CONFIG ?? path21.join(conf
24977
25756
  }
24978
25757
  }
24979
25758
  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 });
25759
+ const featuresDir = path22.join(repoPath, "features");
25760
+ const existing = existsSync14(featuresDir) ? readdirSync7(featuresDir).find((d) => new RegExp(`^20\\d{2}-\\d{2}-${slug}$`).test(d)) : undefined;
25761
+ const targetDir = path22.join(featuresDir, existing ?? `${prefix}-${slug}`);
25762
+ mkdirSync9(targetDir, { recursive: true });
24984
25763
  const files = ["spec.md"];
24985
- writeFileSync7(path21.join(targetDir, "spec.md"), specText, "utf8");
25764
+ writeFileSync8(path22.join(targetDir, "spec.md"), specText, "utf8");
24986
25765
  if (planText !== null) {
24987
- writeFileSync7(path21.join(targetDir, "plan.md"), planText, "utf8");
25766
+ writeFileSync8(path22.join(targetDir, "plan.md"), planText, "utf8");
24988
25767
  files.push("plan.md");
24989
25768
  }
24990
25769
  const title = specText.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? slug;
@@ -25005,9 +25784,9 @@ ${specSummary(specText)}
25005
25784
  | [spec.md](./spec.md) | Especificación completa |
25006
25785
  ${planText !== null ? `| [plan.md](./plan.md) | Plan de implementación |
25007
25786
  ` : ""}`;
25008
- writeFileSync7(path21.join(targetDir, "README.md"), readme, "utf8");
25787
+ writeFileSync8(path22.join(targetDir, "README.md"), readme, "utf8");
25009
25788
  files.push("README.md");
25010
- const indexPath = path21.join(repoPath, "features", "README.md");
25789
+ const indexPath = path22.join(repoPath, "features", "README.md");
25011
25790
  const indexText = readSafe4(indexPath) ?? `# Features
25012
25791
 
25013
25792
  Especificaciones y planes por feature.
@@ -25030,7 +25809,7 @@ Especificaciones y planes por feature.
25030
25809
  ${row}
25031
25810
  `;
25032
25811
  }
25033
- writeFileSync7(indexPath, newIndex, "utf8");
25812
+ writeFileSync8(indexPath, newIndex, "utf8");
25034
25813
  return { ok: true, target_dir: targetDir, files, index_updated: true };
25035
25814
  };
25036
25815
  var init_docs_repo = __esm(() => {
@@ -25040,13 +25819,13 @@ var init_docs_repo = __esm(() => {
25040
25819
  });
25041
25820
 
25042
25821
  // 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";
25822
+ import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
25823
+ import path23 from "node:path";
25045
25824
  var GITIGNORE_ENTRIES, ensureProjectGitignore = (workspaceRoot, confirmed) => {
25046
25825
  if (!confirmed)
25047
25826
  return { ok: false, error: "confirmed: true required" };
25048
- const file = path22.join(workspaceRoot, ".gitignore");
25049
- const existing = existsSync14(file) ? readFileSync15(file, "utf8") : "";
25827
+ const file = path23.join(workspaceRoot, ".gitignore");
25828
+ const existing = existsSync15(file) ? readFileSync16(file, "utf8") : "";
25050
25829
  const existingLines = new Set(existing.split(`
25051
25830
  `).map((l) => l.trim()).filter(Boolean));
25052
25831
  const added = [];
@@ -25061,12 +25840,12 @@ var GITIGNORE_ENTRIES, ensureProjectGitignore = (workspaceRoot, confirmed) => {
25061
25840
  const separator = existing && !existing.endsWith(`
25062
25841
  `) ? `
25063
25842
  ` : "";
25064
- writeFileSync8(file, existing + separator + (existing ? `
25843
+ writeFileSync9(file, existing + separator + (existing ? `
25065
25844
  ` : "") + append.join(`
25066
25845
  `) + `
25067
25846
  `, "utf8");
25068
- } else if (!existsSync14(file)) {
25069
- writeFileSync8(file, "", "utf8");
25847
+ } else if (!existsSync15(file)) {
25848
+ writeFileSync9(file, "", "utf8");
25070
25849
  }
25071
25850
  return { ok: true, path: file, added };
25072
25851
  };
@@ -25090,29 +25869,29 @@ var init_gitignore = __esm(() => {
25090
25869
  });
25091
25870
 
25092
25871
  // 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) => {
25872
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
25873
+ import path24 from "node:path";
25874
+ var repoRoot4, templatePath = (name) => path24.join(configDir(), "templates", `${name}.md`), readTemplate = (name) => {
25096
25875
  const cfg = templatePath(name);
25097
- if (existsSync15(cfg))
25098
- return { source: "config", content: readFileSync16(cfg, "utf8") };
25876
+ if (existsSync16(cfg))
25877
+ return { source: "config", content: readFileSync17(cfg, "utf8") };
25099
25878
  return {
25100
25879
  source: "repo",
25101
- content: readFileSync16(path23.join(repoRoot4, "templates", `${name}.md`), "utf8")
25880
+ content: readFileSync17(path24.join(repoRoot4, "templates", `${name}.md`), "utf8")
25102
25881
  };
25103
25882
  }, writeTemplate = (name, content, confirmed) => {
25104
25883
  if (!confirmed)
25105
25884
  return { ok: false, error: "confirmed: true required" };
25106
25885
  const file = templatePath(name);
25107
- mkdirSync9(path23.dirname(file), { recursive: true });
25108
- writeFileSync9(file, content, "utf8");
25886
+ mkdirSync10(path24.dirname(file), { recursive: true });
25887
+ writeFileSync10(file, content, "utf8");
25109
25888
  return { ok: true, path: file };
25110
25889
  }, listTemplates = () => ["issue-update", "greeting", "headers"].map((name) => {
25111
25890
  const cfg = templatePath(name);
25112
- const repoFile = path23.join(repoRoot4, "templates", `${name}.md`);
25113
- if (existsSync15(cfg))
25891
+ const repoFile = path24.join(repoRoot4, "templates", `${name}.md`);
25892
+ if (existsSync16(cfg))
25114
25893
  return { name, source: "config", path: cfg };
25115
- if (existsSync15(repoFile))
25894
+ if (existsSync16(repoFile))
25116
25895
  return { name, source: "repo", path: repoFile };
25117
25896
  return { name, source: "missing", path: cfg };
25118
25897
  });
@@ -25123,9 +25902,9 @@ var init_templates = __esm(() => {
25123
25902
  });
25124
25903
 
25125
25904
  // 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) => {
25905
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
25906
+ import path25 from "node:path";
25907
+ var rulesDir = () => path25.join(configDir(), "rules"), parseRule = (markdown) => {
25129
25908
  const fm = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
25130
25909
  if (!fm)
25131
25910
  return { error: "rule must start with frontmatter (--- name/description/platforms ---)" };
@@ -25152,12 +25931,12 @@ var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) =
25152
25931
  }, listRules = () => {
25153
25932
  const result = [];
25154
25933
  const dir = rulesDir();
25155
- if (existsSync16(dir)) {
25934
+ if (existsSync17(dir)) {
25156
25935
  for (const entry of readdirSync8(dir)) {
25157
- const file = path24.join(dir, entry, "rule.md");
25158
- if (!existsSync16(file))
25936
+ const file = path25.join(dir, entry, "rule.md");
25937
+ if (!existsSync17(file))
25159
25938
  continue;
25160
- const parsed = parseRule(readFileSync17(file, "utf8"));
25939
+ const parsed = parseRule(readFileSync18(file, "utf8"));
25161
25940
  if ("error" in parsed)
25162
25941
  continue;
25163
25942
  result.push({ name: parsed.name, platforms: parsed.platforms, source: "config" });
@@ -25169,16 +25948,16 @@ var rulesDir = () => path24.join(configDir(), "rules"), parseRule = (markdown) =
25169
25948
  return { ok: false, error: "confirmed: true required" };
25170
25949
  if (!RULE_NAME_RE.test(rule.name))
25171
25950
  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");
25951
+ const dir = path25.join(rulesDir(), rule.name);
25952
+ mkdirSync11(dir, { recursive: true });
25953
+ const file = path25.join(dir, "rule.md");
25175
25954
  const md = `---
25176
25955
  name: ${rule.name}
25177
25956
  description: ${rule.description}
25178
25957
  platforms: [${rule.platforms.join(", ")}]
25179
25958
  ---
25180
25959
  ${rule.body}`;
25181
- writeFileSync10(file, md, "utf8");
25960
+ writeFileSync11(file, md, "utf8");
25182
25961
  return { ok: true, path: file };
25183
25962
  };
25184
25963
  var init_rules = __esm(() => {
@@ -25187,10 +25966,10 @@ var init_rules = __esm(() => {
25187
25966
  });
25188
25967
 
25189
25968
  // packages/workit-core/src/core/safe-write.ts
25190
- import { writeFileSync as writeFileSync11 } from "node:fs";
25969
+ import { writeFileSync as writeFileSync12 } from "node:fs";
25191
25970
  function writeFileExclusive(file, content, mode) {
25192
25971
  try {
25193
- writeFileSync11(file, content, { encoding: "utf8", flag: "wx", mode });
25972
+ writeFileSync12(file, content, { encoding: "utf8", flag: "wx", mode });
25194
25973
  return "created";
25195
25974
  } catch (err3) {
25196
25975
  if (err3.code === "EEXIST")
@@ -25272,26 +26051,26 @@ var init_setup_state = __esm(() => {
25272
26051
  import {
25273
26052
  copyFileSync as copyFileSync2,
25274
26053
  cpSync as cpSync2,
25275
- existsSync as existsSync17,
25276
- mkdirSync as mkdirSync11,
26054
+ existsSync as existsSync18,
26055
+ mkdirSync as mkdirSync12,
25277
26056
  mkdtempSync,
25278
- readFileSync as readFileSync18,
26057
+ readFileSync as readFileSync19,
25279
26058
  readdirSync as readdirSync9,
25280
26059
  renameSync as renameSync3,
25281
26060
  rmSync as rmSync3,
25282
- statSync as statSync7,
25283
- writeFileSync as writeFileSync12
26061
+ statSync as statSync9,
26062
+ writeFileSync as writeFileSync13
25284
26063
  } from "node:fs";
25285
- import path25 from "node:path";
26064
+ import path26 from "node:path";
25286
26065
  import { isDeepStrictEqual } from "node:util";
25287
26066
  function applyWorkspaceBranchPolicy(opts) {
25288
26067
  const { workspace_root, env = process.env } = opts;
25289
- const dir = path25.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
26068
+ const dir = path26.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
25290
26069
  const { status, path: wsPath, entries } = readWorkspacesResult(dir);
25291
26070
  if (status === "malformed")
25292
26071
  return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
25293
26072
  const detection = detectBranchPolicy(workspace_root);
25294
- const name = String(env.WORKFLOW_BP_NAME ?? path25.basename(workspace_root));
26073
+ const name = String(env.WORKFLOW_BP_NAME ?? path26.basename(workspace_root));
25295
26074
  const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
25296
26075
  const policy2 = {
25297
26076
  preset: detection.preset,
@@ -25316,8 +26095,8 @@ function applyWorkspaceBranchPolicy(opts) {
25316
26095
  };
25317
26096
  }
25318
26097
  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) + `
26098
+ mkdirSync12(path26.dirname(wsPath), { recursive: true });
26099
+ writeFileSync13(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
25321
26100
  `, "utf8");
25322
26101
  return {
25323
26102
  ok: true,
@@ -25359,7 +26138,7 @@ var init_setup = __esm(() => {
25359
26138
 
25360
26139
  // packages/workit-core/src/core/youtrack.ts
25361
26140
  import fs4 from "node:fs";
25362
- import path26 from "node:path";
26141
+ import path27 from "node:path";
25363
26142
  function readYouTrackConfig(required2) {
25364
26143
  const cfgPath = youTrackConfigPath();
25365
26144
  if (!fs4.existsSync(cfgPath)) {
@@ -25383,7 +26162,7 @@ function youTrackConfigLoad() {
25383
26162
  }
25384
26163
  const cfgPath = loaded.path;
25385
26164
  const tokenFile = String(loaded.config.tokenFile ?? "");
25386
- const tokenPath = tokenFile ? path26.isAbsolute(tokenFile) ? path26.resolve(tokenFile) : path26.resolve(process.cwd(), tokenFile) : "";
26165
+ const tokenPath = tokenFile ? path27.isAbsolute(tokenFile) ? path27.resolve(tokenFile) : path27.resolve(process.cwd(), tokenFile) : "";
25387
26166
  if (!tokenPath || !fs4.existsSync(tokenPath)) {
25388
26167
  return { error: "missing youtrack.token" };
25389
26168
  }
@@ -25397,8 +26176,8 @@ function youTrackConfigLoad() {
25397
26176
  const redacted = { ...loaded.config };
25398
26177
  delete redacted.tokenFile;
25399
26178
  redacted.tokenPresent = true;
25400
- redacted.configPath = path26.resolve(cfgPath);
25401
- redacted.tokenPath = path26.resolve(tokenPath);
26179
+ redacted.configPath = path27.resolve(cfgPath);
26180
+ redacted.tokenPath = path27.resolve(tokenPath);
25402
26181
  return { data: redacted };
25403
26182
  }
25404
26183
  function tzParts(date4, tz) {
@@ -25643,7 +26422,7 @@ function youTrackTokenCreateUrl() {
25643
26422
  const desc = String(defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings");
25644
26423
  const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
25645
26424
  const base = String(config2.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
25646
- const tokenFile = String(config2.tokenFile ?? path26.join(path26.dirname(loaded.path), "youtrack.token"));
26425
+ const tokenFile = String(config2.tokenFile ?? path27.join(path27.dirname(loaded.path), "youtrack.token"));
25647
26426
  const tab = String(defaults.profileTab ?? "account-security");
25648
26427
  const createUrl = `${base}/users/me?${new URLSearchParams({ tab })}`;
25649
26428
  const docsUrl = "https://www.jetbrains.com/help/youtrack/cloud/manage-permanent-token.html";
@@ -25652,7 +26431,7 @@ function youTrackTokenCreateUrl() {
25652
26431
  tokenName: name,
25653
26432
  tokenDescription: desc,
25654
26433
  scopes,
25655
- tokenFile: path26.resolve(tokenFile),
26434
+ tokenFile: path27.resolve(tokenFile),
25656
26435
  createUrl,
25657
26436
  docsUrl,
25658
26437
  prefillSupported: false,
@@ -25689,7 +26468,7 @@ function verifyYouTrackToken(scripts = defaultScripts) {
25689
26468
  function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25690
26469
  const root = resolveWorkspaceRoot(workspace_root);
25691
26470
  for (const rel of [spec_path, plan_path].filter(Boolean)) {
25692
- const full = path26.isAbsolute(rel) ? rel : path26.join(root, rel);
26471
+ const full = path27.isAbsolute(rel) ? rel : path27.join(root, rel);
25693
26472
  if (!fs4.existsSync(full))
25694
26473
  continue;
25695
26474
  const text = fs4.readFileSync(full, "utf8");
@@ -25868,7 +26647,7 @@ async function postUpdate({
25868
26647
  }
25869
26648
  return { ok: true, issueId, postedComment: true };
25870
26649
  }
25871
- var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path26.join(configDir(), "youtrack.json"), youTrackTokenModeOk = (p) => {
26650
+ var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path27.join(configDir(), "youtrack.json"), youTrackTokenModeOk = (p) => {
25872
26651
  if (process.platform === "win32")
25873
26652
  return true;
25874
26653
  const mode = fs4.statSync(p).mode & 511;
@@ -25878,7 +26657,7 @@ var ISSUE_RE, TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE", youTrackConfigPath = () =>
25878
26657
  if ("error" in loaded)
25879
26658
  return loaded;
25880
26659
  const tokenFile = String(loaded.config.tokenFile ?? "");
25881
- const tokenPath = tokenFile ? path26.isAbsolute(tokenFile) ? path26.resolve(tokenFile) : path26.resolve(process.cwd(), tokenFile) : "";
26660
+ const tokenPath = tokenFile ? path27.isAbsolute(tokenFile) ? path27.resolve(tokenFile) : path27.resolve(process.cwd(), tokenFile) : "";
25882
26661
  if (!tokenPath || !fs4.existsSync(tokenPath))
25883
26662
  return { error: "missing youtrack.token" };
25884
26663
  if (!youTrackTokenModeOk(tokenPath))
@@ -25911,10 +26690,10 @@ var init_youtrack = __esm(() => {
25911
26690
 
25912
26691
  // packages/workit-core/src/core/init.ts
25913
26692
  import fs5 from "node:fs";
25914
- import path27 from "node:path";
26693
+ import path28 from "node:path";
25915
26694
  function initStatusData(configDirPath = configDir()) {
25916
- const ytJson = path27.join(configDirPath, "youtrack.json");
25917
- const vcsJson = path27.join(configDirPath, "vcs.json");
26695
+ const ytJson = path28.join(configDirPath, "youtrack.json");
26696
+ const vcsJson = path28.join(configDirPath, "vcs.json");
25918
26697
  const items = [];
25919
26698
  let youtrackConfig = null;
25920
26699
  let youtrackTokenCreate = null;
@@ -25937,8 +26716,8 @@ function initStatusData(configDirPath = configDir()) {
25937
26716
  });
25938
26717
  }
25939
26718
  }
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;
26719
+ const expanded = tokenFile ? path28.resolve(tokenFile) : null;
26720
+ const resolvedTokenFile = expanded && fs5.existsSync(expanded) ? resolvePath(expanded) : expanded ? path28.isAbsolute(expanded) ? expanded : path28.resolve(configDirPath, expanded) : null;
25942
26721
  youtrackConfig = {
25943
26722
  config_edit_path: resolvePath(ytJson),
25944
26723
  baseUrl: base,
@@ -25985,11 +26764,11 @@ function initStatusData(configDirPath = configDir()) {
25985
26764
  id: "youtrack_json",
25986
26765
  label: "YouTrack config",
25987
26766
  ok: fs5.existsSync(ytJson) && youtrackConfig !== null && !("error" in youtrackConfig),
25988
- path: fs5.existsSync(ytJson) ? resolvePath(ytJson) : path27.resolve(ytJson),
26767
+ path: fs5.existsSync(ytJson) ? resolvePath(ytJson) : path28.resolve(ytJson),
25989
26768
  config_edit_path: resolvePath(ytJson),
25990
26769
  fix: "workflow_toolkit_init_apply action=youtrack_scaffold"
25991
26770
  });
25992
- const ytTokenPath = youtrackConfig?.tokenFile ?? path27.join(configDirPath, "youtrack.token");
26771
+ const ytTokenPath = youtrackConfig?.tokenFile ?? path28.join(configDirPath, "youtrack.token");
25993
26772
  const tokenText = fs5.existsSync(ytTokenPath) ? fs5.readFileSync(ytTokenPath, "utf8").trim() : "";
25994
26773
  const placeholder = fs5.existsSync(ytTokenPath) && isPlaceholder2(tokenText);
25995
26774
  const tokenOk = fs5.existsSync(ytTokenPath) && modeOk(ytTokenPath) && Boolean(tokenText) && !isPlaceholder2(tokenText);
@@ -25997,8 +26776,8 @@ function initStatusData(configDirPath = configDir()) {
25997
26776
  id: "youtrack_token",
25998
26777
  label: "YouTrack API token (mode 600, not placeholder)",
25999
26778
  ok: tokenOk,
26000
- path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path27.resolve(ytTokenPath),
26001
- token_edit_path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path27.resolve(ytTokenPath),
26779
+ path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path28.resolve(ytTokenPath),
26780
+ token_edit_path: fs5.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path28.resolve(ytTokenPath),
26002
26781
  placeholder,
26003
26782
  fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER4} with your permanent token, save, then /wk-status`
26004
26783
  };
@@ -26023,7 +26802,7 @@ function initStatusData(configDirPath = configDir()) {
26023
26802
  const provider = String(vcsParsed.provider ?? "gitlab").toLowerCase();
26024
26803
  const tokenFiles = {};
26025
26804
  for (const k of ["gitlab", "github"]) {
26026
- tokenFiles[k] = String(vcsParsed[k]?.tokenFile ?? path27.join(configDirPath, `${k}.token`));
26805
+ tokenFiles[k] = String(vcsParsed[k]?.tokenFile ?? path28.join(configDirPath, `${k}.token`));
26027
26806
  }
26028
26807
  vcsCfg = {
26029
26808
  config_edit_path: resolvePath(vcsJson),
@@ -26048,14 +26827,14 @@ function initStatusData(configDirPath = configDir()) {
26048
26827
  id: "vcs_json",
26049
26828
  label: "VCS config (GitLab / GitHub)",
26050
26829
  ok: fs5.existsSync(vcsJson) && vcsCfg !== null && !("error" in vcsCfg),
26051
- path: fs5.existsSync(vcsJson) ? resolvePath(vcsJson) : path27.resolve(vcsJson),
26830
+ path: fs5.existsSync(vcsJson) ? resolvePath(vcsJson) : path28.resolve(vcsJson),
26052
26831
  config_edit_path: resolvePath(vcsJson),
26053
26832
  fix: "workflow_toolkit_init_apply action=vcs_scaffold"
26054
26833
  });
26055
26834
  const provActive = vcsCfg && !("error" in vcsCfg) ? vcsCfg.provider : null;
26056
26835
  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);
26836
+ const t = path28.isAbsolute(rawPath) ? rawPath : path28.resolve(configDirPath, rawPath);
26837
+ const abs = fs5.existsSync(t) ? resolvePath(t) : path28.resolve(t);
26059
26838
  const text = fs5.existsSync(t) ? fs5.readFileSync(t, "utf8").trim() : "";
26060
26839
  const ph = isPlaceholder2(text);
26061
26840
  const ok2 = fs5.existsSync(t) && modeOk(t) && Boolean(text) && !ph;
@@ -26086,7 +26865,7 @@ function initStatusData(configDirPath = configDir()) {
26086
26865
  };
26087
26866
  const vcsTokenFiles = {};
26088
26867
  for (const k of ["gitlab", "github"]) {
26089
- vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path27.join(configDirPath, `${k}.token`));
26868
+ vcsTokenFiles[k] = String(vcsParsed?.[k]?.tokenFile ?? path28.join(configDirPath, `${k}.token`));
26090
26869
  }
26091
26870
  items.push(tokenItem("gitlab_token", "GitLab token (mode 600, not placeholder)", vcsTokenFiles.gitlab, "gitlab"));
26092
26871
  items.push(tokenItem("github_token", "GitHub token (mode 600, not placeholder)", vcsTokenFiles.github, "github"));
@@ -26143,16 +26922,16 @@ function initApplyData(action, env = process.env) {
26143
26922
  fs5.mkdirSync(dir, { recursive: true });
26144
26923
  switch (action) {
26145
26924
  case "youtrack_json": {
26146
- const out = path27.join(dir, "youtrack.json");
26925
+ const out = path28.join(dir, "youtrack.json");
26147
26926
  fs5.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
26148
26927
  `, "utf8");
26149
26928
  return { action, ok: true, path: out };
26150
26929
  }
26151
26930
  case "youtrack_token_placeholder": {
26152
- const p = path27.join(dir, "youtrack.token");
26931
+ const p = path28.join(dir, "youtrack.token");
26153
26932
  const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER4 + `
26154
26933
  `, 384) === "preserved";
26155
- const abs = path27.resolve(p);
26934
+ const abs = path28.resolve(p);
26156
26935
  return {
26157
26936
  action,
26158
26937
  ok: true,
@@ -26164,14 +26943,14 @@ function initApplyData(action, env = process.env) {
26164
26943
  };
26165
26944
  }
26166
26945
  case "youtrack_scaffold": {
26167
- const jsonOut = path27.join(dir, "youtrack.json");
26168
- const tokenOut = path27.join(dir, "youtrack.token");
26946
+ const jsonOut = path28.join(dir, "youtrack.json");
26947
+ const tokenOut = path28.join(dir, "youtrack.token");
26169
26948
  fs5.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
26170
26949
  `, "utf8");
26171
26950
  const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER4 + `
26172
26951
  `, 384) === "preserved";
26173
- const configPath2 = path27.resolve(jsonOut);
26174
- const tokenPath = path27.resolve(tokenOut);
26952
+ const configPath2 = path28.resolve(jsonOut);
26953
+ const tokenPath = path28.resolve(tokenOut);
26175
26954
  const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
26176
26955
  process.env.WORKFLOW_YOUTRACK_CONFIG = configPath2;
26177
26956
  let tokenCreate = {};
@@ -26225,19 +27004,19 @@ function initApplyData(action, env = process.env) {
26225
27004
  }
26226
27005
  }
26227
27006
  case "vcs_scaffold": {
26228
- const jsonOut = path27.join(dir, "vcs.json");
27007
+ const jsonOut = path28.join(dir, "vcs.json");
26229
27008
  fs5.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(dir), null, 2) + `
26230
27009
  `, "utf8");
26231
- const glPath = path27.join(dir, "gitlab.token");
26232
- const ghPath = path27.join(dir, "github.token");
27010
+ const glPath = path28.join(dir, "gitlab.token");
27011
+ const ghPath = path28.join(dir, "github.token");
26233
27012
  const preservedTokens = [];
26234
27013
  for (const p of [glPath, ghPath]) {
26235
27014
  if (writeFileExclusive(p, TOKEN_PLACEHOLDER4 + `
26236
27015
  `, 384) === "preserved") {
26237
- preservedTokens.push(path27.resolve(p));
27016
+ preservedTokens.push(path28.resolve(p));
26238
27017
  }
26239
27018
  }
26240
- const configPath2 = path27.resolve(jsonOut);
27019
+ const configPath2 = path28.resolve(jsonOut);
26241
27020
  const prev = process.env.WORKFLOW_VCS_CONFIG;
26242
27021
  process.env.WORKFLOW_VCS_CONFIG = configPath2;
26243
27022
  try {
@@ -26245,14 +27024,14 @@ function initApplyData(action, env = process.env) {
26245
27024
  const provider = String(cfg.provider ?? "gitlab");
26246
27025
  const tokenUrls = vcsTokenCreateUrls();
26247
27026
  const active = tokenUrls.active ?? {};
26248
- const activePath = provider === "gitlab" ? path27.resolve(glPath) : path27.resolve(ghPath);
27027
+ const activePath = provider === "gitlab" ? path28.resolve(glPath) : path28.resolve(ghPath);
26249
27028
  return {
26250
27029
  action,
26251
27030
  ok: true,
26252
27031
  vcs_json: configPath2,
26253
27032
  config_edit_path: configPath2,
26254
- gitlab_token: path27.resolve(glPath),
26255
- github_token: path27.resolve(ghPath),
27033
+ gitlab_token: path28.resolve(glPath),
27034
+ github_token: path28.resolve(ghPath),
26256
27035
  token_edit_path: activePath,
26257
27036
  token_create_url: active.createUrl,
26258
27037
  token_create_urls: tokenUrls,
@@ -26317,11 +27096,11 @@ var TOKEN_PLACEHOLDER4 = "YOUR_TOKEN_HERE", readJson2 = (p) => {
26317
27096
  try {
26318
27097
  return fs5.realpathSync(p);
26319
27098
  } catch {
26320
- return path27.resolve(p);
27099
+ return path28.resolve(p);
26321
27100
  }
26322
27101
  }, youtrackJsonContent = (dir) => ({
26323
27102
  baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
26324
- tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path27.join(dir, "youtrack.token"),
27103
+ tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path28.join(dir, "youtrack.token"),
26325
27104
  timezone: process.env.WORKFLOW_YT_TIMEZONE ?? "America/Santiago",
26326
27105
  locale: "es-CL",
26327
27106
  defaultMention: process.env.WORKFLOW_YT_MENTION ?? "Alejandra.Flores",
@@ -26357,11 +27136,11 @@ var TOKEN_PLACEHOLDER4 = "YOUR_TOKEN_HERE", readJson2 = (p) => {
26357
27136
  gitlab: {
26358
27137
  host: process.env.WORKFLOW_GITLAB_HOST ?? "gitlab.com",
26359
27138
  apiUrl: process.env.WORKFLOW_GITLAB_API_URL ?? "https://gitlab.com/api/v4",
26360
- tokenFile: path27.join(dir, "gitlab.token")
27139
+ tokenFile: path28.join(dir, "gitlab.token")
26361
27140
  },
26362
27141
  github: {
26363
27142
  host: process.env.WORKFLOW_GITHUB_HOST ?? "github.com",
26364
- tokenFile: path27.join(dir, "github.token")
27143
+ tokenFile: path28.join(dir, "github.token")
26365
27144
  },
26366
27145
  pr: { squashOnMerge: true, removeSourceBranch: true, pushBranch: true, confirmSkip: true },
26367
27146
  tokenDefaults: {
@@ -26382,185 +27161,6 @@ var init_init = __esm(() => {
26382
27161
  init_youtrack();
26383
27162
  });
26384
27163
 
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
27164
  // packages/workit-core/src/core/present.ts
26565
27165
  function boxLine(text, width) {
26566
27166
  const inner = width - 4;
@@ -26713,7 +27313,38 @@ var logger, readServerVersion = () => {
26713
27313
  };
26714
27314
  }
26715
27315
  });
26716
- }, changelogCategorySchema, transport;
27316
+ }, changelogCategorySchema, lifecycleTool = (action, description) => {
27317
+ registerTool(`workflow_plan_${action}`, {
27318
+ description,
27319
+ inputSchema: {
27320
+ plan_path: exports_external.string(),
27321
+ workspace_root: workspaceRootSchema
27322
+ }
27323
+ }, async ({ plan_path, workspace_root }) => {
27324
+ const resolved = resolveCanonicalLayout({ workspace_root, plan_path });
27325
+ if (!resolved.ok) {
27326
+ return jsonResult(withWorkspace(workspace_root, { error: resolved.error }));
27327
+ }
27328
+ const { workspace, slug } = resolved.layout;
27329
+ const result = transitionExecution(workspace, slug, plan_path, action, cursorConfirmation(), cursorMutationContext(workspace));
27330
+ if (result.ok === false) {
27331
+ return jsonResult(withWorkspace(workspace_root, {
27332
+ error: result.error,
27333
+ code: result.code,
27334
+ ...result.details ? { details: result.details } : {}
27335
+ }));
27336
+ }
27337
+ const effective = readEffectiveFlowState(workspace, slug);
27338
+ if (!effective.ok) {
27339
+ return jsonResult(withWorkspace(workspace_root, { error: effective.error, code: effective.code }));
27340
+ }
27341
+ return jsonResult(withWorkspace(workspace_root, {
27342
+ plan: plan_path,
27343
+ execution: effective.state.execution,
27344
+ drift: effective.drift
27345
+ }));
27346
+ });
27347
+ }, transport;
26717
27348
  var init_server3 = __esm(async () => {
26718
27349
  init_mcp();
26719
27350
  init_stdio2();
@@ -27217,40 +27848,54 @@ var init_server3 = __esm(async () => {
27217
27848
  return jsonResult(withWorkspace(workspace_root, { error: built.error }));
27218
27849
  }
27219
27850
  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 = {
27851
+ if (!planPath) {
27852
+ return jsonResult(withWorkspace(workspace_root, {
27853
+ error: "Could not resolve spec and plan for handoff"
27854
+ }));
27855
+ }
27856
+ const tasksData = parsePlanTasks(planPath, root);
27857
+ if ("error" in tasksData) {
27858
+ return jsonResult(withWorkspace(workspace_root, {
27229
27859
  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;
27860
+ error: tasksData.error
27861
+ }));
27862
+ }
27863
+ const slug = slugFromPath(planPath);
27864
+ const marked = markHandoffDestination(root, slug, planPath);
27865
+ if (marked.ok === false) {
27866
+ return jsonResult(withWorkspace(workspace_root, {
27867
+ error: marked.error,
27868
+ code: marked.code,
27869
+ ...marked.details ? { details: marked.details } : {}
27870
+ }));
27871
+ }
27872
+ const payload = {
27873
+ prompt,
27874
+ tasks: tasksData.tasks,
27875
+ task_count: tasksData.task_count,
27876
+ workspace_root: root
27877
+ };
27878
+ if (specPath) {
27879
+ const branchData = resolveHandoffBranch(specPath, planPath, root);
27880
+ if (!("error" in branchData)) {
27881
+ payload.branch = branchData.branch;
27248
27882
  }
27249
- return jsonResult(withWorkspace(workspace_root, payload));
27250
27883
  }
27251
- return jsonResult(withWorkspace(workspace_root, {
27252
- error: "Could not resolve spec and plan for handoff"
27253
- }));
27884
+ const sdd = sddContext({ plan_path: planPath, workspace_root: root });
27885
+ if (!sdd.error) {
27886
+ payload.slug = sdd.slug;
27887
+ payload.sdd_dir = sdd.sdd_dir;
27888
+ payload.progress_path = sdd.progress_path;
27889
+ payload.completed_task_ids = sdd.completed_task_ids;
27890
+ payload.todos = sdd.todos;
27891
+ payload.todo_write_required = true;
27892
+ }
27893
+ const effective = readEffectiveFlowState(root, slug);
27894
+ if (effective.ok) {
27895
+ payload.handoff_destination = effective.state.handoff_destination;
27896
+ payload.menu = effective.state.menu;
27897
+ }
27898
+ return jsonResult(withWorkspace(workspace_root, payload));
27254
27899
  });
27255
27900
  registerTool("workflow_toolkit_init_status", {
27256
27901
  description: "Check workit setup (MCP deps, YouTrack config, token)",
@@ -27547,18 +28192,23 @@ var init_server3 = __esm(async () => {
27547
28192
  if (!resolved.ok)
27548
28193
  return jsonResult(withWorkspace(workspace_root, { error: resolved.error }));
27549
28194
  const { workspace, slug } = resolved.layout;
27550
- let state = readFlowState(workspace, slug);
27551
- if (!state.activated) {
28195
+ let effective = readEffectiveFlowState(workspace, slug);
28196
+ if (!effective.ok && effective.code === "flow_not_activated") {
27552
28197
  const prepared = prepareFlowState(workspace, slug, { spec_path, plan_path }, cursorMutationContext(workspace));
27553
28198
  if (!prepared.ok)
27554
28199
  return jsonResult({ error: prepared.error, code: prepared.code });
27555
- state = readFlowState(workspace, slug);
28200
+ effective = readEffectiveFlowState(workspace, slug);
27556
28201
  }
28202
+ if (!effective.ok)
28203
+ return jsonResult({ error: effective.error, code: effective.code });
28204
+ const { state, drift } = effective;
27557
28205
  return jsonResult({
27558
28206
  slug,
27559
28207
  spec: state.spec,
27560
28208
  plan: state.plan,
27561
28209
  menu: state.menu,
28210
+ execution: state.execution,
28211
+ drift,
27562
28212
  flow_path: `docs/${slug}/sdd/flow.json`
27563
28213
  });
27564
28214
  });
@@ -27611,6 +28261,9 @@ var init_server3 = __esm(async () => {
27611
28261
  return jsonResult({ error: result.error, code: result.code });
27612
28262
  return jsonResult({ menu: { presented: true, chosen: choice } });
27613
28263
  });
28264
+ lifecycleTool("pause", "Pause a running plan with the Cursor policy-only confirmation: active -> paused. The MCP cannot observe AskQuestion results, so it records attested: false; there is no evidence argument (CA-42). Requires plan_path and workspace_root.");
28265
+ lifecycleTool("resume", "Resume a paused plan with the Cursor policy-only confirmation: paused -> active. The MCP cannot observe AskQuestion results, so it records attested: false; there is no evidence argument (CA-42). Requires plan_path and workspace_root.");
28266
+ lifecycleTool("complete", "Complete a running plan with the Cursor policy-only confirmation: active/paused -> completed, after the SDD ledger is complete and repository verification passes. The MCP cannot observe AskQuestion results, so it records attested: false; there is no evidence argument (CA-42). Requires plan_path and workspace_root.");
27614
28267
  registerTool("workflow_docs_repo_link", {
27615
28268
  description: "Link the component docs repo in the toolkit config",
27616
28269
  inputSchema: {