@workit-dev/core 0.0.1 → 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/LICENSE +21 -0
- package/README.md +22 -43
- package/package.json +56 -6
- package/schemas/config.json +74 -0
- package/schemas/vcs.json +79 -0
- package/schemas/workspaces.json +1029 -0
- package/schemas/youtrack.json +84 -0
- package/scripts/analyze-release-scope.ts +215 -0
- package/scripts/bundled-deps.json +131 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +146 -0
- package/scripts/install-opencode-plugin.sh +79 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/lib/config-dir.sh +28 -0
- package/scripts/publish-changed-packages.ts +395 -0
- package/scripts/rewrite-workspace-deps.ts +53 -0
- package/scripts/run-cursor-mcp.sh +6 -0
- package/scripts/sync-release-manifests.ts +82 -0
- package/scripts/sync-runtime.sh +142 -0
- package/scripts/validate-cursor-marketplace.ts +171 -0
- package/scripts/verify-bundle-sources.ts +134 -0
- package/scripts/write-schemas.ts +16 -0
- package/skills/workit-architecture/SKILL.md +72 -0
- package/skills/workit-architecture/agents/openai.yaml +3 -0
- package/skills/workit-architecture/references/instruction-files.md +52 -0
- package/skills/workit-architecture/references/vocabulary.md +53 -0
- package/skills/workit-bdd/SKILL.md +53 -0
- package/skills/workit-continue/SKILL.md +54 -0
- package/skills/workit-debug/SKILL.md +45 -0
- package/skills/workit-deslop/SKILL.md +58 -0
- package/skills/workit-fanout/SKILL.md +71 -0
- package/skills/workit-fanout/references/brief.md +141 -0
- package/skills/workit-fanout/references/orchestration.md +169 -0
- package/skills/workit-implement/SKILL.md +71 -0
- package/skills/workit-retro/SKILL.md +72 -0
- package/skills/workit-retro/agents/openai.yaml +3 -0
- package/skills/workit-retro/references/sources.md +35 -0
- package/skills/workit-retro/references/steering.md +39 -0
- package/skills/workit-review/SKILL.md +66 -0
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +61 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +44 -0
- package/skills/workit-ship/SKILL.md +71 -0
- package/skills/workit-ship/references/changelog.md +44 -0
- package/skills/workit-ship/references/pr-body.md +32 -0
- package/skills/workit-ship/references/release.md +39 -0
- package/skills/workit-test-audit/SKILL.md +54 -0
- package/skills/workit-verify-app/SKILL.md +68 -0
- package/skills/workit-verify-app/references/template.md +51 -0
- package/src/autonomy.ts +910 -0
- package/src/check-config.ts +613 -0
- package/src/checks.ts +534 -0
- package/src/ci-waiter.ts +159 -0
- package/src/core/boundary.ts +55 -0
- package/src/core/branch-policy.ts +112 -0
- package/src/core/branch.ts +1192 -0
- package/src/core/changelog-gate.ts +349 -0
- package/src/core/changelog-merge.ts +214 -0
- package/src/core/changelog.ts +392 -0
- package/src/core/cleanup.ts +638 -0
- package/src/core/cli-locate.ts +173 -0
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-schemas.ts +653 -0
- package/src/core/config.ts +351 -0
- package/src/core/context-read.ts +144 -0
- package/src/core/docs-gate.ts +69 -0
- package/src/core/finding-evidence.ts +89 -0
- package/src/core/finding-queue.ts +36 -0
- package/src/core/git.ts +73 -0
- package/src/core/gitignore.ts +52 -0
- package/src/core/heavy-eta.ts +170 -0
- package/src/core/heavy-lease.ts +290 -0
- package/src/core/heavy-rules.ts +373 -0
- package/src/core/heavy-scope.ts +131 -0
- package/src/core/heavy-words.ts +83 -0
- package/src/core/heavy.ts +535 -0
- package/src/core/hygiene.ts +149 -0
- package/src/core/init.ts +218 -0
- package/src/core/live-session.ts +333 -0
- package/src/core/logger.ts +310 -0
- package/src/core/methods.ts +185 -0
- package/src/core/model-fallback.ts +208 -0
- package/src/core/node-resolve.mjs +249 -0
- package/src/core/operation-input.ts +632 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/policy/attributes.ts +97 -0
- package/src/core/policy/bookkeeping.ts +119 -0
- package/src/core/policy/carry.ts +122 -0
- package/src/core/policy/check-evidence.ts +115 -0
- package/src/core/policy/current-judgment.ts +20 -0
- package/src/core/policy/derive.ts +387 -0
- package/src/core/policy/judgment.ts +246 -0
- package/src/core/policy/lane.ts +562 -0
- package/src/core/policy/pr-lane.ts +32 -0
- package/src/core/policy/substance.ts +365 -0
- package/src/core/policy-resolver.ts +94 -0
- package/src/core/pr-create.ts +448 -0
- package/src/core/pr-template.ts +162 -0
- package/src/core/release-notes.ts +197 -0
- package/src/core/release-tracks.ts +779 -0
- package/src/core/remotes.ts +436 -0
- package/src/core/repo-context.ts +542 -0
- package/src/core/repo-paths.ts +29 -0
- package/src/core/resources.ts +1299 -0
- package/src/core/role-models.ts +603 -0
- package/src/core/route-intent.ts +305 -0
- package/src/core/rules.ts +65 -0
- package/src/core/runtime-identity.ts +63 -0
- package/src/core/safe-write.ts +87 -0
- package/src/core/scope-glob.ts +56 -0
- package/src/core/scripts.ts +6 -0
- package/src/core/skill-manifests.ts +173 -0
- package/src/core/store-lock.ts +374 -0
- package/src/core/support-matrix.ts +24 -0
- package/src/core/task-context.ts +247 -0
- package/src/core/task-contract.ts +1279 -0
- package/src/core/task-delivery.ts +109 -0
- package/src/core/task-engine.ts +2248 -0
- package/src/core/task-evaluation.ts +1147 -0
- package/src/core/task-store.ts +2649 -0
- package/src/core/task-view.ts +180 -0
- package/src/core/templates.ts +22 -0
- package/src/core/tracker-issues.ts +174 -0
- package/src/core/vcs-config.ts +540 -0
- package/src/core/workers.ts +62 -0
- package/src/core/workit-paths.mjs +242 -0
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/core/workspaces.ts +1745 -0
- package/src/core/youtrack-backend.ts +288 -0
- package/src/core/youtrack-tools.ts +260 -0
- package/src/core/youtrack-writes.ts +73 -0
- package/src/core/youtrack-ytrack.ts +217 -0
- package/src/core/youtrack.ts +454 -0
- package/src/core/ytrack-skill.ts +297 -0
- package/src/core.ts +171 -0
- package/src/deslop/lex.ts +271 -0
- package/src/deslop/scan.ts +232 -0
- package/src/duration.ts +27 -0
- package/src/fanout-brief.ts +486 -0
- package/src/fanout-check.ts +455 -0
- package/src/fanout-landed.ts +370 -0
- package/src/fanout-stack.ts +108 -0
- package/src/fanout-status.ts +362 -0
- package/src/fanout-worktree.ts +893 -0
- package/src/fanout.ts +1473 -0
- package/src/forge/exec.ts +385 -0
- package/src/forge/github.ts +831 -0
- package/src/forge/gitlab.ts +906 -0
- package/src/forge/issue-forge.ts +209 -0
- package/src/forge/issue-link.ts +468 -0
- package/src/forge/issue-ops.ts +314 -0
- package/src/forge/merge-gate-base-red.ts +72 -0
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-edit.ts +292 -0
- package/src/forge/pr-ops.ts +1109 -0
- package/src/forge/pr-status.ts +10 -0
- package/src/forge/redact.ts +195 -0
- package/src/forge/report.ts +994 -0
- package/src/forge/reruns.ts +132 -0
- package/src/forge/resolve.ts +509 -0
- package/src/forge/types.ts +242 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/default-base.ts +130 -0
- package/src/git/env-redirect.ts +28 -0
- package/src/git/facts.ts +122 -0
- package/src/git/ops.ts +1823 -0
- package/src/git/refs-read.ts +332 -0
- package/src/git/release.ts +1020 -0
- package/src/git/rev.ts +1175 -0
- package/src/git/session-hook.ts +597 -0
- package/src/git/stage.ts +63 -0
- package/src/hooks/agent-model.ts +149 -0
- package/src/hooks/budget-signal.ts +284 -0
- package/src/hooks/context.ts +461 -0
- package/src/hooks/cwd-normalize.ts +79 -0
- package/src/hooks/deadline.ts +228 -0
- package/src/hooks/delivery-verbs.ts +182 -0
- package/src/hooks/descriptor.ts +124 -0
- package/src/hooks/handle.ts +433 -0
- package/src/hooks/heavy-gate.ts +99 -0
- package/src/hooks/hook-runs.ts +130 -0
- package/src/hooks/hosts/claude-code.ts +380 -0
- package/src/hooks/hosts/codex.ts +372 -0
- package/src/hooks/hosts/cursor.ts +419 -0
- package/src/hooks/hosts/fields.ts +98 -0
- package/src/hooks/hosts/opencode.ts +79 -0
- package/src/hooks/hosts/pi.ts +78 -0
- package/src/hooks/index.ts +60 -0
- package/src/hooks/model-limit.ts +378 -0
- package/src/hooks/model-switch.ts +35 -0
- package/src/hooks/policy.ts +29 -0
- package/src/hooks/process.ts +48 -0
- package/src/hooks/protocol.ts +135 -0
- package/src/hooks/raw-git.ts +2289 -0
- package/src/hooks/repeat-failure.ts +115 -0
- package/src/hooks/run.ts +54 -0
- package/src/hooks/session-marker.ts +151 -0
- package/src/hooks/shell-words.ts +532 -0
- package/src/hooks/skill-nudge.ts +295 -0
- package/src/hooks/spawn-gate.ts +72 -0
- package/src/hooks/standing-signal.ts +99 -0
- package/src/hooks/standing.ts +225 -0
- package/src/hooks/steering.ts +74 -0
- package/src/hooks/stop.ts +408 -0
- package/src/hooks/worktree-confine.ts +415 -0
- package/src/hooks/write-gate.ts +1174 -0
- package/src/host-session.ts +459 -0
- package/src/knowledge.ts +368 -0
- package/src/ledger.ts +2913 -0
- package/src/stack.ts +2570 -0
- package/src/store/event-log.ts +217 -0
- package/src/store/notes.ts +29 -0
- package/src/store/patch.ts +135 -0
- package/src/store/paths.ts +368 -0
- package/src/store/reduce.ts +203 -0
- package/src/test-audit-mocks.ts +309 -0
- package/src/verdict-cover.ts +151 -0
- package/src/worktree-deps.ts +142 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +5 -0
- package/templates/plan-template.md +26 -0
- package/templates/spec-template.md +64 -0
- package/templates/workit-contract.md +13 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// `workit pr ready|edit|threads|reply` (audit M13): the PR lifecycle steps
|
|
2
|
+
// `pr status` prescribes, so an agent never reaches for gh/glab directly.
|
|
3
|
+
//
|
|
4
|
+
// Every write needs the `pr` grant and an open PR/MR. `edit --base` retargets
|
|
5
|
+
// only to the branch's default target or to a branch the workspace policy does
|
|
6
|
+
// not protect, so a feature PR is never pointed at a production branch by an
|
|
7
|
+
// edit. `reply` and `resolve` act only on a thread `pr status` lists as
|
|
8
|
+
// unresolved on that PR, so a stray id never lands on another PR.
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { requireGrant } from "../autonomy";
|
|
11
|
+
import { isProtectedTarget } from "../core/branch";
|
|
12
|
+
import { recordedBaseKey, workspaceReleaseTracks } from "../core/release-tracks";
|
|
13
|
+
import { configFilePath } from "../core/config";
|
|
14
|
+
import { vcsConfig } from "../core/vcs-config";
|
|
15
|
+
import { GIT_TIMEOUTS } from "../git/rev";
|
|
16
|
+
import { listStacks } from "../stack";
|
|
17
|
+
import { baseTarget, checkNames } from "./pr-ops";
|
|
18
|
+
import { selectPr } from "./report";
|
|
19
|
+
import type { ResolvedForge } from "./resolve";
|
|
20
|
+
import {
|
|
21
|
+
failure,
|
|
22
|
+
success,
|
|
23
|
+
type ForgeResult,
|
|
24
|
+
type ForgeThread,
|
|
25
|
+
type PrEdit,
|
|
26
|
+
type PrMeta,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
const label = (resolved: ResolvedForge, number: number): string =>
|
|
30
|
+
`${resolved.forge.kind === "github" ? "PR #" : "MR !"}${number}`;
|
|
31
|
+
|
|
32
|
+
/** The grant, the PR number and its metadata; refuses a closed or merged PR. */
|
|
33
|
+
function openPr(cwd: string, resolved: ResolvedForge, pr: number | null): ForgeResult<PrMeta> {
|
|
34
|
+
const grant = requireGrant(cwd, "pr", { target: baseTarget(resolved) });
|
|
35
|
+
if (!grant.allowed) return failure("blocked", grant.error, grant.unblock);
|
|
36
|
+
const number = selectPr(cwd, resolved, { pr });
|
|
37
|
+
if (!number.ok) return number;
|
|
38
|
+
const meta = resolved.forge.prMeta(number.data);
|
|
39
|
+
if (!meta.ok) return meta;
|
|
40
|
+
if (meta.data.state !== "open")
|
|
41
|
+
return failure(
|
|
42
|
+
"blocked",
|
|
43
|
+
`${meta.data.state}: ${label(resolved, meta.data.number)} is ${meta.data.state}`,
|
|
44
|
+
`workit pr status --pr ${meta.data.number}`,
|
|
45
|
+
);
|
|
46
|
+
return meta;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ReadyOutcome = { number: number; url: string; draft: boolean; changed: boolean };
|
|
50
|
+
|
|
51
|
+
/** Mark a draft ready for review, or (`undo`) convert it back to a draft; verified after. */
|
|
52
|
+
export function readyPullRequest(
|
|
53
|
+
cwd: string,
|
|
54
|
+
resolved: ResolvedForge,
|
|
55
|
+
input: { pr: number | null; undo: boolean },
|
|
56
|
+
): ForgeResult<ReadyOutcome> {
|
|
57
|
+
const meta = openPr(cwd, resolved, input.pr);
|
|
58
|
+
if (!meta.ok) return meta;
|
|
59
|
+
const { number, url } = meta.data;
|
|
60
|
+
const want = input.undo;
|
|
61
|
+
if (meta.data.draft === want) return success({ number, url, draft: want, changed: false });
|
|
62
|
+
const set = resolved.forge.setDraft(number, want);
|
|
63
|
+
if (!set.ok) return set;
|
|
64
|
+
const after = resolved.forge.prMeta(number);
|
|
65
|
+
if (!after.ok) return after;
|
|
66
|
+
if (after.data.draft !== want)
|
|
67
|
+
return failure(
|
|
68
|
+
"failed",
|
|
69
|
+
`ready_unverified: the forge accepted the change but ${label(resolved, number)} still reads ${after.data.draft ? "draft" : "ready"}`,
|
|
70
|
+
`workit pr status --pr ${number}`,
|
|
71
|
+
);
|
|
72
|
+
return success({ number, url, draft: want, changed: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type EditOutcome = {
|
|
76
|
+
number: number;
|
|
77
|
+
url: string;
|
|
78
|
+
title: string;
|
|
79
|
+
base: string;
|
|
80
|
+
/** What was asked for, by field (title, body, base, labels, reviewers). */
|
|
81
|
+
changed: string[];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const git = (cwd: string, args: string[]): string | null => {
|
|
85
|
+
const run = spawnSync("git", args, {
|
|
86
|
+
cwd,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
timeout: GIT_TIMEOUTS.local,
|
|
89
|
+
killSignal: "SIGKILL",
|
|
90
|
+
});
|
|
91
|
+
return run.status === 0 ? run.stdout.trim() || null : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Where `pr edit --base` may point the PR: the head branch's default target
|
|
96
|
+
* (from the release track that owns the PR's current base, else the head's
|
|
97
|
+
* own track; an undetermined line refuses), or the head's stack parent —
|
|
98
|
+
* its recorded `workitBase` or its parent entry in a stack. A protected
|
|
99
|
+
* branch other than the default target never qualifies.
|
|
100
|
+
*/
|
|
101
|
+
function checkBase(cwd: string, meta: PrMeta, base: string): ForgeResult<void> {
|
|
102
|
+
const head = meta.headBranch;
|
|
103
|
+
if (!base || /\s/u.test(base) || base.startsWith("refs/") || base.startsWith("-"))
|
|
104
|
+
return failure("invalid_input", `--base must be a plain branch name, not "${base}"`);
|
|
105
|
+
if (base === head) return failure("invalid_input", `${head} cannot be its own base`);
|
|
106
|
+
const read = workspaceReleaseTracks(cwd);
|
|
107
|
+
if (read.error) return failure("blocked", read.error, "fix the workspace's releaseTracks");
|
|
108
|
+
const owners = read.tracks.filter((track) =>
|
|
109
|
+
[
|
|
110
|
+
track.integrationBranch,
|
|
111
|
+
track.productionBranch,
|
|
112
|
+
track.baseBranch,
|
|
113
|
+
track.pullRequestTarget,
|
|
114
|
+
].includes(meta.base),
|
|
115
|
+
);
|
|
116
|
+
const config = vcsConfig("resolve", cwd, {
|
|
117
|
+
branch: head,
|
|
118
|
+
...(owners.length === 1 ? { track: owners[0].name } : {}),
|
|
119
|
+
});
|
|
120
|
+
if (config.ok === false)
|
|
121
|
+
return failure("blocked", String(config.error), `fix ${configFilePath("vcs.json")}`);
|
|
122
|
+
const blocking = config.releaseTrack?.blocking;
|
|
123
|
+
if (blocking)
|
|
124
|
+
return failure(
|
|
125
|
+
"blocked",
|
|
126
|
+
String(blocking),
|
|
127
|
+
"retarget it on the forge yourself, or fix WORKIT_RELEASE_TRACK",
|
|
128
|
+
);
|
|
129
|
+
const target = String(config.defaultTargetBranch ?? "") || null;
|
|
130
|
+
if (base === target) return success(undefined);
|
|
131
|
+
const instead = target
|
|
132
|
+
? `workit pr edit --pr ${meta.number} --base ${target} # or retarget it on the forge yourself`
|
|
133
|
+
: "retarget it on the forge yourself";
|
|
134
|
+
if (isProtectedTarget(cwd, base))
|
|
135
|
+
return failure(
|
|
136
|
+
"blocked",
|
|
137
|
+
`protected_base: ${base} is protected by the workspace branch policy and is not ${head}'s default target${target ? ` (${target})` : ""}; workit never retargets a PR onto it`,
|
|
138
|
+
instead,
|
|
139
|
+
);
|
|
140
|
+
const stacks = listStacks(cwd);
|
|
141
|
+
// Only the branch directly below the head: a descendant or sibling would
|
|
142
|
+
// make a cycle or cross stacks.
|
|
143
|
+
const parents = new Set<string>(
|
|
144
|
+
stacks.ok
|
|
145
|
+
? stacks.data.flatMap((stack) =>
|
|
146
|
+
stack.branches.filter((entry) => entry.branch === head).map((entry) => entry.parent),
|
|
147
|
+
)
|
|
148
|
+
: [],
|
|
149
|
+
);
|
|
150
|
+
const recorded = git(cwd, ["config", "--get", recordedBaseKey(head)]);
|
|
151
|
+
if (base === recorded || parents.has(base)) return success(undefined);
|
|
152
|
+
return failure(
|
|
153
|
+
"blocked",
|
|
154
|
+
`not_stack_parent: ${base} is neither ${head}'s default target${target ? ` (${target})` : ""} nor its stack parent; workit retargets only to those`,
|
|
155
|
+
instead,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function editPullRequest(
|
|
160
|
+
cwd: string,
|
|
161
|
+
resolved: ResolvedForge,
|
|
162
|
+
input: { pr: number | null; edit: PrEdit },
|
|
163
|
+
): ForgeResult<EditOutcome> {
|
|
164
|
+
const { edit } = input;
|
|
165
|
+
const changed = [
|
|
166
|
+
...(edit.title !== undefined ? ["title"] : []),
|
|
167
|
+
...(edit.body !== undefined ? ["body"] : []),
|
|
168
|
+
...(edit.base !== undefined ? ["base"] : []),
|
|
169
|
+
...(edit.addLabels.length || edit.removeLabels.length ? ["labels"] : []),
|
|
170
|
+
...(edit.addReviewers.length ? ["reviewers"] : []),
|
|
171
|
+
];
|
|
172
|
+
if (!changed.length) return failure("invalid_input", "nothing to edit");
|
|
173
|
+
if (edit.title !== undefined && !edit.title.trim())
|
|
174
|
+
return failure("invalid_input", "--title must not be empty");
|
|
175
|
+
const bad = checkNames(
|
|
176
|
+
resolved.forge.kind,
|
|
177
|
+
resolved.forge.repo,
|
|
178
|
+
[...edit.addLabels, ...edit.removeLabels],
|
|
179
|
+
edit.addReviewers,
|
|
180
|
+
);
|
|
181
|
+
if (bad) return failure("invalid_input", bad);
|
|
182
|
+
|
|
183
|
+
const meta = openPr(cwd, resolved, input.pr);
|
|
184
|
+
if (!meta.ok) return meta;
|
|
185
|
+
const { number } = meta.data;
|
|
186
|
+
if (edit.base !== undefined) {
|
|
187
|
+
const allowed = checkBase(cwd, meta.data, edit.base);
|
|
188
|
+
if (!allowed.ok) return allowed;
|
|
189
|
+
}
|
|
190
|
+
const done = resolved.forge.editPr(number, edit);
|
|
191
|
+
if (!done.ok) return done;
|
|
192
|
+
const after = resolved.forge.prMeta(number);
|
|
193
|
+
if (!after.ok) return after;
|
|
194
|
+
if (edit.base !== undefined && after.data.base !== edit.base)
|
|
195
|
+
return failure(
|
|
196
|
+
"failed",
|
|
197
|
+
`edit_unverified: ${label(resolved, number)} still targets ${after.data.base}, not ${edit.base}`,
|
|
198
|
+
`workit pr status --pr ${number}`,
|
|
199
|
+
);
|
|
200
|
+
return success({
|
|
201
|
+
number,
|
|
202
|
+
url: after.data.url,
|
|
203
|
+
title: after.data.title,
|
|
204
|
+
base: after.data.base,
|
|
205
|
+
changed,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type ThreadsOutcome = {
|
|
210
|
+
number: number;
|
|
211
|
+
url: string;
|
|
212
|
+
threads: ForgeThread[];
|
|
213
|
+
truncated: boolean;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** The PR's unresolved review threads (the ones `pr status` counts). Read-only. */
|
|
217
|
+
export function listThreads(
|
|
218
|
+
cwd: string,
|
|
219
|
+
resolved: ResolvedForge,
|
|
220
|
+
input: { pr: number | null },
|
|
221
|
+
): ForgeResult<ThreadsOutcome> {
|
|
222
|
+
const number = selectPr(cwd, resolved, { pr: input.pr });
|
|
223
|
+
if (!number.ok) return number;
|
|
224
|
+
const status = resolved.forge.prStatus(number.data);
|
|
225
|
+
if (!status.ok) return status;
|
|
226
|
+
return success({
|
|
227
|
+
number: status.data.number,
|
|
228
|
+
url: status.data.url,
|
|
229
|
+
threads: status.data.threads,
|
|
230
|
+
truncated: status.data.truncated,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type ReplyOutcome = {
|
|
235
|
+
number: number;
|
|
236
|
+
thread: string;
|
|
237
|
+
replied: boolean;
|
|
238
|
+
replyUrl: string | null;
|
|
239
|
+
resolved: boolean;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Reply to and/or resolve one unresolved thread of the PR. */
|
|
243
|
+
export function replyToThread(
|
|
244
|
+
cwd: string,
|
|
245
|
+
resolved: ResolvedForge,
|
|
246
|
+
input: { pr: number | null; thread: string; body: string | null; resolve: boolean },
|
|
247
|
+
): ForgeResult<ReplyOutcome> {
|
|
248
|
+
if (input.body === null && !input.resolve)
|
|
249
|
+
return failure("invalid_input", "pass --body-file <f> and/or --resolve");
|
|
250
|
+
if (input.body !== null && !input.body.trim())
|
|
251
|
+
return failure("invalid_input", "the reply body is empty");
|
|
252
|
+
const meta = openPr(cwd, resolved, input.pr);
|
|
253
|
+
if (!meta.ok) return meta;
|
|
254
|
+
const { number } = meta.data;
|
|
255
|
+
const status = resolved.forge.prStatus(number);
|
|
256
|
+
if (!status.ok) return status;
|
|
257
|
+
const listed = status.data.threads.some((thread) => thread.id === input.thread);
|
|
258
|
+
// A list cut at the page cap may miss the thread: ask for it directly.
|
|
259
|
+
const open =
|
|
260
|
+
listed ||
|
|
261
|
+
(status.data.truncated &&
|
|
262
|
+
(() => {
|
|
263
|
+
const state = resolved.forge.threadState(number, input.thread);
|
|
264
|
+
return state.ok && state.data === "open";
|
|
265
|
+
})());
|
|
266
|
+
if (!open)
|
|
267
|
+
return failure(
|
|
268
|
+
"not_found",
|
|
269
|
+
`thread ${input.thread} is not an unresolved thread of ${label(resolved, number)}`,
|
|
270
|
+
`workit pr threads --pr ${number}`,
|
|
271
|
+
);
|
|
272
|
+
let replyUrl: string | null = null;
|
|
273
|
+
if (input.body !== null) {
|
|
274
|
+
const replied = resolved.forge.replyThread(number, input.thread, input.body);
|
|
275
|
+
if (!replied.ok) return replied;
|
|
276
|
+
replyUrl = replied.data.url;
|
|
277
|
+
}
|
|
278
|
+
if (input.resolve) {
|
|
279
|
+
const done = resolved.forge.resolveThread(number, input.thread);
|
|
280
|
+
if (!done.ok)
|
|
281
|
+
return input.body === null
|
|
282
|
+
? done
|
|
283
|
+
: failure(done.code, `replied, but not resolved: ${done.error}`, done.unblock);
|
|
284
|
+
}
|
|
285
|
+
return success({
|
|
286
|
+
number,
|
|
287
|
+
thread: input.thread,
|
|
288
|
+
replied: input.body !== null,
|
|
289
|
+
replyUrl,
|
|
290
|
+
resolved: input.resolve,
|
|
291
|
+
});
|
|
292
|
+
}
|