@brainervirus/workit-cli 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +2350 -143
- 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);
|
|
@@ -42972,7 +43044,7 @@ function cursorHooksEntry(_packageDir) {
|
|
|
42972
43044
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
42973
43045
|
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
42974
43046
|
import os4 from "node:os";
|
|
42975
|
-
import
|
|
43047
|
+
import path9 from "node:path";
|
|
42976
43048
|
|
|
42977
43049
|
// packages/workit-core/src/core/support-matrix.ts
|
|
42978
43050
|
var SUPPORT_MATRIX = {
|
|
@@ -42984,7 +43056,7 @@ var SUPPORT_MATRIX = {
|
|
|
42984
43056
|
|
|
42985
43057
|
// packages/workit-core/src/core/skill-manifests.ts
|
|
42986
43058
|
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
42987
|
-
import
|
|
43059
|
+
import path8 from "node:path";
|
|
42988
43060
|
var CANONICAL_SKILLS = {
|
|
42989
43061
|
superpowers: [
|
|
42990
43062
|
"brainstorming",
|
|
@@ -43017,7 +43089,7 @@ var CANONICAL_SKILLS = {
|
|
|
43017
43089
|
"wk-verify"
|
|
43018
43090
|
]
|
|
43019
43091
|
};
|
|
43020
|
-
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() : [];
|
|
43021
43093
|
var validateSkillManifests = (root, expected, label) => {
|
|
43022
43094
|
const actual = skillManifestNames(root);
|
|
43023
43095
|
const missing = expected.filter((name) => !actual.includes(name));
|
|
@@ -43025,10 +43097,10 @@ var validateSkillManifests = (root, expected, label) => {
|
|
|
43025
43097
|
return missing.length === 0 && extra.length === 0 ? null : `${label} mismatch at ${root} (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`;
|
|
43026
43098
|
};
|
|
43027
43099
|
var validateCursorSkills = (pluginDir) => {
|
|
43028
|
-
const workit = validateSkillManifests(
|
|
43100
|
+
const workit = validateSkillManifests(path8.join(pluginDir, "skills"), CANONICAL_SKILLS.workit, "Cursor Workit skills");
|
|
43029
43101
|
if (workit)
|
|
43030
43102
|
return workit;
|
|
43031
|
-
const vendor =
|
|
43103
|
+
const vendor = path8.join(pluginDir, "vendor/superpowers/skills");
|
|
43032
43104
|
const superpowers = validateSkillManifests(vendor, CANONICAL_SKILLS.superpowers, "Cursor Superpowers skills");
|
|
43033
43105
|
if (superpowers)
|
|
43034
43106
|
return superpowers;
|
|
@@ -43036,7 +43108,7 @@ var validateCursorSkills = (pluginDir) => {
|
|
|
43036
43108
|
while (pending.length > 0) {
|
|
43037
43109
|
const dir = pending.pop();
|
|
43038
43110
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
43039
|
-
const file =
|
|
43111
|
+
const file = path8.join(dir, entry.name);
|
|
43040
43112
|
if (entry.isDirectory())
|
|
43041
43113
|
pending.push(file);
|
|
43042
43114
|
else if ((statSync(file).mode & 73) !== 0 || readFileSync6(file).subarray(0, 2).toString("latin1") === "#!") {
|
|
@@ -43051,11 +43123,11 @@ if (false) {}
|
|
|
43051
43123
|
// packages/workit-core/src/core/doctor.ts
|
|
43052
43124
|
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
43053
43125
|
var findDevFromCwd = (cwd2) => {
|
|
43054
|
-
let dir =
|
|
43126
|
+
let dir = path9.resolve(cwd2);
|
|
43055
43127
|
while (true) {
|
|
43056
|
-
if (existsSync7(
|
|
43128
|
+
if (existsSync7(path9.join(dir, "packages", "workit-core", "package.json")))
|
|
43057
43129
|
return dir;
|
|
43058
|
-
const parent =
|
|
43130
|
+
const parent = path9.dirname(dir);
|
|
43059
43131
|
if (parent === dir)
|
|
43060
43132
|
return null;
|
|
43061
43133
|
dir = parent;
|
|
@@ -43064,8 +43136,8 @@ var findDevFromCwd = (cwd2) => {
|
|
|
43064
43136
|
var resolve = (options) => {
|
|
43065
43137
|
const env3 = options.env ?? process.env;
|
|
43066
43138
|
const home = options.home ?? env3.HOME ?? os4.homedir();
|
|
43067
|
-
const configDir2 = options.configDir ?? env3.WORKFLOW_TOOLKIT_CONFIG ?? env3.WORKFLOW_TOOLKIT_CONFIG_DIR ??
|
|
43068
|
-
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");
|
|
43069
43141
|
const cwd2 = options.cwd ?? process.cwd();
|
|
43070
43142
|
const dev = options.dev ?? env3.WORKFLOW_TOOLKIT_DEV ?? findDevFromCwd(cwd2);
|
|
43071
43143
|
return {
|
|
@@ -43075,10 +43147,10 @@ var resolve = (options) => {
|
|
|
43075
43147
|
stateDir,
|
|
43076
43148
|
cwd: cwd2,
|
|
43077
43149
|
dev,
|
|
43078
|
-
opencodeConfig: options.opencodeConfig ??
|
|
43079
|
-
cursorSettings: options.cursorSettings ??
|
|
43080
|
-
cursorMcp: options.cursorMcp ??
|
|
43081
|
-
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"),
|
|
43082
43154
|
env: env3,
|
|
43083
43155
|
installer: options.installer ?? false
|
|
43084
43156
|
};
|
|
@@ -43104,13 +43176,13 @@ var pluginEntries = (cfg) => {
|
|
|
43104
43176
|
return list.map(String).filter(isWorkitPlugin);
|
|
43105
43177
|
};
|
|
43106
43178
|
var commandOnPath = (name, env3) => {
|
|
43107
|
-
const dirs = (env3.PATH ?? process.env.PATH ?? "").split(
|
|
43179
|
+
const dirs = (env3.PATH ?? process.env.PATH ?? "").split(path9.delimiter);
|
|
43108
43180
|
const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
|
|
43109
43181
|
for (const dir of dirs) {
|
|
43110
43182
|
if (!dir)
|
|
43111
43183
|
continue;
|
|
43112
43184
|
for (const candidateName of names) {
|
|
43113
|
-
const candidate =
|
|
43185
|
+
const candidate = path9.join(dir, candidateName);
|
|
43114
43186
|
try {
|
|
43115
43187
|
const st = statSync2(candidate);
|
|
43116
43188
|
if (process.platform !== "win32" && (st.mode & 73) === 0)
|
|
@@ -43142,7 +43214,7 @@ var resolveBun = (env3) => {
|
|
|
43142
43214
|
if (env3.BUN && existsSync7(env3.BUN))
|
|
43143
43215
|
return env3.BUN;
|
|
43144
43216
|
for (const candidate of [
|
|
43145
|
-
|
|
43217
|
+
path9.join(os4.homedir(), ".bun/bin/bun"),
|
|
43146
43218
|
"/usr/local/bin/bun",
|
|
43147
43219
|
"/usr/bin/bun"
|
|
43148
43220
|
]) {
|
|
@@ -43196,7 +43268,7 @@ var checkVersions = (res) => {
|
|
|
43196
43268
|
detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping version parity"
|
|
43197
43269
|
};
|
|
43198
43270
|
}
|
|
43199
|
-
const corePkg = readJson(
|
|
43271
|
+
const corePkg = readJson(path9.join(res.dev, "packages/workit-core/package.json"));
|
|
43200
43272
|
if (!corePkg) {
|
|
43201
43273
|
return {
|
|
43202
43274
|
id: "versions",
|
|
@@ -43206,7 +43278,7 @@ var checkVersions = (res) => {
|
|
|
43206
43278
|
}
|
|
43207
43279
|
const refs = new Set;
|
|
43208
43280
|
for (const name of ["workit-opencode", "workit-cursor", "workit-cli"]) {
|
|
43209
|
-
const pkg = readJson(
|
|
43281
|
+
const pkg = readJson(path9.join(res.dev, "packages", name, "package.json"));
|
|
43210
43282
|
const dep = pkg?.dependencies?.["@brainervirus/workit-core"];
|
|
43211
43283
|
if (typeof dep === "string")
|
|
43212
43284
|
refs.add(dep);
|
|
@@ -43222,7 +43294,7 @@ var checkVersions = (res) => {
|
|
|
43222
43294
|
if (refs.size > 1) {
|
|
43223
43295
|
problems.push(`adapters pin different core versions: ${[...refs].join(", ")}`);
|
|
43224
43296
|
}
|
|
43225
|
-
const opencodePkg = readJson(
|
|
43297
|
+
const opencodePkg = readJson(path9.join(res.dev, "packages/workit-opencode/package.json"));
|
|
43226
43298
|
const sdk = opencodePkg?.dependencies?.["@opencode-ai/plugin"];
|
|
43227
43299
|
const sdkVersion = typeof sdk === "string" ? (sdk.match(/^\d+(?:\.\d+){0,2}/) ?? [])[0] : null;
|
|
43228
43300
|
if (sdkVersion && !semverAtLeast(sdkVersion, SUPPORT_MATRIX.opencode.minimum)) {
|
|
@@ -43243,23 +43315,23 @@ var checkVersions = (res) => {
|
|
|
43243
43315
|
};
|
|
43244
43316
|
};
|
|
43245
43317
|
var assetPathsFor = (host, dev) => {
|
|
43246
|
-
const pkg =
|
|
43318
|
+
const pkg = path9.join(dev, "packages", `workit-${host}`);
|
|
43247
43319
|
switch (host) {
|
|
43248
43320
|
case "opencode":
|
|
43249
43321
|
return [
|
|
43250
|
-
|
|
43251
|
-
|
|
43252
|
-
|
|
43253
|
-
|
|
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")
|
|
43254
43326
|
];
|
|
43255
43327
|
case "cursor":
|
|
43256
43328
|
return [
|
|
43257
|
-
|
|
43258
|
-
|
|
43259
|
-
|
|
43329
|
+
path9.join(pkg, "assets", "templates", "spec-template.md"),
|
|
43330
|
+
path9.join(pkg, "mcp.json"),
|
|
43331
|
+
path9.join(pkg, ".cursor-plugin")
|
|
43260
43332
|
];
|
|
43261
43333
|
case "cli":
|
|
43262
|
-
return [
|
|
43334
|
+
return [path9.join(pkg, "assets", "templates", "spec-template.md")];
|
|
43263
43335
|
}
|
|
43264
43336
|
};
|
|
43265
43337
|
var hostsFor = (host) => host === "cli" ? ["opencode", "cursor", "cli"] : [host];
|
|
@@ -43293,17 +43365,17 @@ var checkAssets = (res) => {
|
|
|
43293
43365
|
};
|
|
43294
43366
|
};
|
|
43295
43367
|
var launcherSlotsFor = (host, dev) => {
|
|
43296
|
-
const pkg =
|
|
43368
|
+
const pkg = path9.join(dev, "packages", `workit-${host}`);
|
|
43297
43369
|
switch (host) {
|
|
43298
43370
|
case "opencode":
|
|
43299
|
-
return [[
|
|
43371
|
+
return [[path9.join(pkg, "src", "plugin.ts"), path9.join(pkg, "dist", "plugin.js")]];
|
|
43300
43372
|
case "cursor":
|
|
43301
43373
|
return [
|
|
43302
|
-
[
|
|
43303
|
-
[
|
|
43374
|
+
[path9.join(pkg, "dist", "mcp-server.js")],
|
|
43375
|
+
[path9.join(pkg, "dist", "cursor-session-start.js")]
|
|
43304
43376
|
];
|
|
43305
43377
|
case "cli":
|
|
43306
|
-
return [[
|
|
43378
|
+
return [[path9.join(pkg, "src", "index.tsx"), path9.join(pkg, "dist", "index.js")]];
|
|
43307
43379
|
}
|
|
43308
43380
|
};
|
|
43309
43381
|
var validNodeEntry = (entry, runtime, env3) => {
|
|
@@ -43332,7 +43404,7 @@ var registeredCursorLauncher = (res) => {
|
|
|
43332
43404
|
if (typeof command !== "string" || !Array.isArray(args) || typeof args[0] !== "string") {
|
|
43333
43405
|
return "invalid";
|
|
43334
43406
|
}
|
|
43335
|
-
const executable =
|
|
43407
|
+
const executable = path9.basename(command).toLowerCase();
|
|
43336
43408
|
if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
|
|
43337
43409
|
if (args[0] !== "-y")
|
|
43338
43410
|
return "invalid";
|
|
@@ -43347,12 +43419,12 @@ var registeredCursorLauncher = (res) => {
|
|
|
43347
43419
|
return {
|
|
43348
43420
|
kind: "node",
|
|
43349
43421
|
runtime: command,
|
|
43350
|
-
entry:
|
|
43422
|
+
entry: path9.isAbsolute(args[0]) ? args[0] : path9.resolve(path9.dirname(res.cursorMcp), args[0])
|
|
43351
43423
|
};
|
|
43352
43424
|
};
|
|
43353
43425
|
var canonicalCursorHook = cursorHooksEntry("").command;
|
|
43354
43426
|
var registeredCursorHook = (res) => {
|
|
43355
|
-
const hooksFile =
|
|
43427
|
+
const hooksFile = path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
|
|
43356
43428
|
if (!existsSync7(hooksFile))
|
|
43357
43429
|
return "invalid";
|
|
43358
43430
|
const config = readJson(hooksFile);
|
|
@@ -43371,7 +43443,7 @@ var checkLauncher = (res) => {
|
|
|
43371
43443
|
const hosts = hostsFor(res.host);
|
|
43372
43444
|
const registered = hosts.includes("cursor") ? registeredCursorLauncher(res) : null;
|
|
43373
43445
|
const runtime = registered && registered !== "invalid" && registered.kind === "node" ? registered.runtime : "node";
|
|
43374
|
-
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}`) : [];
|
|
43375
43447
|
if (hosts.includes("cursor")) {
|
|
43376
43448
|
if (registered === "invalid") {
|
|
43377
43449
|
missing.push(`cursor: canonical workit MCP launcher in ${res.cursorMcp}`);
|
|
@@ -43380,9 +43452,9 @@ var checkLauncher = (res) => {
|
|
|
43380
43452
|
}
|
|
43381
43453
|
const hook = registeredCursorHook(res);
|
|
43382
43454
|
if (hook === "invalid") {
|
|
43383
|
-
missing.push(`cursor: canonical session-start hook in ${
|
|
43455
|
+
missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
|
|
43384
43456
|
} else if (hook !== null && hook !== canonicalCursorHook) {
|
|
43385
|
-
missing.push(`cursor: canonical session-start hook in ${
|
|
43457
|
+
missing.push(`cursor: canonical session-start hook in ${path9.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
|
|
43386
43458
|
}
|
|
43387
43459
|
}
|
|
43388
43460
|
if (dev) {
|
|
@@ -43523,7 +43595,7 @@ var checkDuplicateRegistration = (res) => {
|
|
|
43523
43595
|
var checkMalformedConfig = (res) => {
|
|
43524
43596
|
const files = [];
|
|
43525
43597
|
for (const name of ["config.json", "youtrack.json", "vcs.json", "workspaces.json"]) {
|
|
43526
|
-
const p =
|
|
43598
|
+
const p = path9.join(res.configDir, name);
|
|
43527
43599
|
if (existsSync7(p))
|
|
43528
43600
|
files.push(p);
|
|
43529
43601
|
}
|
|
@@ -43546,7 +43618,7 @@ var checkMalformedConfig = (res) => {
|
|
|
43546
43618
|
};
|
|
43547
43619
|
};
|
|
43548
43620
|
var checkWorkspaceMismatch = (res) => {
|
|
43549
|
-
const file =
|
|
43621
|
+
const file = path9.join(res.configDir, "workspaces.json");
|
|
43550
43622
|
if (!existsSync7(file))
|
|
43551
43623
|
return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
|
|
43552
43624
|
const ws = readJson(file);
|
|
@@ -43576,20 +43648,20 @@ var isPlaceholder = (p) => {
|
|
|
43576
43648
|
};
|
|
43577
43649
|
var checkCredentialMetadata = (res) => {
|
|
43578
43650
|
const tokenPaths = [];
|
|
43579
|
-
const youtrackJson = readJson(
|
|
43651
|
+
const youtrackJson = readJson(path9.join(res.configDir, "youtrack.json"));
|
|
43580
43652
|
if (youtrackJson) {
|
|
43581
|
-
tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile :
|
|
43582
|
-
} else if (existsSync7(
|
|
43583
|
-
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"));
|
|
43584
43656
|
}
|
|
43585
|
-
const vcsJson = readJson(
|
|
43657
|
+
const vcsJson = readJson(path9.join(res.configDir, "vcs.json"));
|
|
43586
43658
|
for (const key of ["gitlab", "github"]) {
|
|
43587
43659
|
const provider = vcsJson?.[key];
|
|
43588
43660
|
if (provider && typeof provider === "object") {
|
|
43589
43661
|
const tf = provider.tokenFile;
|
|
43590
|
-
tokenPaths.push(typeof tf === "string" ? tf :
|
|
43591
|
-
} else if (!vcsJson && existsSync7(
|
|
43592
|
-
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`));
|
|
43593
43665
|
}
|
|
43594
43666
|
}
|
|
43595
43667
|
if (tokenPaths.length === 0) {
|
|
@@ -43597,7 +43669,7 @@ var checkCredentialMetadata = (res) => {
|
|
|
43597
43669
|
}
|
|
43598
43670
|
const problems = [];
|
|
43599
43671
|
for (const raw of tokenPaths) {
|
|
43600
|
-
const p =
|
|
43672
|
+
const p = path9.isAbsolute(raw) ? raw : path9.resolve(res.configDir, raw);
|
|
43601
43673
|
if (!existsSync7(p)) {
|
|
43602
43674
|
problems.push(`${p} is missing`);
|
|
43603
43675
|
continue;
|
|
@@ -43625,8 +43697,8 @@ var checkCredentialMetadata = (res) => {
|
|
|
43625
43697
|
};
|
|
43626
43698
|
};
|
|
43627
43699
|
var checkLogWritable = (res) => {
|
|
43628
|
-
const logsDir =
|
|
43629
|
-
const probe =
|
|
43700
|
+
const logsDir = path9.join(res.stateDir, "logs");
|
|
43701
|
+
const probe = path9.join(logsDir, "doctor-probe.tmp");
|
|
43630
43702
|
try {
|
|
43631
43703
|
mkdirSync2(logsDir, { recursive: true, mode: 448 });
|
|
43632
43704
|
writeFileSync4(probe, `{"probe":true}
|
|
@@ -43814,7 +43886,7 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43814
43886
|
const current = readConfigFromDir(state.configDir);
|
|
43815
43887
|
mutations.push({
|
|
43816
43888
|
type: "merge-json",
|
|
43817
|
-
path:
|
|
43889
|
+
path: path10.join(state.configDir, "config.json"),
|
|
43818
43890
|
value: {
|
|
43819
43891
|
locale: values2.locale,
|
|
43820
43892
|
localeOptions: current.localeOptions,
|
|
@@ -43839,16 +43911,16 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43839
43911
|
if (blocked.length === 0) {
|
|
43840
43912
|
mutations.push({
|
|
43841
43913
|
type: "update-workspaces",
|
|
43842
|
-
path:
|
|
43914
|
+
path: path10.join(state.configDir, "workspaces.json"),
|
|
43843
43915
|
entries: values2.workspaces
|
|
43844
43916
|
});
|
|
43845
43917
|
}
|
|
43846
43918
|
}
|
|
43847
43919
|
if (values2.baseUrl.trim()) {
|
|
43848
|
-
const ytPath =
|
|
43920
|
+
const ytPath = path10.join(state.configDir, "youtrack.json");
|
|
43849
43921
|
const ytExisting = readConfigRecord(ytPath);
|
|
43850
43922
|
const ytConfigured = configuredTokenPath(ytExisting, "tokenFile");
|
|
43851
|
-
const tokenPath = resolveTokenPath(ytConfigured,
|
|
43923
|
+
const tokenPath = resolveTokenPath(ytConfigured, path10.join(state.configDir, "youtrack.token"), values2.tokenPaths?.youtrack);
|
|
43852
43924
|
const draft = youtrackDraft(values2, tokenPath);
|
|
43853
43925
|
if (ytConfigured !== null && tokenPath !== ytConfigured) {
|
|
43854
43926
|
delete draft.tokenFile;
|
|
@@ -43876,12 +43948,12 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43876
43948
|
});
|
|
43877
43949
|
}
|
|
43878
43950
|
if (values2.vcsProvider !== "skip") {
|
|
43879
|
-
const vcsPath =
|
|
43951
|
+
const vcsPath = path10.join(state.configDir, "vcs.json");
|
|
43880
43952
|
const vcsExisting = readConfigRecord(vcsPath);
|
|
43881
43953
|
const glConfigured = configuredTokenPath(vcsExisting, "gitlab", "tokenFile");
|
|
43882
43954
|
const ghConfigured = configuredTokenPath(vcsExisting, "github", "tokenFile");
|
|
43883
|
-
const gitlabToken = resolveTokenPath(glConfigured,
|
|
43884
|
-
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);
|
|
43885
43957
|
const draft = vcsDraft(values2.vcsProvider, gitlabToken, githubToken);
|
|
43886
43958
|
for (const [configured, token, key] of [
|
|
43887
43959
|
[glConfigured, gitlabToken, "gitlab.tokenFile"],
|
|
@@ -43928,8 +44000,8 @@ function buildSetupPreview(values2, opts = {}) {
|
|
|
43928
44000
|
}
|
|
43929
44001
|
}
|
|
43930
44002
|
if (values2.applyProject) {
|
|
43931
|
-
const root =
|
|
43932
|
-
const gitignorePath =
|
|
44003
|
+
const root = path10.resolve(opts.cwd ?? process.cwd());
|
|
44004
|
+
const gitignorePath = path10.join(root, ".gitignore");
|
|
43933
44005
|
const existing = existsSync8(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
|
|
43934
44006
|
const existingLines = new Set(existing.split(`
|
|
43935
44007
|
`).map((l) => l.trim()).filter(Boolean));
|
|
@@ -43958,10 +44030,10 @@ var resolveSetupPaths = (options, configDir2) => {
|
|
|
43958
44030
|
configDir: configDir2,
|
|
43959
44031
|
cwd: options.cwd ?? process.cwd(),
|
|
43960
44032
|
env: env3,
|
|
43961
|
-
opencodeConfig: options.opencodeConfig ??
|
|
43962
|
-
cursorSettings: options.cursorSettings ??
|
|
43963
|
-
cursorMcp: options.cursorMcp ??
|
|
43964
|
-
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")
|
|
43965
44037
|
};
|
|
43966
44038
|
};
|
|
43967
44039
|
function resolveApply(preview, options) {
|
|
@@ -44007,7 +44079,7 @@ var readFileSafe = (p) => {
|
|
|
44007
44079
|
}
|
|
44008
44080
|
};
|
|
44009
44081
|
function applyMutation(m) {
|
|
44010
|
-
const dir =
|
|
44082
|
+
const dir = path10.dirname(m.path);
|
|
44011
44083
|
switch (m.type) {
|
|
44012
44084
|
case "create-file": {
|
|
44013
44085
|
mkdirSync3(dir, { recursive: true });
|
|
@@ -44111,7 +44183,7 @@ function applyMutation(m) {
|
|
|
44111
44183
|
detail: "already configured"
|
|
44112
44184
|
};
|
|
44113
44185
|
}
|
|
44114
|
-
mkdirSync3(
|
|
44186
|
+
mkdirSync3(path10.dirname(m.path), { recursive: true });
|
|
44115
44187
|
writeFileSync5(m.path, JSON.stringify(merged, null, 2) + `
|
|
44116
44188
|
`, "utf8");
|
|
44117
44189
|
return existing.kind === "record" ? { platform: "core", file: m.path, status: "Configured" } : { platform: "core", file: m.path, status: "Installed" };
|
|
@@ -44120,7 +44192,7 @@ function applyMutation(m) {
|
|
|
44120
44192
|
}
|
|
44121
44193
|
var isAdapter = (root, platform2) => {
|
|
44122
44194
|
try {
|
|
44123
|
-
const pkg = JSON.parse(readFileSync8(
|
|
44195
|
+
const pkg = JSON.parse(readFileSync8(path10.join(root, "package.json"), "utf8"));
|
|
44124
44196
|
return pkg.name === `@brainervirus/workit-${platform2}`;
|
|
44125
44197
|
} catch {
|
|
44126
44198
|
return false;
|
|
@@ -44129,19 +44201,19 @@ var isAdapter = (root, platform2) => {
|
|
|
44129
44201
|
function adapterRoot(platform2, res) {
|
|
44130
44202
|
const candidates = [];
|
|
44131
44203
|
if (res.dev) {
|
|
44132
|
-
candidates.push(
|
|
44204
|
+
candidates.push(path10.join(res.dev, "packages", `workit-${platform2}`));
|
|
44133
44205
|
} else {
|
|
44134
|
-
candidates.push(
|
|
44135
|
-
let dir =
|
|
44206
|
+
candidates.push(path10.join(packageRoot(), "..", `workit-${platform2}`));
|
|
44207
|
+
let dir = path10.resolve(res.cwd);
|
|
44136
44208
|
while (true) {
|
|
44137
|
-
candidates.push(
|
|
44138
|
-
const parent =
|
|
44209
|
+
candidates.push(path10.join(dir, "packages", `workit-${platform2}`));
|
|
44210
|
+
const parent = path10.dirname(dir);
|
|
44139
44211
|
if (parent === dir)
|
|
44140
44212
|
break;
|
|
44141
44213
|
dir = parent;
|
|
44142
44214
|
}
|
|
44143
44215
|
}
|
|
44144
|
-
candidates.push(
|
|
44216
|
+
candidates.push(path10.join(res.home, ".local", "share", "workflow-toolkit", "packages", `workit-${platform2}`));
|
|
44145
44217
|
for (const candidate of candidates) {
|
|
44146
44218
|
if (isAdapter(candidate, platform2))
|
|
44147
44219
|
return candidate;
|
|
@@ -44150,7 +44222,7 @@ function adapterRoot(platform2, res) {
|
|
|
44150
44222
|
}
|
|
44151
44223
|
var opencodePin = (root) => {
|
|
44152
44224
|
for (const rel of ["src/plugin.ts", "dist/plugin.js"]) {
|
|
44153
|
-
const entry =
|
|
44225
|
+
const entry = path10.join(root, rel);
|
|
44154
44226
|
if (existsSync8(entry))
|
|
44155
44227
|
return `file://${entry}`;
|
|
44156
44228
|
}
|
|
@@ -44166,7 +44238,7 @@ function applyOpenCode(root, res) {
|
|
|
44166
44238
|
detail: "workit-opencode package has no loadable plugin entry (src/plugin.ts or dist/plugin.js)"
|
|
44167
44239
|
};
|
|
44168
44240
|
}
|
|
44169
|
-
mkdirSync3(
|
|
44241
|
+
mkdirSync3(path10.dirname(res.opencodeConfig), { recursive: true });
|
|
44170
44242
|
const existing = readExisting(res.opencodeConfig);
|
|
44171
44243
|
if (existing.kind === "malformed") {
|
|
44172
44244
|
return {
|
|
@@ -44204,19 +44276,19 @@ var samePluginContent = (src, dest, relative = "") => {
|
|
|
44204
44276
|
for (const entry of readdirSync3(src, { withFileTypes: true })) {
|
|
44205
44277
|
if (entry.name === "node_modules")
|
|
44206
44278
|
continue;
|
|
44207
|
-
const source =
|
|
44208
|
-
const installed =
|
|
44279
|
+
const source = path10.join(src, entry.name);
|
|
44280
|
+
const installed = path10.join(dest, entry.name);
|
|
44209
44281
|
if (entry.isDirectory()) {
|
|
44210
|
-
if (!statSync3(installed).isDirectory() || !samePluginContent(source, installed,
|
|
44282
|
+
if (!statSync3(installed).isDirectory() || !samePluginContent(source, installed, path10.join(relative, entry.name)))
|
|
44211
44283
|
return false;
|
|
44212
44284
|
} else if (!readFileSync8(source).equals(readFileSync8(installed))) {
|
|
44213
44285
|
return false;
|
|
44214
44286
|
}
|
|
44215
44287
|
}
|
|
44216
44288
|
for (const entry of readdirSync3(dest, { withFileTypes: true })) {
|
|
44217
|
-
if (existsSync8(
|
|
44289
|
+
if (existsSync8(path10.join(src, entry.name)))
|
|
44218
44290
|
continue;
|
|
44219
|
-
const rel =
|
|
44291
|
+
const rel = path10.join(relative, entry.name);
|
|
44220
44292
|
if (rel === ".workflow-toolkit-root")
|
|
44221
44293
|
continue;
|
|
44222
44294
|
if (relative === "rules" && entry.isFile() && entry.name.endsWith(".mdc"))
|
|
@@ -44230,41 +44302,41 @@ var samePluginContent = (src, dest, relative = "") => {
|
|
|
44230
44302
|
};
|
|
44231
44303
|
var preservedCursorRules = (src, dest) => {
|
|
44232
44304
|
const preserved = new Map;
|
|
44233
|
-
const sourceRules =
|
|
44234
|
-
const installedRules =
|
|
44305
|
+
const sourceRules = path10.join(src, "rules");
|
|
44306
|
+
const installedRules = path10.join(dest, "rules");
|
|
44235
44307
|
try {
|
|
44236
44308
|
for (const entry of readdirSync3(installedRules, { withFileTypes: true })) {
|
|
44237
44309
|
if (!entry.isFile() || !entry.name.endsWith(".mdc"))
|
|
44238
44310
|
continue;
|
|
44239
|
-
if (existsSync8(
|
|
44311
|
+
if (existsSync8(path10.join(sourceRules, entry.name)))
|
|
44240
44312
|
continue;
|
|
44241
|
-
preserved.set(entry.name, readFileSync8(
|
|
44313
|
+
preserved.set(entry.name, readFileSync8(path10.join(installedRules, entry.name)));
|
|
44242
44314
|
}
|
|
44243
44315
|
} catch {}
|
|
44244
44316
|
return preserved;
|
|
44245
44317
|
};
|
|
44246
44318
|
function copyPluginDir(src, dest) {
|
|
44247
|
-
const marker =
|
|
44319
|
+
const marker = path10.join(dest, ".workflow-toolkit-root");
|
|
44248
44320
|
const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest);
|
|
44249
44321
|
if (synced)
|
|
44250
44322
|
return "Skipped";
|
|
44251
44323
|
const hadDir = existsSync8(dest);
|
|
44252
44324
|
const rules = preservedCursorRules(src, dest);
|
|
44253
|
-
const parent =
|
|
44325
|
+
const parent = path10.dirname(dest);
|
|
44254
44326
|
mkdirSync3(parent, { recursive: true });
|
|
44255
|
-
const swap = mkdtempSync(
|
|
44256
|
-
const stage =
|
|
44257
|
-
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");
|
|
44258
44330
|
try {
|
|
44259
44331
|
cpSync2(src, stage, {
|
|
44260
44332
|
recursive: true,
|
|
44261
|
-
filter: (entry) => !
|
|
44333
|
+
filter: (entry) => !path10.relative(src, entry).split(path10.sep).includes("node_modules")
|
|
44262
44334
|
});
|
|
44263
44335
|
for (const [name, content] of rules) {
|
|
44264
|
-
mkdirSync3(
|
|
44265
|
-
writeFileSync5(
|
|
44336
|
+
mkdirSync3(path10.join(stage, "rules"), { recursive: true });
|
|
44337
|
+
writeFileSync5(path10.join(stage, "rules", name), content);
|
|
44266
44338
|
}
|
|
44267
|
-
writeFileSync5(
|
|
44339
|
+
writeFileSync5(path10.join(stage, ".workflow-toolkit-root"), src + `
|
|
44268
44340
|
`, "utf8");
|
|
44269
44341
|
if (!samePluginContent(src, stage))
|
|
44270
44342
|
throw new Error("staged adapter content is incomplete");
|
|
@@ -44284,26 +44356,26 @@ function copyPluginDir(src, dest) {
|
|
|
44284
44356
|
return hadDir ? "Configured" : "Installed";
|
|
44285
44357
|
}
|
|
44286
44358
|
var removeLegacyCursorDir = (res) => {
|
|
44287
|
-
const legacy =
|
|
44359
|
+
const legacy = path10.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
|
|
44288
44360
|
if (legacy === res.cursorPluginDir || !existsSync8(legacy))
|
|
44289
44361
|
return;
|
|
44290
|
-
const legacyRules =
|
|
44362
|
+
const legacyRules = path10.join(legacy, "rules");
|
|
44291
44363
|
if (existsSync8(legacyRules)) {
|
|
44292
44364
|
try {
|
|
44293
|
-
mkdirSync3(
|
|
44365
|
+
mkdirSync3(path10.join(res.cursorPluginDir, "rules"), { recursive: true });
|
|
44294
44366
|
for (const entry of readdirSync3(legacyRules, { withFileTypes: true })) {
|
|
44295
44367
|
if (!entry.isFile() || !entry.name.endsWith(".mdc"))
|
|
44296
44368
|
continue;
|
|
44297
|
-
const target =
|
|
44369
|
+
const target = path10.join(res.cursorPluginDir, "rules", entry.name);
|
|
44298
44370
|
if (!existsSync8(target))
|
|
44299
|
-
copyFileSync2(
|
|
44371
|
+
copyFileSync2(path10.join(legacyRules, entry.name), target);
|
|
44300
44372
|
}
|
|
44301
44373
|
} catch {}
|
|
44302
44374
|
}
|
|
44303
44375
|
rmSync(legacy, { recursive: true, force: true });
|
|
44304
44376
|
};
|
|
44305
44377
|
function applyCursorSettings(root, res) {
|
|
44306
|
-
mkdirSync3(
|
|
44378
|
+
mkdirSync3(path10.dirname(res.cursorSettings), { recursive: true });
|
|
44307
44379
|
const settingsExisting = readExisting(res.cursorSettings);
|
|
44308
44380
|
if (settingsExisting.kind === "malformed") {
|
|
44309
44381
|
return {
|
|
@@ -44331,7 +44403,7 @@ function applyCursorSettings(root, res) {
|
|
|
44331
44403
|
};
|
|
44332
44404
|
}
|
|
44333
44405
|
function applyCursorMcp(root, res) {
|
|
44334
|
-
mkdirSync3(
|
|
44406
|
+
mkdirSync3(path10.dirname(res.cursorMcp), { recursive: true });
|
|
44335
44407
|
const mcpExisting = readExisting(res.cursorMcp);
|
|
44336
44408
|
if (mcpExisting.kind === "malformed") {
|
|
44337
44409
|
return {
|
|
@@ -44520,12 +44592,12 @@ var setupCompletionGuidance = () => [
|
|
|
44520
44592
|
];
|
|
44521
44593
|
function applyWorkspaceBranchPolicy(opts) {
|
|
44522
44594
|
const { workspace_root, env: env3 = process.env } = opts;
|
|
44523
|
-
const dir =
|
|
44595
|
+
const dir = path10.join(env3.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
|
|
44524
44596
|
const { status, path: wsPath, entries } = readWorkspacesResult(dir);
|
|
44525
44597
|
if (status === "malformed")
|
|
44526
44598
|
return { ok: false, error: `malformed workspaces.json: ${wsPath}` };
|
|
44527
44599
|
const detection = detectBranchPolicy(workspace_root);
|
|
44528
|
-
const name = String(env3.WORKFLOW_BP_NAME ??
|
|
44600
|
+
const name = String(env3.WORKFLOW_BP_NAME ?? path10.basename(workspace_root));
|
|
44529
44601
|
const integration = env3.WORKFLOW_BP_INTEGRATION ?? detection.integration;
|
|
44530
44602
|
const policy = {
|
|
44531
44603
|
preset: detection.preset,
|
|
@@ -44550,7 +44622,7 @@ function applyWorkspaceBranchPolicy(opts) {
|
|
|
44550
44622
|
};
|
|
44551
44623
|
}
|
|
44552
44624
|
const next = existing ? entries.map((w, i) => i === idx ? { ...w, branchPolicy: policy } : w) : [...entries, { name, glob, branchPolicy: policy }];
|
|
44553
|
-
mkdirSync3(
|
|
44625
|
+
mkdirSync3(path10.dirname(wsPath), { recursive: true });
|
|
44554
44626
|
writeFileSync5(wsPath, JSON.stringify({ workspaces: next }, null, 2) + `
|
|
44555
44627
|
`, "utf8");
|
|
44556
44628
|
return {
|
|
@@ -44608,7 +44680,7 @@ function applyWizardBranchPolicy(branchPolicy, workspace_root, env3 = process.en
|
|
|
44608
44680
|
|
|
44609
44681
|
// packages/workit-cli/src/wizard-state.ts
|
|
44610
44682
|
import { existsSync as existsSync9 } from "node:fs";
|
|
44611
|
-
import
|
|
44683
|
+
import path11 from "node:path";
|
|
44612
44684
|
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
44613
44685
|
var NEXT = {
|
|
44614
44686
|
platforms: "locale",
|
|
@@ -44653,7 +44725,7 @@ var PREV = {
|
|
|
44653
44725
|
exit: null
|
|
44654
44726
|
};
|
|
44655
44727
|
var skipsCustomBranch = (screen, preset) => (screen === "branchAllowed" || screen === "branchProtected") && preset !== "custom";
|
|
44656
|
-
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"));
|
|
44657
44729
|
function nextScreen(screen, preset) {
|
|
44658
44730
|
let next = NEXT[screen];
|
|
44659
44731
|
while (next && (skipsCustomBranch(next, preset) || skipsBranchPolicy(next)))
|
|
@@ -45959,7 +46031,7 @@ function Screen({ draft, dispatch }) {
|
|
|
45959
46031
|
// packages/workit-core/src/core/logger.ts
|
|
45960
46032
|
import { appendFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
|
|
45961
46033
|
import os6 from "node:os";
|
|
45962
|
-
import
|
|
46034
|
+
import path12 from "node:path";
|
|
45963
46035
|
var REDACTED = "[REDACTED]";
|
|
45964
46036
|
var DEFAULT_MAX_RATE = 20;
|
|
45965
46037
|
var DEFAULT_RATE_WINDOW_MS = 1000;
|
|
@@ -46015,14 +46087,14 @@ var resolveStateDir = () => {
|
|
|
46015
46087
|
return override;
|
|
46016
46088
|
const home = os6.homedir();
|
|
46017
46089
|
if (process.env.XDG_STATE_HOME)
|
|
46018
|
-
return
|
|
46090
|
+
return path12.join(process.env.XDG_STATE_HOME, "workit");
|
|
46019
46091
|
switch (os6.platform()) {
|
|
46020
46092
|
case "darwin":
|
|
46021
|
-
return
|
|
46093
|
+
return path12.join(home, "Library", "Application Support", "workit");
|
|
46022
46094
|
case "win32":
|
|
46023
|
-
return
|
|
46095
|
+
return path12.join(process.env.LOCALAPPDATA ?? path12.join(home, "AppData", "Local"), "workit");
|
|
46024
46096
|
default:
|
|
46025
|
-
return
|
|
46097
|
+
return path12.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
46026
46098
|
}
|
|
46027
46099
|
};
|
|
46028
46100
|
var homePattern = new RegExp(`${escapeRegExp2(os6.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
@@ -46095,14 +46167,14 @@ var pruneOldFiles = (dir) => {
|
|
|
46095
46167
|
const files = readdirSync4(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
|
|
46096
46168
|
for (const file of files.slice(RETAINED_DAYS)) {
|
|
46097
46169
|
try {
|
|
46098
|
-
unlinkSync2(
|
|
46170
|
+
unlinkSync2(path12.join(dir, file));
|
|
46099
46171
|
} catch {}
|
|
46100
46172
|
}
|
|
46101
46173
|
} catch {}
|
|
46102
46174
|
};
|
|
46103
46175
|
var createLogger = (options = {}) => {
|
|
46104
46176
|
const stateDir = options.stateDir ?? resolveStateDir();
|
|
46105
|
-
const logDir =
|
|
46177
|
+
const logDir = path12.join(stateDir, "logs");
|
|
46106
46178
|
const now2 = options.now ?? (() => new Date);
|
|
46107
46179
|
const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
|
|
46108
46180
|
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
@@ -46136,7 +46208,7 @@ var createLogger = (options = {}) => {
|
|
|
46136
46208
|
const line = `${JSON.stringify(event)}
|
|
46137
46209
|
`;
|
|
46138
46210
|
mkdirSync4(logDir, { recursive: true, mode: 448 });
|
|
46139
|
-
appendFileSync(
|
|
46211
|
+
appendFileSync(path12.join(logDir, dailyFileName(date)), line, { mode: 384 });
|
|
46140
46212
|
pruneOldFiles(logDir);
|
|
46141
46213
|
} catch {
|
|
46142
46214
|
try {
|
|
@@ -46147,7 +46219,7 @@ var createLogger = (options = {}) => {
|
|
|
46147
46219
|
context: { redaction_failed: true }
|
|
46148
46220
|
};
|
|
46149
46221
|
mkdirSync4(logDir, { recursive: true, mode: 448 });
|
|
46150
|
-
appendFileSync(
|
|
46222
|
+
appendFileSync(path12.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
|
|
46151
46223
|
`, {
|
|
46152
46224
|
mode: 384
|
|
46153
46225
|
});
|
|
@@ -46182,21 +46254,2152 @@ var createLogger = (options = {}) => {
|
|
|
46182
46254
|
};
|
|
46183
46255
|
};
|
|
46184
46256
|
|
|
46185
|
-
// packages/workit-cli/src/
|
|
46186
|
-
|
|
46187
|
-
|
|
46188
|
-
|
|
46189
|
-
|
|
46190
|
-
|
|
46191
|
-
|
|
46192
|
-
|
|
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
|
+
});
|
|
46193
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
|
|
46194
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
|
+
];
|
|
46195
48396
|
var HELP = `workit — workflow rails for agentic coding
|
|
46196
48397
|
|
|
46197
48398
|
Usage:
|
|
46198
48399
|
workit init Run the interactive setup wizard
|
|
46199
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
|
+
`)}
|
|
46200
48403
|
workit Show this help
|
|
46201
48404
|
|
|
46202
48405
|
Run \`npx workit init\` to configure platforms, YouTrack, VCS and project hygiene.
|
|
@@ -46292,14 +48495,18 @@ if (__require.main == __require.module) {
|
|
|
46292
48495
|
setDiagnosticLogger(logger);
|
|
46293
48496
|
logger.info(EVENT.initialization, { host: "cli", command: subcommand });
|
|
46294
48497
|
process.on("unhandledRejection", (reason) => logger.error(EVENT.uncaughtFailure, { phase: "unhandledRejection", ...errorDetail(reason) }));
|
|
46295
|
-
process.on("uncaughtException", (
|
|
46296
|
-
logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(
|
|
48498
|
+
process.on("uncaughtException", (err3) => {
|
|
48499
|
+
logger.error(EVENT.uncaughtFailure, { phase: "uncaughtException", ...errorDetail(err3) });
|
|
46297
48500
|
process.exit(1);
|
|
46298
48501
|
});
|
|
46299
48502
|
if (subcommand === "init") {
|
|
46300
48503
|
await runInit();
|
|
46301
48504
|
} else if (subcommand === "doctor") {
|
|
46302
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)));
|
|
46303
48510
|
} else {
|
|
46304
48511
|
console.log(HELP);
|
|
46305
48512
|
process.exit(0);
|