@brainervirus/workit-core 0.11.0 → 1.0.0
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 +5 -3
- package/package.json +8 -3
- package/scripts/analyze-release-scope.ts +4 -1
- package/scripts/doctor-check.ts +3 -1
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +1 -0
- package/scripts/install-opencode-plugin.sh +1 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/rewrite-workspace-deps.ts +22 -5
- package/scripts/sync-release-manifests.ts +8 -5
- package/scripts/sync-runtime.sh +7 -3
- package/scripts/validate-cursor-marketplace.ts +16 -62
- package/skills/workit-babysit/SKILL.md +33 -0
- package/skills/workit-behavioral-tdd/SKILL.md +53 -0
- package/skills/workit-blast-radius/SKILL.md +31 -0
- package/skills/workit-challenge/SKILL.md +62 -0
- package/skills/workit-debug/SKILL.md +61 -0
- package/skills/workit-deslop/SKILL.md +36 -0
- package/skills/workit-diagram/SKILL.md +32 -0
- package/skills/workit-green-run/SKILL.md +29 -0
- package/skills/workit-handoff/SKILL.md +43 -0
- package/skills/workit-implement/SKILL.md +46 -0
- package/skills/workit-mockup/SKILL.md +28 -0
- package/skills/workit-plan/SKILL.md +66 -0
- package/skills/workit-review/SKILL.md +60 -0
- package/skills/workit-steer/SKILL.md +32 -0
- package/src/core/authority.ts +918 -0
- package/src/core/boundary.ts +29 -0
- package/src/core/branch.ts +26 -255
- package/src/core/changelog.ts +95 -18
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-conversion.ts +183 -0
- package/src/core/config.ts +42 -0
- package/src/core/cutover.ts +721 -0
- package/src/core/detect-hosts.ts +107 -0
- package/src/core/doctor.ts +428 -18
- package/src/core/external-action-effects.ts +1171 -0
- package/src/core/external-action.ts +541 -0
- package/src/core/init.ts +83 -32
- package/src/core/methods.ts +125 -0
- package/src/core/policy-resolver.ts +523 -0
- package/src/core/pr-create.ts +7 -1
- package/src/core/registration.ts +69 -4
- package/src/core/repo-context.ts +59 -1
- package/src/core/setup.ts +28 -0
- package/src/core/skill-manifests.ts +40 -42
- package/src/core/support-matrix.ts +8 -3
- package/src/core/sync-runtime.ts +15 -3
- package/src/core/task-context.ts +179 -0
- package/src/core/task-contract.ts +1136 -0
- package/src/core/task-engine.ts +2562 -0
- package/src/core/task-evaluation.ts +679 -0
- package/src/core/task-store.ts +1071 -0
- package/src/core/tracker-issues.ts +237 -0
- package/src/core/uninstall.ts +67 -3
- package/src/core/vcs-config.ts +53 -22
- package/src/core/workers.ts +166 -0
- package/src/core/workspaces.ts +3 -1
- package/src/core/youtrack-tools.ts +2 -0
- package/src/core/youtrack.ts +145 -10
- package/src/core.ts +126 -0
- package/templates/execution-contract.md +17 -50
- package/templates/plan-template.md +3 -3
- package/templates/spec-template.md +15 -3
- package/templates/workit-contract.md +12 -0
- package/commands/wk-changelog.md +0 -2
- package/commands/wk-commit.md +0 -2
- package/commands/wk-docs-refresh.md +0 -2
- package/commands/wk-handoff.md +0 -2
- package/commands/wk-implement.md +0 -2
- package/commands/wk-init.md +0 -2
- package/commands/wk-issue-update.md +0 -2
- package/commands/wk-meetings.md +0 -2
- package/commands/wk-pr.md +0 -2
- package/commands/wk-release-notes.md +0 -2
- package/commands/wk-status.md +0 -2
- package/commands/wk-verify.md +0 -2
- package/scripts/update-superpowers.sh +0 -82
- package/scripts/vendor-assets.ts +0 -37
- package/skills/wk-changelog/SKILL.md +0 -15
- package/skills/wk-commit/SKILL.md +0 -16
- package/skills/wk-docs-refresh/SKILL.md +0 -15
- package/skills/wk-handoff/SKILL.md +0 -20
- package/skills/wk-implement/SKILL.md +0 -47
- package/skills/wk-init/SKILL.md +0 -31
- package/skills/wk-issue-update/SKILL.md +0 -27
- package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
- package/skills/wk-meetings/SKILL.md +0 -17
- package/skills/wk-pr/SKILL.md +0 -27
- package/skills/wk-release-notes/SKILL.md +0 -15
- package/skills/wk-status/SKILL.md +0 -16
- package/skills/wk-verify/SKILL.md +0 -16
- package/src/core/detector.ts +0 -239
- package/src/core/flow-state.ts +0 -3308
- package/src/core/handoff-context.ts +0 -136
- package/src/core/handoff-tools.ts +0 -133
- package/src/core/menu.ts +0 -70
- package/src/core/plan-tasks.ts +0 -43
- package/src/core/reminder.ts +0 -124
- package/src/core/sdd.ts +0 -353
- package/src/state.ts +0 -22
- package/templates/superpowers-doc-contract.md +0 -75
- package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
- package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -428
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
- package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
- package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +0 -384
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { vcsConfig } from "./vcs-config";
|
|
4
|
+
import { hostingCliAvailable, parseGhIssue } from "./pr-create";
|
|
5
|
+
|
|
6
|
+
// Read-only issue fetches for context.read (GitHub + GitLab), mirroring the
|
|
7
|
+
// YouTrack triple shape. CLI-first: the native gh/glab CLI serves the read
|
|
8
|
+
// when installed (per-directory identity comes free); token files are the
|
|
9
|
+
// fallback and fail closed without one. No writes, no new secret files.
|
|
10
|
+
|
|
11
|
+
export type TrackerIssueBody = {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
body: string | null;
|
|
15
|
+
state: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TrackerIssueFailure = {
|
|
19
|
+
error: string;
|
|
20
|
+
kind: "creds" | "request" | "input";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type TrackerRequest = (
|
|
24
|
+
url: string,
|
|
25
|
+
init: { method: string; headers: Record<string, string> },
|
|
26
|
+
) => Promise<{ status: number; stdout: string; stderr: string }>;
|
|
27
|
+
|
|
28
|
+
export const trackerRequest: TrackerRequest = async (url, init) => {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(url, { method: init.method, headers: init.headers });
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
if (!res.ok) return { status: res.status, stdout: "", stderr: text.slice(0, 200) };
|
|
33
|
+
return { status: 0, stdout: text, stderr: "" };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return { status: 1, stdout: "", stderr: err instanceof Error ? err.message : "network error" };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Full repo path from an origin URL (scp-style, https, or bare); subgroups kept. */
|
|
40
|
+
export const repoPathFromRemote = (remote: string): string | null => {
|
|
41
|
+
let rest = (remote || "").trim().replace(/\/+$/, "");
|
|
42
|
+
if (!rest) return null;
|
|
43
|
+
if (rest.endsWith(".git")) rest = rest.slice(0, -4);
|
|
44
|
+
const scp = /^[^@/]+@[^:]+:(.+)$/.exec(rest);
|
|
45
|
+
if (scp) return scp[1].replace(/^\//, "") || null;
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(rest.includes("://") ? rest : `https://${rest}`);
|
|
48
|
+
return url.pathname.replace(/^\//, "") || null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type TrackerCli = (
|
|
55
|
+
args: string[],
|
|
56
|
+
root: string,
|
|
57
|
+
) => Promise<{ status: number; stdout: string; stderr: string }> | null;
|
|
58
|
+
|
|
59
|
+
export type TrackerDeps = {
|
|
60
|
+
creds?: (root: string) => TrackerCreds;
|
|
61
|
+
request?: TrackerRequest;
|
|
62
|
+
remote?: (root: string) => string | null;
|
|
63
|
+
cli?: TrackerCli;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Native CLI runner (gh/glab) when installed; null when absent. */
|
|
67
|
+
const defaultCli = (bin: string, available: boolean): TrackerCli | null => {
|
|
68
|
+
if (!available) return null;
|
|
69
|
+
return (args, root) => {
|
|
70
|
+
const out = spawnSync(bin, args, { cwd: root, encoding: "utf8" });
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
status: out.status ?? 1,
|
|
73
|
+
stdout: out.stdout ?? "",
|
|
74
|
+
stderr: (out.error as Error | undefined)?.message ?? out.stderr ?? "",
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const tryCliTriple = async (
|
|
80
|
+
cli: TrackerCli | null | undefined,
|
|
81
|
+
args: string[],
|
|
82
|
+
root: string,
|
|
83
|
+
id: string,
|
|
84
|
+
idKeys: string[],
|
|
85
|
+
bodyKeys: string[],
|
|
86
|
+
): Promise<TrackerIssueBody | null> => {
|
|
87
|
+
if (!cli) return null;
|
|
88
|
+
const run = cli(args, root);
|
|
89
|
+
if (!run) return null;
|
|
90
|
+
const out = await run;
|
|
91
|
+
if (out.status !== 0) return null;
|
|
92
|
+
try {
|
|
93
|
+
return parseTriple(JSON.parse(out.stdout) as Record<string, unknown>, id, idKeys, bodyKeys);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const originRemote = (root: string): string | null => {
|
|
100
|
+
const out = spawnSync("git", ["remote", "get-url", "origin"], { cwd: root, encoding: "utf8" });
|
|
101
|
+
return out.status === 0 ? (out.stdout ?? "").trim() || null : null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type TrackerCreds = { token: string; api: string } | { error: string };
|
|
105
|
+
|
|
106
|
+
const trackerCreds = (root: string, provider: "github" | "gitlab"): TrackerCreds => {
|
|
107
|
+
const cfg = vcsConfig("load", root) as Record<string, unknown>;
|
|
108
|
+
if (!cfg || cfg.provider !== provider || typeof cfg.tokenPath !== "string" || !cfg.tokenPath)
|
|
109
|
+
return { error: `unconfigured ${provider} provider` };
|
|
110
|
+
let token = "";
|
|
111
|
+
try {
|
|
112
|
+
token = fs.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
113
|
+
} catch {
|
|
114
|
+
return { error: `missing ${provider} token` };
|
|
115
|
+
}
|
|
116
|
+
if (!token) return { error: `empty ${provider} token file` };
|
|
117
|
+
if (provider === "gitlab") {
|
|
118
|
+
const gitlab = cfg.gitlab as Record<string, unknown> | undefined;
|
|
119
|
+
const apiUrl = String(gitlab?.apiUrl ?? "https://gitlab.com/api/v4").replace(/\/+$/, "");
|
|
120
|
+
return { token, api: apiUrl };
|
|
121
|
+
}
|
|
122
|
+
const github = cfg.github as Record<string, unknown> | undefined;
|
|
123
|
+
const host = String(github?.host ?? "github.com").toLowerCase();
|
|
124
|
+
return {
|
|
125
|
+
token,
|
|
126
|
+
api: host === "github.com" ? "https://api.github.com" : `https://${host}/api/v3`,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const parseTriple = (
|
|
131
|
+
parsed: Record<string, unknown>,
|
|
132
|
+
id: string,
|
|
133
|
+
idKeys: string[],
|
|
134
|
+
bodyKeys: string[],
|
|
135
|
+
): TrackerIssueBody | null => {
|
|
136
|
+
const title = parsed.title;
|
|
137
|
+
if (typeof title !== "string") return null;
|
|
138
|
+
const pick = (keys: string[]): string | null => {
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
const value = parsed[key];
|
|
141
|
+
if (typeof value === "string") return value;
|
|
142
|
+
if (typeof value === "number") return String(value);
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
const foundId = pick(idKeys) ?? id;
|
|
147
|
+
const state = typeof parsed.state === "string" ? parsed.state : null;
|
|
148
|
+
return { id: foundId, title, body: pick(bodyKeys), state };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Fetch a GitHub issue triple for context.read: gh CLI first, token fallback (fail-closed). */
|
|
152
|
+
export const fetchGitHubIssueBody = async (
|
|
153
|
+
ref: string,
|
|
154
|
+
root: string,
|
|
155
|
+
deps: TrackerDeps = {},
|
|
156
|
+
): Promise<{ data: TrackerIssueBody } | TrackerIssueFailure> => {
|
|
157
|
+
const id = parseGhIssue(ref);
|
|
158
|
+
if (!/^\d+$/.test(id)) return { error: `invalid GitHub issue ref "${ref}"`, kind: "input" };
|
|
159
|
+
const repo = repoPathFromRemote((deps.remote ?? originRemote)(root) ?? "");
|
|
160
|
+
if (!repo) return { error: "no origin remote to resolve the repository", kind: "input" };
|
|
161
|
+
const viaCli = await tryCliTriple(
|
|
162
|
+
deps.cli ?? defaultCli("gh", hostingCliAvailable("github")),
|
|
163
|
+
["issue", "view", id, "--repo", repo, "--json", "number,title,body,state"],
|
|
164
|
+
root,
|
|
165
|
+
id,
|
|
166
|
+
["number"],
|
|
167
|
+
["body"],
|
|
168
|
+
);
|
|
169
|
+
if (viaCli) return { data: viaCli };
|
|
170
|
+
const credentials = (deps.creds ?? ((r) => trackerCreds(r, "github")))(root);
|
|
171
|
+
if ("error" in credentials) return { ...credentials, kind: "creds" as const };
|
|
172
|
+
const out = await (deps.request ?? trackerRequest)(
|
|
173
|
+
`${credentials.api}/repos/${repo}/issues/${id}`,
|
|
174
|
+
{
|
|
175
|
+
method: "GET",
|
|
176
|
+
headers: {
|
|
177
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
178
|
+
Accept: "application/vnd.github+json",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
if (out.status !== 0)
|
|
183
|
+
return { error: out.stderr || "GitHub issue fetch failed", kind: "request" };
|
|
184
|
+
try {
|
|
185
|
+
const triple = parseTriple(
|
|
186
|
+
JSON.parse(out.stdout) as Record<string, unknown>,
|
|
187
|
+
id,
|
|
188
|
+
["number"],
|
|
189
|
+
["body"],
|
|
190
|
+
);
|
|
191
|
+
if (!triple) return { error: "unexpected GitHub issue shape", kind: "request" };
|
|
192
|
+
return { data: triple };
|
|
193
|
+
} catch {
|
|
194
|
+
return { error: "invalid GitHub issue response", kind: "request" };
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Fetch a GitLab issue triple for context.read: glab CLI first, token fallback (fail-closed). */
|
|
199
|
+
export const fetchGitLabIssueBody = async (
|
|
200
|
+
ref: string,
|
|
201
|
+
root: string,
|
|
202
|
+
deps: TrackerDeps = {},
|
|
203
|
+
): Promise<{ data: TrackerIssueBody } | TrackerIssueFailure> => {
|
|
204
|
+
const id = parseGhIssue(ref);
|
|
205
|
+
if (!/^\d+$/.test(id)) return { error: `invalid GitLab issue ref "${ref}"`, kind: "input" };
|
|
206
|
+
const repo = repoPathFromRemote((deps.remote ?? originRemote)(root) ?? "");
|
|
207
|
+
if (!repo) return { error: "no origin remote to resolve the project", kind: "input" };
|
|
208
|
+
const viaCli = await tryCliTriple(
|
|
209
|
+
deps.cli ?? defaultCli("glab", hostingCliAvailable("gitlab")),
|
|
210
|
+
["issue", "view", id, "-R", repo, "-F", "json"],
|
|
211
|
+
root,
|
|
212
|
+
id,
|
|
213
|
+
["iid"],
|
|
214
|
+
["description"],
|
|
215
|
+
);
|
|
216
|
+
if (viaCli) return { data: viaCli };
|
|
217
|
+
const credentials = (deps.creds ?? ((r) => trackerCreds(r, "gitlab")))(root);
|
|
218
|
+
if ("error" in credentials) return { ...credentials, kind: "creds" as const };
|
|
219
|
+
const out = await (deps.request ?? trackerRequest)(
|
|
220
|
+
`${credentials.api}/projects/${encodeURIComponent(repo)}/issues/${id}`,
|
|
221
|
+
{ method: "GET", headers: { "PRIVATE-TOKEN": credentials.token } },
|
|
222
|
+
);
|
|
223
|
+
if (out.status !== 0)
|
|
224
|
+
return { error: out.stderr || "GitLab issue fetch failed", kind: "request" };
|
|
225
|
+
try {
|
|
226
|
+
const triple = parseTriple(
|
|
227
|
+
JSON.parse(out.stdout) as Record<string, unknown>,
|
|
228
|
+
id,
|
|
229
|
+
["iid"],
|
|
230
|
+
["description"],
|
|
231
|
+
);
|
|
232
|
+
if (!triple) return { error: "unexpected GitLab issue shape", kind: "request" };
|
|
233
|
+
return { data: triple };
|
|
234
|
+
} catch {
|
|
235
|
+
return { error: "invalid GitLab issue response", kind: "request" };
|
|
236
|
+
}
|
|
237
|
+
};
|
package/src/core/uninstall.ts
CHANGED
|
@@ -11,7 +11,7 @@ import os from "node:os";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { isWorkitPlugin } from "./registration";
|
|
13
13
|
|
|
14
|
-
export type UninstallHost = "opencode" | "cursor";
|
|
14
|
+
export type UninstallHost = "opencode" | "cursor" | "codex" | "pi";
|
|
15
15
|
|
|
16
16
|
export type UninstallAction =
|
|
17
17
|
| { kind: "edit-json-remove"; path: string; detail: string }
|
|
@@ -56,6 +56,8 @@ type ResolvedUninstall = {
|
|
|
56
56
|
cursorSettings: string;
|
|
57
57
|
cursorMcp: string;
|
|
58
58
|
cursorPluginDir: string;
|
|
59
|
+
codexPluginDir: string;
|
|
60
|
+
piConfig: string;
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
const resolveUninstallPaths = (options: UninstallPaths = {}): ResolvedUninstall => {
|
|
@@ -70,6 +72,8 @@ const resolveUninstallPaths = (options: UninstallPaths = {}): ResolvedUninstall
|
|
|
70
72
|
cursorMcp: options.cursorMcp ?? path.join(home, ".cursor", "mcp.json"),
|
|
71
73
|
cursorPluginDir:
|
|
72
74
|
options.cursorPluginDir ?? path.join(home, ".cursor", "plugins", "local", "workit"),
|
|
75
|
+
codexPluginDir: path.join(home, ".codex", "plugins", "workit"),
|
|
76
|
+
piConfig: path.join(home, ".pi", "config.json"),
|
|
73
77
|
};
|
|
74
78
|
};
|
|
75
79
|
|
|
@@ -192,10 +196,38 @@ type JsonCleaner = (record: Record<string, unknown>) => {
|
|
|
192
196
|
changed: boolean;
|
|
193
197
|
};
|
|
194
198
|
|
|
199
|
+
function cleanPiConfig(config: Record<string, unknown>): {
|
|
200
|
+
next: Record<string, unknown>;
|
|
201
|
+
changed: boolean;
|
|
202
|
+
} {
|
|
203
|
+
const next = { ...config };
|
|
204
|
+
let changed = false;
|
|
205
|
+
for (const key of ["extensions", "skills"] as const) {
|
|
206
|
+
const value = next[key];
|
|
207
|
+
if (!Array.isArray(value)) continue;
|
|
208
|
+
const kept = value.filter((entry) => !String(entry).includes("workit"));
|
|
209
|
+
if (kept.length !== value.length) {
|
|
210
|
+
next[key] = kept;
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const pi = isRecord(next.pi) ? { ...(next.pi as Record<string, unknown>) } : null;
|
|
215
|
+
if (pi && Array.isArray(pi.extensions)) {
|
|
216
|
+
const kept = pi.extensions.filter((entry) => !String(entry).includes("workit"));
|
|
217
|
+
if (kept.length !== pi.extensions.length) {
|
|
218
|
+
pi.extensions = kept;
|
|
219
|
+
next.pi = pi;
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { next, changed };
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
const jsonCleanerFor = (target: string, res: ResolvedUninstall): JsonCleaner | null => {
|
|
196
227
|
if (target === res.opencodeConfig) return cleanOpenCodeConfig;
|
|
197
228
|
if (target === res.cursorSettings) return (r) => cleanCursorSettings(r, res.cursorPluginDir);
|
|
198
229
|
if (target === res.cursorMcp) return cleanCursorMcp;
|
|
230
|
+
if (target === res.piConfig) return cleanPiConfig;
|
|
199
231
|
return null;
|
|
200
232
|
};
|
|
201
233
|
|
|
@@ -262,7 +294,34 @@ export function planUninstall(paths: UninstallPaths = {}): UninstallPlan {
|
|
|
262
294
|
actions,
|
|
263
295
|
};
|
|
264
296
|
|
|
265
|
-
|
|
297
|
+
const codexInstalled = existsSync(res.codexPluginDir);
|
|
298
|
+
const codex: UninstallHostPlan = {
|
|
299
|
+
host: "codex",
|
|
300
|
+
installed: codexInstalled,
|
|
301
|
+
actions: codexInstalled
|
|
302
|
+
? [{ kind: "remove-dir", path: res.codexPluginDir, detail: "remove Codex workit plugin dir" }]
|
|
303
|
+
: [],
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const piExisting = readJsonRecord(res.piConfig);
|
|
307
|
+
const piDirty =
|
|
308
|
+
piExisting.kind === "malformed" ||
|
|
309
|
+
(piExisting.kind === "record" && cleanPiConfig(piExisting.value).changed);
|
|
310
|
+
const pi: UninstallHostPlan = {
|
|
311
|
+
host: "pi",
|
|
312
|
+
installed: piDirty,
|
|
313
|
+
actions: piDirty
|
|
314
|
+
? [
|
|
315
|
+
{
|
|
316
|
+
kind: "edit-json-remove",
|
|
317
|
+
path: res.piConfig,
|
|
318
|
+
detail: "remove workit pi extension/skills entries",
|
|
319
|
+
},
|
|
320
|
+
]
|
|
321
|
+
: [],
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return { hosts: [opencode, cursor, codex, pi] };
|
|
266
325
|
}
|
|
267
326
|
|
|
268
327
|
// CA-14 traversal guard: rm -rf is permitted ONLY on the exact resolved
|
|
@@ -344,7 +403,12 @@ export function applyUninstall(plan: UninstallPlan, paths: UninstallPaths = {}):
|
|
|
344
403
|
if (action.kind === "remove-dir") {
|
|
345
404
|
// Resolve before comparing so ".."/symlink tricks can never widen the rm.
|
|
346
405
|
const resolved = path.resolve(action.path);
|
|
347
|
-
|
|
406
|
+
const allowed =
|
|
407
|
+
canonicalRemoveDirAllowed(resolved, res) ||
|
|
408
|
+
(resolved === path.resolve(res.codexPluginDir) &&
|
|
409
|
+
path.basename(resolved) === "workit" &&
|
|
410
|
+
path.basename(path.dirname(resolved)) === "plugins");
|
|
411
|
+
if (!allowed) {
|
|
348
412
|
status = "failed";
|
|
349
413
|
detail = `refusing to remove non-canonical plugin directory: ${resolved}`;
|
|
350
414
|
} else if (!existsSync(resolved)) {
|
package/src/core/vcs-config.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const vcsConfigPath = (): string =>
|
|
|
15
15
|
const workspacesPath = (): string => path.join(configDir(), "workspaces.json");
|
|
16
16
|
|
|
17
17
|
const vcsCwd = (cwd?: string): string =>
|
|
18
|
-
process.env.WORKFLOW_WORKSPACE_ROOT ??
|
|
18
|
+
cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
19
19
|
|
|
20
20
|
// RL-03b: the origin remote is the ground truth for PR creation. A stale global
|
|
21
21
|
// provider (e.g. gitlab from another repo) must not drive glab on a
|
|
@@ -81,9 +81,16 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
81
81
|
// contaminate host-agnostic reads.
|
|
82
82
|
const root = vcsCwd(cwd);
|
|
83
83
|
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
// No silent default: a provider resolves from the workspace, the origin
|
|
85
|
+
// remote, or the global config — otherwise it is null, explicitly. Branch
|
|
86
|
+
// policy never needed a provider (preset defaults apply); credential and
|
|
87
|
+
// PR flows fail closed on null instead of assuming a host.
|
|
88
|
+
const rawProvider =
|
|
89
|
+
wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
90
|
+
const provider =
|
|
91
|
+
rawProvider === null || rawProvider === undefined
|
|
92
|
+
? null
|
|
93
|
+
: String(rawProvider).toLowerCase() || null;
|
|
87
94
|
// CA-05: workspace/global explicit target wins; when unset the resolved
|
|
88
95
|
// branch-policy preset supplies the default (gitflow->develop,
|
|
89
96
|
// github-flow->main, trunk-based->master, custom->develop). Shared by load
|
|
@@ -140,9 +147,26 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
|
|
|
140
147
|
if (cfgStatus === "missing") {
|
|
141
148
|
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
142
149
|
}
|
|
150
|
+
// Credentials cannot resolve without a provider: fail closed instead of
|
|
151
|
+
// assuming a host. Branch policy (resolve mode above) never needed one.
|
|
152
|
+
if (provider === null) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
156
|
+
configPath: cfgPath,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
143
159
|
|
|
144
160
|
const prov = (cfg[provider] ?? {}) as Record<string, any>;
|
|
145
|
-
|
|
161
|
+
// Workspace-tight tokens: an explicit workspace vcs.tokenFile wins (per-area
|
|
162
|
+
// accounts without collisions), else the global provider tokenFile, else the
|
|
163
|
+
// default <provider>.token path. Same order as the per-workspace
|
|
164
|
+
// commitPolicy/branchPolicy overrides.
|
|
165
|
+
const wsTokenFile =
|
|
166
|
+
typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
167
|
+
const tokenFile = String(
|
|
168
|
+
wsTokenFile ?? prov.tokenFile ?? path.join(configDir(), `${provider}.token`),
|
|
169
|
+
);
|
|
146
170
|
const tokenPath = path.resolve(tokenFile);
|
|
147
171
|
let tokenOk = false;
|
|
148
172
|
if (fs.existsSync(tokenPath)) {
|
|
@@ -281,24 +305,31 @@ export function vcsTokenCreateUrls(): Record<string, any> {
|
|
|
281
305
|
: ["repo"];
|
|
282
306
|
const githubClassicUrl = `https://github.com/settings/tokens/new?${new URLSearchParams({ description: name, scopes: classicScopes.join(",") })}`;
|
|
283
307
|
|
|
284
|
-
const
|
|
308
|
+
const rawProvider = cfg.provider;
|
|
309
|
+
// Display-only URL helper: both providers' URLs are always returned, but
|
|
310
|
+
// nothing is preselected without an explicit provider — no silent default.
|
|
311
|
+
const provider =
|
|
312
|
+
typeof rawProvider === "string" && rawProvider.trim() ? rawProvider.toLowerCase() : null;
|
|
285
313
|
const active =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
provider === null
|
|
315
|
+
? undefined
|
|
316
|
+
: ({
|
|
317
|
+
gitlab: {
|
|
318
|
+
tokenFile: gitlab.tokenFile ?? path.join(configDir(), "gitlab.token"),
|
|
319
|
+
createUrl: gitlabUrl,
|
|
320
|
+
scopes: gitlabScopes,
|
|
321
|
+
name,
|
|
322
|
+
},
|
|
323
|
+
github: {
|
|
324
|
+
tokenFile:
|
|
325
|
+
(cfg.github as Record<string, any>)?.tokenFile ??
|
|
326
|
+
path.join(configDir(), "github.token"),
|
|
327
|
+
createUrl: githubFineUrl,
|
|
328
|
+
createUrlClassic: githubClassicUrl,
|
|
329
|
+
permissions: githubPerms,
|
|
330
|
+
name,
|
|
331
|
+
},
|
|
332
|
+
}[provider] ?? {});
|
|
302
333
|
|
|
303
334
|
return {
|
|
304
335
|
tokenName: name,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
failure,
|
|
4
|
+
success,
|
|
5
|
+
type Caller,
|
|
6
|
+
type Id,
|
|
7
|
+
type Owner,
|
|
8
|
+
type Provenance,
|
|
9
|
+
type Ref,
|
|
10
|
+
type Result,
|
|
11
|
+
type Revision,
|
|
12
|
+
type TaskRecord,
|
|
13
|
+
type Worker,
|
|
14
|
+
type WorkspaceRecord,
|
|
15
|
+
} from "./task-contract";
|
|
16
|
+
import { TaskStore } from "./task-store";
|
|
17
|
+
|
|
18
|
+
export type WorkerState = Worker["state"];
|
|
19
|
+
export type HostSession = Extract<Ref, { kind: "host" }>;
|
|
20
|
+
|
|
21
|
+
/** The adapter-owned caller identity used by all writer checks. */
|
|
22
|
+
export type CallerContext = Caller & {
|
|
23
|
+
workerId?: Id | null;
|
|
24
|
+
session?: HostSession;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NativeWorkerObservation = {
|
|
28
|
+
taskId: Id;
|
|
29
|
+
workerId: Id;
|
|
30
|
+
expectedRevision: Revision;
|
|
31
|
+
expectedWorkspaceRevision: Revision;
|
|
32
|
+
state: WorkerState;
|
|
33
|
+
session: HostSession | null;
|
|
34
|
+
observation: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NativeWorkerVerification = {
|
|
38
|
+
observation: unknown;
|
|
39
|
+
expected: Omit<NativeWorkerObservation, "observation"> & {
|
|
40
|
+
workspaceId: Id;
|
|
41
|
+
};
|
|
42
|
+
caller: Caller;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Host attestation for a worker that has no session yet: `prepare` claims the launch
|
|
47
|
+
* slot, `not_started` claims that no child was ever created for that same reservation.
|
|
48
|
+
*/
|
|
49
|
+
export type WorkerDispatchStage = "prepare" | "not_started";
|
|
50
|
+
|
|
51
|
+
export type NativeWorkerDispatchVerification = {
|
|
52
|
+
observation: unknown;
|
|
53
|
+
expected: {
|
|
54
|
+
taskId: Id;
|
|
55
|
+
workspaceId: Id;
|
|
56
|
+
workerId: Id;
|
|
57
|
+
expectedRevision: Revision;
|
|
58
|
+
expectedWorkspaceRevision: Revision;
|
|
59
|
+
stage: WorkerDispatchStage;
|
|
60
|
+
};
|
|
61
|
+
caller: Caller;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type NativeWorkerVerifier = {
|
|
65
|
+
verifyWorker: (input: NativeWorkerVerification) => Result<Provenance>;
|
|
66
|
+
/** Hosts that can observe their own launch surface also attest dispatch stages. */
|
|
67
|
+
verifyDispatch?: (input: NativeWorkerDispatchVerification) => Result<Provenance>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
declare const workerDispatch: unique symbol;
|
|
71
|
+
/** Opaque in-process launch reservation. Adapters hold it; it is never serialized. */
|
|
72
|
+
export type WorkerDispatch = { readonly [workerDispatch]: true };
|
|
73
|
+
|
|
74
|
+
export type WorkerDispatchRequest = {
|
|
75
|
+
taskId: Id;
|
|
76
|
+
workerId: Id;
|
|
77
|
+
expectedRevision: Revision;
|
|
78
|
+
expectedWorkspaceRevision: Revision;
|
|
79
|
+
observation: unknown;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type WorkerDispatchCommit = WorkerDispatchRequest & {
|
|
83
|
+
dispatch: WorkerDispatch;
|
|
84
|
+
outcome: "started" | "not_started";
|
|
85
|
+
session: HostSession | null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const same = (left: unknown, right: unknown): boolean => {
|
|
89
|
+
try {
|
|
90
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const callerSession = (caller: CallerContext): HostSession =>
|
|
97
|
+
caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
98
|
+
|
|
99
|
+
const validPath = (value: unknown): value is string =>
|
|
100
|
+
typeof value === "string" &&
|
|
101
|
+
value.length > 0 &&
|
|
102
|
+
![...value].some((char) => {
|
|
103
|
+
const code = char.charCodeAt(0);
|
|
104
|
+
return code < 32 || code === 127;
|
|
105
|
+
}) &&
|
|
106
|
+
!value.split(/[\\/]/).includes("..");
|
|
107
|
+
|
|
108
|
+
const callerValue = (value: CallerContext): Caller => ({ host: value.host, actor: value.actor });
|
|
109
|
+
const workerIdOf = (value: CallerContext): Id | null => value.workerId ?? null;
|
|
110
|
+
|
|
111
|
+
export type ProductWriteInput = {
|
|
112
|
+
task: TaskRecord;
|
|
113
|
+
workspace: WorkspaceRecord;
|
|
114
|
+
caller: CallerContext;
|
|
115
|
+
paths: string[];
|
|
116
|
+
store?: TaskStore;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Check the authoritative workspace owner and the caller's role. File paths
|
|
120
|
+
* are host-policy territory: only malformed paths are rejected here, never
|
|
121
|
+
* out-of-scope ones. */
|
|
122
|
+
export function assertProductWriteAllowed(input: ProductWriteInput): Result<Owner> {
|
|
123
|
+
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
124
|
+
return failure("invalid_input", "invalid product write path");
|
|
125
|
+
if (!input.store) return failure("permission_denied", "write authorization requires core state");
|
|
126
|
+
const task = input.store.readTask(input.task.id);
|
|
127
|
+
if (!task.ok) return task as Result<never>;
|
|
128
|
+
const workspace = input.store.readWorkspace();
|
|
129
|
+
if (!workspace.ok) return workspace as Result<never>;
|
|
130
|
+
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
131
|
+
const workspaceRecord = workspace.data;
|
|
132
|
+
if (task.data.status !== "active")
|
|
133
|
+
return failure("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
134
|
+
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
135
|
+
return failure("recovery_required", "task and workspace bindings are invalid");
|
|
136
|
+
const writer = workspaceRecord.writer;
|
|
137
|
+
if (!writer) return failure("permission_denied", "checkout has no writer owner");
|
|
138
|
+
if (writer.state === "uncertain")
|
|
139
|
+
return failure("recovery_required", "checkout writer requires recovery", {
|
|
140
|
+
owner: writer.owner,
|
|
141
|
+
});
|
|
142
|
+
const caller = callerValue(input.caller);
|
|
143
|
+
const workerId = workerIdOf(input.caller);
|
|
144
|
+
const owner = writer.owner;
|
|
145
|
+
if (
|
|
146
|
+
owner.taskId !== task.data.id ||
|
|
147
|
+
owner.workerId !== workerId ||
|
|
148
|
+
owner.session.host !== caller.host ||
|
|
149
|
+
owner.session.handle !== caller.actor
|
|
150
|
+
)
|
|
151
|
+
return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
152
|
+
const worker =
|
|
153
|
+
workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
154
|
+
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
155
|
+
return failure("permission_denied", "only implementers can own product writes");
|
|
156
|
+
if (
|
|
157
|
+
workerId !== null &&
|
|
158
|
+
(!worker?.data.session ||
|
|
159
|
+
worker.data.state === "cancelling" ||
|
|
160
|
+
worker.data.state === "unknown" ||
|
|
161
|
+
worker.data.state === "stopped" ||
|
|
162
|
+
!same(worker.data.session, callerSession(input.caller)))
|
|
163
|
+
)
|
|
164
|
+
return failure("recovery_required", "worker session is not an active writer");
|
|
165
|
+
return success(null, workspaceRecord.revision, owner);
|
|
166
|
+
}
|
package/src/core/workspaces.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { configDir, isConfigObject, type BranchPreset } from "./config";
|
|
4
|
+
import type { CommitFlavorPreset } from "./commit-flavors";
|
|
4
5
|
|
|
5
6
|
export type VcsProvider = "gitlab" | "github";
|
|
6
7
|
|
|
@@ -18,10 +19,11 @@ export type WorkspaceBranchPolicy = {
|
|
|
18
19
|
export type WorkspaceConfig = {
|
|
19
20
|
name: string;
|
|
20
21
|
glob: string;
|
|
21
|
-
vcs?: { provider: VcsProvider; defaultTargetBranch?: string };
|
|
22
|
+
vcs?: { provider: VcsProvider; defaultTargetBranch?: string; tokenFile?: string };
|
|
22
23
|
youtrack?: { baseUrl?: string; link_issues?: boolean };
|
|
23
24
|
issues?: { provider?: "github"; link_on_pr?: boolean };
|
|
24
25
|
branchPolicy?: WorkspaceBranchPolicy;
|
|
26
|
+
commitPolicy?: { preset: CommitFlavorPreset; pattern?: string };
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export const workspacesPath = (): string => path.join(configDir(), "workspaces.json");
|
|
@@ -93,6 +93,7 @@ type PostInput = {
|
|
|
93
93
|
issueId: string;
|
|
94
94
|
markdown: string;
|
|
95
95
|
minutes?: number;
|
|
96
|
+
dateMs?: number;
|
|
96
97
|
workspace_root?: string;
|
|
97
98
|
};
|
|
98
99
|
|
|
@@ -147,6 +148,7 @@ export async function postUpdate(
|
|
|
147
148
|
const time = await operations.logTime({
|
|
148
149
|
issueId: input.issueId,
|
|
149
150
|
minutes: input.minutes,
|
|
151
|
+
...(input.dateMs === undefined ? {} : { dateMs: input.dateMs }),
|
|
150
152
|
text: "workit update",
|
|
151
153
|
workspace_root: input.workspace_root,
|
|
152
154
|
});
|