@brainervirus/workit-cli 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/assets/templates/execution-contract.md +11 -0
- package/assets/templates/superpowers-doc-contract.md +2 -0
- package/dist/index.js +2378 -147
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -42527,6 +42527,22 @@ var readConfigFromDir = (dir) => {
|
|
|
42527
42527
|
throw new Error(result2.error);
|
|
42528
42528
|
return result2.config ?? DEFAULTS;
|
|
42529
42529
|
};
|
|
42530
|
+
var resolveBranchPolicy = (config, workspace) => {
|
|
42531
|
+
const wp = workspace?.branchPolicy ?? {};
|
|
42532
|
+
const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
|
|
42533
|
+
const merged = mergePreset(preset, {
|
|
42534
|
+
allowed: wp.allowed,
|
|
42535
|
+
protectedNames: wp.protected
|
|
42536
|
+
}, config);
|
|
42537
|
+
const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
|
|
42538
|
+
return {
|
|
42539
|
+
preset,
|
|
42540
|
+
allowed,
|
|
42541
|
+
protected: new Set(merged.protected.map((p) => p.toLowerCase())),
|
|
42542
|
+
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
42543
|
+
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
42544
|
+
};
|
|
42545
|
+
};
|
|
42530
42546
|
|
|
42531
42547
|
// packages/workit-core/src/core/workspaces.ts
|
|
42532
42548
|
import { readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
@@ -42650,6 +42666,7 @@ var resolveWorkspaceFrom = (cwd2, dir) => {
|
|
|
42650
42666
|
}
|
|
42651
42667
|
return null;
|
|
42652
42668
|
};
|
|
42669
|
+
var resolveWorkspace = (cwd2) => resolveWorkspaceFrom(cwd2, configDir());
|
|
42653
42670
|
|
|
42654
42671
|
// packages/workit-core/src/core/gitignore.ts
|
|
42655
42672
|
var GITIGNORE_ENTRIES = [
|
|
@@ -42671,6 +42688,10 @@ var GITIGNORE_ENTRIES = [
|
|
|
42671
42688
|
|
|
42672
42689
|
// packages/workit-core/src/core/hygiene.ts
|
|
42673
42690
|
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
42691
|
+
import path5 from "node:path";
|
|
42692
|
+
|
|
42693
|
+
// packages/workit-core/src/core/changelog.ts
|
|
42694
|
+
import fs3 from "node:fs";
|
|
42674
42695
|
import path4 from "node:path";
|
|
42675
42696
|
|
|
42676
42697
|
// packages/workit-core/src/core/package-root.ts
|
|
@@ -42695,26 +42716,49 @@ var assetRoot = () => {
|
|
|
42695
42716
|
// packages/workit-core/src/core/scripts.ts
|
|
42696
42717
|
var PLUGIN_ROOT = packageRoot();
|
|
42697
42718
|
var ASSET_ROOT = assetRoot();
|
|
42719
|
+
var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
|
|
42720
|
+
|
|
42721
|
+
// packages/workit-core/src/core/changelog.ts
|
|
42722
|
+
function changelogUnreleasedStats(workspace_root, changelogPath = "CHANGELOG.md") {
|
|
42723
|
+
const cwd2 = resolveWorkspaceRoot(workspace_root);
|
|
42724
|
+
const abs = path4.isAbsolute(changelogPath) ? changelogPath : path4.join(cwd2, changelogPath);
|
|
42725
|
+
if (!fs3.existsSync(abs))
|
|
42726
|
+
return { exists: false };
|
|
42727
|
+
const text = fs3.readFileSync(abs, "utf8");
|
|
42728
|
+
const m = text.match(/##\s+\[Unreleased\]([\s\S]*?)(?=\n##\s+\[|$)/i);
|
|
42729
|
+
if (!m)
|
|
42730
|
+
return { exists: true, has_unreleased: false };
|
|
42731
|
+
const body = m[1];
|
|
42732
|
+
const headings = [...body.matchAll(/^###\s+(\w+)\s*$/gim)].map((x) => x[1]);
|
|
42733
|
+
const dupes = headings.filter((h, i) => headings.slice(0, i).some((p) => p.toLowerCase() === h.toLowerCase()));
|
|
42734
|
+
return {
|
|
42735
|
+
exists: true,
|
|
42736
|
+
has_unreleased: true,
|
|
42737
|
+
category_headings: headings,
|
|
42738
|
+
duplicate_category_headings: dupes,
|
|
42739
|
+
needs_normalize: dupes.length > 0
|
|
42740
|
+
};
|
|
42741
|
+
}
|
|
42698
42742
|
|
|
42699
42743
|
// packages/workit-core/src/core/hygiene.ts
|
|
42700
42744
|
var repoRoot = assetRoot();
|
|
42701
|
-
var templatesDir = () =>
|
|
42745
|
+
var templatesDir = () => path5.join(repoRoot, "templates", "hygiene");
|
|
42702
42746
|
var packageJson = (root) => {
|
|
42703
|
-
if (!existsSync4(
|
|
42747
|
+
if (!existsSync4(path5.join(root, "package.json")))
|
|
42704
42748
|
return null;
|
|
42705
42749
|
try {
|
|
42706
|
-
return JSON.parse(readFileSync4(
|
|
42750
|
+
return JSON.parse(readFileSync4(path5.join(root, "package.json"), "utf8"));
|
|
42707
42751
|
} catch {
|
|
42708
42752
|
return null;
|
|
42709
42753
|
}
|
|
42710
42754
|
};
|
|
42711
42755
|
var isOpenSource = (root) => {
|
|
42712
|
-
if (existsSync4(
|
|
42756
|
+
if (existsSync4(path5.join(root, "LICENSE")))
|
|
42713
42757
|
return true;
|
|
42714
42758
|
const pkg = packageJson(root);
|
|
42715
42759
|
if (pkg && !pkg.private)
|
|
42716
42760
|
return true;
|
|
42717
|
-
return
|
|
42761
|
+
return path5.basename(root) === "workflow-toolkit";
|
|
42718
42762
|
};
|
|
42719
42763
|
var licenseHolder = (root) => {
|
|
42720
42764
|
const pkg = packageJson(root);
|
|
@@ -42730,6 +42774,34 @@ var licenseHolder = (root) => {
|
|
|
42730
42774
|
}
|
|
42731
42775
|
return "";
|
|
42732
42776
|
};
|
|
42777
|
+
var hygieneFiles = (root) => {
|
|
42778
|
+
const openSource = isOpenSource(root);
|
|
42779
|
+
const state = {};
|
|
42780
|
+
for (const file of [
|
|
42781
|
+
"CHANGELOG.md",
|
|
42782
|
+
"README.md",
|
|
42783
|
+
".editorconfig",
|
|
42784
|
+
".gitattributes",
|
|
42785
|
+
"LICENSE",
|
|
42786
|
+
"CONTRIBUTING.md"
|
|
42787
|
+
]) {
|
|
42788
|
+
if (file === "LICENSE" || file === "CONTRIBUTING.md") {
|
|
42789
|
+
state[file] = openSource ? existsSync4(path5.join(root, file)) ? "ok" : "missing" : "skip";
|
|
42790
|
+
continue;
|
|
42791
|
+
}
|
|
42792
|
+
if (!existsSync4(path5.join(root, file))) {
|
|
42793
|
+
state[file] = "missing";
|
|
42794
|
+
continue;
|
|
42795
|
+
}
|
|
42796
|
+
if (file === "CHANGELOG.md") {
|
|
42797
|
+
const stats = changelogUnreleasedStats(root);
|
|
42798
|
+
state[file] = stats.exists && stats.has_unreleased ? "ok" : "invalid";
|
|
42799
|
+
continue;
|
|
42800
|
+
}
|
|
42801
|
+
state[file] = "ok";
|
|
42802
|
+
}
|
|
42803
|
+
return { state, openSource };
|
|
42804
|
+
};
|
|
42733
42805
|
var planHygieneFiles = (root, opts = {}) => {
|
|
42734
42806
|
const openSource = opts.includeOpenSource ?? isOpenSource(root);
|
|
42735
42807
|
const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"];
|
|
@@ -42737,13 +42809,13 @@ var planHygieneFiles = (root, opts = {}) => {
|
|
|
42737
42809
|
files.push("LICENSE", "CONTRIBUTING.md");
|
|
42738
42810
|
const planned = [];
|
|
42739
42811
|
for (const file of files) {
|
|
42740
|
-
if (existsSync4(
|
|
42812
|
+
if (existsSync4(path5.join(root, file)))
|
|
42741
42813
|
continue;
|
|
42742
|
-
const tpl =
|
|
42814
|
+
const tpl = path5.join(templatesDir(), file);
|
|
42743
42815
|
if (!existsSync4(tpl))
|
|
42744
42816
|
continue;
|
|
42745
|
-
const content = readFileSync4(tpl, "utf8").replace(/<PROJECT>/g,
|
|
42746
|
-
planned.push({ path:
|
|
42817
|
+
const content = readFileSync4(tpl, "utf8").replace(/<PROJECT>/g, path5.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
|
|
42818
|
+
planned.push({ path: path5.join(root, file), content });
|
|
42747
42819
|
}
|
|
42748
42820
|
return planned;
|
|
42749
42821
|
};
|
|
@@ -42776,7 +42848,7 @@ import {
|
|
|
42776
42848
|
writeFileSync as writeFileSync5
|
|
42777
42849
|
} from "node:fs";
|
|
42778
42850
|
import os5 from "node:os";
|
|
42779
|
-
import
|
|
42851
|
+
import path10 from "node:path";
|
|
42780
42852
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
42781
42853
|
|
|
42782
42854
|
// packages/workit-core/src/core/branch-policy.ts
|
|
@@ -42841,9 +42913,9 @@ var detectBranchPolicy = (workspaceRoot) => {
|
|
|
42841
42913
|
|
|
42842
42914
|
// packages/workit-core/src/core/setup-state.ts
|
|
42843
42915
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
42844
|
-
import
|
|
42916
|
+
import path6 from "node:path";
|
|
42845
42917
|
var classifySetupFile = (dir, name) => {
|
|
42846
|
-
const file =
|
|
42918
|
+
const file = path6.join(dir, name);
|
|
42847
42919
|
let raw;
|
|
42848
42920
|
try {
|
|
42849
42921
|
raw = readFileSync5(file, "utf8");
|
|
@@ -42872,7 +42944,7 @@ var readSetupState = (dir = resolveConfigDir()) => ({
|
|
|
42872
42944
|
});
|
|
42873
42945
|
|
|
42874
42946
|
// packages/workit-core/src/core/registration.ts
|
|
42875
|
-
import
|
|
42947
|
+
import path7 from "node:path";
|
|
42876
42948
|
var isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
42877
42949
|
var named = (s, name) => s === name || s.startsWith(`${name}@`);
|
|
42878
42950
|
function isWorkitPlugin(value) {
|
|
@@ -42918,11 +42990,11 @@ function mergeCursorEnabledPlugins(enabled) {
|
|
|
42918
42990
|
}
|
|
42919
42991
|
function mergeCursorPluginDirs(pluginDirs, pluginDir) {
|
|
42920
42992
|
const strip = (p) => {
|
|
42921
|
-
const j =
|
|
42922
|
-
return
|
|
42993
|
+
const j = path7.join(p);
|
|
42994
|
+
return path7.dirname(j) === j ? j : j.replace(/[\\/]+$/, "");
|
|
42923
42995
|
};
|
|
42924
42996
|
const normalized = strip(pluginDir);
|
|
42925
|
-
const legacy = strip(
|
|
42997
|
+
const legacy = strip(path7.join(path7.dirname(pluginDir), "workflow-toolkit"));
|
|
42926
42998
|
const prev = Array.isArray(pluginDirs) ? pluginDirs.map(String) : [];
|
|
42927
42999
|
const kept = prev.filter((d) => strip(d) !== legacy);
|
|
42928
43000
|
const exists = kept.some((d) => strip(d) === normalized);
|
|
@@ -42954,15 +43026,17 @@ function mergeCursorMcp(mcp, serverName, server) {
|
|
|
42954
43026
|
base2.mcpServers = servers;
|
|
42955
43027
|
return { config: base2, changed };
|
|
42956
43028
|
}
|
|
43029
|
+
var CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@0.8.0";
|
|
42957
43030
|
function cursorMcpServerEntry(_packageDir) {
|
|
42958
43031
|
return {
|
|
42959
43032
|
command: "npx",
|
|
42960
|
-
args: [
|
|
42961
|
-
|
|
42962
|
-
|
|
42963
|
-
|
|
42964
|
-
|
|
42965
|
-
|
|
43033
|
+
args: ["-y", `--package=${CURSOR_RUNTIME_PACKAGE}`, "workit-cursor-mcp", "${workspaceFolder}"]
|
|
43034
|
+
};
|
|
43035
|
+
}
|
|
43036
|
+
function cursorHooksEntry(_packageDir) {
|
|
43037
|
+
return {
|
|
43038
|
+
command: `npx -y --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
|
|
43039
|
+
args: []
|
|
42966
43040
|
};
|
|
42967
43041
|
}
|
|
42968
43042
|
|
|
@@ -42970,7 +43044,7 @@ function cursorMcpServerEntry(_packageDir) {
|
|
|
42970
43044
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
42971
43045
|
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
42972
43046
|
import os4 from "node:os";
|
|
42973
|
-
import
|
|
43047
|
+
import path9 from "node:path";
|
|
42974
43048
|
|
|
42975
43049
|
// packages/workit-core/src/core/support-matrix.ts
|
|
42976
43050
|
var SUPPORT_MATRIX = {
|
|
@@ -42982,7 +43056,7 @@ var SUPPORT_MATRIX = {
|
|
|
42982
43056
|
|
|
42983
43057
|
// packages/workit-core/src/core/skill-manifests.ts
|
|
42984
43058
|
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
42985
|
-
import
|
|
43059
|
+
import path8 from "node:path";
|
|
42986
43060
|
var CANONICAL_SKILLS = {
|
|
42987
43061
|
superpowers: [
|
|
42988
43062
|
"brainstorming",
|
|
@@ -43015,7 +43089,7 @@ var CANONICAL_SKILLS = {
|
|
|
43015
43089
|
"wk-verify"
|
|
43016
43090
|
]
|
|
43017
43091
|
};
|
|
43018
|
-
var skillManifestNames = (root) => existsSync6(root) ? readdirSync2(root).filter((name) => existsSync6(
|
|
43092
|
+
var skillManifestNames = (root) => existsSync6(root) ? readdirSync2(root).filter((name) => existsSync6(path8.join(root, name, "SKILL.md"))).sort() : [];
|
|
43019
43093
|
var validateSkillManifests = (root, expected, label) => {
|
|
43020
43094
|
const actual = skillManifestNames(root);
|
|
43021
43095
|
const missing = expected.filter((name) => !actual.includes(name));
|
|
@@ -43023,10 +43097,10 @@ var validateSkillManifests = (root, expected, label) => {
|
|
|
43023
43097
|
return missing.length === 0 && extra.length === 0 ? null : `${label} mismatch at ${root} (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`;
|
|
43024
43098
|
};
|
|
43025
43099
|
var validateCursorSkills = (pluginDir) => {
|
|
43026
|
-
const workit = validateSkillManifests(
|
|
43100
|
+
const workit = validateSkillManifests(path8.join(pluginDir, "skills"), CANONICAL_SKILLS.workit, "Cursor Workit skills");
|
|
43027
43101
|
if (workit)
|
|
43028
43102
|
return workit;
|
|
43029
|
-
const vendor =
|
|
43103
|
+
const vendor = path8.join(pluginDir, "vendor/superpowers/skills");
|
|
43030
43104
|
const superpowers = validateSkillManifests(vendor, CANONICAL_SKILLS.superpowers, "Cursor Superpowers skills");
|
|
43031
43105
|
if (superpowers)
|
|
43032
43106
|
return superpowers;
|
|
@@ -43034,7 +43108,7 @@ var validateCursorSkills = (pluginDir) => {
|
|
|
43034
43108
|
while (pending.length > 0) {
|
|
43035
43109
|
const dir = pending.pop();
|
|
43036
43110
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
43037
|
-
const file =
|
|
43111
|
+
const file = path8.join(dir, entry.name);
|
|
43038
43112
|
if (entry.isDirectory())
|
|
43039
43113
|
pending.push(file);
|
|
43040
43114
|
else if ((statSync(file).mode & 73) !== 0 || readFileSync6(file).subarray(0, 2).toString("latin1") === "#!") {
|
|
@@ -43049,11 +43123,11 @@ if (false) {}
|
|
|
43049
43123
|
// packages/workit-core/src/core/doctor.ts
|
|
43050
43124
|
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
43051
43125
|
var findDevFromCwd = (cwd2) => {
|
|
43052
|
-
let dir =
|
|
43126
|
+
let dir = path9.resolve(cwd2);
|
|
43053
43127
|
while (true) {
|
|
43054
|
-
if (existsSync7(
|
|
43128
|
+
if (existsSync7(path9.join(dir, "packages", "workit-core", "package.json")))
|
|
43055
43129
|
return dir;
|
|
43056
|
-
const parent =
|
|
43130
|
+
const parent = path9.dirname(dir);
|
|
43057
43131
|
if (parent === dir)
|
|
43058
43132
|
return null;
|
|
43059
43133
|
dir = parent;
|
|
@@ -43062,8 +43136,8 @@ var findDevFromCwd = (cwd2) => {
|
|
|
43062
43136
|
var resolve = (options) => {
|
|
43063
43137
|
const env3 = options.env ?? process.env;
|
|
43064
43138
|
const home = options.home ?? env3.HOME ?? os4.homedir();
|
|
43065
|
-
const configDir2 = options.configDir ?? env3.WORKFLOW_TOOLKIT_CONFIG ?? env3.WORKFLOW_TOOLKIT_CONFIG_DIR ??
|
|
43066
|
-
const stateDir = options.stateDir ?? env3.WORKFLOW_TOOLKIT_STATE ??
|
|
43139
|
+
const configDir2 = options.configDir ?? env3.WORKFLOW_TOOLKIT_CONFIG ?? env3.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path9.join(home, ".config", "workit");
|
|
43140
|
+
const stateDir = options.stateDir ?? env3.WORKFLOW_TOOLKIT_STATE ?? path9.join(home, ".local", "state", "workit");
|
|
43067
43141
|
const cwd2 = options.cwd ?? process.cwd();
|
|
43068
43142
|
const dev = options.dev ?? env3.WORKFLOW_TOOLKIT_DEV ?? findDevFromCwd(cwd2);
|
|
43069
43143
|
return {
|
|
@@ -43073,10 +43147,10 @@ var resolve = (options) => {
|
|
|
43073
43147
|
stateDir,
|
|
43074
43148
|
cwd: cwd2,
|
|
43075
43149
|
dev,
|
|
43076
|
-
opencodeConfig: options.opencodeConfig ??
|
|
43077
|
-
cursorSettings: options.cursorSettings ??
|
|
43078
|
-
cursorMcp: options.cursorMcp ??
|
|
43079
|
-
cursorPluginDir: options.cursorPluginDir ??
|
|
43150
|
+
opencodeConfig: options.opencodeConfig ?? path9.join(home, ".config", "opencode", "opencode.json"),
|
|
43151
|
+
cursorSettings: options.cursorSettings ?? path9.join(home, ".cursor", "settings.json"),
|
|
43152
|
+
cursorMcp: options.cursorMcp ?? path9.join(home, ".cursor", "mcp.json"),
|
|
43153
|
+
cursorPluginDir: options.cursorPluginDir ?? path9.join(home, ".cursor", "plugins", "local", "workit"),
|
|
43080
43154
|
env: env3,
|
|
43081
43155
|
installer: options.installer ?? false
|
|
43082
43156
|
};
|
|
@@ -43102,13 +43176,13 @@ var pluginEntries = (cfg) => {
|
|
|
43102
43176
|
return list.map(String).filter(isWorkitPlugin);
|
|
43103
43177
|
};
|
|
43104
43178
|
var commandOnPath = (name, env3) => {
|
|
43105
|
-
const dirs = (env3.PATH ?? process.env.PATH ?? "").split(
|
|
43179
|
+
const dirs = (env3.PATH ?? process.env.PATH ?? "").split(path9.delimiter);
|
|
43106
43180
|
const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
|
|
43107
43181
|
for (const dir of dirs) {
|
|
43108
43182
|
if (!dir)
|
|
43109
43183
|
continue;
|
|
43110
43184
|
for (const candidateName of names) {
|
|
43111
|
-
const candidate =
|
|
43185
|
+
const candidate = path9.join(dir, candidateName);
|
|
43112
43186
|
try {
|
|
43113
43187
|
const st = statSync2(candidate);
|
|
43114
43188
|
if (process.platform !== "win32" && (st.mode & 73) === 0)
|
|
@@ -43140,7 +43214,7 @@ var resolveBun = (env3) => {
|
|
|
43140
43214
|
if (env3.BUN && existsSync7(env3.BUN))
|
|
43141
43215
|
return env3.BUN;
|
|
43142
43216
|
for (const candidate of [
|
|
43143
|
-
|
|
43217
|
+
path9.join(os4.homedir(), ".bun/bin/bun"),
|
|
43144
43218
|
"/usr/local/bin/bun",
|
|
43145
43219
|
"/usr/bin/bun"
|
|
43146
43220
|
]) {
|
|
@@ -43194,7 +43268,7 @@ var checkVersions = (res) => {
|
|
|
43194
43268
|
detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping version parity"
|
|
43195
43269
|
};
|
|
43196
43270
|
}
|
|
43197
|
-
const corePkg = readJson(
|
|
43271
|
+
const corePkg = readJson(path9.join(res.dev, "packages/workit-core/package.json"));
|
|
43198
43272
|
if (!corePkg) {
|
|
43199
43273
|
return {
|
|
43200
43274
|
id: "versions",
|
|
@@ -43204,7 +43278,7 @@ var checkVersions = (res) => {
|
|
|
43204
43278
|
}
|
|
43205
43279
|
const refs = new Set;
|
|
43206
43280
|
for (const name of ["workit-opencode", "workit-cursor", "workit-cli"]) {
|
|
43207
|
-
const pkg = readJson(
|
|
43281
|
+
const pkg = readJson(path9.join(res.dev, "packages", name, "package.json"));
|
|
43208
43282
|
const dep = pkg?.dependencies?.["@brainervirus/workit-core"];
|
|
43209
43283
|
if (typeof dep === "string")
|
|
43210
43284
|
refs.add(dep);
|
|
@@ -43220,7 +43294,7 @@ var checkVersions = (res) => {
|
|
|
43220
43294
|
if (refs.size > 1) {
|
|
43221
43295
|
problems.push(`adapters pin different core versions: ${[...refs].join(", ")}`);
|
|
43222
43296
|
}
|
|
43223
|
-
const opencodePkg = readJson(
|
|
43297
|
+
const opencodePkg = readJson(path9.join(res.dev, "packages/workit-opencode/package.json"));
|
|
43224
43298
|
const sdk = opencodePkg?.dependencies?.["@opencode-ai/plugin"];
|
|
43225
43299
|
const sdkVersion = typeof sdk === "string" ? (sdk.match(/^\d+(?:\.\d+){0,2}/) ?? [])[0] : null;
|
|
43226
43300
|
if (sdkVersion && !semverAtLeast(sdkVersion, SUPPORT_MATRIX.opencode.minimum)) {
|
|
@@ -43241,23 +43315,23 @@ var checkVersions = (res) => {
|
|
|
43241
43315
|
};
|
|
43242
43316
|
};
|
|
43243
43317
|
var assetPathsFor = (host, dev) => {
|
|
43244
|
-
const pkg =
|
|
43318
|
+
const pkg = path9.join(dev, "packages", `workit-${host}`);
|
|
43245
43319
|
switch (host) {
|
|
43246
43320
|
case "opencode":
|
|
43247
43321
|
return [
|
|
43248
|
-
|
|
43249
|
-
|
|
43250
|
-
|
|
43251
|
-
|
|
43322
|
+
path9.join(pkg, "assets", "commands", "wk-init.md"),
|
|
43323
|
+
path9.join(pkg, "assets", "skills", "wk-init", "SKILL.md"),
|
|
43324
|
+
path9.join(pkg, "assets", "templates", "spec-template.md"),
|
|
43325
|
+
path9.join(pkg, "assets", "vendor", "superpowers", "skills", "brainstorming", "SKILL.md")
|
|
43252
43326
|
];
|
|
43253
43327
|
case "cursor":
|
|
43254
43328
|
return [
|
|
43255
|
-
|
|
43256
|
-
|
|
43257
|
-
|
|
43329
|
+
path9.join(pkg, "assets", "templates", "spec-template.md"),
|
|
43330
|
+
path9.join(pkg, "mcp.json"),
|
|
43331
|
+
path9.join(pkg, ".cursor-plugin")
|
|
43258
43332
|
];
|
|
43259
43333
|
case "cli":
|
|
43260
|
-
return [
|
|
43334
|
+
return [path9.join(pkg, "assets", "templates", "spec-template.md")];
|
|
43261
43335
|
}
|
|
43262
43336
|
};
|
|
43263
43337
|
var hostsFor = (host) => host === "cli" ? ["opencode", "cursor", "cli"] : [host];
|
|
@@ -43291,17 +43365,17 @@ var checkAssets = (res) => {
|
|
|
43291
43365
|
};
|
|
43292
43366
|
};
|
|
43293
43367
|
var launcherSlotsFor = (host, dev) => {
|
|
43294
|
-
const pkg =
|
|
43368
|
+
const pkg = path9.join(dev, "packages", `workit-${host}`);
|
|
43295
43369
|
switch (host) {
|
|
43296
43370
|
case "opencode":
|
|
43297
|
-
return [[
|
|
43371
|
+
return [[path9.join(pkg, "src", "plugin.ts"), path9.join(pkg, "dist", "plugin.js")]];
|
|
43298
43372
|
case "cursor":
|
|
43299
43373
|
return [
|
|
43300
|
-
[
|
|
43301
|
-
[
|
|
43374
|
+
[path9.join(pkg, "dist", "mcp-server.js")],
|
|
43375
|
+
[path9.join(pkg, "dist", "cursor-session-start.js")]
|
|
43302
43376
|
];
|
|
43303
43377
|
case "cli":
|
|
43304
|
-
return [[
|
|
43378
|
+
return [[path9.join(pkg, "src", "index.tsx"), path9.join(pkg, "dist", "index.js")]];
|
|
43305
43379
|
}
|
|
43306
43380
|
};
|
|
43307
43381
|
var validNodeEntry = (entry, runtime, env3) => {
|
|
@@ -43330,11 +43404,11 @@ var registeredCursorLauncher = (res) => {
|
|
|
43330
43404
|
if (typeof command !== "string" || !Array.isArray(args) || typeof args[0] !== "string") {
|
|
43331
43405
|
return "invalid";
|
|
43332
43406
|
}
|
|
43333
|
-
const executable =
|
|
43407
|
+
const executable = path9.basename(command).toLowerCase();
|
|
43334
43408
|
if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
|
|
43335
43409
|
if (args[0] !== "-y")
|
|
43336
43410
|
return "invalid";
|
|
43337
|
-
if (args[1] !==
|
|
43411
|
+
if (args[1] !== `--package=${CURSOR_RUNTIME_PACKAGE}`)
|
|
43338
43412
|
return "invalid";
|
|
43339
43413
|
if (args[2] !== "workit-cursor-mcp")
|
|
43340
43414
|
return "invalid";
|
|
@@ -43345,21 +43419,43 @@ var registeredCursorLauncher = (res) => {
|
|
|
43345
43419
|
return {
|
|
43346
43420
|
kind: "node",
|
|
43347
43421
|
runtime: command,
|
|
43348
|
-
entry:
|
|
43422
|
+
entry: path9.isAbsolute(args[0]) ? args[0] : path9.resolve(path9.dirname(res.cursorMcp), args[0])
|
|
43349
43423
|
};
|
|
43350
43424
|
};
|
|
43425
|
+
var canonicalCursorHook = cursorHooksEntry("").command;
|
|
43426
|
+
var registeredCursorHook = (res) => {
|
|
43427
|
+
const hooksFile = path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
|
|
43428
|
+
if (!existsSync7(hooksFile))
|
|
43429
|
+
return "invalid";
|
|
43430
|
+
const config = readJson(hooksFile);
|
|
43431
|
+
if (!config)
|
|
43432
|
+
return "invalid";
|
|
43433
|
+
const sessionStart = config.hooks?.sessionStart;
|
|
43434
|
+
if (!Array.isArray(sessionStart) || sessionStart.length !== 1)
|
|
43435
|
+
return "invalid";
|
|
43436
|
+
const entry = sessionStart[0];
|
|
43437
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
43438
|
+
return "invalid";
|
|
43439
|
+
return typeof entry.command === "string" ? entry.command : "invalid";
|
|
43440
|
+
};
|
|
43351
43441
|
var checkLauncher = (res) => {
|
|
43352
43442
|
const dev = res.dev;
|
|
43353
43443
|
const hosts = hostsFor(res.host);
|
|
43354
43444
|
const registered = hosts.includes("cursor") ? registeredCursorLauncher(res) : null;
|
|
43355
43445
|
const runtime = registered && registered !== "invalid" && registered.kind === "node" ? registered.runtime : "node";
|
|
43356
|
-
const missing = hosts.includes("cursor") ? ["dist/mcp-server.js", "dist/cursor-session-start.js"].map((rel) =>
|
|
43446
|
+
const missing = hosts.includes("cursor") ? ["dist/mcp-server.js", "dist/cursor-session-start.js"].map((rel) => path9.join(res.cursorPluginDir, rel)).filter((p) => !validNodeEntry(p, runtime, res.env)).map((p) => `cursor: ${p}`) : [];
|
|
43357
43447
|
if (hosts.includes("cursor")) {
|
|
43358
43448
|
if (registered === "invalid") {
|
|
43359
43449
|
missing.push(`cursor: canonical workit MCP launcher in ${res.cursorMcp}`);
|
|
43360
43450
|
} else if (registered && registered.kind === "node" && !validNodeEntry(registered.entry, registered.runtime, res.env)) {
|
|
43361
43451
|
missing.push(`cursor: registered ${registered.entry}`);
|
|
43362
43452
|
}
|
|
43453
|
+
const hook = registeredCursorHook(res);
|
|
43454
|
+
if (hook === "invalid") {
|
|
43455
|
+
missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
|
|
43456
|
+
} else if (hook !== null && hook !== canonicalCursorHook) {
|
|
43457
|
+
missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
|
|
43458
|
+
}
|
|
43363
43459
|
}
|
|
43364
43460
|
if (dev) {
|
|
43365
43461
|
missing.push(...hosts.filter((h) => h !== "cursor").flatMap((h) => launcherSlotsFor(h, dev).filter((slot) => !slot.some((p) => existsSync7(p))).map((slot) => `${h}: ${slot.join(" or ")}`)));
|
|
@@ -43499,7 +43595,7 @@ var checkDuplicateRegistration = (res) => {
|
|
|
43499
43595
|
var checkMalformedConfig = (res) => {
|
|
43500
43596
|
const files = [];
|
|
43501
43597
|
for (const name of ["config.json", "youtrack.json", "vcs.json", "workspaces.json"]) {
|
|
43502
|
-
const p =
|
|
43598
|
+
const p = path9.join(res.configDir, name);
|
|
43503
43599
|
if (existsSync7(p))
|
|
43504
43600
|
files.push(p);
|
|
43505
43601
|
}
|
|
@@ -43522,7 +43618,7 @@ var checkMalformedConfig = (res) => {
|
|
|
43522
43618
|
};
|
|
43523
43619
|
};
|
|
43524
43620
|
var checkWorkspaceMismatch = (res) => {
|
|
43525
|
-
const file =
|
|
43621
|
+
const file = path9.join(res.configDir, "workspaces.json");
|
|
43526
43622
|
if (!existsSync7(file))
|
|
43527
43623
|
return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
|
|
43528
43624
|
const ws = readJson(file);
|
|
@@ -43552,20 +43648,20 @@ var isPlaceholder = (p) => {
|
|
|
43552
43648
|
};
|
|
43553
43649
|
var checkCredentialMetadata = (res) => {
|
|
43554
43650
|
const tokenPaths = [];
|
|
43555
|
-
const youtrackJson = readJson(
|
|
43651
|
+
const youtrackJson = readJson(path9.join(res.configDir, "youtrack.json"));
|
|
43556
43652
|
if (youtrackJson) {
|
|
43557
|
-
tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile :
|
|
43558
|
-
} else if (existsSync7(
|
|
43559
|
-
tokenPaths.push(
|
|
43653
|
+
tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile : path9.join(res.configDir, "youtrack.token"));
|
|
43654
|
+
} else if (existsSync7(path9.join(res.configDir, "youtrack.token"))) {
|
|
43655
|
+
tokenPaths.push(path9.join(res.configDir, "youtrack.token"));
|
|
43560
43656
|
}
|
|
43561
|
-
const vcsJson = readJson(
|
|
43657
|
+
const vcsJson = readJson(path9.join(res.configDir, "vcs.json"));
|
|
43562
43658
|
for (const key of ["gitlab", "github"]) {
|
|
43563
43659
|
const provider = vcsJson?.[key];
|
|
43564
43660
|
if (provider && typeof provider === "object") {
|
|
43565
43661
|
const tf = provider.tokenFile;
|
|
43566
|
-
tokenPaths.push(typeof tf === "string" ? tf :
|
|
43567
|
-
} else if (!vcsJson && existsSync7(
|
|
43568
|
-
tokenPaths.push(
|
|
43662
|
+
tokenPaths.push(typeof tf === "string" ? tf : path9.join(res.configDir, `${key}.token`));
|
|
43663
|
+
} else if (!vcsJson && existsSync7(path9.join(res.configDir, `${key}.token`))) {
|
|
43664
|
+
tokenPaths.push(path9.join(res.configDir, `${key}.token`));
|
|
43569
43665
|
}
|
|
43570
43666
|
}
|
|
43571
43667
|
if (tokenPaths.length === 0) {
|
|
@@ -43573,7 +43669,7 @@ var checkCredentialMetadata = (res) => {
|
|
|
43573
43669
|
}
|
|
43574
43670
|
const problems = [];
|
|
43575
43671
|
for (const raw of tokenPaths) {
|
|
43576
|
-
const p =
|
|
43672
|
+
const p = path9.isAbsolute(raw) ? raw : path9.resolve(res.configDir, raw);
|
|
43577
43673
|
if (!existsSync7(p)) {
|
|
43578
43674
|
problems.push(`${p} is missing`);
|
|
43579
43675
|
continue;
|
|
@@ -43601,8 +43697,8 @@ var checkCredentialMetadata = (res) => {
|
|
|
43601
43697
|
};
|
|
43602
43698
|
};
|
|
43603
43699
|
var checkLogWritable = (res) => {
|
|
43604
|
-
const logsDir =
|
|
43605
|
-
const probe =
|
|
43700
|
+
const logsDir = path9.join(res.stateDir, "logs");
|
|
43701
|
+
const probe = path9.join(logsDir, "doctor-probe.tmp");
|
|
43606
43702
|
try {
|
|
43607
43703
|
mkdirSync2(logsDir, { recursive: true, mode: 448 });
|
|
43608
43704
|
writeFileSync4(probe, `{"probe":true}
|
|
@@ -43790,7 +43886,7 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43790
43886
|
const current = readConfigFromDir(state.configDir);
|
|
43791
43887
|
mutations.push({
|
|
43792
43888
|
type: "merge-json",
|
|
43793
|
-
path:
|
|
43889
|
+
path: path10.join(state.configDir, "config.json"),
|
|
43794
43890
|
value: {
|
|
43795
43891
|
locale: values2.locale,
|
|
43796
43892
|
localeOptions: current.localeOptions,
|
|
@@ -43815,16 +43911,16 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43815
43911
|
if (blocked.length === 0) {
|
|
43816
43912
|
mutations.push({
|
|
43817
43913
|
type: "update-workspaces",
|
|
43818
|
-
path:
|
|
43914
|
+
path: path10.join(state.configDir, "workspaces.json"),
|
|
43819
43915
|
entries: values2.workspaces
|
|
43820
43916
|
});
|
|
43821
43917
|
}
|
|
43822
43918
|
}
|
|
43823
43919
|
if (values2.baseUrl.trim()) {
|
|
43824
|
-
const ytPath =
|
|
43920
|
+
const ytPath = path10.join(state.configDir, "youtrack.json");
|
|
43825
43921
|
const ytExisting = readConfigRecord(ytPath);
|
|
43826
43922
|
const ytConfigured = configuredTokenPath(ytExisting, "tokenFile");
|
|
43827
|
-
const tokenPath = resolveTokenPath(ytConfigured,
|
|
43923
|
+
const tokenPath = resolveTokenPath(ytConfigured, path10.join(state.configDir, "youtrack.token"), values2.tokenPaths?.youtrack);
|
|
43828
43924
|
const draft = youtrackDraft(values2, tokenPath);
|
|
43829
43925
|
if (ytConfigured !== null && tokenPath !== ytConfigured) {
|
|
43830
43926
|
delete draft.tokenFile;
|
|
@@ -43852,12 +43948,12 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43852
43948
|
});
|
|
43853
43949
|
}
|
|
43854
43950
|
if (values2.vcsProvider !== "skip") {
|
|
43855
|
-
const vcsPath =
|
|
43951
|
+
const vcsPath = path10.join(state.configDir, "vcs.json");
|
|
43856
43952
|
const vcsExisting = readConfigRecord(vcsPath);
|
|
43857
43953
|
const glConfigured = configuredTokenPath(vcsExisting, "gitlab", "tokenFile");
|
|
43858
43954
|
const ghConfigured = configuredTokenPath(vcsExisting, "github", "tokenFile");
|
|
43859
|
-
const gitlabToken = resolveTokenPath(glConfigured,
|
|
43860
|
-
const githubToken = resolveTokenPath(ghConfigured,
|
|
43955
|
+
const gitlabToken = resolveTokenPath(glConfigured, path10.join(state.configDir, "gitlab.token"), values2.tokenPaths?.gitlab);
|
|
43956
|
+
const githubToken = resolveTokenPath(ghConfigured, path10.join(state.configDir, "github.token"), values2.tokenPaths?.github);
|
|
43861
43957
|
const draft = vcsDraft(values2.vcsProvider, gitlabToken, githubToken);
|
|
43862
43958
|
for (const [configured, token, key] of [
|
|
43863
43959
|
[glConfigured, gitlabToken, "gitlab.tokenFile"],
|
|
@@ -43904,8 +44000,8 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43904
44000
|
}
|
|
43905
44001
|
}
|
|
43906
44002
|
if (values2.applyProject) {
|
|
43907
|
-
const root =
|
|
43908
|
-
const gitignorePath =
|
|
44003
|
+
const root = path10.resolve(opts.cwd ?? process.cwd());
|
|
44004
|
+
const gitignorePath = path10.join(root, ".gitignore");
|
|
43909
44005
|
const existing = existsSync8(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
|
|
43910
44006
|
const existingLines = new Set(existing.split(`
|
|
43911
44007
|
`).map((l) => l.trim()).filter(Boolean));
|
|
@@ -43934,10 +44030,10 @@ var resolveSetupPaths = (options, configDir2) => {
|
|
|
43934
44030
|
configDir: configDir2,
|
|
43935
44031
|
cwd: options.cwd ?? process.cwd(),
|
|
43936
44032
|
env: env3,
|
|
43937
|
-
opencodeConfig: options.opencodeConfig ??
|
|
43938
|
-
cursorSettings: options.cursorSettings ??
|
|
43939
|
-
cursorMcp: options.cursorMcp ??
|
|
43940
|
-
cursorPluginDir: options.cursorPluginDir ??
|
|
44033
|
+
opencodeConfig: options.opencodeConfig ?? path10.join(home, ".config", "opencode", "opencode.json"),
|
|
44034
|
+
cursorSettings: options.cursorSettings ?? path10.join(home, ".cursor", "settings.json"),
|
|
44035
|
+
cursorMcp: options.cursorMcp ?? path10.join(home, ".cursor", "mcp.json"),
|
|
44036
|
+
cursorPluginDir: options.cursorPluginDir ?? path10.join(home, ".cursor", "plugins", "local", "workit")
|
|
43941
44037
|
};
|
|
43942
44038
|
};
|
|
43943
44039
|
function resolveApply(preview, options) {
|
|
@@ -43983,7 +44079,7 @@ var readFileSafe = (p) => {
|
|
|
43983
44079
|
}
|
|
43984
44080
|
};
|
|
43985
44081
|
function applyMutation(m) {
|
|
43986
|
-
const dir =
|
|
44082
|
+
const dir = path10.dirname(m.path);
|
|
43987
44083
|
switch (m.type) {
|
|
43988
44084
|
case "create-file": {
|
|
43989
44085
|
mkdirSync3(dir, { recursive: true });
|
|
@@ -44087,7 +44183,7 @@ function applyMutation(m) {
|
|
|
44087
44183
|
detail: "already configured"
|
|
44088
44184
|
};
|
|
44089
44185
|
}
|
|
44090
|
-
mkdirSync3(
|
|
44186
|
+
mkdirSync3(path10.dirname(m.path), { recursive: true });
|
|
44091
44187
|
writeFileSync5(m.path, JSON.stringify(merged, null, 2) + `
|
|
44092
44188
|
`, "utf8");
|
|
44093
44189
|
return existing.kind === "record" ? { platform: "core", file: m.path, status: "Configured" } : { platform: "core", file: m.path, status: "Installed" };
|
|
@@ -44096,7 +44192,7 @@ function applyMutation(m) {
|
|
|
44096
44192
|
}
|
|
44097
44193
|
var isAdapter = (root, platform2) => {
|
|
44098
44194
|
try {
|
|
44099
|
-
const pkg = JSON.parse(readFileSync8(
|
|
44195
|
+
const pkg = JSON.parse(readFileSync8(path10.join(root, "package.json"), "utf8"));
|
|
44100
44196
|
return pkg.name === `@brainervirus/workit-${platform2}`;
|
|
44101
44197
|
} catch {
|
|
44102
44198
|
return false;
|
|
@@ -44105,19 +44201,19 @@ var isAdapter = (root, platform2) => {
|
|
|
44105
44201
|
function adapterRoot(platform2, res) {
|
|
44106
44202
|
const candidates = [];
|
|
44107
44203
|
if (res.dev) {
|
|
44108
|
-
candidates.push(
|
|
44204
|
+
candidates.push(path10.join(res.dev, "packages", `workit-${platform2}`));
|
|
44109
44205
|
} else {
|
|
44110
|
-
candidates.push(
|
|
44111
|
-
let dir =
|
|
44206
|
+
candidates.push(path10.join(packageRoot(), "..", `workit-${platform2}`));
|
|
44207
|
+
let dir = path10.resolve(res.cwd);
|
|
44112
44208
|
while (true) {
|
|
44113
|
-
candidates.push(
|
|
44114
|
-
const parent =
|
|
44209
|
+
candidates.push(path10.join(dir, "packages", `workit-${platform2}`));
|
|
44210
|
+
const parent = path10.dirname(dir);
|
|
44115
44211
|
if (parent === dir)
|
|
44116
44212
|
break;
|
|
44117
44213
|
dir = parent;
|
|
44118
44214
|
}
|
|
44119
44215
|
}
|
|
44120
|
-
candidates.push(
|
|
44216
|
+
candidates.push(path10.join(res.home, ".local", "share", "workflow-toolkit", "packages", `workit-${platform2}`));
|
|
44121
44217
|
for (const candidate of candidates) {
|
|
44122
44218
|
if (isAdapter(candidate, platform2))
|
|
44123
44219
|
return candidate;
|
|
@@ -44126,7 +44222,7 @@ function adapterRoot(platform2, res) {
|
|
|
44126
44222
|
}
|
|
44127
44223
|
var opencodePin = (root) => {
|
|
44128
44224
|
for (const rel of ["src/plugin.ts", "dist/plugin.js"]) {
|
|
44129
|
-
const entry =
|
|
44225
|
+
const entry = path10.join(root, rel);
|
|
44130
44226
|
if (existsSync8(entry))
|
|
44131
44227
|
return `file://${entry}`;
|
|
44132
44228
|
}
|
|
@@ -44142,7 +44238,7 @@ function applyOpenCode(root, res) {
|
|
|
44142
44238
|
detail: "workit-opencode package has no loadable plugin entry (src/plugin.ts or dist/plugin.js)"
|
|
44143
44239
|
};
|
|
44144
44240
|
}
|
|
44145
|
-
mkdirSync3(
|
|
44241
|
+
mkdirSync3(path10.dirname(res.opencodeConfig), { recursive: true });
|
|
44146
44242
|
const existing = readExisting(res.opencodeConfig);
|
|
44147
44243
|
if (existing.kind === "malformed") {
|
|
44148
44244
|
return {
|
|
@@ -44180,19 +44276,19 @@ var samePluginContent = (src, dest, relative = "") => {
|
|
|
44180
44276
|
for (const entry of readdirSync3(src, { withFileTypes: true })) {
|
|
44181
44277
|
if (entry.name === "node_modules")
|
|
44182
44278
|
continue;
|
|
44183
|
-
const source =
|
|
44184
|
-
const installed =
|
|
44279
|
+
const source = path10.join(src, entry.name);
|
|
44280
|
+
const installed = path10.join(dest, entry.name);
|
|
44185
44281
|
if (entry.isDirectory()) {
|
|
44186
|
-
if (!statSync3(installed).isDirectory() || !samePluginContent(source, installed,
|
|
44282
|
+
if (!statSync3(installed).isDirectory() || !samePluginContent(source, installed, path10.join(relative, entry.name)))
|
|
44187
44283
|
return false;
|
|
44188
44284
|
} else if (!readFileSync8(source).equals(readFileSync8(installed))) {
|
|
44189
44285
|
return false;
|
|
44190
44286
|
}
|
|
44191
44287
|
}
|
|
44192
44288
|
for (const entry of readdirSync3(dest, { withFileTypes: true })) {
|
|
44193
|
-
if (existsSync8(
|
|
44289
|
+
if (existsSync8(path10.join(src, entry.name)))
|
|
44194
44290
|
continue;
|
|
44195
|
-
const rel =
|
|
44291
|
+
const rel = path10.join(relative, entry.name);
|
|
44196
44292
|
if (rel === ".workflow-toolkit-root")
|
|
44197
44293
|
continue;
|
|
44198
44294
|
if (relative === "rules" && entry.isFile() && entry.name.endsWith(".mdc"))
|
|
@@ -44206,41 +44302,41 @@ var samePluginContent = (src, dest, relative = "") => {
|
|
|
44206
44302
|
};
|
|
44207
44303
|
var preservedCursorRules = (src, dest) => {
|
|
44208
44304
|
const preserved = new Map;
|
|
44209
|
-
const sourceRules =
|
|
44210
|
-
const installedRules =
|
|
44305
|
+
const sourceRules = path10.join(src, "rules");
|
|
44306
|
+
const installedRules = path10.join(dest, "rules");
|
|
44211
44307
|
try {
|
|
44212
44308
|
for (const entry of readdirSync3(installedRules, { withFileTypes: true })) {
|
|
44213
44309
|
if (!entry.isFile() || !entry.name.endsWith(".mdc"))
|
|
44214
44310
|
continue;
|
|
44215
|
-
if (existsSync8(
|
|
44311
|
+
if (existsSync8(path10.join(sourceRules, entry.name)))
|
|
44216
44312
|
continue;
|
|
44217
|
-
preserved.set(entry.name, readFileSync8(
|
|
44313
|
+
preserved.set(entry.name, readFileSync8(path10.join(installedRules, entry.name)));
|
|
44218
44314
|
}
|
|
44219
44315
|
} catch {}
|
|
44220
44316
|
return preserved;
|
|
44221
44317
|
};
|
|
44222
44318
|
function copyPluginDir(src, dest) {
|
|
44223
|
-
const marker =
|
|
44319
|
+
const marker = path10.join(dest, ".workflow-toolkit-root");
|
|
44224
44320
|
const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest);
|
|
44225
44321
|
if (synced)
|
|
44226
44322
|
return "Skipped";
|
|
44227
44323
|
const hadDir = existsSync8(dest);
|
|
44228
44324
|
const rules = preservedCursorRules(src, dest);
|
|
44229
|
-
const parent =
|
|
44325
|
+
const parent = path10.dirname(dest);
|
|
44230
44326
|
mkdirSync3(parent, { recursive: true });
|
|
44231
|
-
const swap = mkdtempSync(
|
|
44232
|
-
const stage =
|
|
44233
|
-
const backup =
|
|
44327
|
+
const swap = mkdtempSync(path10.join(parent, `.${path10.basename(dest)}.swap-`));
|
|
44328
|
+
const stage = path10.join(swap, "stage");
|
|
44329
|
+
const backup = path10.join(swap, "backup");
|
|
44234
44330
|
try {
|
|
44235
44331
|
cpSync2(src, stage, {
|
|
44236
44332
|
recursive: true,
|
|
44237
|
-
filter: (entry) => !
|
|
44333
|
+
filter: (entry) => !path10.relative(src, entry).split(path10.sep).includes("node_modules")
|
|
44238
44334
|
});
|
|
44239
44335
|
for (const [name, content] of rules) {
|
|
44240
|
-
mkdirSync3(
|
|
44241
|
-
writeFileSync5(
|
|
44336
|
+
mkdirSync3(path10.join(stage, "rules"), { recursive: true });
|
|
44337
|
+
writeFileSync5(path10.join(stage, "rules", name), content);
|
|
44242
44338
|
}
|
|
44243
|
-
writeFileSync5(
|
|
44339
|
+
writeFileSync5(path10.join(stage, ".workflow-toolkit-root"), src + `
|
|
44244
44340
|
`, "utf8");
|
|
44245
44341
|
if (!samePluginContent(src, stage))
|
|
44246
44342
|
throw new Error("staged adapter content is incomplete");
|
|
@@ -44260,26 +44356,26 @@ function copyPluginDir(src, dest) {
|
|
|
44260
44356
|
return hadDir ? "Configured" : "Installed";
|
|
44261
44357
|
}
|
|
44262
44358
|
var removeLegacyCursorDir = (res) => {
|
|
44263
|
-
const legacy =
|
|
44359
|
+
const legacy = path10.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
|
|
44264
44360
|
if (legacy === res.cursorPluginDir || !existsSync8(legacy))
|
|
44265
44361
|
return;
|
|
44266
|
-
const legacyRules =
|
|
44362
|
+
const legacyRules = path10.join(legacy, "rules");
|
|
44267
44363
|
if (existsSync8(legacyRules)) {
|
|
44268
44364
|
try {
|
|
44269
|
-
mkdirSync3(
|
|
44365
|
+
mkdirSync3(path10.join(res.cursorPluginDir, "rules"), { recursive: true });
|
|
44270
44366
|
for (const entry of readdirSync3(legacyRules, { withFileTypes: true })) {
|
|
44271
44367
|
if (!entry.isFile() || !entry.name.endsWith(".mdc"))
|
|
44272
44368
|
continue;
|
|
44273
|
-
const target =
|
|
44369
|
+
const target = path10.join(res.cursorPluginDir, "rules", entry.name);
|
|
44274
44370
|
if (!existsSync8(target))
|
|
44275
|
-
copyFileSync2(
|
|
44371
|
+
copyFileSync2(path10.join(legacyRules, entry.name), target);
|
|
44276
44372
|
}
|
|
44277
44373
|
} catch {}
|
|
44278
44374
|
}
|
|
44279
44375
|
rmSync(legacy, { recursive: true, force: true });
|
|
44280
44376
|
};
|
|
44281
44377
|
function applyCursorSettings(root, res) {
|
|
44282
|
-
mkdirSync3(
|
|
44378
|
+
mkdirSync3(path10.dirname(res.cursorSettings), { recursive: true });
|
|
44283
44379
|
const settingsExisting = readExisting(res.cursorSettings);
|
|
44284
44380
|
if (settingsExisting.kind === "malformed") {
|
|
44285
44381
|
return {
|
|
@@ -44307,7 +44403,7 @@ function applyCursorSettings(root, res) {
|
|
|
44307
44403
|
};
|
|
44308
44404
|
}
|
|
44309
44405
|
function applyCursorMcp(root, res) {
|
|
44310
|
-
mkdirSync3(
|
|
44406
|
+
mkdirSync3(path10.dirname(res.cursorMcp), { recursive: true });
|
|
44311
44407
|
const mcpExisting = readExisting(res.cursorMcp);
|
|
44312
44408
|
if (mcpExisting.kind === "malformed") {
|
|
44313
44409
|
return {
|
|
@@ -44496,12 +44592,12 @@ var setupCompletionGuidance = () => [
|
|
|
44496
44592
|
];
|
|
44497
44593
|
function applyWorkspaceBranchPolicy(opts) {
|
|
44498
44594
|
const { workspace_root, env: env3 = process.env } = opts;
|
|
44499
|
-
const dir =
|
|
44595
|
+
const dir = path10.join(env3.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
|
|
44500
44596
|
const { status, path: wsPath, entries } = readWorkspacesResult(dir);
|
|
44501
44597
|
if (status === "malformed")
|
|
44502
44598
|
return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
|
|
44503
44599
|
const detection = detectBranchPolicy(workspace_root);
|
|
44504
|
-
const name = String(env3.WORKFLOW_BP_NAME ??
|
|
44600
|
+
const name = String(env3.WORKFLOW_BP_NAME ?? path10.basename(workspace_root));
|
|
44505
44601
|
const integration = env3.WORKFLOW_BP_INTEGRATION ?? detection.integration;
|
|
44506
44602
|
const policy = {
|
|
44507
44603
|
preset: detection.preset,
|
|
@@ -44526,7 +44622,7 @@ function applyWorkspaceBranchPolicy(opts) {
|
|
|
44526
44622
|
};
|
|
44527
44623
|
}
|
|
44528
44624
|
const next = existing ? entries.map((w, i) => i === idx ? { ...w, branchPolicy: policy } : w) : [...entries, { name, glob, branchPolicy: policy }];
|
|
44529
|
-
mkdirSync3(
|
|
44625
|
+
mkdirSync3(path10.dirname(wsPath), { recursive: true });
|
|
44530
44626
|
writeFileSync5(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
|
|
44531
44627
|
`, "utf8");
|
|
44532
44628
|
return {
|
|
@@ -44584,7 +44680,7 @@ function applyWizardBranchPolicy(branchPolicy, workspace_root, env3 = process.en
|
|
|
44584
44680
|
|
|
44585
44681
|
// packages/workit-cli/src/wizard-state.ts
|
|
44586
44682
|
import { existsSync as existsSync9 } from "node:fs";
|
|
44587
|
-
import
|
|
44683
|
+
import path11 from "node:path";
|
|
44588
44684
|
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
44589
44685
|
var NEXT = {
|
|
44590
44686
|
platforms: "locale",
|
|
@@ -44629,7 +44725,7 @@ var PREV = {
|
|
|
44629
44725
|
exit: null
|
|
44630
44726
|
};
|
|
44631
44727
|
var skipsCustomBranch = (screen, preset) => (screen === "branchAllowed" || screen === "branchProtected") && preset !== "custom";
|
|
44632
|
-
var skipsBranchPolicy = (screen) => (screen === "branchPolicy" || screen === "branchPolicyDevelop") && !existsSync9(
|
|
44728
|
+
var skipsBranchPolicy = (screen) => (screen === "branchPolicy" || screen === "branchPolicyDevelop") && !existsSync9(path11.join(process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), ".git"));
|
|
44633
44729
|
function nextScreen(screen, preset) {
|
|
44634
44730
|
let next = NEXT[screen];
|
|
44635
44731
|
while (next && (skipsCustomBranch(next, preset) || skipsBranchPolicy(next)))
|
|
@@ -45935,7 +46031,7 @@ function Screen({ draft, dispatch }) {
|
|
|
45935
46031
|
// packages/workit-core/src/core/logger.ts
|
|
45936
46032
|
import { appendFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
|
|
45937
46033
|
import os6 from "node:os";
|
|
45938
|
-
import
|
|
46034
|
+
import path12 from "node:path";
|
|
45939
46035
|
var REDACTED = "[REDACTED]";
|
|
45940
46036
|
var DEFAULT_MAX_RATE = 20;
|
|
45941
46037
|
var DEFAULT_RATE_WINDOW_MS = 1000;
|
|
@@ -45991,14 +46087,14 @@ var resolveStateDir = () => {
|
|
|
45991
46087
|
return override;
|
|
45992
46088
|
const home = os6.homedir();
|
|
45993
46089
|
if (process.env.XDG_STATE_HOME)
|
|
45994
|
-
return
|
|
46090
|
+
return path12.join(process.env.XDG_STATE_HOME, "workit");
|
|
45995
46091
|
switch (os6.platform()) {
|
|
45996
46092
|
case "darwin":
|
|
45997
|
-
return
|
|
46093
|
+
return path12.join(home, "Library", "Application Support", "workit");
|
|
45998
46094
|
case "win32":
|
|
45999
|
-
return
|
|
46095
|
+
return path12.join(process.env.LOCALAPPDATA ?? path12.join(home, "AppData", "Local"), "workit");
|
|
46000
46096
|
default:
|
|
46001
|
-
return
|
|
46097
|
+
return path12.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
46002
46098
|
}
|
|
46003
46099
|
};
|
|
46004
46100
|
var homePattern = new RegExp(`${escapeRegExp2(os6.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
@@ -46071,14 +46167,14 @@ var pruneOldFiles = (dir) => {
|
|
|
46071
46167
|
const files = readdirSync4(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
|
|
46072
46168
|
for (const file of files.slice(RETAINED_DAYS)) {
|
|
46073
46169
|
try {
|
|
46074
|
-
unlinkSync2(
|
|
46170
|
+
unlinkSync2(path12.join(dir, file));
|
|
46075
46171
|
} catch {}
|
|
46076
46172
|
}
|
|
46077
46173
|
} catch {}
|
|
46078
46174
|
};
|
|
46079
46175
|
var createLogger = (options = {}) => {
|
|
46080
46176
|
const stateDir = options.stateDir ?? resolveStateDir();
|
|
46081
|
-
const logDir =
|
|
46177
|
+
const logDir = path12.join(stateDir, "logs");
|
|
46082
46178
|
const now2 = options.now ?? (() => new Date);
|
|
46083
46179
|
const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
|
|
46084
46180
|
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
@@ -46112,7 +46208,7 @@ var createLogger = (options = {}) => {
|
|
|
46112
46208
|
const line = `${JSON.stringify(event)}
|
|
46113
46209
|
`;
|
|
46114
46210
|
mkdirSync4(logDir, { recursive: true, mode: 448 });
|
|
46115
|
-
appendFileSync(
|
|
46211
|
+
appendFileSync(path12.join(logDir, dailyFileName(date)), line, { mode: 384 });
|
|
46116
46212
|
pruneOldFiles(logDir);
|
|
46117
46213
|
} catch {
|
|
46118
46214
|
try {
|
|
@@ -46123,7 +46219,7 @@ var createLogger = (options = {}) => {
|
|
|
46123
46219
|
context: { redaction_failed: true }
|
|
46124
46220
|
};
|
|
46125
46221
|
mkdirSync4(logDir, { recursive: true, mode: 448 });
|
|
46126
|
-
appendFileSync(
|
|
46222
|
+
appendFileSync(path12.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
|
|
46127
46223
|
`, {
|
|
46128
46224
|
mode: 384
|
|
46129
46225
|
});
|
|
@@ -46158,21 +46254,2152 @@ var createLogger = (options = {}) => {
|
|
|
46158
46254
|
};
|
|
46159
46255
|
};
|
|
46160
46256
|
|
|
46161
|
-
// packages/workit-cli/src/
|
|
46162
|
-
|
|
46163
|
-
|
|
46164
|
-
|
|
46165
|
-
|
|
46166
|
-
|
|
46167
|
-
|
|
46168
|
-
|
|
46257
|
+
// packages/workit-cli/src/flow.ts
|
|
46258
|
+
import { createInterface } from "node:readline/promises";
|
|
46259
|
+
import path21 from "node:path";
|
|
46260
|
+
|
|
46261
|
+
// packages/workit-core/src/core/flow-state.ts
|
|
46262
|
+
import {
|
|
46263
|
+
closeSync,
|
|
46264
|
+
existsSync as existsSync14,
|
|
46265
|
+
fstatSync,
|
|
46266
|
+
fsyncSync,
|
|
46267
|
+
mkdirSync as mkdirSync8,
|
|
46268
|
+
openSync,
|
|
46269
|
+
readFileSync as readFileSync13,
|
|
46270
|
+
renameSync as renameSync2,
|
|
46271
|
+
rmSync as rmSync2,
|
|
46272
|
+
statSync as statSync7,
|
|
46273
|
+
unlinkSync as unlinkSync3,
|
|
46274
|
+
writeFileSync as writeFileSync8
|
|
46275
|
+
} from "node:fs";
|
|
46276
|
+
import { createHash } from "node:crypto";
|
|
46277
|
+
import path18 from "node:path";
|
|
46278
|
+
|
|
46279
|
+
// packages/workit-core/src/core/docs-validate.ts
|
|
46280
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
|
|
46281
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
46282
|
+
import path14 from "node:path";
|
|
46283
|
+
|
|
46284
|
+
// packages/workit-core/src/core/docs-layout.ts
|
|
46285
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
46286
|
+
import path13 from "node:path";
|
|
46287
|
+
var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
46288
|
+
var LEGACY_SLUG = "superpowers";
|
|
46289
|
+
var posix = (p) => p.split(path13.sep).join("/");
|
|
46290
|
+
var canonicalize = (base2, candidate) => {
|
|
46291
|
+
const abs = path13.resolve(base2, candidate);
|
|
46292
|
+
let ancestor = abs;
|
|
46293
|
+
while (!existsSync10(ancestor))
|
|
46294
|
+
ancestor = path13.dirname(ancestor);
|
|
46295
|
+
let real;
|
|
46296
|
+
try {
|
|
46297
|
+
real = realpathSync2(ancestor);
|
|
46298
|
+
} catch (error) {
|
|
46299
|
+
if (error.code === "EACCES" && !lstatSync(ancestor).isSymbolicLink()) {
|
|
46300
|
+
real = path13.join(realpathSync2(path13.dirname(ancestor)), path13.basename(ancestor));
|
|
46301
|
+
} else {
|
|
46302
|
+
throw error;
|
|
46303
|
+
}
|
|
46304
|
+
}
|
|
46305
|
+
if (real !== base2 && !real.startsWith(base2 + path13.sep)) {
|
|
46306
|
+
throw new Error(`path must stay inside repository root: ${candidate}`);
|
|
46307
|
+
}
|
|
46308
|
+
return path13.join(real, path13.relative(ancestor, abs));
|
|
46309
|
+
};
|
|
46310
|
+
var buildLayout = (workspace, slug) => {
|
|
46311
|
+
const docs = canonicalize(workspace, "docs");
|
|
46312
|
+
const dir = canonicalize(workspace, path13.join(docs, slug));
|
|
46313
|
+
return {
|
|
46314
|
+
workspace,
|
|
46315
|
+
slug,
|
|
46316
|
+
docs,
|
|
46317
|
+
dir,
|
|
46318
|
+
spec: path13.join(dir, "spec.md"),
|
|
46319
|
+
plan: path13.join(dir, "plan.md"),
|
|
46320
|
+
sdd: path13.join(dir, "sdd")
|
|
46321
|
+
};
|
|
46322
|
+
};
|
|
46323
|
+
var resolveCanonicalLayout = (input) => {
|
|
46324
|
+
const { workspace_root, slug, spec_path, plan_path } = input;
|
|
46325
|
+
if (!workspace_root)
|
|
46326
|
+
return { ok: false, error: "workspace_root required" };
|
|
46327
|
+
let workspace;
|
|
46328
|
+
try {
|
|
46329
|
+
workspace = realpathSync2(path13.resolve(workspace_root));
|
|
46330
|
+
} catch {
|
|
46331
|
+
return { ok: false, error: `workspace root not found: ${workspace_root}` };
|
|
46332
|
+
}
|
|
46333
|
+
let derived = null;
|
|
46334
|
+
for (const [candidate, kind] of [
|
|
46335
|
+
[spec_path, "spec"],
|
|
46336
|
+
[plan_path, "plan"]
|
|
46337
|
+
]) {
|
|
46338
|
+
if (!candidate)
|
|
46339
|
+
continue;
|
|
46340
|
+
if (path13.isAbsolute(candidate)) {
|
|
46341
|
+
return { ok: false, error: `absolute path not allowed: ${candidate}` };
|
|
46342
|
+
}
|
|
46343
|
+
const spelling = posix(candidate);
|
|
46344
|
+
const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
|
|
46345
|
+
if (!match) {
|
|
46346
|
+
return {
|
|
46347
|
+
ok: false,
|
|
46348
|
+
error: `path must be docs/<slug>/(spec|plan).md inside workspace_root: ${candidate}`
|
|
46349
|
+
};
|
|
46350
|
+
}
|
|
46351
|
+
const pathSlug = match[1];
|
|
46352
|
+
if (pathSlug === LEGACY_SLUG) {
|
|
46353
|
+
return { ok: false, error: `legacy path not allowed: ${candidate}` };
|
|
46354
|
+
}
|
|
46355
|
+
if (!SLUG_RE.test(pathSlug)) {
|
|
46356
|
+
return { ok: false, error: `invalid slug derived from path: ${JSON.stringify(pathSlug)}` };
|
|
46357
|
+
}
|
|
46358
|
+
if (match[2] !== kind) {
|
|
46359
|
+
return {
|
|
46360
|
+
ok: false,
|
|
46361
|
+
error: `wrong basename for ${kind}: expected ${kind}.md, got ${path13.basename(candidate)}`
|
|
46362
|
+
};
|
|
46363
|
+
}
|
|
46364
|
+
if (derived && derived !== pathSlug) {
|
|
46365
|
+
return {
|
|
46366
|
+
ok: false,
|
|
46367
|
+
error: "cross-slug pair: spec_path and plan_path must share the same docs/<slug>/"
|
|
46368
|
+
};
|
|
46369
|
+
}
|
|
46370
|
+
derived = pathSlug;
|
|
46371
|
+
let abs;
|
|
46372
|
+
try {
|
|
46373
|
+
abs = canonicalize(workspace, candidate);
|
|
46374
|
+
} catch (error) {
|
|
46375
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
46376
|
+
}
|
|
46377
|
+
if (posix(path13.relative(workspace, abs)) !== spelling) {
|
|
46378
|
+
return {
|
|
46379
|
+
ok: false,
|
|
46380
|
+
error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`
|
|
46381
|
+
};
|
|
46382
|
+
}
|
|
46383
|
+
}
|
|
46384
|
+
let resolvedSlug = slug;
|
|
46385
|
+
if (resolvedSlug !== undefined) {
|
|
46386
|
+
if (!SLUG_RE.test(resolvedSlug)) {
|
|
46387
|
+
return { ok: false, error: `invalid slug: ${JSON.stringify(resolvedSlug)}` };
|
|
46388
|
+
}
|
|
46389
|
+
if (resolvedSlug === LEGACY_SLUG) {
|
|
46390
|
+
return { ok: false, error: `reserved slug: ${LEGACY_SLUG}` };
|
|
46391
|
+
}
|
|
46392
|
+
if (derived && derived !== resolvedSlug) {
|
|
46393
|
+
return {
|
|
46394
|
+
ok: false,
|
|
46395
|
+
error: `slug ${JSON.stringify(resolvedSlug)} does not match docs path ${JSON.stringify(derived)}`
|
|
46396
|
+
};
|
|
46397
|
+
}
|
|
46398
|
+
} else if (derived) {
|
|
46399
|
+
resolvedSlug = derived;
|
|
46400
|
+
} else {
|
|
46401
|
+
return { ok: false, error: "slug or spec_path/plan_path required" };
|
|
46402
|
+
}
|
|
46403
|
+
try {
|
|
46404
|
+
return { ok: true, layout: buildLayout(workspace, resolvedSlug) };
|
|
46405
|
+
} catch (error) {
|
|
46406
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
46407
|
+
}
|
|
46408
|
+
};
|
|
46409
|
+
var resolveDocsPath = (input) => {
|
|
46410
|
+
if (path13.isAbsolute(input.path)) {
|
|
46411
|
+
return { ok: false, error: `absolute path not allowed: ${input.path}` };
|
|
46412
|
+
}
|
|
46413
|
+
let workspace;
|
|
46414
|
+
try {
|
|
46415
|
+
workspace = realpathSync2(path13.resolve(input.workspace_root));
|
|
46416
|
+
} catch {
|
|
46417
|
+
return { ok: false, error: `workspace root not found: ${input.workspace_root}` };
|
|
46418
|
+
}
|
|
46419
|
+
try {
|
|
46420
|
+
const abs = canonicalize(workspace, input.path);
|
|
46421
|
+
const relative = posix(path13.relative(workspace, abs));
|
|
46422
|
+
if (!relative.startsWith("docs/") || relative === `docs/${LEGACY_SLUG}` || relative.startsWith(`docs/${LEGACY_SLUG}/`)) {
|
|
46423
|
+
return {
|
|
46424
|
+
ok: false,
|
|
46425
|
+
error: `path must live under docs/ and not under docs/${LEGACY_SLUG}/: ${input.path}`
|
|
46426
|
+
};
|
|
46427
|
+
}
|
|
46428
|
+
return { ok: true, path: abs, relative, base: workspace };
|
|
46429
|
+
} catch (error) {
|
|
46430
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
46431
|
+
}
|
|
46432
|
+
};
|
|
46433
|
+
|
|
46434
|
+
// packages/workit-core/src/core/docs-validate.ts
|
|
46435
|
+
var BRANCH_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/im;
|
|
46436
|
+
var SPEC_LINK_RE = /^\s*\*+Spec:\*+\s*(?:`([^`]+)`|(\S+))\s*$/im;
|
|
46437
|
+
var TASK_RE = /^###\s+Task\s+(\d+):\s*(.*)$/i;
|
|
46438
|
+
var err = (code, message, path15) => {
|
|
46439
|
+
const item = { code, message };
|
|
46440
|
+
if (path15)
|
|
46441
|
+
item.path = path15;
|
|
46442
|
+
return item;
|
|
46443
|
+
};
|
|
46444
|
+
var failValidate = (errors) => ({
|
|
46445
|
+
ok: false,
|
|
46446
|
+
errors,
|
|
46447
|
+
error: errors.map((e) => e.message).filter(Boolean).join("; ") || "docs validation failed"
|
|
46448
|
+
});
|
|
46449
|
+
var readBranch = (text, label) => {
|
|
46450
|
+
const match = text.match(BRANCH_RE);
|
|
46451
|
+
if (!match)
|
|
46452
|
+
return [null, err("missing_branch", `**Branch:** feature/* or bugfix/* required in ${label}`)];
|
|
46453
|
+
return [match[1].trim().replace(/`/g, ""), null];
|
|
46454
|
+
};
|
|
46455
|
+
var scanTaskHeadings = (planText) => {
|
|
46456
|
+
const ids = [];
|
|
46457
|
+
const titles = [];
|
|
46458
|
+
let inFence = false;
|
|
46459
|
+
for (const line of planText.split(`
|
|
46460
|
+
`)) {
|
|
46461
|
+
if (line.startsWith("```")) {
|
|
46462
|
+
inFence = !inFence;
|
|
46463
|
+
continue;
|
|
46464
|
+
}
|
|
46465
|
+
if (inFence)
|
|
46466
|
+
continue;
|
|
46467
|
+
const match = line.match(TASK_RE);
|
|
46468
|
+
if (match) {
|
|
46469
|
+
ids.push(Number(match[1]));
|
|
46470
|
+
titles.push(match[2].trim());
|
|
46471
|
+
}
|
|
46472
|
+
}
|
|
46473
|
+
if (ids.length === 0)
|
|
46474
|
+
return [ids, titles, err("task_order", "no ### Task N sections found outside fences")];
|
|
46475
|
+
const expected = ids.map((_, i) => i + 1);
|
|
46476
|
+
const sorted = [...ids].sort((a, b) => a - b);
|
|
46477
|
+
if (JSON.stringify(sorted) !== JSON.stringify(expected) || new Set(ids).size !== ids.length) {
|
|
46478
|
+
return [
|
|
46479
|
+
ids,
|
|
46480
|
+
titles,
|
|
46481
|
+
err("task_order", `task headings must be contiguous from 1..${ids.length}; found ${ids}`)
|
|
46482
|
+
];
|
|
46483
|
+
}
|
|
46484
|
+
return [ids, titles, null];
|
|
46485
|
+
};
|
|
46486
|
+
var parseTasksFromPlan = (planText) => {
|
|
46487
|
+
const tasks = [];
|
|
46488
|
+
let current = null;
|
|
46489
|
+
let inFence = false;
|
|
46490
|
+
for (const line of planText.split(`
|
|
46491
|
+
`)) {
|
|
46492
|
+
if (line.startsWith("```")) {
|
|
46493
|
+
inFence = !inFence;
|
|
46494
|
+
continue;
|
|
46495
|
+
}
|
|
46496
|
+
if (inFence)
|
|
46497
|
+
continue;
|
|
46498
|
+
const match = line.match(TASK_RE);
|
|
46499
|
+
if (match) {
|
|
46500
|
+
if (current)
|
|
46501
|
+
tasks.push({ id: current.id, title: current.title, section_text: current.body.join(`
|
|
46502
|
+
`) });
|
|
46503
|
+
current = { id: Number(match[1]), title: match[2].trim(), body: [] };
|
|
46504
|
+
} else if (current) {
|
|
46505
|
+
current.body.push(line);
|
|
46506
|
+
}
|
|
46507
|
+
}
|
|
46508
|
+
if (current)
|
|
46509
|
+
tasks.push({ id: current.id, title: current.title, section_text: current.body.join(`
|
|
46510
|
+
`) });
|
|
46511
|
+
return tasks;
|
|
46512
|
+
};
|
|
46513
|
+
var docsValidate = ({
|
|
46514
|
+
spec_path,
|
|
46515
|
+
plan_path,
|
|
46516
|
+
workspace_root
|
|
46517
|
+
}) => {
|
|
46518
|
+
const resolved = resolveCanonicalLayout({ workspace_root, spec_path, plan_path });
|
|
46519
|
+
if (!resolved.ok)
|
|
46520
|
+
return failValidate([err("path_contract", resolved.error)]);
|
|
46521
|
+
const cwd2 = resolved.layout.workspace;
|
|
46522
|
+
const specAbs = resolved.layout.spec;
|
|
46523
|
+
const planAbs = resolved.layout.plan;
|
|
46524
|
+
const slug = resolved.layout.slug;
|
|
46525
|
+
const errors = [];
|
|
46526
|
+
const read = (p) => {
|
|
46527
|
+
try {
|
|
46528
|
+
return readFileSync9(p, "utf8");
|
|
46529
|
+
} catch {
|
|
46530
|
+
return null;
|
|
46531
|
+
}
|
|
46532
|
+
};
|
|
46533
|
+
const specText = read(specAbs);
|
|
46534
|
+
const planText = read(planAbs);
|
|
46535
|
+
if (specText === null)
|
|
46536
|
+
errors.push(err("missing_file", `spec not found: ${spec_path}`, spec_path));
|
|
46537
|
+
if (planText === null)
|
|
46538
|
+
errors.push(err("missing_file", `plan not found: ${plan_path}`, plan_path));
|
|
46539
|
+
if (errors.length)
|
|
46540
|
+
return failValidate(errors);
|
|
46541
|
+
const [specBranch, specErr] = readBranch(specText, "spec");
|
|
46542
|
+
if (specErr)
|
|
46543
|
+
errors.push(specErr);
|
|
46544
|
+
const [planBranch, planErr] = readBranch(planText, "plan");
|
|
46545
|
+
if (planErr)
|
|
46546
|
+
errors.push(planErr);
|
|
46547
|
+
const linkMatch = planText.match(SPEC_LINK_RE);
|
|
46548
|
+
if (!linkMatch) {
|
|
46549
|
+
errors.push(err("missing_spec_link", "**Spec:** link required in plan", plan_path));
|
|
46550
|
+
} else {
|
|
46551
|
+
const linked = (linkMatch[1] ?? linkMatch[2] ?? "").trim();
|
|
46552
|
+
const linkedResolved = resolveDocsPath({ workspace_root: cwd2, path: linked });
|
|
46553
|
+
if (!linkedResolved.ok) {
|
|
46554
|
+
errors.push(err("spec_link_escape", `plan **Spec:** ${linked} is not a contained docs path`, plan_path));
|
|
46555
|
+
} else if (linkedResolved.path !== specAbs) {
|
|
46556
|
+
errors.push(err("spec_mismatch", `plan **Spec:** ${linked} does not match spec_path ${spec_path}`, plan_path));
|
|
46557
|
+
}
|
|
46558
|
+
}
|
|
46559
|
+
if (specBranch && planBranch && specBranch !== planBranch) {
|
|
46560
|
+
errors.push(err("branch_mismatch", `spec branch ${JSON.stringify(specBranch)} != plan branch ${JSON.stringify(planBranch)}`, plan_path));
|
|
46561
|
+
}
|
|
46562
|
+
const [, , taskErr] = scanTaskHeadings(planText);
|
|
46563
|
+
if (taskErr)
|
|
46564
|
+
errors.push(taskErr);
|
|
46565
|
+
if (errors.length)
|
|
46566
|
+
return failValidate(errors);
|
|
46567
|
+
const tasks = parseTasksFromPlan(planText);
|
|
46568
|
+
const [headingIds, headingTitles, headingErr] = scanTaskHeadings(planText);
|
|
46569
|
+
if (headingErr)
|
|
46570
|
+
return failValidate([headingErr]);
|
|
46571
|
+
if (tasks.length !== headingIds.length) {
|
|
46572
|
+
return failValidate([
|
|
46573
|
+
err("task_order", `parse count ${tasks.length} != heading count ${headingIds.length}`, plan_path)
|
|
46574
|
+
]);
|
|
46575
|
+
}
|
|
46576
|
+
for (let i = 0;i < tasks.length; i++) {
|
|
46577
|
+
if (String(tasks[i].id) !== String(headingIds[i]) || tasks[i].title.trim() !== headingTitles[i]) {
|
|
46578
|
+
return failValidate([err("task_order", `task mismatch at position ${i + 1}`, plan_path)]);
|
|
46579
|
+
}
|
|
46580
|
+
}
|
|
46581
|
+
const relSpec = path14.isAbsolute(spec_path) ? path14.relative(cwd2, specAbs) : spec_path;
|
|
46582
|
+
const relPlan = path14.isAbsolute(plan_path) ? path14.relative(cwd2, planAbs) : plan_path;
|
|
46583
|
+
const quality = qualitySpec(specText);
|
|
46584
|
+
if (!sddIgnored(cwd2, slug)) {
|
|
46585
|
+
quality.push({
|
|
46586
|
+
code: "sdd_not_ignored",
|
|
46587
|
+
message: `docs/${slug}/sdd/ exists but is not gitignored — add 'docs/*/sdd/' to .gitignore (or run wk-init)`,
|
|
46588
|
+
severity: "hard"
|
|
46589
|
+
});
|
|
46169
46590
|
}
|
|
46591
|
+
const hygiene = hygieneFiles(cwd2);
|
|
46592
|
+
const hyState = hygiene.state;
|
|
46593
|
+
if (hyState["CHANGELOG.md"] === "missing")
|
|
46594
|
+
quality.push({
|
|
46595
|
+
code: "changelog_missing",
|
|
46596
|
+
message: "CHANGELOG.md missing — create it with Keep a Changelog format (run wk-init hygiene)",
|
|
46597
|
+
severity: "warning"
|
|
46598
|
+
});
|
|
46599
|
+
if (hyState["CHANGELOG.md"] === "invalid")
|
|
46600
|
+
quality.push({
|
|
46601
|
+
code: "changelog_invalid_format",
|
|
46602
|
+
message: "CHANGELOG.md lacks ## [Unreleased] — Keep a Changelog format required",
|
|
46603
|
+
severity: "warning"
|
|
46604
|
+
});
|
|
46605
|
+
if (hyState["README.md"] === "missing")
|
|
46606
|
+
quality.push({ code: "readme_missing", message: "README.md missing", severity: "warning" });
|
|
46607
|
+
if (hyState[".editorconfig"] === "missing")
|
|
46608
|
+
quality.push({
|
|
46609
|
+
code: "editorconfig_missing",
|
|
46610
|
+
message: ".editorconfig missing",
|
|
46611
|
+
severity: "warning"
|
|
46612
|
+
});
|
|
46613
|
+
if (hyState[".gitattributes"] === "missing")
|
|
46614
|
+
quality.push({
|
|
46615
|
+
code: "gitattributes_missing",
|
|
46616
|
+
message: ".gitattributes missing",
|
|
46617
|
+
severity: "warning"
|
|
46618
|
+
});
|
|
46619
|
+
if (hygiene.openSource && hyState.LICENSE === "missing")
|
|
46620
|
+
quality.push({
|
|
46621
|
+
code: "license_missing",
|
|
46622
|
+
message: "LICENSE missing (open-source repo)",
|
|
46623
|
+
severity: "warning"
|
|
46624
|
+
});
|
|
46625
|
+
if (hygiene.openSource && hyState["CONTRIBUTING.md"] === "missing")
|
|
46626
|
+
quality.push({
|
|
46627
|
+
code: "contributing_missing",
|
|
46628
|
+
message: "CONTRIBUTING.md missing (open-source repo)",
|
|
46629
|
+
severity: "warning"
|
|
46630
|
+
});
|
|
46631
|
+
return {
|
|
46632
|
+
ok: true,
|
|
46633
|
+
spec: relSpec,
|
|
46634
|
+
plan: relPlan,
|
|
46635
|
+
branch: specBranch,
|
|
46636
|
+
task_count: tasks.length,
|
|
46637
|
+
quality
|
|
46638
|
+
};
|
|
46639
|
+
};
|
|
46640
|
+
var REQUIRED_SECTIONS = [
|
|
46641
|
+
"## Context",
|
|
46642
|
+
"## Goals",
|
|
46643
|
+
"## Non-goals",
|
|
46644
|
+
"## Architecture",
|
|
46645
|
+
"## Acceptance criteria"
|
|
46646
|
+
];
|
|
46647
|
+
var UI_KEYWORDS = [
|
|
46648
|
+
/\bui\b/,
|
|
46649
|
+
/\binterface\b/,
|
|
46650
|
+
/\bscreen\b/,
|
|
46651
|
+
/\bmodal\b/,
|
|
46652
|
+
/\bform\b/,
|
|
46653
|
+
/\bcomponent\b/
|
|
46654
|
+
];
|
|
46655
|
+
var FLOW_KEYWORDS = [/\bflow\b/, /\bpipeline\b/, /\bsequence\b/, /\bdiagram\b/];
|
|
46656
|
+
var GLOSSARY_KEYWORDS = [/\bglossary\b/, /\bcontracts?\b/, /\bscope\b/];
|
|
46657
|
+
var finding = (code, message, severity) => ({
|
|
46658
|
+
code,
|
|
46659
|
+
message,
|
|
46660
|
+
severity
|
|
46170
46661
|
});
|
|
46662
|
+
var stripFences = (text) => {
|
|
46663
|
+
const lines = text.split(`
|
|
46664
|
+
`);
|
|
46665
|
+
const out = [];
|
|
46666
|
+
let inFence = false;
|
|
46667
|
+
for (const line of lines) {
|
|
46668
|
+
if (line.startsWith("```")) {
|
|
46669
|
+
if (!inFence)
|
|
46670
|
+
out.push(line);
|
|
46671
|
+
inFence = !inFence;
|
|
46672
|
+
continue;
|
|
46673
|
+
}
|
|
46674
|
+
if (!inFence)
|
|
46675
|
+
out.push(line);
|
|
46676
|
+
}
|
|
46677
|
+
return out.join(`
|
|
46678
|
+
`);
|
|
46679
|
+
};
|
|
46680
|
+
var qualitySpec = (text) => {
|
|
46681
|
+
const findings = [];
|
|
46682
|
+
const body = stripFences(text);
|
|
46683
|
+
const lower = body.toLowerCase();
|
|
46684
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
46685
|
+
if (!body.includes(section)) {
|
|
46686
|
+
findings.push(finding("missing_section", `required section ${section} missing`, "hard"));
|
|
46687
|
+
}
|
|
46688
|
+
}
|
|
46689
|
+
const hasCa = /^\s*(?:- CA-\d+|CA-\d+[.:])/m.test(body);
|
|
46690
|
+
if (!hasCa) {
|
|
46691
|
+
findings.push(finding("missing_acceptance_criteria", "no enumerable CA-XX acceptance criteria found", "hard"));
|
|
46692
|
+
}
|
|
46693
|
+
const hasAsciiFence = /```(?:text|ascii)/.test(body);
|
|
46694
|
+
const mentionsUi = UI_KEYWORDS.some((k) => k.test(lower));
|
|
46695
|
+
if (mentionsUi && !hasAsciiFence) {
|
|
46696
|
+
findings.push(finding("missing_ascii_for_ui", "spec mentions UI but has no ASCII wireframe fence", "warning"));
|
|
46697
|
+
}
|
|
46698
|
+
const hasMermaid = /```mermaid/.test(body);
|
|
46699
|
+
const explicitlyNoFlow = /\bno (?:flow|pipeline|sequence|diagram)\b/.test(lower);
|
|
46700
|
+
const mentionsFlow = FLOW_KEYWORDS.some((k) => k.test(lower));
|
|
46701
|
+
if (mentionsFlow && !hasMermaid && !explicitlyNoFlow) {
|
|
46702
|
+
findings.push(finding("missing_mermaid_for_flow", "spec describes a flow/pipeline/sequence but has no mermaid fence", "warning"));
|
|
46703
|
+
}
|
|
46704
|
+
const hasTable = /^\s*\|.+\|.+\|/m.test(body);
|
|
46705
|
+
const mentionsGlossary = GLOSSARY_KEYWORDS.some((k) => k.test(lower));
|
|
46706
|
+
const onlyOutOfScope = /\bout of scope\b/.test(lower) && !/\bglossary\b/.test(lower) && !/\bcontracts?\b/.test(lower);
|
|
46707
|
+
if (mentionsGlossary && !hasTable && !onlyOutOfScope) {
|
|
46708
|
+
findings.push(finding("missing_table", "spec has glossary/contract/scope content but no markdown table", "warning"));
|
|
46709
|
+
}
|
|
46710
|
+
return findings;
|
|
46711
|
+
};
|
|
46712
|
+
var sddIgnored = (cwd2, slug) => {
|
|
46713
|
+
const sddDir = path14.join(cwd2, "docs", slug, "sdd");
|
|
46714
|
+
if (!existsSync11(sddDir))
|
|
46715
|
+
return true;
|
|
46716
|
+
try {
|
|
46717
|
+
execFileSync2("git", ["-C", cwd2, "rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
46718
|
+
} catch {
|
|
46719
|
+
return true;
|
|
46720
|
+
}
|
|
46721
|
+
try {
|
|
46722
|
+
execFileSync2("git", ["-C", cwd2, "check-ignore", path14.posix.join("docs", slug, "sdd", "progress.md")], { stdio: "pipe" });
|
|
46723
|
+
return true;
|
|
46724
|
+
} catch {
|
|
46725
|
+
return false;
|
|
46726
|
+
}
|
|
46727
|
+
};
|
|
46728
|
+
|
|
46729
|
+
// packages/workit-core/src/core/sdd.ts
|
|
46730
|
+
import {
|
|
46731
|
+
appendFileSync as appendFileSync2,
|
|
46732
|
+
existsSync as existsSync12,
|
|
46733
|
+
mkdirSync as mkdirSync6,
|
|
46734
|
+
readFileSync as readFileSync10,
|
|
46735
|
+
statSync as statSync5,
|
|
46736
|
+
writeFileSync as writeFileSync6
|
|
46737
|
+
} from "node:fs";
|
|
46738
|
+
import path15 from "node:path";
|
|
46739
|
+
function ledgerCompletion(root, slug) {
|
|
46740
|
+
let started = false;
|
|
46741
|
+
const completed = [];
|
|
46742
|
+
const absProgress = path15.join(root, "docs", slug, "sdd", "progress.md");
|
|
46743
|
+
if (existsSync12(absProgress)) {
|
|
46744
|
+
try {
|
|
46745
|
+
for (const line of readFileSync10(absProgress, "utf8").split(`
|
|
46746
|
+
`)) {
|
|
46747
|
+
const match = /^Task\s+(\d+):/i.exec(line);
|
|
46748
|
+
if (match) {
|
|
46749
|
+
started = true;
|
|
46750
|
+
if (/^Task\s+\d+:\s*complete\b/i.test(line))
|
|
46751
|
+
completed.push(Number(match[1]));
|
|
46752
|
+
}
|
|
46753
|
+
}
|
|
46754
|
+
} catch {}
|
|
46755
|
+
}
|
|
46756
|
+
const required = [];
|
|
46757
|
+
try {
|
|
46758
|
+
const absPlan = path15.join(root, "docs", slug, "plan.md");
|
|
46759
|
+
if (existsSync12(absPlan)) {
|
|
46760
|
+
for (const task of parseTasksFromPlan(readFileSync10(absPlan, "utf8")))
|
|
46761
|
+
required.push(task.id);
|
|
46762
|
+
}
|
|
46763
|
+
} catch {}
|
|
46764
|
+
const completedSet = new Set(completed);
|
|
46765
|
+
const missing = required.filter((id) => !completedSet.has(id));
|
|
46766
|
+
return {
|
|
46767
|
+
started,
|
|
46768
|
+
complete: required.length > 0 && missing.length === 0,
|
|
46769
|
+
required,
|
|
46770
|
+
completed,
|
|
46771
|
+
missing
|
|
46772
|
+
};
|
|
46773
|
+
}
|
|
46774
|
+
|
|
46775
|
+
// packages/workit-core/src/core/verify-project.ts
|
|
46776
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
46777
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, statSync as statSync6 } from "node:fs";
|
|
46778
|
+
import path17 from "node:path";
|
|
46779
|
+
|
|
46780
|
+
// packages/workit-core/src/core/repo-context.ts
|
|
46781
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
46782
|
+
|
|
46783
|
+
// packages/workit-core/src/core/branch.ts
|
|
46784
|
+
import { readFileSync as readFileSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
46785
|
+
import path16 from "node:path";
|
|
46786
|
+
|
|
46787
|
+
// packages/workit-core/src/core/git.ts
|
|
46788
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
46789
|
+
var run = (cwd2, args) => {
|
|
46790
|
+
try {
|
|
46791
|
+
const stdout = execFileSync3("git", args, {
|
|
46792
|
+
cwd: cwd2,
|
|
46793
|
+
encoding: "utf8",
|
|
46794
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
46795
|
+
});
|
|
46796
|
+
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
46797
|
+
} catch (error) {
|
|
46798
|
+
const e = error;
|
|
46799
|
+
return {
|
|
46800
|
+
stdout: (e.stdout ?? "").trimEnd(),
|
|
46801
|
+
stderr: (e.stderr ?? "").trimEnd(),
|
|
46802
|
+
exitCode: e.status ?? 1
|
|
46803
|
+
};
|
|
46804
|
+
}
|
|
46805
|
+
};
|
|
46806
|
+
var gitContext = (workspaceRoot, paths = []) => {
|
|
46807
|
+
const cwd2 = workspaceRoot;
|
|
46808
|
+
let failure;
|
|
46809
|
+
const runHere = (args) => {
|
|
46810
|
+
const result2 = run(cwd2, args);
|
|
46811
|
+
if (!failure && result2.exitCode !== 0) {
|
|
46812
|
+
failure = { stderr: result2.stderr, exitCode: result2.exitCode };
|
|
46813
|
+
}
|
|
46814
|
+
return result2.stdout;
|
|
46815
|
+
};
|
|
46816
|
+
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
46817
|
+
const status_short = runHere(["status", "--porcelain"]);
|
|
46818
|
+
const staged = [];
|
|
46819
|
+
const unstaged = [];
|
|
46820
|
+
const untracked = [];
|
|
46821
|
+
for (const line of status_short.split(`
|
|
46822
|
+
`).filter(Boolean)) {
|
|
46823
|
+
const code = line.slice(0, 2);
|
|
46824
|
+
const file = line.slice(3);
|
|
46825
|
+
if (code === "??") {
|
|
46826
|
+
untracked.push(file);
|
|
46827
|
+
} else {
|
|
46828
|
+
if (code[0] !== " " && code[0] !== "?")
|
|
46829
|
+
staged.push(file);
|
|
46830
|
+
if (code[1] !== " " && code[1] !== "?")
|
|
46831
|
+
unstaged.push(file);
|
|
46832
|
+
}
|
|
46833
|
+
}
|
|
46834
|
+
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
46835
|
+
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
46836
|
+
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
46837
|
+
const stagedSet = new Set(staged);
|
|
46838
|
+
const unstagedSet = new Set(unstaged);
|
|
46839
|
+
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
46840
|
+
return {
|
|
46841
|
+
workspace_root: cwd2,
|
|
46842
|
+
branch,
|
|
46843
|
+
status_short,
|
|
46844
|
+
staged,
|
|
46845
|
+
unstaged,
|
|
46846
|
+
untracked,
|
|
46847
|
+
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
46848
|
+
`),
|
|
46849
|
+
partial_staged: partial_staged.length > 0,
|
|
46850
|
+
partial_staged_files: partial_staged,
|
|
46851
|
+
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
46852
|
+
};
|
|
46853
|
+
};
|
|
46854
|
+
|
|
46855
|
+
// packages/workit-core/src/core/branch.ts
|
|
46856
|
+
var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
46857
|
+
var policy = (root) => resolveBranchPolicyFor(root);
|
|
46858
|
+
var allowedBranch = (root, name) => policy(root).allowed.some((r) => r.test(name));
|
|
46859
|
+
var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
|
|
46860
|
+
var DECLARE_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/gim;
|
|
46861
|
+
var USE_CURRENT_RE = /^\s*\*+Branch:\*+\s*use-current\s*$/im;
|
|
46862
|
+
var readSafe2 = (p) => {
|
|
46863
|
+
try {
|
|
46864
|
+
return readFileSync11(p, "utf8");
|
|
46865
|
+
} catch {
|
|
46866
|
+
return null;
|
|
46867
|
+
}
|
|
46868
|
+
};
|
|
46869
|
+
var normalizeBranch = (root, name) => {
|
|
46870
|
+
const n = name.trim().replace(/`/g, "").replace(/\.+$/, "");
|
|
46871
|
+
if (isProtected(root, n))
|
|
46872
|
+
return null;
|
|
46873
|
+
if (!allowedBranch(root, n))
|
|
46874
|
+
return null;
|
|
46875
|
+
const parts = n.toLowerCase().split("/").map((p) => p.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-"));
|
|
46876
|
+
if (parts.some((p) => !p))
|
|
46877
|
+
return null;
|
|
46878
|
+
return parts.join("/");
|
|
46879
|
+
};
|
|
46880
|
+
var deriveSlug = (planPath) => {
|
|
46881
|
+
const dirName = path16.basename(path16.dirname(planPath));
|
|
46882
|
+
return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
|
|
46883
|
+
};
|
|
46884
|
+
var deriveKind = (planPath, fallback = "feature") => {
|
|
46885
|
+
const slug = deriveSlug(planPath);
|
|
46886
|
+
const text = readSafe2(planPath) ?? "";
|
|
46887
|
+
let kind = fallback;
|
|
46888
|
+
if (/\bbugfix\b/i.test(slug) || /^fix-/i.test(slug)) {
|
|
46889
|
+
kind = "bugfix";
|
|
46890
|
+
} else {
|
|
46891
|
+
const goal = text.split(`
|
|
46892
|
+
`).find((line) => line.startsWith("**Goal:**"))?.toLowerCase() ?? "";
|
|
46893
|
+
if (/\b(bugfix|bug fix)\b/.test(goal) && !/\b(feat|feature|upgrade|add)\b/.test(goal)) {
|
|
46894
|
+
kind = "bugfix";
|
|
46895
|
+
}
|
|
46896
|
+
}
|
|
46897
|
+
return kind;
|
|
46898
|
+
};
|
|
46899
|
+
var resolveBranch = ({
|
|
46900
|
+
spec_path,
|
|
46901
|
+
plan_path,
|
|
46902
|
+
workspace_root
|
|
46903
|
+
}) => {
|
|
46904
|
+
const cwd2 = path16.resolve(workspace_root);
|
|
46905
|
+
const abs = (p) => path16.isAbsolute(p) ? p : path16.join(cwd2, p);
|
|
46906
|
+
const spec = abs(spec_path);
|
|
46907
|
+
const plan = abs(plan_path);
|
|
46908
|
+
const git = gitContext(cwd2);
|
|
46909
|
+
const current = git.branch;
|
|
46910
|
+
const finish = (branch, source) => ({
|
|
46911
|
+
branch,
|
|
46912
|
+
source,
|
|
46913
|
+
current_branch: current,
|
|
46914
|
+
dirty: Boolean(git.status_short.trim()),
|
|
46915
|
+
needs_checkout: current !== branch
|
|
46916
|
+
});
|
|
46917
|
+
for (const file of [spec, plan]) {
|
|
46918
|
+
const text = readSafe2(file);
|
|
46919
|
+
if (!text)
|
|
46920
|
+
continue;
|
|
46921
|
+
if (USE_CURRENT_RE.test(text)) {
|
|
46922
|
+
if (!current || !allowedBranch(cwd2, current) || isProtected(cwd2, current)) {
|
|
46923
|
+
return { error: `use-current but HEAD ${current} is not an allowed branch` };
|
|
46924
|
+
}
|
|
46925
|
+
return finish(current, "use-current");
|
|
46926
|
+
}
|
|
46927
|
+
}
|
|
46928
|
+
if (current && allowedBranch(cwd2, current) && !isProtected(cwd2, current))
|
|
46929
|
+
return finish(current, "keep-current");
|
|
46930
|
+
let declaredButInvalid = null;
|
|
46931
|
+
for (const file of [spec, plan]) {
|
|
46932
|
+
const text = readSafe2(file);
|
|
46933
|
+
if (!text)
|
|
46934
|
+
continue;
|
|
46935
|
+
for (const match of text.matchAll(DECLARE_RE)) {
|
|
46936
|
+
const normalized = normalizeBranch(cwd2, match[1]);
|
|
46937
|
+
if (normalized)
|
|
46938
|
+
return finish(normalized, file === spec ? "spec" : "plan");
|
|
46939
|
+
declaredButInvalid ??= match[1];
|
|
46940
|
+
}
|
|
46941
|
+
}
|
|
46942
|
+
if (declaredButInvalid) {
|
|
46943
|
+
return {
|
|
46944
|
+
error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy`
|
|
46945
|
+
};
|
|
46946
|
+
}
|
|
46947
|
+
const slug = deriveSlug(plan);
|
|
46948
|
+
if (!slug)
|
|
46949
|
+
return { error: `cannot derive branch slug from plan ${plan}` };
|
|
46950
|
+
const kind = deriveKind(plan);
|
|
46951
|
+
return finish(`${kind}/${slug}`, "derived");
|
|
46952
|
+
};
|
|
46953
|
+
|
|
46954
|
+
// packages/workit-core/src/core/repo-context.ts
|
|
46955
|
+
var runGit = (cwd2, args) => {
|
|
46956
|
+
const result2 = spawnSync3("git", args, { cwd: cwd2, encoding: "utf8" });
|
|
46957
|
+
return {
|
|
46958
|
+
stdout: (result2.stdout ?? "").trimEnd(),
|
|
46959
|
+
stderr: (result2.stderr ?? "").trimEnd(),
|
|
46960
|
+
exitCode: result2.status ?? 1
|
|
46961
|
+
};
|
|
46962
|
+
};
|
|
46963
|
+
var repoRoot2 = (cwd2) => {
|
|
46964
|
+
const r = runGit(cwd2, ["rev-parse", "--show-toplevel"]);
|
|
46965
|
+
return r.exitCode === 0 && r.stdout ? r.stdout : cwd2;
|
|
46966
|
+
};
|
|
46967
|
+
|
|
46968
|
+
// packages/workit-core/src/core/verify-project.ts
|
|
46969
|
+
var commandOnPath2 = (name) => {
|
|
46970
|
+
const dirs = (process.env.PATH ?? "").split(path17.delimiter);
|
|
46971
|
+
const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
|
|
46972
|
+
for (const dir of dirs) {
|
|
46973
|
+
if (!dir)
|
|
46974
|
+
continue;
|
|
46975
|
+
for (const candidateName of names) {
|
|
46976
|
+
const candidate = path17.join(dir, candidateName);
|
|
46977
|
+
try {
|
|
46978
|
+
statSync6(candidate);
|
|
46979
|
+
if (process.platform !== "win32") {
|
|
46980
|
+
const mode = statSync6(candidate).mode & 73;
|
|
46981
|
+
if (mode === 0)
|
|
46982
|
+
continue;
|
|
46983
|
+
}
|
|
46984
|
+
return true;
|
|
46985
|
+
} catch {}
|
|
46986
|
+
}
|
|
46987
|
+
}
|
|
46988
|
+
return false;
|
|
46989
|
+
};
|
|
46990
|
+
var hasScript = (cwd2, name) => {
|
|
46991
|
+
const pkgPath = path17.join(cwd2, "package.json");
|
|
46992
|
+
if (!existsSync13(pkgPath))
|
|
46993
|
+
return false;
|
|
46994
|
+
try {
|
|
46995
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
46996
|
+
return Boolean(pkg.scripts && typeof pkg.scripts[name] === "string");
|
|
46997
|
+
} catch {
|
|
46998
|
+
return false;
|
|
46999
|
+
}
|
|
47000
|
+
};
|
|
47001
|
+
var packageRunner = (cwd2) => {
|
|
47002
|
+
if (existsSync13(path17.join(cwd2, "pnpm-lock.yaml")))
|
|
47003
|
+
return "pnpm";
|
|
47004
|
+
const pkgPath = path17.join(cwd2, "package.json");
|
|
47005
|
+
if (existsSync13(pkgPath)) {
|
|
47006
|
+
try {
|
|
47007
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
47008
|
+
if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
|
|
47009
|
+
return "pnpm";
|
|
47010
|
+
}
|
|
47011
|
+
} catch {}
|
|
47012
|
+
}
|
|
47013
|
+
if (existsSync13(path17.join(cwd2, "yarn.lock")))
|
|
47014
|
+
return "yarn";
|
|
47015
|
+
return "npm";
|
|
47016
|
+
};
|
|
47017
|
+
function runVerifyProject(root, dryRun = false) {
|
|
47018
|
+
const cwd2 = repoRoot2(root);
|
|
47019
|
+
let passed = 0;
|
|
47020
|
+
let failed = 0;
|
|
47021
|
+
let skipped = 0;
|
|
47022
|
+
const lines = [];
|
|
47023
|
+
const stderrParts = [];
|
|
47024
|
+
const runCheck = (label, cmd, args) => {
|
|
47025
|
+
lines.push(`
|
|
47026
|
+
## ${label}
|
|
47027
|
+
|
|
47028
|
+
`);
|
|
47029
|
+
lines.push(`command: ${[cmd, ...args].join(" ")}
|
|
47030
|
+
|
|
47031
|
+
`);
|
|
47032
|
+
if (dryRun) {
|
|
47033
|
+
lines.push(`status: skipped (dry run)
|
|
47034
|
+
`);
|
|
47035
|
+
skipped += 1;
|
|
47036
|
+
return;
|
|
47037
|
+
}
|
|
47038
|
+
const r = spawnSync4(cmd, args, { cwd: cwd2, encoding: "utf8" });
|
|
47039
|
+
const so = (r.stdout ?? "").trimEnd();
|
|
47040
|
+
if (so)
|
|
47041
|
+
lines.push(so + `
|
|
47042
|
+
`);
|
|
47043
|
+
const se = (r.stderr ?? "").trimEnd();
|
|
47044
|
+
if (se)
|
|
47045
|
+
stderrParts.push(se);
|
|
47046
|
+
if (r.status === 0) {
|
|
47047
|
+
lines.push(`
|
|
47048
|
+
status: pass
|
|
47049
|
+
`);
|
|
47050
|
+
passed += 1;
|
|
47051
|
+
} else {
|
|
47052
|
+
lines.push(`
|
|
47053
|
+
status: fail (exit ${r.status})
|
|
47054
|
+
`);
|
|
47055
|
+
failed += 1;
|
|
47056
|
+
}
|
|
47057
|
+
};
|
|
47058
|
+
const skipCheck = (label, reason) => {
|
|
47059
|
+
lines.push(`
|
|
47060
|
+
## ${label}
|
|
47061
|
+
|
|
47062
|
+
status: skipped (${reason})
|
|
47063
|
+
`);
|
|
47064
|
+
skipped += 1;
|
|
47065
|
+
};
|
|
47066
|
+
lines.push(`# Verification Context
|
|
47067
|
+
`);
|
|
47068
|
+
lines.push(`
|
|
47069
|
+
root: ${cwd2}
|
|
47070
|
+
`);
|
|
47071
|
+
lines.push(`dry_run: ${String(dryRun)}
|
|
47072
|
+
`);
|
|
47073
|
+
const pkgPath = path17.join(cwd2, "package.json");
|
|
47074
|
+
if (existsSync13(pkgPath)) {
|
|
47075
|
+
const runner = packageRunner(cwd2);
|
|
47076
|
+
for (const script of ["lint", "format:check", "test", "build"]) {
|
|
47077
|
+
if (hasScript(cwd2, script)) {
|
|
47078
|
+
if (runner === "npm")
|
|
47079
|
+
runCheck(script, "npm", ["run", script]);
|
|
47080
|
+
else
|
|
47081
|
+
runCheck(script, runner, [script]);
|
|
47082
|
+
} else {
|
|
47083
|
+
skipCheck(script, `package.json has no ${script} script`);
|
|
47084
|
+
}
|
|
47085
|
+
}
|
|
47086
|
+
} else {
|
|
47087
|
+
skipCheck("javascript", "package.json not found");
|
|
47088
|
+
}
|
|
47089
|
+
const cargoManifest = existsSync13(path17.join(cwd2, "Cargo.toml")) ? "Cargo.toml" : existsSync13(path17.join(cwd2, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
|
|
47090
|
+
if (cargoManifest) {
|
|
47091
|
+
runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
|
|
47092
|
+
runCheck("cargo clippy", "cargo", [
|
|
47093
|
+
"clippy",
|
|
47094
|
+
"--manifest-path",
|
|
47095
|
+
cargoManifest,
|
|
47096
|
+
"--all-targets",
|
|
47097
|
+
"--",
|
|
47098
|
+
"-D",
|
|
47099
|
+
"warnings"
|
|
47100
|
+
]);
|
|
47101
|
+
runCheck("cargo test", "cargo", ["test", "--manifest-path", cargoManifest, "--all-targets"]);
|
|
47102
|
+
} else {
|
|
47103
|
+
skipCheck("rust", "Cargo.toml not found");
|
|
47104
|
+
}
|
|
47105
|
+
if (existsSync13(path17.join(cwd2, "pyproject.toml")) || existsSync13(path17.join(cwd2, "pytest.ini")) || existsSync13(path17.join(cwd2, "tests"))) {
|
|
47106
|
+
if (commandOnPath2("pytest"))
|
|
47107
|
+
runCheck("pytest", "pytest", []);
|
|
47108
|
+
else
|
|
47109
|
+
skipCheck("pytest", "pytest not available");
|
|
47110
|
+
if (commandOnPath2("ruff"))
|
|
47111
|
+
runCheck("ruff check", "ruff", ["check", "."]);
|
|
47112
|
+
else
|
|
47113
|
+
skipCheck("ruff check", "ruff not available");
|
|
47114
|
+
}
|
|
47115
|
+
lines.push(`
|
|
47116
|
+
## CHANGELOG.md format
|
|
47117
|
+
|
|
47118
|
+
`);
|
|
47119
|
+
lines.push(`command: grep -qi "## [Unreleased]" CHANGELOG.md
|
|
47120
|
+
|
|
47121
|
+
`);
|
|
47122
|
+
const changelogPath = path17.join(cwd2, "CHANGELOG.md");
|
|
47123
|
+
if (dryRun) {
|
|
47124
|
+
lines.push(`status: skipped (dry run)
|
|
47125
|
+
`);
|
|
47126
|
+
skipped += 1;
|
|
47127
|
+
} else if (existsSync13(changelogPath)) {
|
|
47128
|
+
if (/## \[Unreleased\]/i.test(readFileSync12(changelogPath, "utf8"))) {
|
|
47129
|
+
lines.push(`status: pass
|
|
47130
|
+
`);
|
|
47131
|
+
passed += 1;
|
|
47132
|
+
} else {
|
|
47133
|
+
lines.push(`status: fail (missing ## [Unreleased])
|
|
47134
|
+
`);
|
|
47135
|
+
failed += 1;
|
|
47136
|
+
}
|
|
47137
|
+
} else {
|
|
47138
|
+
lines.push(`status: fail (missing CHANGELOG.md)
|
|
47139
|
+
`);
|
|
47140
|
+
failed += 1;
|
|
47141
|
+
}
|
|
47142
|
+
lines.push(`
|
|
47143
|
+
# Summary
|
|
47144
|
+
|
|
47145
|
+
`);
|
|
47146
|
+
lines.push(`passed: ${passed}
|
|
47147
|
+
`);
|
|
47148
|
+
lines.push(`failed: ${failed}
|
|
47149
|
+
`);
|
|
47150
|
+
lines.push(`skipped: ${skipped}
|
|
47151
|
+
`);
|
|
47152
|
+
return {
|
|
47153
|
+
stdout: lines.join(""),
|
|
47154
|
+
stderr: stderrParts.join(`
|
|
47155
|
+
`),
|
|
47156
|
+
exitCode: failed > 0 ? 1 : 0,
|
|
47157
|
+
cwd: cwd2
|
|
47158
|
+
};
|
|
47159
|
+
}
|
|
47160
|
+
|
|
47161
|
+
// packages/workit-core/src/core/flow-state.ts
|
|
47162
|
+
var 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.";
|
|
47163
|
+
var 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`.";
|
|
47164
|
+
var MENU_CHOICES = [
|
|
47165
|
+
"subagent-driven",
|
|
47166
|
+
"inline",
|
|
47167
|
+
"handoff",
|
|
47168
|
+
"review-spec",
|
|
47169
|
+
"review-plan"
|
|
47170
|
+
];
|
|
47171
|
+
var err2 = (code, error, details) => ({
|
|
47172
|
+
ok: false,
|
|
47173
|
+
code,
|
|
47174
|
+
error,
|
|
47175
|
+
...details ? { details } : {}
|
|
47176
|
+
});
|
|
47177
|
+
var SLUG_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
47178
|
+
var flowPath = (root, slug) => {
|
|
47179
|
+
if (!SLUG_RE2.test(slug))
|
|
47180
|
+
throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
|
|
47181
|
+
return path18.join(root, "docs", slug, "sdd", "flow.json");
|
|
47182
|
+
};
|
|
47183
|
+
var resolveDoc = (root, slug, docPath, kind) => {
|
|
47184
|
+
const resolved = resolveCanonicalLayout({
|
|
47185
|
+
workspace_root: root,
|
|
47186
|
+
...slug ? { slug } : {},
|
|
47187
|
+
[kind === "spec" ? "spec_path" : "plan_path"]: docPath
|
|
47188
|
+
});
|
|
47189
|
+
if (!resolved.ok)
|
|
47190
|
+
return { ok: false, error: resolved.error };
|
|
47191
|
+
return { ok: true, path: resolved.layout[kind === "spec" ? "spec" : "plan"] };
|
|
47192
|
+
};
|
|
47193
|
+
var HEX64_RE = /^[0-9a-f]{64}$/;
|
|
47194
|
+
var FLOW_STATUSES = ["draft", "self_reviewed", "approved"];
|
|
47195
|
+
var EXECUTION_STATUSES = ["pending", "active", "paused", "completed"];
|
|
47196
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
47197
|
+
var validateEvidenceValue = (v, allowCli) => {
|
|
47198
|
+
if (v === null)
|
|
47199
|
+
return true;
|
|
47200
|
+
if (!isRecord3(v))
|
|
47201
|
+
return false;
|
|
47202
|
+
if (v.host === "opencode") {
|
|
47203
|
+
return v.attested === true && typeof v.callID === "string" && typeof v.selectedLabel === "string" && typeof v.recordedAt === "number";
|
|
47204
|
+
}
|
|
47205
|
+
if (v.host === "cursor")
|
|
47206
|
+
return v.attested === false && v.confirmation === "contract";
|
|
47207
|
+
if (allowCli && v.host === "cli") {
|
|
47208
|
+
return v.attested === false && (v.confirmation === "flag" || v.confirmation === "tty");
|
|
47209
|
+
}
|
|
47210
|
+
return false;
|
|
47211
|
+
};
|
|
47212
|
+
var validateState = (parsed, slug) => {
|
|
47213
|
+
if (!isRecord3(parsed))
|
|
47214
|
+
return { ok: false, error: "flow state must be a JSON object" };
|
|
47215
|
+
if (parsed.slug !== undefined && (typeof parsed.slug !== "string" || parsed.slug !== slug)) {
|
|
47216
|
+
return { ok: false, error: `flow state slug must be ${JSON.stringify(slug)}` };
|
|
47217
|
+
}
|
|
47218
|
+
if (parsed.activated !== undefined && typeof parsed.activated !== "boolean") {
|
|
47219
|
+
return { ok: false, error: "flow state activated must be a boolean" };
|
|
47220
|
+
}
|
|
47221
|
+
if (parsed.handoff_destination !== undefined && typeof parsed.handoff_destination !== "boolean") {
|
|
47222
|
+
return { ok: false, error: "flow state handoff_destination must be a boolean" };
|
|
47223
|
+
}
|
|
47224
|
+
if (parsed.updated_at !== undefined && (typeof parsed.updated_at !== "number" || !Number.isFinite(parsed.updated_at))) {
|
|
47225
|
+
return { ok: false, error: "flow state updated_at must be a finite number" };
|
|
47226
|
+
}
|
|
47227
|
+
const doc = (value, name) => {
|
|
47228
|
+
const p = isRecord3(value) ? value : {};
|
|
47229
|
+
if (!isRecord3(value) && value !== undefined) {
|
|
47230
|
+
return `flow state ${name} must be an object`;
|
|
47231
|
+
}
|
|
47232
|
+
if (p.status !== undefined && !FLOW_STATUSES.includes(p.status)) {
|
|
47233
|
+
return `flow state ${name}.status must be draft, self_reviewed, or approved`;
|
|
47234
|
+
}
|
|
47235
|
+
if (p.path !== undefined && typeof p.path !== "string") {
|
|
47236
|
+
return `flow state ${name}.path must be a string`;
|
|
47237
|
+
}
|
|
47238
|
+
if (p.approved_digest !== undefined && p.approved_digest !== null && (typeof p.approved_digest !== "string" || !HEX64_RE.test(p.approved_digest))) {
|
|
47239
|
+
return `flow state ${name}.approved_digest must be 64-char lowercase hex or null`;
|
|
47240
|
+
}
|
|
47241
|
+
if (p.evidence !== undefined && !validateEvidenceValue(p.evidence, false)) {
|
|
47242
|
+
return `flow state ${name}.evidence has an unsupported shape`;
|
|
47243
|
+
}
|
|
47244
|
+
return {
|
|
47245
|
+
path: p.path ?? "",
|
|
47246
|
+
status: p.status ?? "draft",
|
|
47247
|
+
evidence: p.evidence ?? null,
|
|
47248
|
+
approved_digest: p.approved_digest ?? null
|
|
47249
|
+
};
|
|
47250
|
+
};
|
|
47251
|
+
const spec = doc(parsed.spec, "spec");
|
|
47252
|
+
if (typeof spec === "string")
|
|
47253
|
+
return { ok: false, error: spec };
|
|
47254
|
+
const plan = doc(parsed.plan, "plan");
|
|
47255
|
+
if (typeof plan === "string")
|
|
47256
|
+
return { ok: false, error: plan };
|
|
47257
|
+
const menuRaw = isRecord3(parsed.menu) ? parsed.menu : undefined;
|
|
47258
|
+
if (parsed.menu !== undefined && !isRecord3(parsed.menu)) {
|
|
47259
|
+
return { ok: false, error: "flow state menu must be an object" };
|
|
47260
|
+
}
|
|
47261
|
+
if (menuRaw?.presented !== undefined && typeof menuRaw.presented !== "boolean") {
|
|
47262
|
+
return { ok: false, error: "flow state menu.presented must be a boolean" };
|
|
47263
|
+
}
|
|
47264
|
+
if (menuRaw?.chosen !== undefined && typeof menuRaw.chosen !== "string") {
|
|
47265
|
+
return { ok: false, error: "flow state menu.chosen must be a string" };
|
|
47266
|
+
}
|
|
47267
|
+
if (menuRaw?.chosen !== undefined && menuRaw.chosen !== "" && !MENU_CHOICES.includes(menuRaw.chosen)) {
|
|
47268
|
+
return {
|
|
47269
|
+
ok: false,
|
|
47270
|
+
error: `flow state menu.chosen must be one of: ${MENU_CHOICES.join(", ")} (or an empty string when the menu is unpresented)`
|
|
47271
|
+
};
|
|
47272
|
+
}
|
|
47273
|
+
if (menuRaw?.evidence !== undefined && !validateEvidenceValue(menuRaw.evidence, false)) {
|
|
47274
|
+
return { ok: false, error: "flow state menu.evidence has an unsupported shape" };
|
|
47275
|
+
}
|
|
47276
|
+
const execRaw = isRecord3(parsed.execution) ? parsed.execution : undefined;
|
|
47277
|
+
if (parsed.execution !== undefined && !isRecord3(parsed.execution)) {
|
|
47278
|
+
return { ok: false, error: "flow state execution must be an object" };
|
|
47279
|
+
}
|
|
47280
|
+
if (execRaw?.status !== undefined && !EXECUTION_STATUSES.includes(execRaw.status)) {
|
|
47281
|
+
return {
|
|
47282
|
+
ok: false,
|
|
47283
|
+
error: "flow state execution.status must be pending, active, paused, or completed"
|
|
47284
|
+
};
|
|
47285
|
+
}
|
|
47286
|
+
if (execRaw?.mode !== undefined && execRaw.mode !== null && execRaw.mode !== "subagent-driven" && execRaw.mode !== "inline") {
|
|
47287
|
+
return {
|
|
47288
|
+
ok: false,
|
|
47289
|
+
error: "flow state execution.mode must be subagent-driven, inline, or null"
|
|
47290
|
+
};
|
|
47291
|
+
}
|
|
47292
|
+
if (execRaw?.evidence !== undefined && !validateEvidenceValue(execRaw.evidence, true)) {
|
|
47293
|
+
return { ok: false, error: "flow state execution.evidence has an unsupported shape" };
|
|
47294
|
+
}
|
|
47295
|
+
return {
|
|
47296
|
+
ok: true,
|
|
47297
|
+
state: {
|
|
47298
|
+
slug,
|
|
47299
|
+
activated: parsed.activated ?? true,
|
|
47300
|
+
spec,
|
|
47301
|
+
plan,
|
|
47302
|
+
menu: {
|
|
47303
|
+
presented: menuRaw?.presented ?? false,
|
|
47304
|
+
chosen: menuRaw?.chosen ?? "",
|
|
47305
|
+
evidence: menuRaw?.evidence ?? null
|
|
47306
|
+
},
|
|
47307
|
+
execution: {
|
|
47308
|
+
status: execRaw?.status ?? "pending",
|
|
47309
|
+
mode: execRaw?.mode ?? null,
|
|
47310
|
+
evidence: execRaw?.evidence ?? null
|
|
47311
|
+
},
|
|
47312
|
+
handoff_destination: parsed.handoff_destination ?? false,
|
|
47313
|
+
updated_at: parsed.updated_at ?? Date.now()
|
|
47314
|
+
}
|
|
47315
|
+
};
|
|
47316
|
+
};
|
|
47317
|
+
var readFlowStrict = (root, slug) => {
|
|
47318
|
+
const file = flowPath(root, slug);
|
|
47319
|
+
const rel = path18.posix.join("docs", slug, "sdd", "flow.json");
|
|
47320
|
+
if (!existsSync14(file)) {
|
|
47321
|
+
return err2("flow_not_activated", `flow not activated for ${slug} — run workflow_flow_status first`);
|
|
47322
|
+
}
|
|
47323
|
+
let text;
|
|
47324
|
+
try {
|
|
47325
|
+
text = readFileSync13(file, "utf8");
|
|
47326
|
+
} catch (error) {
|
|
47327
|
+
return err2("flow_io_error", `cannot read flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
47328
|
+
}
|
|
47329
|
+
let parsed;
|
|
47330
|
+
try {
|
|
47331
|
+
parsed = JSON.parse(text);
|
|
47332
|
+
} catch (error) {
|
|
47333
|
+
return err2("flow_state_invalid", `invalid flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`, { path: rel, original_bytes_preserved: true });
|
|
47334
|
+
}
|
|
47335
|
+
const validated = validateState(parsed, slug);
|
|
47336
|
+
if (!validated.ok) {
|
|
47337
|
+
return err2("flow_state_invalid", `invalid flow state at ${file}: ${validated.error}`, {
|
|
47338
|
+
path: rel,
|
|
47339
|
+
original_bytes_preserved: true
|
|
47340
|
+
});
|
|
47341
|
+
}
|
|
47342
|
+
return { ok: true, state: validated.state, raw: parsed };
|
|
47343
|
+
};
|
|
47344
|
+
var uniqueTempPath = (file) => `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`;
|
|
47345
|
+
var writeFlowFileAtomic = (file, state) => {
|
|
47346
|
+
const text = JSON.stringify(state, null, 2) + `
|
|
47347
|
+
`;
|
|
47348
|
+
const tmp = uniqueTempPath(file);
|
|
47349
|
+
mkdirSync8(path18.dirname(file), { recursive: true });
|
|
47350
|
+
let fd = null;
|
|
47351
|
+
try {
|
|
47352
|
+
fd = openSync(tmp, "w");
|
|
47353
|
+
writeFileSync8(fd, text, "utf8");
|
|
47354
|
+
fsyncSync(fd);
|
|
47355
|
+
closeSync(fd);
|
|
47356
|
+
fd = null;
|
|
47357
|
+
renameSync2(tmp, file);
|
|
47358
|
+
} finally {
|
|
47359
|
+
try {
|
|
47360
|
+
if (fd !== null)
|
|
47361
|
+
closeSync(fd);
|
|
47362
|
+
} catch {}
|
|
47363
|
+
try {
|
|
47364
|
+
if (existsSync14(tmp))
|
|
47365
|
+
rmSync2(tmp, { force: true });
|
|
47366
|
+
} catch {}
|
|
47367
|
+
}
|
|
47368
|
+
};
|
|
47369
|
+
var MAX_WRITE_ATTEMPTS = 5;
|
|
47370
|
+
var STALE_LOCK_MS = 1000;
|
|
47371
|
+
var lockMtimeMs = (lock) => {
|
|
47372
|
+
try {
|
|
47373
|
+
return statSync7(lock).mtimeMs;
|
|
47374
|
+
} catch {
|
|
47375
|
+
return null;
|
|
47376
|
+
}
|
|
47377
|
+
};
|
|
47378
|
+
var lockOwnedBy = (fd, lock) => {
|
|
47379
|
+
try {
|
|
47380
|
+
return fstatSync(fd).ino === statSync7(lock).ino;
|
|
47381
|
+
} catch {
|
|
47382
|
+
return false;
|
|
47383
|
+
}
|
|
47384
|
+
};
|
|
47385
|
+
var writeFlowStateIfCurrent = (root, expected, next) => {
|
|
47386
|
+
const file = flowPath(root, next.slug);
|
|
47387
|
+
const expectedText = JSON.stringify(expected, null, 2) + `
|
|
47388
|
+
`;
|
|
47389
|
+
const nextText = JSON.stringify(next, null, 2) + `
|
|
47390
|
+
`;
|
|
47391
|
+
if (expectedText === nextText)
|
|
47392
|
+
return { ok: true };
|
|
47393
|
+
const tmp = uniqueTempPath(file);
|
|
47394
|
+
let fd = null;
|
|
47395
|
+
try {
|
|
47396
|
+
const currentText = existsSync14(file) ? readFileSync13(file, "utf8") : null;
|
|
47397
|
+
if (currentText !== expectedText)
|
|
47398
|
+
return { ok: false, conflict: true };
|
|
47399
|
+
mkdirSync8(path18.dirname(file), { recursive: true });
|
|
47400
|
+
fd = openSync(tmp, "w");
|
|
47401
|
+
writeFileSync8(fd, nextText, "utf8");
|
|
47402
|
+
fsyncSync(fd);
|
|
47403
|
+
closeSync(fd);
|
|
47404
|
+
fd = null;
|
|
47405
|
+
const reRead = existsSync14(file) ? readFileSync13(file, "utf8") : null;
|
|
47406
|
+
if (reRead !== expectedText)
|
|
47407
|
+
return { ok: false, conflict: true };
|
|
47408
|
+
renameSync2(tmp, file);
|
|
47409
|
+
return { ok: true };
|
|
47410
|
+
} catch (error) {
|
|
47411
|
+
return { ok: false, io_error: error instanceof Error ? error.message : String(error) };
|
|
47412
|
+
} finally {
|
|
47413
|
+
try {
|
|
47414
|
+
if (fd !== null)
|
|
47415
|
+
closeSync(fd);
|
|
47416
|
+
} catch {}
|
|
47417
|
+
try {
|
|
47418
|
+
if (existsSync14(tmp))
|
|
47419
|
+
rmSync2(tmp, { force: true });
|
|
47420
|
+
} catch {}
|
|
47421
|
+
}
|
|
47422
|
+
};
|
|
47423
|
+
var readCanonicalDigest = (root, rel) => {
|
|
47424
|
+
const abs = path18.join(root, ...rel.split("/"));
|
|
47425
|
+
let bytes;
|
|
47426
|
+
try {
|
|
47427
|
+
bytes = readFileSync13(abs);
|
|
47428
|
+
} catch (error) {
|
|
47429
|
+
if (error.code === "ENOENT") {
|
|
47430
|
+
return { ok: false, code: "document_missing" };
|
|
47431
|
+
}
|
|
47432
|
+
return { ok: false, code: "document_unreadable" };
|
|
47433
|
+
}
|
|
47434
|
+
let text;
|
|
47435
|
+
try {
|
|
47436
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
47437
|
+
} catch {
|
|
47438
|
+
return { ok: false, code: "document_unreadable" };
|
|
47439
|
+
}
|
|
47440
|
+
return { ok: true, text, digest: createHash("sha256").update(bytes).digest("hex") };
|
|
47441
|
+
};
|
|
47442
|
+
var resetForSpecDrift = (state) => ({
|
|
47443
|
+
...state,
|
|
47444
|
+
spec: { ...state.spec, status: "draft", evidence: null, approved_digest: null },
|
|
47445
|
+
plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
|
|
47446
|
+
menu: { presented: false, chosen: "", evidence: null },
|
|
47447
|
+
execution: { status: "pending", mode: null, evidence: null },
|
|
47448
|
+
handoff_destination: false,
|
|
47449
|
+
updated_at: Date.now()
|
|
47450
|
+
});
|
|
47451
|
+
var resetForPlanDrift = (state) => ({
|
|
47452
|
+
...state,
|
|
47453
|
+
plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
|
|
47454
|
+
menu: { presented: false, chosen: "", evidence: null },
|
|
47455
|
+
execution: { status: "pending", mode: null, evidence: null },
|
|
47456
|
+
handoff_destination: false,
|
|
47457
|
+
updated_at: Date.now()
|
|
47458
|
+
});
|
|
47459
|
+
var driftCodeFor = (root, relPath, storedDigest) => {
|
|
47460
|
+
if (storedDigest === null)
|
|
47461
|
+
return "digest_missing";
|
|
47462
|
+
const current = readCanonicalDigest(root, relPath);
|
|
47463
|
+
if (!current.ok)
|
|
47464
|
+
return current.code;
|
|
47465
|
+
return current.digest !== storedDigest ? "digest_mismatch" : null;
|
|
47466
|
+
};
|
|
47467
|
+
var reconcileState = (root, slug, state) => {
|
|
47468
|
+
const specPath = path18.posix.join("docs", slug, "spec.md");
|
|
47469
|
+
const planPath = path18.posix.join("docs", slug, "plan.md");
|
|
47470
|
+
if (state.spec.status === "approved") {
|
|
47471
|
+
const code = driftCodeFor(root, specPath, state.spec.approved_digest);
|
|
47472
|
+
if (code) {
|
|
47473
|
+
return {
|
|
47474
|
+
state: resetForSpecDrift(state),
|
|
47475
|
+
drift: [{ document: "spec", code, path: specPath }]
|
|
47476
|
+
};
|
|
47477
|
+
}
|
|
47478
|
+
}
|
|
47479
|
+
if (state.plan.status === "approved") {
|
|
47480
|
+
const code = driftCodeFor(root, planPath, state.plan.approved_digest);
|
|
47481
|
+
if (code) {
|
|
47482
|
+
return {
|
|
47483
|
+
state: resetForPlanDrift(state),
|
|
47484
|
+
drift: [{ document: "plan", code, path: planPath }]
|
|
47485
|
+
};
|
|
47486
|
+
}
|
|
47487
|
+
}
|
|
47488
|
+
return { state, drift: [] };
|
|
47489
|
+
};
|
|
47490
|
+
var deriveLegacyExecution = (root, slug, state) => {
|
|
47491
|
+
const ledger = ledgerCompletion(root, slug);
|
|
47492
|
+
if (state.plan.status === "approved" && state.menu.chosen === "subagent-driven" && ledger.started && !ledger.complete) {
|
|
47493
|
+
return { status: "active", mode: "subagent-driven", evidence: null };
|
|
47494
|
+
}
|
|
47495
|
+
return { status: "pending", mode: null, evidence: null };
|
|
47496
|
+
};
|
|
47497
|
+
var normalizeCompatibility = (root, slug, parsed, state) => {
|
|
47498
|
+
if (!isRecord3(parsed) || !("execution" in parsed)) {
|
|
47499
|
+
const derived = deriveLegacyExecution(root, slug, state);
|
|
47500
|
+
const current = state.execution;
|
|
47501
|
+
if (derived.status !== current.status || derived.mode !== current.mode) {
|
|
47502
|
+
return { state: { ...state, execution: derived, updated_at: Date.now() }, changed: true };
|
|
47503
|
+
}
|
|
47504
|
+
}
|
|
47505
|
+
return { state, changed: false };
|
|
47506
|
+
};
|
|
47507
|
+
var withFlowLock = (file, fn) => {
|
|
47508
|
+
const lock = `${file}.lock`;
|
|
47509
|
+
if (!existsSync14(path18.dirname(file)))
|
|
47510
|
+
return { locked: true, value: fn() };
|
|
47511
|
+
try {
|
|
47512
|
+
if (existsSync14(`${lock}.stale`))
|
|
47513
|
+
rmSync2(`${lock}.stale`, { force: true });
|
|
47514
|
+
} catch {}
|
|
47515
|
+
const wait = new Int32Array(new SharedArrayBuffer(4));
|
|
47516
|
+
let fd = null;
|
|
47517
|
+
for (let attempt2 = 0;attempt2 < MAX_WRITE_ATTEMPTS; attempt2++) {
|
|
47518
|
+
try {
|
|
47519
|
+
fd = openSync(lock, "wx");
|
|
47520
|
+
break;
|
|
47521
|
+
} catch (error) {
|
|
47522
|
+
const code = error.code;
|
|
47523
|
+
if (code !== "EEXIST") {
|
|
47524
|
+
return {
|
|
47525
|
+
locked: false,
|
|
47526
|
+
error: err2("flow_io_error", `flow lock failed for ${file}: ${error instanceof Error ? error.message : String(error)}`)
|
|
47527
|
+
};
|
|
47528
|
+
}
|
|
47529
|
+
const mtime = lockMtimeMs(lock);
|
|
47530
|
+
if (mtime !== null && Date.now() - mtime > STALE_LOCK_MS) {
|
|
47531
|
+
try {
|
|
47532
|
+
renameSync2(lock, `${lock}.stale`);
|
|
47533
|
+
unlinkSync3(`${lock}.stale`);
|
|
47534
|
+
} catch {}
|
|
47535
|
+
try {
|
|
47536
|
+
fd = openSync(lock, "wx");
|
|
47537
|
+
break;
|
|
47538
|
+
} catch (innerError) {
|
|
47539
|
+
const innerCode = innerError.code;
|
|
47540
|
+
if (innerCode !== "EEXIST") {
|
|
47541
|
+
return {
|
|
47542
|
+
locked: false,
|
|
47543
|
+
error: err2("flow_io_error", `flow lock failed for ${file}: ${innerError instanceof Error ? innerError.message : String(innerError)}`)
|
|
47544
|
+
};
|
|
47545
|
+
}
|
|
47546
|
+
}
|
|
47547
|
+
}
|
|
47548
|
+
if (attempt2 === MAX_WRITE_ATTEMPTS - 1) {
|
|
47549
|
+
return {
|
|
47550
|
+
locked: false,
|
|
47551
|
+
error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path18.dirname(file)}: re-read the flow state and retry the transition`)
|
|
47552
|
+
};
|
|
47553
|
+
}
|
|
47554
|
+
Atomics.wait(wait, 0, 0, 10);
|
|
47555
|
+
}
|
|
47556
|
+
}
|
|
47557
|
+
if (fd === null) {
|
|
47558
|
+
return {
|
|
47559
|
+
locked: false,
|
|
47560
|
+
error: err2("flow_concurrent_conflict", `concurrent flow update detected for ${path18.dirname(file)}: re-read the flow state and retry the transition`)
|
|
47561
|
+
};
|
|
47562
|
+
}
|
|
47563
|
+
try {
|
|
47564
|
+
return { locked: true, value: fn() };
|
|
47565
|
+
} finally {
|
|
47566
|
+
try {
|
|
47567
|
+
if (fd !== null && lockOwnedBy(fd, lock))
|
|
47568
|
+
rmSync2(lock, { force: true });
|
|
47569
|
+
} catch {}
|
|
47570
|
+
try {
|
|
47571
|
+
if (fd !== null)
|
|
47572
|
+
closeSync(fd);
|
|
47573
|
+
} catch {}
|
|
47574
|
+
}
|
|
47575
|
+
};
|
|
47576
|
+
var readEffectiveFlowState = (root, slug) => {
|
|
47577
|
+
const file = flowPath(root, slug);
|
|
47578
|
+
const rel = path18.posix.join("docs", slug, "sdd", "flow.json");
|
|
47579
|
+
const locked = withFlowLock(file, () => {
|
|
47580
|
+
const strict = readFlowStrict(root, slug);
|
|
47581
|
+
if (!strict.ok)
|
|
47582
|
+
return strict;
|
|
47583
|
+
const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
|
|
47584
|
+
const { state, drift } = reconcileState(root, slug, normalized.state);
|
|
47585
|
+
if (normalized.changed || drift.length > 0) {
|
|
47586
|
+
try {
|
|
47587
|
+
writeFlowFileAtomic(file, state);
|
|
47588
|
+
} catch (error) {
|
|
47589
|
+
return err2("flow_io_error", `cannot persist reconciled flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`, { path: rel, original_bytes_preserved: true });
|
|
47590
|
+
}
|
|
47591
|
+
}
|
|
47592
|
+
return { ok: true, state, drift };
|
|
47593
|
+
});
|
|
47594
|
+
if (!locked.locked)
|
|
47595
|
+
return locked.error;
|
|
47596
|
+
return locked.value;
|
|
47597
|
+
};
|
|
47598
|
+
var readModifyWrite = (root, slug, mutate) => {
|
|
47599
|
+
const file = flowPath(root, slug);
|
|
47600
|
+
const locked = withFlowLock(file, () => {
|
|
47601
|
+
for (let attempt2 = 0;attempt2 < MAX_WRITE_ATTEMPTS; attempt2++) {
|
|
47602
|
+
const strict = readFlowStrict(root, slug);
|
|
47603
|
+
if (!strict.ok)
|
|
47604
|
+
return strict;
|
|
47605
|
+
const normalized = normalizeCompatibility(root, slug, strict.raw, strict.state);
|
|
47606
|
+
const reconciled = reconcileState(root, slug, normalized.state);
|
|
47607
|
+
const result2 = mutate(reconciled.state);
|
|
47608
|
+
if (!result2.ok)
|
|
47609
|
+
return result2;
|
|
47610
|
+
let baseline = strict.state;
|
|
47611
|
+
if (normalized.changed) {
|
|
47612
|
+
try {
|
|
47613
|
+
writeFlowFileAtomic(file, normalized.state);
|
|
47614
|
+
} catch (error) {
|
|
47615
|
+
return err2("flow_io_error", `cannot persist normalized flow state at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
47616
|
+
}
|
|
47617
|
+
baseline = normalized.state;
|
|
47618
|
+
}
|
|
47619
|
+
const commit = writeFlowStateIfCurrent(root, baseline, result2.next);
|
|
47620
|
+
if (commit.ok)
|
|
47621
|
+
return { ok: true };
|
|
47622
|
+
if ("io_error" in commit) {
|
|
47623
|
+
return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
|
|
47624
|
+
}
|
|
47625
|
+
}
|
|
47626
|
+
return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry the transition`);
|
|
47627
|
+
});
|
|
47628
|
+
if (!locked.locked)
|
|
47629
|
+
return locked.error;
|
|
47630
|
+
return locked.value;
|
|
47631
|
+
};
|
|
47632
|
+
var assertMutationWorkspace = (root, ctx) => {
|
|
47633
|
+
if (ctx && ctx.hostWorkspace !== root) {
|
|
47634
|
+
return err2("workspace_mismatch", `mutation context workspace ${JSON.stringify(ctx.hostWorkspace)} does not match flow workspace ${JSON.stringify(root)}`);
|
|
47635
|
+
}
|
|
47636
|
+
return { ok: true };
|
|
47637
|
+
};
|
|
47638
|
+
var MAX_CLOCK_SKEW_MS = 60000;
|
|
47639
|
+
var MAX_RECEIPTS_PER_SESSION = 10;
|
|
47640
|
+
var RECEIPT_FRESHNESS_MS = 10 * 60 * 1000;
|
|
47641
|
+
var EVIDENCE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
47642
|
+
var NEGATIVE_ANSWER_LABELS = [
|
|
47643
|
+
"no",
|
|
47644
|
+
"nope",
|
|
47645
|
+
"nah",
|
|
47646
|
+
"reject",
|
|
47647
|
+
"cancel",
|
|
47648
|
+
"decline",
|
|
47649
|
+
"not now",
|
|
47650
|
+
"not yet",
|
|
47651
|
+
"skip",
|
|
47652
|
+
"back",
|
|
47653
|
+
"deny"
|
|
47654
|
+
];
|
|
47655
|
+
var isNegativeLabel = (label) => {
|
|
47656
|
+
const normalized = label.trim().toLowerCase();
|
|
47657
|
+
return NEGATIVE_ANSWER_LABELS.some((entry) => {
|
|
47658
|
+
const firstWord = normalized.split(/\s+/)[0] ?? "";
|
|
47659
|
+
if (entry.includes(" ")) {
|
|
47660
|
+
const plain = normalized.replace(/[^a-z ]/g, "");
|
|
47661
|
+
return plain === entry || plain.startsWith(`${entry} `);
|
|
47662
|
+
}
|
|
47663
|
+
return firstWord.replace(/[^a-z]/g, "") === entry;
|
|
47664
|
+
});
|
|
47665
|
+
};
|
|
47666
|
+
|
|
47667
|
+
class HostReceiptStore {
|
|
47668
|
+
#bySession = new Map;
|
|
47669
|
+
record(sessionId, callID, selectedLabel, recordedAt = Date.now(), question) {
|
|
47670
|
+
const label = selectedLabel.trim();
|
|
47671
|
+
if (!label)
|
|
47672
|
+
return;
|
|
47673
|
+
if (recordedAt > Date.now() + MAX_CLOCK_SKEW_MS)
|
|
47674
|
+
return;
|
|
47675
|
+
const queue = this.#bySession.get(sessionId) ?? [];
|
|
47676
|
+
if (queue.length >= MAX_RECEIPTS_PER_SESSION)
|
|
47677
|
+
queue.shift();
|
|
47678
|
+
queue.push({ sessionId, callID, selectedLabel: label, recordedAt, question });
|
|
47679
|
+
this.#bySession.set(sessionId, queue);
|
|
47680
|
+
}
|
|
47681
|
+
count(sessionId) {
|
|
47682
|
+
return this.#bySession.get(sessionId)?.length ?? 0;
|
|
47683
|
+
}
|
|
47684
|
+
peek(sessionId, opts = {}) {
|
|
47685
|
+
return this.#take(sessionId, opts, false);
|
|
47686
|
+
}
|
|
47687
|
+
consume(sessionId, opts = {}) {
|
|
47688
|
+
return this.#take(sessionId, opts, true);
|
|
47689
|
+
}
|
|
47690
|
+
#take(sessionId, opts, remove2) {
|
|
47691
|
+
const queue = this.#bySession.get(sessionId);
|
|
47692
|
+
if (!queue || queue.length === 0) {
|
|
47693
|
+
return err2("receipt_missing", "no host-observed native-question receipt for this session — ask the native " + "`question` tool and have the user answer before calling this tool");
|
|
47694
|
+
}
|
|
47695
|
+
const index = queue.length - 1;
|
|
47696
|
+
const receipt = queue[index];
|
|
47697
|
+
if (isNegativeLabel(receipt.selectedLabel)) {
|
|
47698
|
+
this.#bySession.delete(sessionId);
|
|
47699
|
+
return err2("receipt_rejected", `the user's most recent answer (${JSON.stringify(receipt.selectedLabel)}) is a ` + "negative answer — it cannot authorize an approval; ask the native question again");
|
|
47700
|
+
}
|
|
47701
|
+
if (opts.label !== undefined && !sameChoiceLabel(receipt.selectedLabel, opts.label)) {
|
|
47702
|
+
return err2("evidence_mismatch", `receipt selectedLabel does not match ${JSON.stringify(opts.label)} — fabricated menu choice rejected`);
|
|
47703
|
+
}
|
|
47704
|
+
if (Date.now() - receipt.recordedAt > RECEIPT_FRESHNESS_MS) {
|
|
47705
|
+
if (remove2) {
|
|
47706
|
+
queue.splice(index, 1);
|
|
47707
|
+
if (queue.length === 0)
|
|
47708
|
+
this.#bySession.delete(sessionId);
|
|
47709
|
+
}
|
|
47710
|
+
return err2("receipt_stale", "the question receipt is too old — ask the native question again and re-answer");
|
|
47711
|
+
}
|
|
47712
|
+
if (remove2) {
|
|
47713
|
+
queue.splice(index, 1);
|
|
47714
|
+
if (queue.length === 0)
|
|
47715
|
+
this.#bySession.delete(sessionId);
|
|
47716
|
+
}
|
|
47717
|
+
return { ok: true, receipt };
|
|
47718
|
+
}
|
|
47719
|
+
}
|
|
47720
|
+
var sameChoiceLabel = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
47721
|
+
var CURSOR_KEYS = ["attested", "confirmation", "host"];
|
|
47722
|
+
var assertEvidenceShape = (input) => {
|
|
47723
|
+
if (typeof input !== "object" || input === null) {
|
|
47724
|
+
return {
|
|
47725
|
+
ok: false,
|
|
47726
|
+
error: "native choice evidence required — bare booleans and other primitives are not approval evidence"
|
|
47727
|
+
};
|
|
47728
|
+
}
|
|
47729
|
+
const record = input;
|
|
47730
|
+
if (record.host === "cursor") {
|
|
47731
|
+
if (record.attested !== false || record.confirmation !== "contract") {
|
|
47732
|
+
return {
|
|
47733
|
+
ok: false,
|
|
47734
|
+
error: 'cursor confirmations are policy-only: exactly { host: "cursor", attested: false, confirmation: "contract" } — Cursor cannot attest a host-observed answer'
|
|
47735
|
+
};
|
|
47736
|
+
}
|
|
47737
|
+
const keys2 = Object.keys(record).sort();
|
|
47738
|
+
if (keys2.length !== CURSOR_KEYS.length || !CURSOR_KEYS.every((key) => keys2.includes(key))) {
|
|
47739
|
+
return {
|
|
47740
|
+
ok: false,
|
|
47741
|
+
error: "cursor confirmations carry no caller-supplied question data — the attested: false constant only"
|
|
47742
|
+
};
|
|
47743
|
+
}
|
|
47744
|
+
return { ok: true, evidence: { host: "cursor", attested: false, confirmation: "contract" } };
|
|
47745
|
+
}
|
|
47746
|
+
if (record.host !== "opencode") {
|
|
47747
|
+
return {
|
|
47748
|
+
ok: false,
|
|
47749
|
+
error: `evidence host must be 'opencode' or 'cursor', got ${JSON.stringify(record.host)}`
|
|
47750
|
+
};
|
|
47751
|
+
}
|
|
47752
|
+
if (record.attested !== true) {
|
|
47753
|
+
return {
|
|
47754
|
+
ok: false,
|
|
47755
|
+
error: "opencode evidence requires host attestation (attested: true) — only host-observed question receipts are accepted"
|
|
47756
|
+
};
|
|
47757
|
+
}
|
|
47758
|
+
const { callID, selectedLabel, recordedAt } = record;
|
|
47759
|
+
if (typeof callID !== "string" || callID.trim() === "") {
|
|
47760
|
+
return {
|
|
47761
|
+
ok: false,
|
|
47762
|
+
error: "opencode evidence callID must be a non-empty string (host question tool call)"
|
|
47763
|
+
};
|
|
47764
|
+
}
|
|
47765
|
+
if (typeof selectedLabel !== "string" || selectedLabel.trim() === "") {
|
|
47766
|
+
return {
|
|
47767
|
+
ok: false,
|
|
47768
|
+
error: "opencode evidence selectedLabel must be the exact label the user selected on the native question"
|
|
47769
|
+
};
|
|
47770
|
+
}
|
|
47771
|
+
if (typeof recordedAt !== "number" || !Number.isFinite(recordedAt) || recordedAt <= 0) {
|
|
47772
|
+
return {
|
|
47773
|
+
ok: false,
|
|
47774
|
+
error: "opencode evidence recordedAt must be a positive epoch-ms timestamp"
|
|
47775
|
+
};
|
|
47776
|
+
}
|
|
47777
|
+
const now2 = Date.now();
|
|
47778
|
+
if (recordedAt > now2 + MAX_CLOCK_SKEW_MS) {
|
|
47779
|
+
return {
|
|
47780
|
+
ok: false,
|
|
47781
|
+
error: "opencode evidence recordedAt is in the future — forged evidence is rejected"
|
|
47782
|
+
};
|
|
47783
|
+
}
|
|
47784
|
+
if (now2 - recordedAt > EVIDENCE_WINDOW_MS) {
|
|
47785
|
+
return {
|
|
47786
|
+
ok: false,
|
|
47787
|
+
error: "opencode evidence recordedAt is too old — ask the native question again and re-record the answer"
|
|
47788
|
+
};
|
|
47789
|
+
}
|
|
47790
|
+
return {
|
|
47791
|
+
ok: true,
|
|
47792
|
+
evidence: {
|
|
47793
|
+
host: "opencode",
|
|
47794
|
+
attested: true,
|
|
47795
|
+
callID: callID.trim(),
|
|
47796
|
+
selectedLabel: selectedLabel.trim(),
|
|
47797
|
+
recordedAt
|
|
47798
|
+
}
|
|
47799
|
+
};
|
|
47800
|
+
};
|
|
47801
|
+
var markHandoffDestination = (root, slug, planPath) => {
|
|
47802
|
+
const doc = resolveDoc(root, slug, planPath, "plan");
|
|
47803
|
+
if (!doc.ok)
|
|
47804
|
+
return err2("path_invalid", doc.error);
|
|
47805
|
+
return readModifyWrite(root, slug, (state) => {
|
|
47806
|
+
if (state.spec.status !== "approved")
|
|
47807
|
+
return err2("spec_not_approved", "spec must be approved before marking a handoff destination");
|
|
47808
|
+
if (state.plan.status !== "approved")
|
|
47809
|
+
return err2("plan_not_approved", "plan must be approved before marking a handoff destination");
|
|
47810
|
+
if (state.handoff_destination) {
|
|
47811
|
+
return err2("recursive_handoff", "this flow is already a handoff destination — a second handoff is rejected");
|
|
47812
|
+
}
|
|
47813
|
+
if (state.menu.chosen !== "handoff") {
|
|
47814
|
+
return err2("handoff_not_chosen", `source menu choice must be "handoff" to mark a handoff destination (chosen: ${JSON.stringify(state.menu.chosen)})`);
|
|
47815
|
+
}
|
|
47816
|
+
return {
|
|
47817
|
+
ok: true,
|
|
47818
|
+
next: {
|
|
47819
|
+
...state,
|
|
47820
|
+
handoff_destination: true,
|
|
47821
|
+
menu: { presented: false, chosen: "", evidence: null },
|
|
47822
|
+
updated_at: Date.now()
|
|
47823
|
+
}
|
|
47824
|
+
};
|
|
47825
|
+
});
|
|
47826
|
+
};
|
|
47827
|
+
var CLI_CONFIRMATION_KEYS = ["attested", "confirmation", "host"];
|
|
47828
|
+
var validateLifecycleEvidence = (input) => {
|
|
47829
|
+
if (typeof input !== "object" || input === null) {
|
|
47830
|
+
return {
|
|
47831
|
+
ok: false,
|
|
47832
|
+
error: "lifecycle evidence required — native choice evidence or an exact CLI confirmation"
|
|
47833
|
+
};
|
|
47834
|
+
}
|
|
47835
|
+
const record = input;
|
|
47836
|
+
if (record.host === "cli") {
|
|
47837
|
+
const validValue = record.attested === false && (record.confirmation === "flag" || record.confirmation === "tty");
|
|
47838
|
+
const keys2 = Object.keys(record).sort();
|
|
47839
|
+
const exactShape = keys2.length === CLI_CONFIRMATION_KEYS.length && CLI_CONFIRMATION_KEYS.every((key) => keys2.includes(key));
|
|
47840
|
+
if (validValue && exactShape) {
|
|
47841
|
+
return {
|
|
47842
|
+
ok: true,
|
|
47843
|
+
evidence: {
|
|
47844
|
+
host: "cli",
|
|
47845
|
+
attested: false,
|
|
47846
|
+
confirmation: record.confirmation
|
|
47847
|
+
}
|
|
47848
|
+
};
|
|
47849
|
+
}
|
|
47850
|
+
return {
|
|
47851
|
+
ok: false,
|
|
47852
|
+
error: 'cli confirmations accept only the exact { host: "cli", attested: false, confirmation: "flag" | "tty" } shape'
|
|
47853
|
+
};
|
|
47854
|
+
}
|
|
47855
|
+
return assertEvidenceShape(input);
|
|
47856
|
+
};
|
|
47857
|
+
var errPendingFlow = (action) => err2("flow_not_active", `cannot ${action} a pending flow — the execution menu has not started it`);
|
|
47858
|
+
var errCompletedFlow = (action) => err2("flow_already_completed", `cannot ${action} a completed flow`);
|
|
47859
|
+
var completeExecution = (root, slug, deps) => {
|
|
47860
|
+
const file = flowPath(root, slug);
|
|
47861
|
+
const captured = readEffectiveFlowState(root, slug);
|
|
47862
|
+
if (!captured.ok)
|
|
47863
|
+
return captured;
|
|
47864
|
+
const exec3 = captured.state.execution;
|
|
47865
|
+
if (exec3.status === "pending")
|
|
47866
|
+
return errPendingFlow("complete");
|
|
47867
|
+
if (exec3.status === "completed")
|
|
47868
|
+
return errCompletedFlow("complete");
|
|
47869
|
+
const ledger = ledgerCompletion(root, slug);
|
|
47870
|
+
if (!ledger.complete) {
|
|
47871
|
+
return err2("execution_incomplete", `execution ledger incomplete for ${slug}: missing tasks ${ledger.missing.join(", ")}`, { required: ledger.required, completed: ledger.completed, missing: ledger.missing });
|
|
47872
|
+
}
|
|
47873
|
+
const verifier = deps?.verifyProject ?? runVerifyProject;
|
|
47874
|
+
const verify = verifier(root, false);
|
|
47875
|
+
if (verify.exitCode !== 0) {
|
|
47876
|
+
return err2("verification_failed", `repository verification failed for ${slug} (exit ${verify.exitCode}) — see the verification output`, { exitCode: verify.exitCode });
|
|
47877
|
+
}
|
|
47878
|
+
const locked = withFlowLock(file, () => {
|
|
47879
|
+
const strict = readFlowStrict(root, slug);
|
|
47880
|
+
if (!strict.ok)
|
|
47881
|
+
return strict;
|
|
47882
|
+
const reconciled = reconcileState(root, slug, strict.state);
|
|
47883
|
+
const currentExec = reconciled.state.execution;
|
|
47884
|
+
if (currentExec.status !== exec3.status || currentExec.mode !== exec3.mode) {
|
|
47885
|
+
return err2("flow_concurrent_conflict", `concurrent execution state change detected for ${slug}: re-read the flow state and retry completion`);
|
|
47886
|
+
}
|
|
47887
|
+
const next = {
|
|
47888
|
+
...reconciled.state,
|
|
47889
|
+
execution: { ...exec3, status: "completed" },
|
|
47890
|
+
handoff_destination: false,
|
|
47891
|
+
updated_at: Date.now()
|
|
47892
|
+
};
|
|
47893
|
+
const commit = writeFlowStateIfCurrent(root, captured.state, next);
|
|
47894
|
+
if (commit.ok)
|
|
47895
|
+
return { ok: true };
|
|
47896
|
+
if ("io_error" in commit) {
|
|
47897
|
+
return err2("flow_io_error", `flow state write failed for ${slug}: ${commit.io_error}`);
|
|
47898
|
+
}
|
|
47899
|
+
return err2("flow_concurrent_conflict", `concurrent flow update detected for ${slug}: re-read the flow state and retry completion`);
|
|
47900
|
+
});
|
|
47901
|
+
if (!locked.locked)
|
|
47902
|
+
return locked.error;
|
|
47903
|
+
return locked.value;
|
|
47904
|
+
};
|
|
47905
|
+
var transitionExecution = (root, slug, planPath, action, evidence, ctx, deps) => {
|
|
47906
|
+
const bound = assertMutationWorkspace(root, ctx);
|
|
47907
|
+
if (!bound.ok)
|
|
47908
|
+
return bound;
|
|
47909
|
+
const validated = validateLifecycleEvidence(evidence);
|
|
47910
|
+
if (!validated.ok)
|
|
47911
|
+
return err2("evidence_invalid", validated.error);
|
|
47912
|
+
const doc = resolveDoc(root, slug, planPath, "plan");
|
|
47913
|
+
if (!doc.ok)
|
|
47914
|
+
return err2("path_invalid", doc.error);
|
|
47915
|
+
if (action === "complete")
|
|
47916
|
+
return completeExecution(root, slug, deps);
|
|
47917
|
+
if (action === "pause") {
|
|
47918
|
+
return readModifyWrite(root, slug, (state) => {
|
|
47919
|
+
const exec3 = state.execution;
|
|
47920
|
+
if (exec3.status === "pending")
|
|
47921
|
+
return errPendingFlow("pause");
|
|
47922
|
+
if (exec3.status === "completed")
|
|
47923
|
+
return errCompletedFlow("pause");
|
|
47924
|
+
if (exec3.status === "paused")
|
|
47925
|
+
return err2("flow_already_paused", "flow is already paused");
|
|
47926
|
+
return {
|
|
47927
|
+
ok: true,
|
|
47928
|
+
next: { ...state, execution: { ...exec3, status: "paused" }, updated_at: Date.now() }
|
|
47929
|
+
};
|
|
47930
|
+
});
|
|
47931
|
+
}
|
|
47932
|
+
return readModifyWrite(root, slug, (state) => {
|
|
47933
|
+
const exec3 = state.execution;
|
|
47934
|
+
if (exec3.status === "completed")
|
|
47935
|
+
return errCompletedFlow("resume");
|
|
47936
|
+
if (exec3.status !== "paused") {
|
|
47937
|
+
return err2("flow_not_paused", exec3.status === "active" ? "flow is already active — cannot resume" : "cannot resume a pending flow — the execution menu has not started it");
|
|
47938
|
+
}
|
|
47939
|
+
return {
|
|
47940
|
+
ok: true,
|
|
47941
|
+
next: { ...state, execution: { ...exec3, status: "active" }, updated_at: Date.now() }
|
|
47942
|
+
};
|
|
47943
|
+
});
|
|
47944
|
+
};
|
|
47945
|
+
var slugFromPath = (p) => {
|
|
47946
|
+
const dirName = path18.basename(path18.dirname(p));
|
|
47947
|
+
return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
|
|
47948
|
+
};
|
|
47949
|
+
var BASH_READ_TOKENS = new Set([
|
|
47950
|
+
"cat",
|
|
47951
|
+
"head",
|
|
47952
|
+
"tail",
|
|
47953
|
+
"less",
|
|
47954
|
+
"more",
|
|
47955
|
+
"grep",
|
|
47956
|
+
"rg",
|
|
47957
|
+
"ag",
|
|
47958
|
+
"find",
|
|
47959
|
+
"ls",
|
|
47960
|
+
"stat",
|
|
47961
|
+
"wc",
|
|
47962
|
+
"file",
|
|
47963
|
+
"diff",
|
|
47964
|
+
"sort",
|
|
47965
|
+
"uniq",
|
|
47966
|
+
"cut",
|
|
47967
|
+
"tr",
|
|
47968
|
+
"fold",
|
|
47969
|
+
"printf",
|
|
47970
|
+
"echo",
|
|
47971
|
+
"pwd",
|
|
47972
|
+
"date",
|
|
47973
|
+
"which",
|
|
47974
|
+
"type",
|
|
47975
|
+
"du",
|
|
47976
|
+
"df",
|
|
47977
|
+
"tree",
|
|
47978
|
+
"jq",
|
|
47979
|
+
"basename",
|
|
47980
|
+
"dirname",
|
|
47981
|
+
"realpath",
|
|
47982
|
+
"readlink",
|
|
47983
|
+
"rev",
|
|
47984
|
+
"comm",
|
|
47985
|
+
"paste",
|
|
47986
|
+
"nl",
|
|
47987
|
+
"od",
|
|
47988
|
+
"xxd",
|
|
47989
|
+
"awk",
|
|
47990
|
+
"gawk",
|
|
47991
|
+
"mawk",
|
|
47992
|
+
"test",
|
|
47993
|
+
"["
|
|
47994
|
+
]);
|
|
47995
|
+
var BASH_GIT_READ_SUBCOMMANDS = new Set([
|
|
47996
|
+
"status",
|
|
47997
|
+
"log",
|
|
47998
|
+
"diff",
|
|
47999
|
+
"show",
|
|
48000
|
+
"branch",
|
|
48001
|
+
"rev-parse",
|
|
48002
|
+
"merge-base",
|
|
48003
|
+
"remote",
|
|
48004
|
+
"ls-files",
|
|
48005
|
+
"blame",
|
|
48006
|
+
"shortlog",
|
|
48007
|
+
"describe",
|
|
48008
|
+
"check-ignore",
|
|
48009
|
+
"name-rev",
|
|
48010
|
+
"stash",
|
|
48011
|
+
"grep",
|
|
48012
|
+
"tag"
|
|
48013
|
+
]);
|
|
48014
|
+
var BASH_GIT_MUTABLE_SUBCOMMANDS = new Set(["branch", "remote", "tag"]);
|
|
48015
|
+
var BASH_GIT_READ_FLAGS = {
|
|
48016
|
+
branch: new Set([
|
|
48017
|
+
"-a",
|
|
48018
|
+
"--all",
|
|
48019
|
+
"-r",
|
|
48020
|
+
"--remotes",
|
|
48021
|
+
"-v",
|
|
48022
|
+
"--verbose",
|
|
48023
|
+
"-vv",
|
|
48024
|
+
"--show-current",
|
|
48025
|
+
"-l",
|
|
48026
|
+
"--list",
|
|
48027
|
+
"--merged",
|
|
48028
|
+
"--no-merged",
|
|
48029
|
+
"--contains",
|
|
48030
|
+
"--points-at",
|
|
48031
|
+
"--format",
|
|
48032
|
+
"--sort"
|
|
48033
|
+
]),
|
|
48034
|
+
tag: new Set([
|
|
48035
|
+
"-l",
|
|
48036
|
+
"--list",
|
|
48037
|
+
"--sort",
|
|
48038
|
+
"--contains",
|
|
48039
|
+
"--points-at",
|
|
48040
|
+
"--merged",
|
|
48041
|
+
"--no-merged",
|
|
48042
|
+
"--format",
|
|
48043
|
+
"--column"
|
|
48044
|
+
]),
|
|
48045
|
+
remote: new Set(["-v", "--verbose"])
|
|
48046
|
+
};
|
|
48047
|
+
var BASH_FIND_DENIED_FLAGS = new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
|
|
48048
|
+
var BASH_TEST_VERBS = new Set([
|
|
48049
|
+
"test",
|
|
48050
|
+
"check",
|
|
48051
|
+
"lint",
|
|
48052
|
+
"typecheck",
|
|
48053
|
+
"verify",
|
|
48054
|
+
"validate",
|
|
48055
|
+
"vitest",
|
|
48056
|
+
"jest",
|
|
48057
|
+
"mocha"
|
|
48058
|
+
]);
|
|
48059
|
+
var BASH_DENIED_HEADS = new Set(["curl", "sudo", "tee", "wget"]);
|
|
48060
|
+
var BASH_OUTPUT_FLAG_VERBS = new Set(["sort", "tree", "comm", "diff", "jq"]);
|
|
48061
|
+
var BASH_PAREN_EXEMPT_HEADS = new Set(["echo", "printf", "jq"]);
|
|
48062
|
+
var BASH_GIT_VALUE_FLAGS = new Set([
|
|
48063
|
+
"--contains",
|
|
48064
|
+
"--points-at",
|
|
48065
|
+
"--merged",
|
|
48066
|
+
"--no-merged",
|
|
48067
|
+
"--sort",
|
|
48068
|
+
"--format"
|
|
48069
|
+
]);
|
|
48070
|
+
var COORDINATOR_SHELL_DENIED_TEXT = "Coordinator shell commands are restricted while a subagent-driven plan is " + "active: only bounded read/test/review commands are allowed (the exact " + "allowlist is in flow-state.ts, isCoordinatorBashAllowed). " + COORDINATOR_RECOVERY_TEXT;
|
|
48071
|
+
|
|
48072
|
+
// packages/workit-core/src/core/handoff-tools.ts
|
|
48073
|
+
import path20 from "node:path";
|
|
48074
|
+
|
|
48075
|
+
// packages/workit-core/src/core/handoff-context.ts
|
|
48076
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync8 } from "node:fs";
|
|
48077
|
+
import path19 from "node:path";
|
|
48078
|
+
var DOC_RE = /docs\/([A-Za-z0-9][A-Za-z0-9._-]*)\/(spec|plan)\.md/g;
|
|
48079
|
+
var listMd = (dir) => {
|
|
48080
|
+
try {
|
|
48081
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".md")).sort();
|
|
48082
|
+
} catch {
|
|
48083
|
+
return [];
|
|
48084
|
+
}
|
|
48085
|
+
};
|
|
48086
|
+
var extractMessagePaths = (message) => [...new Set(message.match(DOC_RE) ?? [])].sort();
|
|
48087
|
+
var resolveFromMessagePaths = (root, message) => {
|
|
48088
|
+
const paths = extractMessagePaths(message);
|
|
48089
|
+
if (paths.length === 0)
|
|
48090
|
+
return { error: "no paths" };
|
|
48091
|
+
const slugs = [...new Set(paths.map((p) => p.split("/")[1]))];
|
|
48092
|
+
if (slugs.length !== 1)
|
|
48093
|
+
return { error: "multiple features in message — use exactly one docs/<slug>/ pair" };
|
|
48094
|
+
const slug = slugs[0];
|
|
48095
|
+
const plan = `docs/${slug}/plan.md`;
|
|
48096
|
+
const spec = `docs/${slug}/spec.md`;
|
|
48097
|
+
if (!existsSync15(path19.join(root, plan)) || !existsSync15(path19.join(root, spec))) {
|
|
48098
|
+
return { error: `docs/${slug}/ must contain both plan.md and spec.md` };
|
|
48099
|
+
}
|
|
48100
|
+
return { spec, plan, source: "message_paths" };
|
|
48101
|
+
};
|
|
48102
|
+
var resolveActivePair = (root) => {
|
|
48103
|
+
const docsDir = path19.join(root, "docs");
|
|
48104
|
+
let best = null;
|
|
48105
|
+
let entries = [];
|
|
48106
|
+
try {
|
|
48107
|
+
entries = readdirSync5(docsDir);
|
|
48108
|
+
} catch {
|
|
48109
|
+
return { error: "no pair" };
|
|
48110
|
+
}
|
|
48111
|
+
for (const slug of entries) {
|
|
48112
|
+
if (slug.startsWith("."))
|
|
48113
|
+
continue;
|
|
48114
|
+
const plan = path19.join("docs", slug, "plan.md");
|
|
48115
|
+
const spec = path19.join("docs", slug, "spec.md");
|
|
48116
|
+
if (!existsSync15(path19.join(root, plan)) || !existsSync15(path19.join(root, spec)))
|
|
48117
|
+
continue;
|
|
48118
|
+
const score = Math.max(statSync8(path19.join(root, spec)).mtimeMs, statSync8(path19.join(root, plan)).mtimeMs);
|
|
48119
|
+
if (best === null || score > best.score || score === best.score && slug < best.spec.split("/")[1]) {
|
|
48120
|
+
best = { score, spec, plan, source: "active_pair" };
|
|
48121
|
+
}
|
|
48122
|
+
}
|
|
48123
|
+
if (best === null)
|
|
48124
|
+
return { error: "no pair" };
|
|
48125
|
+
return { spec: best.spec, plan: best.plan, source: best.source };
|
|
48126
|
+
};
|
|
48127
|
+
var resolveWorkflowPaths = (root, message) => {
|
|
48128
|
+
const fromMessage = resolveFromMessagePaths(root, message);
|
|
48129
|
+
if (!("error" in fromMessage))
|
|
48130
|
+
return fromMessage;
|
|
48131
|
+
if (fromMessage.error !== "no paths")
|
|
48132
|
+
return fromMessage;
|
|
48133
|
+
const active = resolveActivePair(root);
|
|
48134
|
+
if (!("error" in active))
|
|
48135
|
+
return active;
|
|
48136
|
+
const docsDir = path19.join(root, "docs");
|
|
48137
|
+
if (!existsSync15(docsDir) || listMd(docsDir).length === 0) {
|
|
48138
|
+
return { error: "no docs/<slug>/ features found under docs/" };
|
|
48139
|
+
}
|
|
48140
|
+
return {
|
|
48141
|
+
error: "could not resolve spec and plan — mention docs/<slug>/plan.md or create docs/<slug>/{spec.md,plan.md}"
|
|
48142
|
+
};
|
|
48143
|
+
};
|
|
48144
|
+
var buildHandoffContract = ({
|
|
48145
|
+
root,
|
|
48146
|
+
spec,
|
|
48147
|
+
plan,
|
|
48148
|
+
templatePath
|
|
48149
|
+
}) => {
|
|
48150
|
+
const validated = docsValidate({ spec_path: spec, plan_path: plan, workspace_root: root });
|
|
48151
|
+
if (validated.ok === false) {
|
|
48152
|
+
return {
|
|
48153
|
+
error: `docs validation failed
|
|
48154
|
+
${JSON.stringify({ ok: false, errors: validated.errors })}`
|
|
48155
|
+
};
|
|
48156
|
+
}
|
|
48157
|
+
const branchResolved = resolveBranch({ spec_path: spec, plan_path: plan, workspace_root: root });
|
|
48158
|
+
if ("error" in branchResolved)
|
|
48159
|
+
return { error: branchResolved.error };
|
|
48160
|
+
const branch = branchResolved.branch;
|
|
48161
|
+
const slug = path19.basename(path19.dirname(plan));
|
|
48162
|
+
const sddDir = `docs/${slug}/sdd`;
|
|
48163
|
+
const planText = readFileSync14(path19.join(root, plan), "utf8");
|
|
48164
|
+
const tasks = parseTasksFromPlan(planText);
|
|
48165
|
+
const taskList = tasks.map((t) => `- Task ${t.id}: ${t.title}`).join(`
|
|
48166
|
+
`);
|
|
48167
|
+
let contract;
|
|
48168
|
+
try {
|
|
48169
|
+
contract = readFileSync14(templatePath, "utf8");
|
|
48170
|
+
} catch {
|
|
48171
|
+
return { error: "missing template templates/execution-contract.md" };
|
|
48172
|
+
}
|
|
48173
|
+
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);
|
|
48174
|
+
if (!/^<workflow-handoff-destination>true<\/workflow-handoff-destination>$/m.test(contract)) {
|
|
48175
|
+
return { error: "handoff destination contract missing its destination marker" };
|
|
48176
|
+
}
|
|
48177
|
+
return { prompt: contract };
|
|
48178
|
+
};
|
|
48179
|
+
|
|
48180
|
+
// packages/workit-core/src/core/handoff-tools.ts
|
|
48181
|
+
var buildHandoffPrompt = (root, message) => {
|
|
48182
|
+
const resolved = resolveWorkflowPaths(root, message);
|
|
48183
|
+
if ("error" in resolved)
|
|
48184
|
+
return { error: resolved.error };
|
|
48185
|
+
const templatePath = path20.join(assetRoot(), "templates", "execution-contract.md");
|
|
48186
|
+
const contract = buildHandoffContract({
|
|
48187
|
+
root,
|
|
48188
|
+
spec: resolved.spec,
|
|
48189
|
+
plan: resolved.plan,
|
|
48190
|
+
templatePath
|
|
48191
|
+
});
|
|
48192
|
+
if ("error" in contract)
|
|
48193
|
+
return { error: contract.error };
|
|
48194
|
+
const sdd = `docs/${path20.basename(path20.dirname(resolved.plan))}/sdd`;
|
|
48195
|
+
return { prompt: contract.prompt, spec: resolved.spec, plan: resolved.plan, sdd };
|
|
48196
|
+
};
|
|
48197
|
+
|
|
48198
|
+
// packages/workit-cli/src/flow.ts
|
|
48199
|
+
var FLOW_ACTIONS = ["status", "pause", "resume", "complete"];
|
|
48200
|
+
var COMMANDS = {
|
|
48201
|
+
status: "workit flow status --plan <path>",
|
|
48202
|
+
pause: "workit flow pause --plan <path> [--confirm]",
|
|
48203
|
+
resume: "workit flow resume --plan <path> [--confirm]",
|
|
48204
|
+
complete: "workit flow complete --plan <path> [--confirm]",
|
|
48205
|
+
handoff: "workit handoff --message <text>"
|
|
48206
|
+
};
|
|
48207
|
+
var FLOW_COMMANDS = {
|
|
48208
|
+
status: COMMANDS.status,
|
|
48209
|
+
pause: COMMANDS.pause,
|
|
48210
|
+
resume: COMMANDS.resume,
|
|
48211
|
+
complete: COMMANDS.complete
|
|
48212
|
+
};
|
|
48213
|
+
var CLI_FLAG_EVIDENCE = { host: "cli", attested: false, confirmation: "flag" };
|
|
48214
|
+
var CLI_TTY_EVIDENCE = { host: "cli", attested: false, confirmation: "tty" };
|
|
48215
|
+
var defaultIsTTY = () => process.stdin.isTTY === true;
|
|
48216
|
+
var defaultConfirm = async (out) => {
|
|
48217
|
+
const rl = createInterface({
|
|
48218
|
+
input: process.stdin,
|
|
48219
|
+
output: out ?? process.stdout
|
|
48220
|
+
});
|
|
48221
|
+
try {
|
|
48222
|
+
const answer = await rl.question("Proceed? [y/N] ");
|
|
48223
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
48224
|
+
} finally {
|
|
48225
|
+
rl.close();
|
|
48226
|
+
}
|
|
48227
|
+
};
|
|
48228
|
+
var outStream = (deps) => deps.out ?? process.stdout;
|
|
48229
|
+
var errStream = (deps) => deps.err ?? process.stderr;
|
|
48230
|
+
var write = (stream, text) => stream.write(text.endsWith(`
|
|
48231
|
+
`) ? text : `${text}
|
|
48232
|
+
`);
|
|
48233
|
+
var writeJSON = (stream, value) => write(stream, JSON.stringify(value, null, 2));
|
|
48234
|
+
var isTTY = (deps) => deps.stdinIsTTY === undefined ? defaultIsTTY() : deps.stdinIsTTY();
|
|
48235
|
+
var workspaceRoot = (deps) => process.env.WORKFLOW_WORKSPACE_ROOT ?? deps.cwd ?? process.cwd();
|
|
48236
|
+
var domainFail = (err3, code, error, details) => {
|
|
48237
|
+
writeJSON(err3, { ok: false, error, code, ...details ? { details } : {} });
|
|
48238
|
+
return 1;
|
|
48239
|
+
};
|
|
48240
|
+
var usage = (err3, text) => {
|
|
48241
|
+
write(err3, text);
|
|
48242
|
+
return 2;
|
|
48243
|
+
};
|
|
48244
|
+
function parseFlowFlags(action, argv, err3) {
|
|
48245
|
+
let plan;
|
|
48246
|
+
let confirm = false;
|
|
48247
|
+
for (let i = 0;i < argv.length; i++) {
|
|
48248
|
+
const token = argv[i];
|
|
48249
|
+
if (token === "--plan") {
|
|
48250
|
+
if (plan !== undefined) {
|
|
48251
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — duplicate --plan flag`);
|
|
48252
|
+
return { ok: false };
|
|
48253
|
+
}
|
|
48254
|
+
const value = argv[i + 1];
|
|
48255
|
+
if (value === undefined || value.trim() === "") {
|
|
48256
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — --plan requires a non-empty path`);
|
|
48257
|
+
return { ok: false };
|
|
48258
|
+
}
|
|
48259
|
+
if (value.startsWith("--")) {
|
|
48260
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${value}`);
|
|
48261
|
+
return { ok: false };
|
|
48262
|
+
}
|
|
48263
|
+
plan = value;
|
|
48264
|
+
i += 1;
|
|
48265
|
+
} else if (token === "--confirm") {
|
|
48266
|
+
if (action === "status") {
|
|
48267
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — status is read-only and accepts no --confirm`);
|
|
48268
|
+
return { ok: false };
|
|
48269
|
+
}
|
|
48270
|
+
if (confirm) {
|
|
48271
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — duplicate --confirm flag`);
|
|
48272
|
+
return { ok: false };
|
|
48273
|
+
}
|
|
48274
|
+
confirm = true;
|
|
48275
|
+
} else {
|
|
48276
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — unknown flag: ${token}`);
|
|
48277
|
+
return { ok: false };
|
|
48278
|
+
}
|
|
48279
|
+
}
|
|
48280
|
+
if (plan === undefined) {
|
|
48281
|
+
usage(err3, `usage: ${FLOW_COMMANDS[action]} — --plan <path> required`);
|
|
48282
|
+
return { ok: false };
|
|
48283
|
+
}
|
|
48284
|
+
return { ok: true, parsed: { plan, confirm } };
|
|
48285
|
+
}
|
|
48286
|
+
async function resolveConfirmation(deps, confirmFlag, out, err3) {
|
|
48287
|
+
if (confirmFlag)
|
|
48288
|
+
return CLI_FLAG_EVIDENCE;
|
|
48289
|
+
if (!isTTY(deps)) {
|
|
48290
|
+
usage(err3, "--confirm required when stdin is not a TTY");
|
|
48291
|
+
return null;
|
|
48292
|
+
}
|
|
48293
|
+
const answered = deps.confirm ? await deps.confirm() : await defaultConfirm(out);
|
|
48294
|
+
if (!answered) {
|
|
48295
|
+
usage(err3, "cancelled — no confirmation");
|
|
48296
|
+
return null;
|
|
48297
|
+
}
|
|
48298
|
+
return CLI_TTY_EVIDENCE;
|
|
48299
|
+
}
|
|
48300
|
+
function statusCommand(root, plan, out, err3) {
|
|
48301
|
+
const resolved = resolveCanonicalLayout({ workspace_root: root, plan_path: plan });
|
|
48302
|
+
if (!resolved.ok)
|
|
48303
|
+
return domainFail(err3, "path_invalid", resolved.error);
|
|
48304
|
+
const effective = readEffectiveFlowState(root, resolved.layout.slug);
|
|
48305
|
+
if (!effective.ok)
|
|
48306
|
+
return domainFail(err3, effective.code, effective.error, effective.details);
|
|
48307
|
+
const { state, drift } = effective;
|
|
48308
|
+
writeJSON(out, {
|
|
48309
|
+
ok: true,
|
|
48310
|
+
slug: state.slug,
|
|
48311
|
+
spec: state.spec,
|
|
48312
|
+
plan: state.plan,
|
|
48313
|
+
menu: state.menu,
|
|
48314
|
+
execution: state.execution,
|
|
48315
|
+
handoff_destination: state.handoff_destination,
|
|
48316
|
+
drift,
|
|
48317
|
+
flow_path: path21.posix.join("docs", state.slug, "sdd", "flow.json")
|
|
48318
|
+
});
|
|
48319
|
+
return 0;
|
|
48320
|
+
}
|
|
48321
|
+
function mutateCommand(root, plan, action, evidence, deps, out, err3) {
|
|
48322
|
+
const resolved = resolveCanonicalLayout({ workspace_root: root, plan_path: plan });
|
|
48323
|
+
if (!resolved.ok)
|
|
48324
|
+
return domainFail(err3, "path_invalid", resolved.error);
|
|
48325
|
+
const result2 = transitionExecution(root, resolved.layout.slug, plan, action, evidence, undefined, deps.verifyProject ? { verifyProject: deps.verifyProject } : undefined);
|
|
48326
|
+
if (!result2.ok)
|
|
48327
|
+
return domainFail(err3, result2.code, result2.error, result2.details);
|
|
48328
|
+
const effective = readEffectiveFlowState(root, resolved.layout.slug);
|
|
48329
|
+
if (!effective.ok)
|
|
48330
|
+
return domainFail(err3, effective.code, effective.error, effective.details);
|
|
48331
|
+
writeJSON(out, {
|
|
48332
|
+
ok: true,
|
|
48333
|
+
plan,
|
|
48334
|
+
execution: effective.state.execution,
|
|
48335
|
+
drift: effective.drift
|
|
48336
|
+
});
|
|
48337
|
+
return 0;
|
|
48338
|
+
}
|
|
48339
|
+
async function runFlowCommand(argv, deps = {}) {
|
|
48340
|
+
const out = outStream(deps);
|
|
48341
|
+
const err3 = errStream(deps);
|
|
48342
|
+
const [action, ...rest2] = argv;
|
|
48343
|
+
if (!action || !FLOW_ACTIONS.includes(action)) {
|
|
48344
|
+
return usage(err3, "usage: workit flow <status|pause|resume|complete> --plan <path> [--confirm]");
|
|
48345
|
+
}
|
|
48346
|
+
const flowAction = action;
|
|
48347
|
+
const parsed = parseFlowFlags(flowAction, rest2, err3);
|
|
48348
|
+
if (!parsed.ok)
|
|
48349
|
+
return 2;
|
|
48350
|
+
const root = workspaceRoot(deps);
|
|
48351
|
+
if (flowAction === "status") {
|
|
48352
|
+
return statusCommand(root, parsed.parsed.plan, out, err3);
|
|
48353
|
+
}
|
|
48354
|
+
const evidence = await resolveConfirmation(deps, parsed.parsed.confirm, out, err3);
|
|
48355
|
+
if (evidence === null)
|
|
48356
|
+
return 2;
|
|
48357
|
+
return mutateCommand(root, parsed.parsed.plan, flowAction, evidence, deps, out, err3);
|
|
48358
|
+
}
|
|
48359
|
+
async function runHandoffCommand(argv, deps = {}) {
|
|
48360
|
+
const out = outStream(deps);
|
|
48361
|
+
const err3 = errStream(deps);
|
|
48362
|
+
const [flag, value, ...rest2] = argv;
|
|
48363
|
+
if (flag !== "--message" || value === undefined || value.trim() === "" || rest2.length > 0) {
|
|
48364
|
+
return usage(err3, `usage: ${COMMANDS.handoff}`);
|
|
48365
|
+
}
|
|
48366
|
+
const root = workspaceRoot(deps);
|
|
48367
|
+
const built = buildHandoffPrompt(root, value.trim());
|
|
48368
|
+
if ("error" in built) {
|
|
48369
|
+
writeJSON(err3, { ok: false, error: built.error, code: "handoff_build_failed" });
|
|
48370
|
+
return 1;
|
|
48371
|
+
}
|
|
48372
|
+
const marked = markHandoffDestination(root, slugFromPath(built.plan), built.plan);
|
|
48373
|
+
if (!marked.ok)
|
|
48374
|
+
return domainFail(err3, marked.code, marked.error, marked.details);
|
|
48375
|
+
write(out, built.prompt);
|
|
48376
|
+
return 0;
|
|
48377
|
+
}
|
|
48378
|
+
|
|
48379
|
+
// packages/workit-cli/src/index.tsx
|
|
48380
|
+
var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
|
|
48381
|
+
var logger = createLogger({
|
|
48382
|
+
stderr: (event) => {
|
|
48383
|
+
if (event.level === "debug" || event.level === "info")
|
|
48384
|
+
return;
|
|
48385
|
+
process.stderr.write(`${JSON.stringify(event)}
|
|
48386
|
+
`);
|
|
48387
|
+
}
|
|
48388
|
+
});
|
|
48389
|
+
var COMMAND_DESCRIPTIONS = [
|
|
48390
|
+
[COMMANDS.status, "Read the effective flow state for a plan"],
|
|
48391
|
+
[COMMANDS.pause, "Pause an active plan"],
|
|
48392
|
+
[COMMANDS.resume, "Resume a paused plan"],
|
|
48393
|
+
[COMMANDS.complete, "Complete a plan (ledger and verification gated)"],
|
|
48394
|
+
[COMMANDS.handoff, "Emit the destination handoff prompt for a plan"]
|
|
48395
|
+
];
|
|
46171
48396
|
var HELP = `workit — workflow rails for agentic coding
|
|
46172
48397
|
|
|
46173
48398
|
Usage:
|
|
46174
48399
|
workit init Run the interactive setup wizard
|
|
46175
48400
|
workit doctor Verify the offline installation health (add --json for a machine-readable report)
|
|
48401
|
+
${COMMAND_DESCRIPTIONS.map(([cmd, desc]) => ` ${cmd.padEnd(49)}${desc}`).join(`
|
|
48402
|
+
`)}
|
|
46176
48403
|
workit Show this help
|
|
46177
48404
|
|
|
46178
48405
|
Run \`npx workit init\` to configure platforms, YouTrack, VCS and project hygiene.
|
|
@@ -46268,14 +48495,18 @@ if (__require.main == __require.module) {
|
|
|
46268
48495
|
setDiagnosticLogger(logger);
|
|
46269
48496
|
logger.info(EVENT.initialization, { host: "cli", command: subcommand });
|
|
46270
48497
|
process.on("unhandledRejection", (reason) => logger.error(EVENT.uncaughtFailure, { phase: "unhandledRejection", ...errorDetail(reason) }));
|
|
46271
|
-
process.on("uncaughtException", (
|
|
46272
|
-
logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(
|
|
48498
|
+
process.on("uncaughtException", (err3) => {
|
|
48499
|
+
logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(err3) });
|
|
46273
48500
|
process.exit(1);
|
|
46274
48501
|
});
|
|
46275
48502
|
if (subcommand === "init") {
|
|
46276
48503
|
await runInit();
|
|
46277
48504
|
} else if (subcommand === "doctor") {
|
|
46278
48505
|
runDoctorCommand(args);
|
|
48506
|
+
} else if (subcommand === "flow") {
|
|
48507
|
+
process.exit(await runFlowCommand(args.slice(1)));
|
|
48508
|
+
} else if (subcommand === "handoff") {
|
|
48509
|
+
process.exit(await runHandoffCommand(args.slice(1)));
|
|
46279
48510
|
} else {
|
|
46280
48511
|
console.log(HELP);
|
|
46281
48512
|
process.exit(0);
|