@brainervirus/workit-opencode 8.14.1 → 8.15.1
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/assets/skills/workit-ship/SKILL.md +2 -2
- package/dist/plugin.js +700 -47
- package/package.json +2 -2
|
@@ -33,8 +33,8 @@ that repeats because the base keeps moving, or after 3 failed fix attempts on
|
|
|
33
33
|
the same check.
|
|
34
34
|
|
|
35
35
|
1. **Open.** `workit git push`, then `workit pr create --title "<title>"
|
|
36
|
-
--body-file <f|->` (
|
|
37
|
-
`workit pr edit`). A stack: `workit stack plan <bottom> ... <top>`, one
|
|
36
|
+
--body-file <f|->` plus `--issue <#n|ID>` per issue (`--refs`: no close; idempotent;
|
|
37
|
+
body: `references/pr-body.md`; edits: `workit pr edit`). A stack: `workit stack plan <bottom> ... <top>`, one
|
|
38
38
|
`workit pr create --base <parent> --fill` per branch, then
|
|
39
39
|
`workit stack sync`. Finish the whole stack before babysitting any PR.
|
|
40
40
|
2. **Read state** with `workit pr status --json`. `REVIEW` alone is a pending
|
package/dist/plugin.js
CHANGED
|
@@ -12384,6 +12384,7 @@ class TaskStore {
|
|
|
12384
12384
|
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
12385
12385
|
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
12386
12386
|
var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
|
|
12387
|
+
var holdsLiveState = (state, dirty) => state === "running" || dirty === true;
|
|
12387
12388
|
|
|
12388
12389
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12389
12390
|
import { createHash as createHash10 } from "node:crypto";
|
|
@@ -12630,6 +12631,36 @@ var line = (run) => {
|
|
|
12630
12631
|
return value ? value : null;
|
|
12631
12632
|
};
|
|
12632
12633
|
var safeArg = (value) => value.length > 0 && !value.startsWith("-");
|
|
12634
|
+
var UNPARSEABLE = "<unparseable remote>";
|
|
12635
|
+
function redactRemote(raw) {
|
|
12636
|
+
const value = raw.trim();
|
|
12637
|
+
if (!value)
|
|
12638
|
+
return UNPARSEABLE;
|
|
12639
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
|
|
12640
|
+
return UNPARSEABLE;
|
|
12641
|
+
if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith("."))
|
|
12642
|
+
return value;
|
|
12643
|
+
if (!/[:@]/u.test(value))
|
|
12644
|
+
return value;
|
|
12645
|
+
if (!value.includes("://")) {
|
|
12646
|
+
if (/^[^/@]*:[^/]*@/u.test(value))
|
|
12647
|
+
return UNPARSEABLE;
|
|
12648
|
+
const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):([^?#]+)/u.exec(value);
|
|
12649
|
+
return scp ? `${scp[1] ? `${scp[1]}@` : ""}${scp[2]}:${scp[3]}` : UNPARSEABLE;
|
|
12650
|
+
}
|
|
12651
|
+
try {
|
|
12652
|
+
const url = new URL(value);
|
|
12653
|
+
const ssh = /^(?:ssh|git\+ssh|ssh\+git):$/u.test(url.protocol);
|
|
12654
|
+
url.password = "";
|
|
12655
|
+
if (!ssh)
|
|
12656
|
+
url.username = "";
|
|
12657
|
+
url.search = "";
|
|
12658
|
+
url.hash = "";
|
|
12659
|
+
return url.toString();
|
|
12660
|
+
} catch {
|
|
12661
|
+
return UNPARSEABLE;
|
|
12662
|
+
}
|
|
12663
|
+
}
|
|
12633
12664
|
function headSha(cwd) {
|
|
12634
12665
|
return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
|
|
12635
12666
|
}
|
|
@@ -12776,6 +12807,52 @@ function pushRemoteName(cwd, branch) {
|
|
|
12776
12807
|
return "origin";
|
|
12777
12808
|
return remotes.length === 1 ? remotes[0] : null;
|
|
12778
12809
|
}
|
|
12810
|
+
function pushUrl(cwd, remote) {
|
|
12811
|
+
if (!safeArg(remote))
|
|
12812
|
+
return null;
|
|
12813
|
+
const urls = git2(cwd, ["remote", "get-url", "--push", "--all", remote]).stdout.split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
|
|
12814
|
+
return urls.length === 1 ? urls[0] : null;
|
|
12815
|
+
}
|
|
12816
|
+
var cleanPath = (value) => value.replace(/^\/+/u, "").replace(/\/+$/u, "").replace(/\.git$/u, "");
|
|
12817
|
+
function parseRemoteUrl(raw) {
|
|
12818
|
+
const value = raw.trim();
|
|
12819
|
+
if (!value || /^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
|
|
12820
|
+
return null;
|
|
12821
|
+
if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith(".")) {
|
|
12822
|
+
return { protocol: "file", user: null, host: "", port: null, path: value };
|
|
12823
|
+
}
|
|
12824
|
+
if (!value.includes("://")) {
|
|
12825
|
+
if (/^[^/@]*:[^/]*@/u.test(value))
|
|
12826
|
+
return null;
|
|
12827
|
+
const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):(.+)$/u.exec(value);
|
|
12828
|
+
if (!scp)
|
|
12829
|
+
return null;
|
|
12830
|
+
return {
|
|
12831
|
+
protocol: "ssh",
|
|
12832
|
+
user: scp[1] ?? null,
|
|
12833
|
+
host: scp[2],
|
|
12834
|
+
port: null,
|
|
12835
|
+
path: cleanPath(scp[3])
|
|
12836
|
+
};
|
|
12837
|
+
}
|
|
12838
|
+
let url;
|
|
12839
|
+
try {
|
|
12840
|
+
url = new URL(value);
|
|
12841
|
+
} catch {
|
|
12842
|
+
return null;
|
|
12843
|
+
}
|
|
12844
|
+
const scheme = url.protocol.replace(/:$/u, "");
|
|
12845
|
+
const protocol = scheme === "ssh" || scheme === "git+ssh" || scheme === "ssh+git" ? "ssh" : scheme === "https" || scheme === "http" || scheme === "git" || scheme === "file" ? scheme : null;
|
|
12846
|
+
if (!protocol)
|
|
12847
|
+
return null;
|
|
12848
|
+
return {
|
|
12849
|
+
protocol,
|
|
12850
|
+
user: protocol === "ssh" && url.username ? decodeURIComponent(url.username) : null,
|
|
12851
|
+
host: url.hostname.replace(/\.$/u, ""),
|
|
12852
|
+
port: url.port || null,
|
|
12853
|
+
path: cleanPath(decodeURIComponent(url.pathname))
|
|
12854
|
+
};
|
|
12855
|
+
}
|
|
12779
12856
|
|
|
12780
12857
|
// packages/workit-core/src/core/policy/derive.ts
|
|
12781
12858
|
import { existsSync as existsSync4 } from "node:fs";
|
|
@@ -13186,20 +13263,19 @@ var workspaceConfigSchema = object({
|
|
|
13186
13263
|
union([boolean2(), literal("verified")]).optional()
|
|
13187
13264
|
]))).passthrough().optional(),
|
|
13188
13265
|
youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
|
|
13189
|
-
issues: object({
|
|
13266
|
+
issues: object({
|
|
13267
|
+
provider: _enum(["github", "gitlab", "youtrack"]).optional(),
|
|
13268
|
+
link: boolean2().optional(),
|
|
13269
|
+
relation: unknown().optional(),
|
|
13270
|
+
backlink: boolean2().optional(),
|
|
13271
|
+
link_on_pr: boolean2().optional()
|
|
13272
|
+
}).passthrough().optional(),
|
|
13190
13273
|
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
13191
13274
|
commitPolicy: workspaceCommitPolicySchema.optional(),
|
|
13192
13275
|
defaultProfile: nonBlank.optional(),
|
|
13193
13276
|
profiles: record(nonBlank, workspaceProfileSchema).optional(),
|
|
13194
13277
|
releaseTracks: record(nonBlank, releaseTrackSchema).optional()
|
|
13195
13278
|
}).passthrough().superRefine((workspace, context) => {
|
|
13196
|
-
if (workspace.issues && workspace.vcs?.provider !== "github") {
|
|
13197
|
-
context.addIssue({
|
|
13198
|
-
code: "custom",
|
|
13199
|
-
path: ["issues"],
|
|
13200
|
-
message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
|
|
13201
|
-
});
|
|
13202
|
-
}
|
|
13203
13279
|
if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
|
|
13204
13280
|
context.addIssue({
|
|
13205
13281
|
code: "custom",
|
|
@@ -14066,6 +14142,44 @@ var readVcsConfig = () => {
|
|
|
14066
14142
|
}
|
|
14067
14143
|
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
14068
14144
|
};
|
|
14145
|
+
var TRACKERS = new Set(["github", "gitlab", "youtrack"]);
|
|
14146
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
14147
|
+
var flag = (value) => typeof value === "boolean" ? value : null;
|
|
14148
|
+
var text2 = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
14149
|
+
function issueTrackerOf(issues, youtrack) {
|
|
14150
|
+
const iss = asRecord(issues);
|
|
14151
|
+
const yt = asRecord(youtrack);
|
|
14152
|
+
const warnings = [];
|
|
14153
|
+
const named = typeof iss.provider === "string" && TRACKERS.has(iss.provider.toLowerCase()) ? iss.provider.toLowerCase() : null;
|
|
14154
|
+
if (iss.relation !== undefined && iss.relation !== "closes" && iss.relation !== "refs")
|
|
14155
|
+
warnings.push(`issues.relation ${JSON.stringify(iss.relation)} is not closes or refs; read as closes`);
|
|
14156
|
+
for (const key of ["repo", "host"])
|
|
14157
|
+
if (iss[key] !== undefined && text2(iss[key]) === null)
|
|
14158
|
+
warnings.push(`issues.${key} is not a non-empty string; ignored`);
|
|
14159
|
+
const common = {
|
|
14160
|
+
relation: iss.relation === "refs" ? "refs" : "closes",
|
|
14161
|
+
backlink: iss.backlink === true,
|
|
14162
|
+
baseUrl: text2(yt.baseUrl),
|
|
14163
|
+
repo: text2(iss.repo)?.replace(/^\/+|\/+$/gu, "") ?? null,
|
|
14164
|
+
host: text2(iss.host)?.toLowerCase() ?? null,
|
|
14165
|
+
warnings
|
|
14166
|
+
};
|
|
14167
|
+
const legacyGithub = named === "github" && flag(iss.link) === null;
|
|
14168
|
+
if (flag(yt.link_issues) === true && (named === null || legacyGithub))
|
|
14169
|
+
return { provider: "youtrack", link: true, ...common, source: "youtrack.link_issues" };
|
|
14170
|
+
if (named) {
|
|
14171
|
+
const link = flag(iss.link) ?? flag(iss.link_on_pr) ?? (named === "youtrack" ? flag(yt.link_issues) : null) ?? false;
|
|
14172
|
+
return {
|
|
14173
|
+
provider: named,
|
|
14174
|
+
link,
|
|
14175
|
+
...common,
|
|
14176
|
+
source: flag(iss.link) === null && flag(iss.link_on_pr) !== null ? "issues.link_on_pr" : "issues"
|
|
14177
|
+
};
|
|
14178
|
+
}
|
|
14179
|
+
if (common.baseUrl)
|
|
14180
|
+
return { provider: "youtrack", link: false, ...common, source: "youtrack.baseUrl" };
|
|
14181
|
+
return null;
|
|
14182
|
+
}
|
|
14069
14183
|
var trackSummary = (resolution) => resolution.status === "resolved" ? {
|
|
14070
14184
|
name: resolution.track?.name ?? null,
|
|
14071
14185
|
source: resolution.source,
|
|
@@ -14086,6 +14200,7 @@ function vcsConfig(mode, cwd, options = {}) {
|
|
|
14086
14200
|
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
14087
14201
|
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
14088
14202
|
const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
|
|
14203
|
+
const issueTracker = issueTrackerOf(wsIssues, wsYt);
|
|
14089
14204
|
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
14090
14205
|
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
14091
14206
|
let issuesProvider = null;
|
|
@@ -14118,7 +14233,8 @@ function vcsConfig(mode, cwd, options = {}) {
|
|
|
14118
14233
|
link_issues: linkIssues,
|
|
14119
14234
|
youtrack_base_url: youtrackBaseUrl,
|
|
14120
14235
|
issues_provider: issuesProvider,
|
|
14121
|
-
link_on_pr: linkOnPr
|
|
14236
|
+
link_on_pr: linkOnPr,
|
|
14237
|
+
issue_tracker: issueTracker
|
|
14122
14238
|
};
|
|
14123
14239
|
}
|
|
14124
14240
|
if (cfgStatus === "malformed") {
|
|
@@ -14144,7 +14260,8 @@ function vcsConfig(mode, cwd, options = {}) {
|
|
|
14144
14260
|
link_issues: linkIssues,
|
|
14145
14261
|
youtrack_base_url: youtrackBaseUrl,
|
|
14146
14262
|
issues_provider: issuesProvider,
|
|
14147
|
-
link_on_pr: linkOnPr
|
|
14263
|
+
link_on_pr: linkOnPr,
|
|
14264
|
+
issue_tracker: issueTracker
|
|
14148
14265
|
};
|
|
14149
14266
|
if (provider === "gitlab") {
|
|
14150
14267
|
out.gitlab = {
|
|
@@ -14248,7 +14365,12 @@ var MAX_LINE_BYTES = 4096;
|
|
|
14248
14365
|
var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
|
|
14249
14366
|
var FAILING = new Set(["failed", "blocked"]);
|
|
14250
14367
|
var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
|
|
14251
|
-
var PR_ROW_TYPES = new Set([
|
|
14368
|
+
var PR_ROW_TYPES = new Set([
|
|
14369
|
+
"pr.created",
|
|
14370
|
+
"pr.status",
|
|
14371
|
+
"pr.merged",
|
|
14372
|
+
"pr.linked"
|
|
14373
|
+
]);
|
|
14252
14374
|
var err = (code, error, unblock) => ({
|
|
14253
14375
|
ok: false,
|
|
14254
14376
|
code,
|
|
@@ -16457,6 +16579,15 @@ var releaseBranchesOf = (cwd) => {
|
|
|
16457
16579
|
}
|
|
16458
16580
|
};
|
|
16459
16581
|
var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
|
|
16582
|
+
var policy = (root) => resolveBranchPolicyFor(root);
|
|
16583
|
+
var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
|
|
16584
|
+
var isProtectedTarget = (root, name) => {
|
|
16585
|
+
try {
|
|
16586
|
+
return isProtected(root, name);
|
|
16587
|
+
} catch {
|
|
16588
|
+
return true;
|
|
16589
|
+
}
|
|
16590
|
+
};
|
|
16460
16591
|
|
|
16461
16592
|
// packages/workit-core/src/hooks/session-marker.ts
|
|
16462
16593
|
import { createHash as createHash12 } from "node:crypto";
|
|
@@ -17906,7 +18037,7 @@ var rewrittenUrl = (url, config) => {
|
|
|
17906
18037
|
}
|
|
17907
18038
|
return url;
|
|
17908
18039
|
};
|
|
17909
|
-
var
|
|
18040
|
+
var pushUrl2 = (config, remote, dir) => {
|
|
17910
18041
|
const configured = configValue(config, `remote.${remote}.pushurl`) ?? configValue(config, `remote.${remote}.url`);
|
|
17911
18042
|
return normalizedUrl(rewrittenUrl(configured ?? remote, config), dir);
|
|
17912
18043
|
};
|
|
@@ -17934,7 +18065,7 @@ var toPushRemote = (repo, remote, config) => {
|
|
|
17934
18065
|
const pushRemote = pushRemoteOf(config, repo.branch);
|
|
17935
18066
|
if (pushRemote === null || pushRemote === remote)
|
|
17936
18067
|
return true;
|
|
17937
|
-
return
|
|
18068
|
+
return pushUrl2(config, remote, repo.dir) === pushUrl2(config, pushRemote, repo.dir);
|
|
17938
18069
|
};
|
|
17939
18070
|
var quote = (text) => `\`${text}\``;
|
|
17940
18071
|
var refusal = (action, branch, dir) => {
|
|
@@ -18533,7 +18664,7 @@ var RISK = {
|
|
|
18533
18664
|
};
|
|
18534
18665
|
var YES = new Set(["true", "yes", "y", "1", "on"]);
|
|
18535
18666
|
var NO = new Set(["false", "no", "n", "0", "off", "none"]);
|
|
18536
|
-
var
|
|
18667
|
+
var flag2 = (value, unknownAs) => {
|
|
18537
18668
|
if (typeof value === "boolean")
|
|
18538
18669
|
return { ok: true, value };
|
|
18539
18670
|
if (typeof value === "number" && (value === 0 || value === 1))
|
|
@@ -18599,12 +18730,12 @@ function normalizeJudgment(raw, previous) {
|
|
|
18599
18730
|
const items = Array.isArray(value) ? value : [value];
|
|
18600
18731
|
judgment.refs.push(...items.filter((item) => item !== null && item !== "").map(refOf));
|
|
18601
18732
|
} else {
|
|
18602
|
-
if (name === "needsPlan" && typeof value === "string" && !
|
|
18733
|
+
if (name === "needsPlan" && typeof value === "string" && !flag2(value, false).ok && value.trim()) {
|
|
18603
18734
|
judgment.needsPlan = true;
|
|
18604
18735
|
judgment.refs.push(refOf(value.trim()));
|
|
18605
18736
|
continue;
|
|
18606
18737
|
}
|
|
18607
|
-
const parsed =
|
|
18738
|
+
const parsed = flag2(value, name === "behaviorChange");
|
|
18608
18739
|
if (parsed.ok)
|
|
18609
18740
|
judgment[name] = parsed.value;
|
|
18610
18741
|
else
|
|
@@ -20966,6 +21097,7 @@ import { spawnSync as spawnSync15 } from "node:child_process";
|
|
|
20966
21097
|
import fs16 from "node:fs";
|
|
20967
21098
|
import path22 from "node:path";
|
|
20968
21099
|
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
21100
|
+
import { pathToFileURL } from "node:url";
|
|
20969
21101
|
// packages/workit-core/src/forge/pr-body.ts
|
|
20970
21102
|
function parseGhIssue(value) {
|
|
20971
21103
|
const m = /issues\/(\d+)/.exec(value);
|
|
@@ -20999,6 +21131,39 @@ var pushRemote = (cwd) => {
|
|
|
20999
21131
|
const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
|
|
21000
21132
|
return urls.length === 1 ? urls[0] : null;
|
|
21001
21133
|
};
|
|
21134
|
+
var safePushUrl = (remote) => {
|
|
21135
|
+
const raw = remote.trim();
|
|
21136
|
+
if (path22.isAbsolute(raw))
|
|
21137
|
+
return pathToFileURL(raw).href;
|
|
21138
|
+
if (!raw.includes("://")) {
|
|
21139
|
+
const scp = /^(?:([^@\s]+)@)?([^:]+):(.+)$/u.exec(raw);
|
|
21140
|
+
if (scp)
|
|
21141
|
+
return `${scp[1] || "git"}@${scp[2]}:${scp[3]}`;
|
|
21142
|
+
}
|
|
21143
|
+
try {
|
|
21144
|
+
const url = new URL(raw);
|
|
21145
|
+
if (!["file:", "git:", "http:", "https:", "ssh:"].includes(url.protocol))
|
|
21146
|
+
return null;
|
|
21147
|
+
url.password = "";
|
|
21148
|
+
if (url.protocol !== "ssh:")
|
|
21149
|
+
url.username = "";
|
|
21150
|
+
url.search = "";
|
|
21151
|
+
url.hash = "";
|
|
21152
|
+
return url.toString();
|
|
21153
|
+
} catch {
|
|
21154
|
+
return null;
|
|
21155
|
+
}
|
|
21156
|
+
};
|
|
21157
|
+
var pushTargetIsStable = (cwd, remote) => {
|
|
21158
|
+
const target = safePushUrl(remote);
|
|
21159
|
+
if (!target)
|
|
21160
|
+
return false;
|
|
21161
|
+
const result = spawnSync14("git", ["config", "--get-regexp", "^url\\..*\\.(insteadof|pushinsteadof)$"], { cwd, encoding: "utf8" });
|
|
21162
|
+
if (result.status !== 0 && result.status !== 1)
|
|
21163
|
+
return false;
|
|
21164
|
+
const prefixes = (result.stdout ?? "").split(/\r?\n/u).map((line) => line.trim().slice(line.trim().indexOf(" ") + 1)).filter(Boolean);
|
|
21165
|
+
return !prefixes.some((prefix) => target.startsWith(prefix));
|
|
21166
|
+
};
|
|
21002
21167
|
|
|
21003
21168
|
// packages/workit-core/src/core/tracker-issues.ts
|
|
21004
21169
|
var repoPathFromRemote = (remote) => {
|
|
@@ -23451,10 +23616,494 @@ var workerCoordinatorFor = (task, worker) => {
|
|
|
23451
23616
|
return coordinator?.kind === "host" && coordinator.host === "opencode" ? coordinator.handle : null;
|
|
23452
23617
|
};
|
|
23453
23618
|
|
|
23619
|
+
// packages/workit-core/src/fanout.ts
|
|
23620
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
23621
|
+
import fs21 from "node:fs";
|
|
23622
|
+
import path37 from "node:path";
|
|
23623
|
+
|
|
23624
|
+
// packages/workit-core/src/stack.ts
|
|
23625
|
+
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
23626
|
+
import { createHash as createHash15, randomBytes as randomBytes4 } from "node:crypto";
|
|
23627
|
+
import fs20 from "node:fs";
|
|
23628
|
+
import path36 from "node:path";
|
|
23629
|
+
|
|
23630
|
+
// packages/workit-core/src/git/ops.ts
|
|
23631
|
+
import { spawnSync as spawnSync20 } from "node:child_process";
|
|
23632
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
|
|
23633
|
+
import path35 from "node:path";
|
|
23634
|
+
|
|
23635
|
+
// packages/workit-core/src/forge/redact.ts
|
|
23636
|
+
var KEY = String.raw`[A-Za-z0-9_.-]*?(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|account[_-]?key|private[_-]?key|client[_-]?secret|credentials?|authorization|cookie|signature)[A-Za-z0-9_.-]*`;
|
|
23637
|
+
var ASSIGNMENT2 = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
|
|
23638
|
+
var QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
|
|
23639
|
+
var ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
|
|
23640
|
+
|
|
23641
|
+
// packages/workit-core/src/forge/types.ts
|
|
23642
|
+
var success2 = (data) => ({ ok: true, data });
|
|
23643
|
+
var failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
|
|
23644
|
+
|
|
23645
|
+
// packages/workit-core/src/git/ops.ts
|
|
23646
|
+
var COMMIT_TIMEOUT_MS = 10 * 60000;
|
|
23647
|
+
var git5 = (cwd, args, options = {}) => {
|
|
23648
|
+
const result = spawnSync20("git", args, {
|
|
23649
|
+
cwd,
|
|
23650
|
+
encoding: "utf8",
|
|
23651
|
+
env: { ...options.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
23652
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
23653
|
+
timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
|
|
23654
|
+
killSignal: "SIGKILL",
|
|
23655
|
+
windowsHide: true,
|
|
23656
|
+
...options.input === undefined ? { stdio: ["ignore", "pipe", "pipe"] } : { input: options.input, stdio: ["pipe", "pipe", "pipe"] }
|
|
23657
|
+
});
|
|
23658
|
+
return {
|
|
23659
|
+
ok: result.status === 0,
|
|
23660
|
+
status: result.status,
|
|
23661
|
+
stdout: result.stdout ?? "",
|
|
23662
|
+
stderr: result.stderr ?? ""
|
|
23663
|
+
};
|
|
23664
|
+
};
|
|
23665
|
+
function operationInProgress(cwd) {
|
|
23666
|
+
const markers = [
|
|
23667
|
+
["rebase-merge", "rebase"],
|
|
23668
|
+
["rebase-apply", "rebase"],
|
|
23669
|
+
["MERGE_HEAD", "merge"],
|
|
23670
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
23671
|
+
["REVERT_HEAD", "revert"]
|
|
23672
|
+
];
|
|
23673
|
+
const run = git5(cwd, ["rev-parse", ...markers.flatMap(([name]) => ["--git-path", name])]);
|
|
23674
|
+
if (!run.ok)
|
|
23675
|
+
return null;
|
|
23676
|
+
const found = run.stdout.split(`
|
|
23677
|
+
`).map((line) => path35.resolve(cwd, line.trim()));
|
|
23678
|
+
const conflicted = () => git5(cwd, ["ls-files", "-u"]).stdout.trim() !== "";
|
|
23679
|
+
for (const [index, [, operation]] of markers.entries()) {
|
|
23680
|
+
const marker = found[index];
|
|
23681
|
+
if (!marker || !existsSync11(marker))
|
|
23682
|
+
continue;
|
|
23683
|
+
if (index === 1 && existsSync11(path35.join(marker, "applying")))
|
|
23684
|
+
return { operation: "am", commitAllowed: false, branch: null };
|
|
23685
|
+
if (index === 0 && existsSync11(path35.join(marker, "amend")) && !conflicted()) {
|
|
23686
|
+
let headName = "";
|
|
23687
|
+
try {
|
|
23688
|
+
headName = readFileSync9(path35.join(marker, "head-name"), "utf8").trim();
|
|
23689
|
+
} catch {}
|
|
23690
|
+
const branch = headName.startsWith("refs/heads/") ? headName.slice("refs/heads/".length) : null;
|
|
23691
|
+
return { operation, commitAllowed: branch !== null, branch };
|
|
23692
|
+
}
|
|
23693
|
+
if ((operation === "revert" || operation === "merge") && !conflicted())
|
|
23694
|
+
return { operation, commitAllowed: true, branch: null };
|
|
23695
|
+
return { operation, commitAllowed: false, branch: null };
|
|
23696
|
+
}
|
|
23697
|
+
return null;
|
|
23698
|
+
}
|
|
23699
|
+
function inProgressFailure(operation, verb) {
|
|
23700
|
+
const command = operation === "am" ? "git am" : `git ${operation}`;
|
|
23701
|
+
return failure3("blocked", `${operation.replace("-", "_")}_in_progress: a ${operation === "am" ? "git am" : operation} is in progress; finish or abort it before workit can ${verb}`, `resolve the conflicts, git add <files>, then ${command} --continue # or ${command} --abort to return to where you started`);
|
|
23702
|
+
}
|
|
23703
|
+
function pushPreflight(cwd, options = {}) {
|
|
23704
|
+
const progress = operationInProgress(cwd);
|
|
23705
|
+
if (progress)
|
|
23706
|
+
return inProgressFailure(progress.operation, "push");
|
|
23707
|
+
const branch = options.branch ?? currentBranch(cwd);
|
|
23708
|
+
if (!branch)
|
|
23709
|
+
return failure3("invalid_input", "HEAD is detached; push a branch");
|
|
23710
|
+
if (isProtectedTarget(cwd, branch))
|
|
23711
|
+
return failure3("blocked", `protected_branch: ${branch} is protected by the workspace branch policy; workit git push never pushes to it`, releaseBranch(cwd, branch) ? `workit git push --release ${branch} # finishing a release or hotfix; needs the release grant` : "workit git branch <feature/name> # push a feature branch and open a PR");
|
|
23712
|
+
const sha = resolveRef(cwd, `refs/heads/${branch}`);
|
|
23713
|
+
if (!sha)
|
|
23714
|
+
return failure3("not_found", `branch ${branch} has no commit to push`);
|
|
23715
|
+
const remote = pushRemoteName(cwd, branch);
|
|
23716
|
+
if (!remote)
|
|
23717
|
+
return failure3("not_found", "no push remote is configured for this branch", "git remote add origin <url>");
|
|
23718
|
+
const raw = pushUrl(cwd, remote);
|
|
23719
|
+
if (!raw)
|
|
23720
|
+
return failure3("blocked", `remote "${remote}" has no single push URL`, `git remote get-url --push --all ${remote} # keep exactly one push URL`);
|
|
23721
|
+
const parsed = parseRemoteUrl(raw);
|
|
23722
|
+
const local = parsed?.protocol === "file";
|
|
23723
|
+
if (!local && !pushTargetIsStable(cwd, raw))
|
|
23724
|
+
return failure3("blocked", `push_target_rewritten: the push URL of "${remote}" is rewritten by a url.*.insteadOf rule, so the destination is not the one checked`, "git config --get-regexp '^url\\.' # remove the rewrite for this remote");
|
|
23725
|
+
return success2({
|
|
23726
|
+
branch,
|
|
23727
|
+
sha,
|
|
23728
|
+
remote,
|
|
23729
|
+
url: redactRemote(raw),
|
|
23730
|
+
rawUrl: raw,
|
|
23731
|
+
local
|
|
23732
|
+
});
|
|
23733
|
+
}
|
|
23734
|
+
|
|
23735
|
+
// packages/workit-core/src/forge/report.ts
|
|
23736
|
+
var GITLAB_MERGEABLE = new Set(["mergeable"]);
|
|
23737
|
+
var GITHUB_MERGEABLE = new Set(["clean", "unstable", "has_hooks"]);
|
|
23738
|
+
|
|
23739
|
+
// packages/workit-core/src/stack.ts
|
|
23740
|
+
var stackFail = (code, error, unblock, data) => ({
|
|
23741
|
+
ok: false,
|
|
23742
|
+
code,
|
|
23743
|
+
error,
|
|
23744
|
+
...unblock ? { unblock } : {},
|
|
23745
|
+
...data ? { data } : {}
|
|
23746
|
+
});
|
|
23747
|
+
var stackDeps = {
|
|
23748
|
+
pushPlan: (cwd, branch) => pushPreflight(cwd, { branch }),
|
|
23749
|
+
adapter: { name: "git", restack: (cwd, input) => gitRestack(cwd, input) },
|
|
23750
|
+
rebaseTimeoutMs: Number(process.env.WORKIT_STACK_REBASE_TIMEOUT_MS) || 10 * 60000
|
|
23751
|
+
};
|
|
23752
|
+
var gitRun3 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
23753
|
+
const run = spawnSync21("git", args, {
|
|
23754
|
+
cwd,
|
|
23755
|
+
encoding: "utf8",
|
|
23756
|
+
timeout: timeoutMs,
|
|
23757
|
+
killSignal: "SIGKILL",
|
|
23758
|
+
windowsHide: true,
|
|
23759
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
23760
|
+
env: {
|
|
23761
|
+
...process.env,
|
|
23762
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
23763
|
+
GIT_EDITOR: "true",
|
|
23764
|
+
GIT_SEQUENCE_EDITOR: "true",
|
|
23765
|
+
LC_ALL: "C",
|
|
23766
|
+
LANGUAGE: "C"
|
|
23767
|
+
}
|
|
23768
|
+
});
|
|
23769
|
+
const failure = run.error;
|
|
23770
|
+
return {
|
|
23771
|
+
ok: run.status === 0,
|
|
23772
|
+
stdout: run.stdout ?? "",
|
|
23773
|
+
stderr: run.error ? String(run.error.message) : run.stderr ?? "",
|
|
23774
|
+
timedOut: failure?.code === "ETIMEDOUT"
|
|
23775
|
+
};
|
|
23776
|
+
};
|
|
23777
|
+
var branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`);
|
|
23778
|
+
function worktreeOf(cwd, branch) {
|
|
23779
|
+
const list = gitRun3(cwd, ["worktree", "list", "--porcelain"]);
|
|
23780
|
+
if (!list.ok)
|
|
23781
|
+
return null;
|
|
23782
|
+
let current = null;
|
|
23783
|
+
for (const line of list.stdout.split(`
|
|
23784
|
+
`)) {
|
|
23785
|
+
if (line.startsWith("worktree "))
|
|
23786
|
+
current = line.slice("worktree ".length);
|
|
23787
|
+
else if (line === `branch refs/heads/${branch}` && current)
|
|
23788
|
+
return current;
|
|
23789
|
+
else if (line === "detached" && current && rebasingBranch2(current) === `refs/heads/${branch}`)
|
|
23790
|
+
return current;
|
|
23791
|
+
}
|
|
23792
|
+
return null;
|
|
23793
|
+
}
|
|
23794
|
+
function rebasingBranch2(dir) {
|
|
23795
|
+
for (const name of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
|
|
23796
|
+
const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
|
|
23797
|
+
if (!found.ok)
|
|
23798
|
+
continue;
|
|
23799
|
+
try {
|
|
23800
|
+
return fs20.readFileSync(found.stdout.trim(), "utf8").trim();
|
|
23801
|
+
} catch {}
|
|
23802
|
+
}
|
|
23803
|
+
return null;
|
|
23804
|
+
}
|
|
23805
|
+
function rebaseInProgress(dir) {
|
|
23806
|
+
for (const name of ["rebase-merge", "rebase-apply"]) {
|
|
23807
|
+
const found = gitRun3(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
|
|
23808
|
+
if (found.ok && fs20.existsSync(found.stdout.trim()))
|
|
23809
|
+
return true;
|
|
23810
|
+
}
|
|
23811
|
+
return false;
|
|
23812
|
+
}
|
|
23813
|
+
var scratchDir = (cwd) => {
|
|
23814
|
+
const dir = stacksDir(cwd);
|
|
23815
|
+
return dir.ok ? path36.join(dir.data, "worktrees") : null;
|
|
23816
|
+
};
|
|
23817
|
+
function gitRestack(cwd, input) {
|
|
23818
|
+
const owner = worktreeOf(cwd, input.branch);
|
|
23819
|
+
let dir = owner;
|
|
23820
|
+
if (!dir) {
|
|
23821
|
+
const scratch = scratchDir(cwd);
|
|
23822
|
+
if (!scratch)
|
|
23823
|
+
return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
|
|
23824
|
+
fs20.mkdirSync(scratch, { recursive: true });
|
|
23825
|
+
dir = path36.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes4(4).toString("hex")}`);
|
|
23826
|
+
const added = gitRun3(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
|
|
23827
|
+
if (!added.ok)
|
|
23828
|
+
return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
|
|
23829
|
+
}
|
|
23830
|
+
const run = gitRun3(dir, [
|
|
23831
|
+
"-c",
|
|
23832
|
+
"rebase.autoStash=false",
|
|
23833
|
+
"-c",
|
|
23834
|
+
"rebase.updateRefs=false",
|
|
23835
|
+
"rebase",
|
|
23836
|
+
"--no-autosquash",
|
|
23837
|
+
"--onto",
|
|
23838
|
+
input.onto,
|
|
23839
|
+
input.from
|
|
23840
|
+
], stackDeps.rebaseTimeoutMs);
|
|
23841
|
+
if (!run.ok) {
|
|
23842
|
+
if (run.timedOut)
|
|
23843
|
+
return {
|
|
23844
|
+
ok: false,
|
|
23845
|
+
conflict: false,
|
|
23846
|
+
timedOut: true,
|
|
23847
|
+
worktree: dir,
|
|
23848
|
+
error: `git rebase did not finish within ${stackDeps.rebaseTimeoutMs} ms`
|
|
23849
|
+
};
|
|
23850
|
+
if (rebaseInProgress(dir))
|
|
23851
|
+
return { ok: false, conflict: true, worktree: dir, error: firstLine(run) };
|
|
23852
|
+
if (!owner)
|
|
23853
|
+
gitRun3(cwd, ["worktree", "remove", "--force", dir]);
|
|
23854
|
+
return { ok: false, conflict: false, worktree: dir, error: firstLine(run) };
|
|
23855
|
+
}
|
|
23856
|
+
if (!owner)
|
|
23857
|
+
gitRun3(cwd, ["worktree", "remove", "--force", dir]);
|
|
23858
|
+
const head = branchTip(cwd, input.branch);
|
|
23859
|
+
return head ? { ok: true, head } : { ok: false, conflict: false, worktree: dir, error: `${input.branch} vanished` };
|
|
23860
|
+
}
|
|
23861
|
+
var firstLine = (run) => (`${run.stderr}
|
|
23862
|
+
${run.stdout}`.split(`
|
|
23863
|
+
`).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300);
|
|
23864
|
+
var stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`;
|
|
23865
|
+
function stacksDir(cwd) {
|
|
23866
|
+
const root = storeRoot(cwd);
|
|
23867
|
+
if (!root.ok)
|
|
23868
|
+
return stackFail(root.code, root.error, root.unblock);
|
|
23869
|
+
return { ok: true, data: path36.join(root.value.root, "stacks") };
|
|
23870
|
+
}
|
|
23871
|
+
var HEARTBEAT_STALE_MS = 2 * 60000;
|
|
23872
|
+
|
|
23873
|
+
// packages/workit-core/src/fanout.ts
|
|
23874
|
+
var FANOUT_VERSION = 1;
|
|
23875
|
+
var TIERS = ["mundane", "standard", "hard"];
|
|
23876
|
+
var fanoutFail = (code, error, unblock, data) => ({
|
|
23877
|
+
ok: false,
|
|
23878
|
+
code,
|
|
23879
|
+
error,
|
|
23880
|
+
...unblock ? { unblock } : {},
|
|
23881
|
+
...data ? { data } : {}
|
|
23882
|
+
});
|
|
23883
|
+
var compiled = new Map;
|
|
23884
|
+
function normalizePattern(raw) {
|
|
23885
|
+
let pattern = raw.trim();
|
|
23886
|
+
if (pattern.includes("\\")) {
|
|
23887
|
+
if (/\\[*?{}]/u.test(pattern))
|
|
23888
|
+
return {
|
|
23889
|
+
error: "has \\*, \\?, \\{ or \\}: only \\[ and \\] are escapes; use / as the separator (src/**/*.ts)"
|
|
23890
|
+
};
|
|
23891
|
+
const brackets = /\\[[\]]/u.test(pattern);
|
|
23892
|
+
if (brackets && pattern.replace(/\\[[\]]/gu, "").includes("\\"))
|
|
23893
|
+
return {
|
|
23894
|
+
error: "mixes \\[ or \\] escapes with other backslashes, which is ambiguous: use / as the separator (app/\\[id\\]/page.tsx)"
|
|
23895
|
+
};
|
|
23896
|
+
if (!brackets)
|
|
23897
|
+
pattern = pattern.replaceAll("\\", "/");
|
|
23898
|
+
}
|
|
23899
|
+
while (pattern.startsWith("./"))
|
|
23900
|
+
pattern = pattern.slice(2);
|
|
23901
|
+
if (pattern === "." || pattern === "")
|
|
23902
|
+
pattern = raw.trim() ? "**" : "";
|
|
23903
|
+
if (pattern.endsWith("/"))
|
|
23904
|
+
pattern = `${pattern}**`;
|
|
23905
|
+
if (!pattern)
|
|
23906
|
+
return { error: "is empty" };
|
|
23907
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
|
|
23908
|
+
return { error: "must be relative to the repository root" };
|
|
23909
|
+
if (pattern.split("/").includes(".."))
|
|
23910
|
+
return { error: "must not contain .." };
|
|
23911
|
+
const bare = pattern.replace(/\\./gu, "");
|
|
23912
|
+
if (/[[\]]/u.test(bare))
|
|
23913
|
+
return {
|
|
23914
|
+
error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
|
|
23915
|
+
};
|
|
23916
|
+
if (bare.startsWith("!"))
|
|
23917
|
+
return { error: "uses ! (negation is not supported)" };
|
|
23918
|
+
let depth = 0;
|
|
23919
|
+
for (const char of bare) {
|
|
23920
|
+
if (char === "{")
|
|
23921
|
+
depth += 1;
|
|
23922
|
+
if (char === "}")
|
|
23923
|
+
depth -= 1;
|
|
23924
|
+
if (depth < 0 || depth > 1)
|
|
23925
|
+
return { error: "has unbalanced or nested braces" };
|
|
23926
|
+
}
|
|
23927
|
+
if (depth !== 0)
|
|
23928
|
+
return { error: "has unbalanced or nested braces" };
|
|
23929
|
+
return { pattern };
|
|
23930
|
+
}
|
|
23931
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23932
|
+
function findCycle(slices) {
|
|
23933
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
23934
|
+
const state = new Map;
|
|
23935
|
+
const trail = [];
|
|
23936
|
+
const visit = (id) => {
|
|
23937
|
+
if (state.get(id) === "done")
|
|
23938
|
+
return null;
|
|
23939
|
+
if (state.get(id) === "open")
|
|
23940
|
+
return [...trail.slice(trail.indexOf(id)), id];
|
|
23941
|
+
state.set(id, "open");
|
|
23942
|
+
trail.push(id);
|
|
23943
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
23944
|
+
if (!byId.has(dep) || dep === id)
|
|
23945
|
+
continue;
|
|
23946
|
+
const found = visit(dep);
|
|
23947
|
+
if (found)
|
|
23948
|
+
return found;
|
|
23949
|
+
}
|
|
23950
|
+
trail.pop();
|
|
23951
|
+
state.set(id, "done");
|
|
23952
|
+
return null;
|
|
23953
|
+
};
|
|
23954
|
+
for (const slice of slices) {
|
|
23955
|
+
const found = visit(slice.id);
|
|
23956
|
+
if (found)
|
|
23957
|
+
return found;
|
|
23958
|
+
}
|
|
23959
|
+
return null;
|
|
23960
|
+
}
|
|
23961
|
+
function sliceHash(slice) {
|
|
23962
|
+
const {
|
|
23963
|
+
worktree: _worktree,
|
|
23964
|
+
hash: _hash,
|
|
23965
|
+
hashSince: _since,
|
|
23966
|
+
tier: _tier,
|
|
23967
|
+
timebox: _timebox,
|
|
23968
|
+
context: _context,
|
|
23969
|
+
issues: _issues,
|
|
23970
|
+
...definition
|
|
23971
|
+
} = slice;
|
|
23972
|
+
return createHash16("sha256").update(JSON.stringify(definition)).digest("hex").slice(0, 16);
|
|
23973
|
+
}
|
|
23974
|
+
function fanoutsDir(cwd) {
|
|
23975
|
+
const root = storeRoot(cwd);
|
|
23976
|
+
if (!root.ok)
|
|
23977
|
+
return fanoutFail(root.code, root.error, root.unblock);
|
|
23978
|
+
return { ok: true, data: path37.join(root.value.root, "fanouts") };
|
|
23979
|
+
}
|
|
23980
|
+
function parseStored(raw) {
|
|
23981
|
+
if (!isRecord5(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
|
|
23982
|
+
return null;
|
|
23983
|
+
if (!Array.isArray(raw.slices))
|
|
23984
|
+
return null;
|
|
23985
|
+
const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
23986
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
23987
|
+
let globsValid = true;
|
|
23988
|
+
const globs = (value) => list(value).map((entry) => {
|
|
23989
|
+
const normalized = normalizePattern(entry);
|
|
23990
|
+
if ("error" in normalized) {
|
|
23991
|
+
globsValid = false;
|
|
23992
|
+
return entry;
|
|
23993
|
+
}
|
|
23994
|
+
return normalized.pattern;
|
|
23995
|
+
});
|
|
23996
|
+
const slices = [];
|
|
23997
|
+
for (const item of raw.slices) {
|
|
23998
|
+
if (!isRecord5(item) || typeof item.id !== "string" || typeof item.branch !== "string")
|
|
23999
|
+
return null;
|
|
24000
|
+
slices.push({
|
|
24001
|
+
id: item.id,
|
|
24002
|
+
branch: item.branch,
|
|
24003
|
+
base: text(item.base) || raw.trunk,
|
|
24004
|
+
worktree: text(item.worktree),
|
|
24005
|
+
tier: TIERS.includes(item.tier) ? item.tier : "standard",
|
|
24006
|
+
dependsOn: list(item.dependsOn),
|
|
24007
|
+
scope: globs(item.scope),
|
|
24008
|
+
owns: globs(item.owns),
|
|
24009
|
+
goal: text(item.goal),
|
|
24010
|
+
acceptance: list(item.acceptance),
|
|
24011
|
+
verify: list(item.verify),
|
|
24012
|
+
forbidden: list(item.forbidden),
|
|
24013
|
+
context: typeof item.context === "string" ? item.context : null,
|
|
24014
|
+
timebox: typeof item.timebox === "string" ? item.timebox : null,
|
|
24015
|
+
issues: list(item.issues),
|
|
24016
|
+
hash: text(item.hash),
|
|
24017
|
+
hashSince: text(item.hashSince) || text(raw.createdAt)
|
|
24018
|
+
});
|
|
24019
|
+
}
|
|
24020
|
+
for (const slice of slices)
|
|
24021
|
+
slice.hash ||= sliceHash(slice);
|
|
24022
|
+
const shared = globs(raw.shared);
|
|
24023
|
+
const ids = new Set(slices.map((slice) => slice.id));
|
|
24024
|
+
if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
|
|
24025
|
+
return null;
|
|
24026
|
+
const fanIn = raw.fanIn === "integration" ? "integration" : "prs";
|
|
24027
|
+
const createdAt = text(raw.createdAt);
|
|
24028
|
+
return {
|
|
24029
|
+
v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
|
|
24030
|
+
name: raw.name,
|
|
24031
|
+
trunk: raw.trunk,
|
|
24032
|
+
fanIn,
|
|
24033
|
+
shared,
|
|
24034
|
+
slices,
|
|
24035
|
+
leadSession: typeof raw.leadSession === "string" && raw.leadSession ? raw.leadSession : null,
|
|
24036
|
+
formerLeads: list(raw.formerLeads),
|
|
24037
|
+
createdAt,
|
|
24038
|
+
updatedAt: text(raw.updatedAt)
|
|
24039
|
+
};
|
|
24040
|
+
}
|
|
24041
|
+
function readFanout(cwd, name) {
|
|
24042
|
+
const dir = fanoutsDir(cwd);
|
|
24043
|
+
if (!dir.ok)
|
|
24044
|
+
return dir;
|
|
24045
|
+
const file = path37.join(dir.data, stackFileName(name));
|
|
24046
|
+
let parsed = null;
|
|
24047
|
+
try {
|
|
24048
|
+
parsed = parseStored(JSON.parse(fs21.readFileSync(file, "utf8")));
|
|
24049
|
+
} catch (error) {
|
|
24050
|
+
if (error.code === "ENOENT")
|
|
24051
|
+
return { ok: true, data: null };
|
|
24052
|
+
parsed = null;
|
|
24053
|
+
}
|
|
24054
|
+
return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
|
|
24055
|
+
}
|
|
24056
|
+
function listFanouts(cwd) {
|
|
24057
|
+
const dir = fanoutsDir(cwd);
|
|
24058
|
+
if (!dir.ok)
|
|
24059
|
+
return dir;
|
|
24060
|
+
let names = [];
|
|
24061
|
+
try {
|
|
24062
|
+
names = fs21.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
|
|
24063
|
+
} catch {
|
|
24064
|
+
return { ok: true, data: [] };
|
|
24065
|
+
}
|
|
24066
|
+
const out = [];
|
|
24067
|
+
for (const entry of names) {
|
|
24068
|
+
let parsed = null;
|
|
24069
|
+
try {
|
|
24070
|
+
parsed = parseStored(JSON.parse(fs21.readFileSync(path37.join(dir.data, entry), "utf8")));
|
|
24071
|
+
} catch {
|
|
24072
|
+
parsed = null;
|
|
24073
|
+
}
|
|
24074
|
+
if (parsed && stackFileName(parsed.name) === entry)
|
|
24075
|
+
out.push(parsed);
|
|
24076
|
+
}
|
|
24077
|
+
return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
|
|
24078
|
+
}
|
|
24079
|
+
function selectFanout(cwd, name, current) {
|
|
24080
|
+
if (name) {
|
|
24081
|
+
const read = readFanout(cwd, name);
|
|
24082
|
+
if (!read.ok)
|
|
24083
|
+
return read;
|
|
24084
|
+
return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
|
|
24085
|
+
}
|
|
24086
|
+
const all = listFanouts(cwd);
|
|
24087
|
+
if (!all.ok)
|
|
24088
|
+
return all;
|
|
24089
|
+
const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
|
|
24090
|
+
if (holding.length === 1)
|
|
24091
|
+
return { ok: true, data: holding[0] };
|
|
24092
|
+
if (all.data.length === 1)
|
|
24093
|
+
return { ok: true, data: all.data[0] };
|
|
24094
|
+
return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
|
|
24095
|
+
}
|
|
24096
|
+
|
|
23454
24097
|
// packages/workit-opencode/src/v2/lifecycle.ts
|
|
23455
24098
|
var record2 = (value) => typeof value === "object" && value !== null ? value : {};
|
|
23456
24099
|
var sessionIDOf = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
23457
24100
|
var parentOf = (session) => typeof session?.parentID === "string" && session.parentID ? session.parentID : undefined;
|
|
24101
|
+
var resumeBriefCommand = (root, directory) => {
|
|
24102
|
+
const branch = directory ? currentBranch(directory) : null;
|
|
24103
|
+
const plan = branch ? selectFanout(root, null, branch) : null;
|
|
24104
|
+
const slice = branch && plan?.ok ? plan.data.slices.find((entry) => entry.branch === branch || branch.startsWith(`${entry.branch}-try`) && /^\d+$/u.test(branch.slice(entry.branch.length + 4))) : undefined;
|
|
24105
|
+
return `workit fanout brief ${slice?.id ?? "<slice>"} --mode resume`;
|
|
24106
|
+
};
|
|
23458
24107
|
var contentText = (content) => {
|
|
23459
24108
|
if (typeof content === "string")
|
|
23460
24109
|
return content;
|
|
@@ -23710,8 +24359,12 @@ var createV2Lifecycle = (deps) => {
|
|
|
23710
24359
|
throw new Error("delegation_lineage_denied: a continuation sessionID must name an existing direct child of the coordinator");
|
|
23711
24360
|
const listed = new TaskStore(root).listTasks();
|
|
23712
24361
|
const bound = listed.ok ? listed.data.filter((task) => task.workers.some((worker) => worker.data.state === "running" && worker.data.session?.kind === "host" && worker.data.session.handle === childID && workerCoordinatorFor(task, worker) === coordinator)).length : 0;
|
|
23713
|
-
if (bound !== 1 && ownsActiveTask(coordinator))
|
|
23714
|
-
|
|
24362
|
+
if (bound !== 1 && ownsActiveTask(coordinator)) {
|
|
24363
|
+
const reported = listed.ok && listed.data.some((task) => task.workers.some((worker) => worker.data.state === "stopped" && worker.data.session?.kind === "host" && worker.data.session.handle === childID && workerCoordinatorFor(task, worker) === coordinator));
|
|
24364
|
+
const dirty = reported && child.directory ? worktreeTree(child.directory)?.dirty ?? null : null;
|
|
24365
|
+
if (!reported || !holdsLiveState("stopped", dirty))
|
|
24366
|
+
throw new Error(reported ? `delegation_lineage_denied: this worker already reported and holds no live state (no running process, no uncommitted changes); continue the slice with a fresh worker: ${resumeBriefCommand(root, child.directory)}` : "delegation_lineage_denied: a continuation sessionID must name a running Workit worker");
|
|
24367
|
+
}
|
|
23715
24368
|
return;
|
|
23716
24369
|
}
|
|
23717
24370
|
if (!ownsActiveTask(coordinator)) {
|
|
@@ -24015,7 +24668,7 @@ var childTaskMissing = (store, input) => {
|
|
|
24015
24668
|
var childDenial = (identity) => `OpenCode child session has no validated Workit worker: as ${identity} it may only record evidence or findings (workit_evidence/workit_finding action "record") on an existing task; for anything else the lead assigns it a worker with workit_worker before launching it`;
|
|
24016
24669
|
|
|
24017
24670
|
// packages/workit-opencode/src/v2/shell.ts
|
|
24018
|
-
import
|
|
24671
|
+
import path38 from "node:path";
|
|
24019
24672
|
var commandOf = (input) => {
|
|
24020
24673
|
if (input === null || typeof input !== "object")
|
|
24021
24674
|
return null;
|
|
@@ -24024,7 +24677,7 @@ var commandOf = (input) => {
|
|
|
24024
24677
|
};
|
|
24025
24678
|
var hookInput = (root, sessionID, id, shell) => ({
|
|
24026
24679
|
host: "opencode",
|
|
24027
|
-
cwd: shell.workdir ?
|
|
24680
|
+
cwd: shell.workdir ? path38.resolve(root, shell.workdir) : root,
|
|
24028
24681
|
session: { id: sessionID, agentId: null, agentType: null, parentId: null },
|
|
24029
24682
|
permissionMode: null,
|
|
24030
24683
|
transcriptPath: null,
|
|
@@ -24208,38 +24861,38 @@ var createStopControl = (root, port) => {
|
|
|
24208
24861
|
};
|
|
24209
24862
|
|
|
24210
24863
|
// packages/workit-opencode/src/v2/registry.ts
|
|
24211
|
-
import { existsSync as
|
|
24212
|
-
import
|
|
24864
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
24865
|
+
import path40 from "node:path";
|
|
24213
24866
|
|
|
24214
24867
|
// packages/workit-opencode/src/shared/assets.ts
|
|
24215
|
-
import { existsSync as
|
|
24216
|
-
import
|
|
24868
|
+
import { existsSync as existsSync12, realpathSync as realpathSync9 } from "node:fs";
|
|
24869
|
+
import path39 from "node:path";
|
|
24217
24870
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
24218
24871
|
var moduleDir = () => {
|
|
24219
24872
|
try {
|
|
24220
|
-
return realpathSync9(
|
|
24873
|
+
return realpathSync9(path39.dirname(fileURLToPath2(import.meta.url)));
|
|
24221
24874
|
} catch {
|
|
24222
|
-
return
|
|
24875
|
+
return path39.dirname(fileURLToPath2(import.meta.url));
|
|
24223
24876
|
}
|
|
24224
24877
|
};
|
|
24225
24878
|
var packageRoot2 = () => {
|
|
24226
24879
|
const dir = moduleDir();
|
|
24227
24880
|
for (const candidate of [
|
|
24228
|
-
|
|
24229
|
-
|
|
24230
|
-
|
|
24881
|
+
path39.resolve(dir, ".."),
|
|
24882
|
+
path39.resolve(dir, "..", ".."),
|
|
24883
|
+
path39.resolve(dir, "..", "..", "..")
|
|
24231
24884
|
]) {
|
|
24232
|
-
if (
|
|
24885
|
+
if (existsSync12(path39.join(candidate, "package.json")))
|
|
24233
24886
|
return candidate;
|
|
24234
24887
|
}
|
|
24235
|
-
return
|
|
24888
|
+
return path39.resolve(dir, "..");
|
|
24236
24889
|
};
|
|
24237
24890
|
var skillsRoot = () => {
|
|
24238
24891
|
const root = packageRoot2();
|
|
24239
|
-
const canonical =
|
|
24240
|
-
if (
|
|
24892
|
+
const canonical = path39.resolve(root, "..", "workit-core", "skills");
|
|
24893
|
+
if (existsSync12(path39.join(root, "src")) && existsSync12(canonical))
|
|
24241
24894
|
return canonical;
|
|
24242
|
-
return
|
|
24895
|
+
return path39.join(root, "assets", "skills");
|
|
24243
24896
|
};
|
|
24244
24897
|
|
|
24245
24898
|
// packages/workit-opencode/src/v2/registry.ts
|
|
@@ -24263,15 +24916,15 @@ var parseFrontmatter = (markdown) => {
|
|
|
24263
24916
|
};
|
|
24264
24917
|
var registerSkills = async (ctx) => {
|
|
24265
24918
|
const skillsDir = skillsRoot();
|
|
24266
|
-
if (!
|
|
24919
|
+
if (!existsSync13(skillsDir))
|
|
24267
24920
|
return new Set;
|
|
24268
24921
|
const listed = await ctx.skill.list().catch(() => ({ data: [] }));
|
|
24269
|
-
const own = (skill) => typeof skill.path === "string" &&
|
|
24922
|
+
const own = (skill) => typeof skill.path === "string" && path40.resolve(skill.path).startsWith(skillsDir + path40.sep);
|
|
24270
24923
|
const userSkills = new Set((listed?.data ?? []).filter((skill) => !own(skill)).map((skill) => skill.id));
|
|
24271
24924
|
const packaged = new Map;
|
|
24272
24925
|
for (const id of WORKIT_METHOD_SKILLS) {
|
|
24273
|
-
const file =
|
|
24274
|
-
if (!userSkills.has(id) &&
|
|
24926
|
+
const file = path40.join(skillsDir, id, "SKILL.md");
|
|
24927
|
+
if (!userSkills.has(id) && existsSync13(file))
|
|
24275
24928
|
packaged.set(id, file);
|
|
24276
24929
|
}
|
|
24277
24930
|
await ctx.skill.transform((editor) => {
|
|
@@ -24279,7 +24932,7 @@ var registerSkills = async (ctx) => {
|
|
|
24279
24932
|
for (const [id, file] of packaged) {
|
|
24280
24933
|
if (existing.has(id))
|
|
24281
24934
|
continue;
|
|
24282
|
-
const parsed = parseFrontmatter(
|
|
24935
|
+
const parsed = parseFrontmatter(readFileSync10(file, "utf8"));
|
|
24283
24936
|
editor.add({
|
|
24284
24937
|
id,
|
|
24285
24938
|
name: parsed.name || id,
|
|
@@ -24317,25 +24970,25 @@ var registerCommands = async (ctx, registeredSkills) => {
|
|
|
24317
24970
|
};
|
|
24318
24971
|
|
|
24319
24972
|
// packages/workit-opencode/src/stale-sources.ts
|
|
24320
|
-
import { existsSync as
|
|
24321
|
-
import
|
|
24973
|
+
import { existsSync as existsSync14, readdirSync as readdirSync6 } from "node:fs";
|
|
24974
|
+
import path41 from "node:path";
|
|
24322
24975
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
24323
24976
|
var typescriptFiles = (directory) => {
|
|
24324
|
-
if (!
|
|
24977
|
+
if (!existsSync14(directory))
|
|
24325
24978
|
return [];
|
|
24326
24979
|
return readdirSync6(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
24327
|
-
const file =
|
|
24980
|
+
const file = path41.join(directory, entry.name);
|
|
24328
24981
|
return entry.isDirectory() ? typescriptFiles(file) : entry.isFile() && file.endsWith(".ts") ? [file] : [];
|
|
24329
24982
|
});
|
|
24330
24983
|
};
|
|
24331
24984
|
var pluginSourceFiles = [
|
|
24332
24985
|
...new Set([
|
|
24333
24986
|
fileURLToPath3(import.meta.url),
|
|
24334
|
-
...typescriptFiles(
|
|
24335
|
-
|
|
24336
|
-
...typescriptFiles(
|
|
24987
|
+
...typescriptFiles(path41.join(packageRoot2(), "src")),
|
|
24988
|
+
path41.join(packageRoot2(), "..", "workit-core", "src", "core.ts"),
|
|
24989
|
+
...typescriptFiles(path41.join(packageRoot2(), "..", "workit-core", "src", "core"))
|
|
24337
24990
|
])
|
|
24338
|
-
].filter((file) =>
|
|
24991
|
+
].filter((file) => existsSync14(file)).toSorted();
|
|
24339
24992
|
|
|
24340
24993
|
// packages/workit-opencode/src/v2/plugin.ts
|
|
24341
24994
|
var sessionFacts = async (ctx, sessionID) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-opencode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.15.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit OpenCode plugin — native tools, adaptive policy, and sixteen method skills",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"typecheck": "tsc --noEmit"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@brainervirus/workit-core": "^8.
|
|
42
|
+
"@brainervirus/workit-core": "^8.15.1",
|
|
43
43
|
"@opencode/plugin": "2.0.18"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|