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