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