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