@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,314 @@
|
|
|
1
|
+
// `workit issue view|comment|close|create` (#439, retro W2.11): issue writes
|
|
2
|
+
// through Workit, so an agent never needs a raw `gh issue close` the host's
|
|
3
|
+
// permission mode blocks.
|
|
4
|
+
//
|
|
5
|
+
// Where: `--repo` (another repository on this forge host), else the
|
|
6
|
+
// workspace tracker (`issues.provider` github or gitlab, its `issues.repo`)
|
|
7
|
+
// when it is on this checkout's forge host, else the forge's own base
|
|
8
|
+
// repository. YouTrack and another host are refused: no account is
|
|
9
|
+
// verified there. Every write needs the `pr` grant (the forge-write grant
|
|
10
|
+
// `pr edit|reply` use) and leaves an `issue.*` ledger row.
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { requireGrant } from "../autonomy";
|
|
13
|
+
import { forgeTarget } from "../core/remotes";
|
|
14
|
+
import { vcsConfig, type IssueTracker } from "../core/vcs-config";
|
|
15
|
+
import { currentBranch, headSha } from "../git/rev";
|
|
16
|
+
import { appendObserved, readLedger, type LedgerActor } from "../ledger";
|
|
17
|
+
import {
|
|
18
|
+
githubIssues,
|
|
19
|
+
gitlabIssues,
|
|
20
|
+
type ForgeIssue,
|
|
21
|
+
type IssueApi,
|
|
22
|
+
type IssueCloseReason,
|
|
23
|
+
} from "./issue-forge";
|
|
24
|
+
import type { ResolvedForge } from "./resolve";
|
|
25
|
+
import { failure, success, type ForgeErrorCode, type ForgeResult } from "./types";
|
|
26
|
+
|
|
27
|
+
const same = (left: string, right: string): boolean => left.toLowerCase() === right.toLowerCase();
|
|
28
|
+
|
|
29
|
+
function trackerOf(cwd: string): IssueTracker | null {
|
|
30
|
+
const resolved = vcsConfig("resolve", cwd);
|
|
31
|
+
if (resolved.ok === false) return null;
|
|
32
|
+
return (resolved.issue_tracker as IssueTracker | null | undefined) ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The issue API for this checkout: the workspace tracker's repository, else the forge's. */
|
|
36
|
+
export function issueTarget(
|
|
37
|
+
cwd: string,
|
|
38
|
+
resolved: ResolvedForge,
|
|
39
|
+
options: { repo?: string | null; tracker?: IssueTracker | null } = {},
|
|
40
|
+
): ForgeResult<IssueApi> {
|
|
41
|
+
const { kind, apiHost } = resolved.forge;
|
|
42
|
+
const make = (repo: string) => {
|
|
43
|
+
const api = { apiHost, repo, runner: resolved.runner };
|
|
44
|
+
return success(kind === "github" ? githubIssues(api) : gitlabIssues(api));
|
|
45
|
+
};
|
|
46
|
+
// `--repo`: another repository on this forge host, under the same verified account.
|
|
47
|
+
if (options.repo) {
|
|
48
|
+
// The owner has no dot, so a host (`gitlab.com/o/r`) is never read as one.
|
|
49
|
+
if (!/^[\w-]+(?:\/\w[\w.-]*)+$/u.test(options.repo))
|
|
50
|
+
return failure(
|
|
51
|
+
"invalid_input",
|
|
52
|
+
`--repo is owner/name or group/project on this forge host, without the host: not "${options.repo}"`,
|
|
53
|
+
);
|
|
54
|
+
return make(options.repo);
|
|
55
|
+
}
|
|
56
|
+
const tracker = options.tracker === undefined ? trackerOf(cwd) : options.tracker;
|
|
57
|
+
if (tracker?.provider === "youtrack")
|
|
58
|
+
return failure(
|
|
59
|
+
"blocked",
|
|
60
|
+
"issues.provider is youtrack: workit issue drives GitHub and GitLab issues",
|
|
61
|
+
"workit youtrack note <ID> --text <t>",
|
|
62
|
+
);
|
|
63
|
+
if (tracker && tracker.provider !== kind)
|
|
64
|
+
return failure(
|
|
65
|
+
"blocked",
|
|
66
|
+
`issues.provider is ${tracker.provider} but this checkout's forge is ${kind}: no account is verified there`,
|
|
67
|
+
);
|
|
68
|
+
if (tracker?.host && !same(tracker.host, apiHost))
|
|
69
|
+
return failure(
|
|
70
|
+
"blocked",
|
|
71
|
+
`issues.host ${tracker.host} is not this checkout's forge host ${apiHost}: no account is verified there`,
|
|
72
|
+
);
|
|
73
|
+
return make(tracker?.repo ?? resolved.forge.repo);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const grant = (cwd: string, api: IssueApi): ForgeResult<void> => {
|
|
77
|
+
const decision = requireGrant(cwd, "pr", {
|
|
78
|
+
target: forgeTarget(api.apiHost, api.repo),
|
|
79
|
+
});
|
|
80
|
+
return decision.allowed
|
|
81
|
+
? success(undefined)
|
|
82
|
+
: failure("blocked", decision.error, decision.unblock);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type Recorded = { id: string } | { error: string };
|
|
86
|
+
|
|
87
|
+
const record = (
|
|
88
|
+
cwd: string,
|
|
89
|
+
api: IssueApi,
|
|
90
|
+
actor: LedgerActor,
|
|
91
|
+
row: Record<string, unknown> & { type: string },
|
|
92
|
+
): Recorded => {
|
|
93
|
+
const written = appendObserved(cwd, {
|
|
94
|
+
...row,
|
|
95
|
+
actor,
|
|
96
|
+
branch: currentBranch(cwd),
|
|
97
|
+
head: headSha(cwd),
|
|
98
|
+
forge: api.kind,
|
|
99
|
+
repo: api.repo,
|
|
100
|
+
});
|
|
101
|
+
return written.ok ? { id: String(written.value.id) } : { error: written.error };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** The issue, refused when it is closed and the caller needs it open. */
|
|
105
|
+
const openIssue = (api: IssueApi, n: number, alternative: string): ForgeResult<ForgeIssue> => {
|
|
106
|
+
const issue = api.view(n);
|
|
107
|
+
if (!issue.ok) return issue;
|
|
108
|
+
if (issue.data.state === "closed")
|
|
109
|
+
return failure("blocked", `closed: issue #${n} is closed`, alternative);
|
|
110
|
+
return issue;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Post a comment; its URL, or (GitLab) the issue URL with the note's anchor. */
|
|
114
|
+
const postComment = (
|
|
115
|
+
api: IssueApi,
|
|
116
|
+
issue: ForgeIssue,
|
|
117
|
+
body: string,
|
|
118
|
+
): ForgeResult<{ url: string | null }> => {
|
|
119
|
+
const posted = api.comment(issue.number, body);
|
|
120
|
+
if (!posted.ok) return posted;
|
|
121
|
+
const { url, id } = posted.data;
|
|
122
|
+
return success({ url: url ?? (issue.url && id ? `${issue.url}#note_${id}` : null) });
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const bodyHash = (body: string): string =>
|
|
126
|
+
createHash("sha256").update(body.trim()).digest("hex").slice(0, 32);
|
|
127
|
+
|
|
128
|
+
const recordComment = (
|
|
129
|
+
cwd: string,
|
|
130
|
+
api: IssueApi,
|
|
131
|
+
actor: LedgerActor,
|
|
132
|
+
issue: number,
|
|
133
|
+
body: string,
|
|
134
|
+
url: string | null,
|
|
135
|
+
): Recorded =>
|
|
136
|
+
record(cwd, api, actor, { type: "issue.commented", issue, url, bodyHash: bodyHash(body) });
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The URL of the same comment this session already posted on the issue
|
|
140
|
+
* (an `issue.commented` row), so a retried close never posts it twice;
|
|
141
|
+
* undefined when there is none or the session is unknown.
|
|
142
|
+
*/
|
|
143
|
+
function postedBefore(
|
|
144
|
+
cwd: string,
|
|
145
|
+
api: IssueApi,
|
|
146
|
+
actor: LedgerActor,
|
|
147
|
+
issue: number,
|
|
148
|
+
body: string,
|
|
149
|
+
): { url: string | null } | undefined {
|
|
150
|
+
if (!actor.session) return undefined;
|
|
151
|
+
const ledger = readLedger(cwd);
|
|
152
|
+
if (!ledger.ok) return undefined;
|
|
153
|
+
const hash = bodyHash(body);
|
|
154
|
+
const row = ledger.value.rows.findLast((candidate) => {
|
|
155
|
+
const value = candidate as unknown as Record<string, unknown>;
|
|
156
|
+
return (
|
|
157
|
+
value.type === "issue.commented" &&
|
|
158
|
+
value.forge === api.kind &&
|
|
159
|
+
value.repo === api.repo &&
|
|
160
|
+
value.issue === issue &&
|
|
161
|
+
value.bodyHash === hash &&
|
|
162
|
+
candidate.actor.session === actor.session
|
|
163
|
+
);
|
|
164
|
+
}) as unknown as Record<string, unknown> | undefined;
|
|
165
|
+
if (!row) return undefined;
|
|
166
|
+
return { url: typeof row.url === "string" ? row.url : null };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type CommentOutcome = {
|
|
170
|
+
number: number;
|
|
171
|
+
url: string | null;
|
|
172
|
+
recorded: Recorded;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Comment on issue `n`. With `requireOpen` a closed issue is refused and
|
|
177
|
+
* that text is the unblock (the queue path: file a new issue instead).
|
|
178
|
+
*/
|
|
179
|
+
export function commentOnIssue(
|
|
180
|
+
cwd: string,
|
|
181
|
+
api: IssueApi,
|
|
182
|
+
input: {
|
|
183
|
+
number: number;
|
|
184
|
+
body: string;
|
|
185
|
+
actor: LedgerActor;
|
|
186
|
+
requireOpen?: string;
|
|
187
|
+
},
|
|
188
|
+
): ForgeResult<CommentOutcome> {
|
|
189
|
+
const allowed = grant(cwd, api);
|
|
190
|
+
if (!allowed.ok) return allowed;
|
|
191
|
+
const issue =
|
|
192
|
+
input.requireOpen === undefined
|
|
193
|
+
? api.view(input.number)
|
|
194
|
+
: openIssue(api, input.number, input.requireOpen);
|
|
195
|
+
if (!issue.ok) return issue;
|
|
196
|
+
const posted = postComment(api, issue.data, input.body);
|
|
197
|
+
if (!posted.ok) return posted;
|
|
198
|
+
const { url } = posted.data;
|
|
199
|
+
const recorded = recordComment(cwd, api, input.actor, input.number, input.body, url);
|
|
200
|
+
return success({ number: input.number, url, recorded });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type CloseOutcome = {
|
|
204
|
+
number: number;
|
|
205
|
+
url: string | null;
|
|
206
|
+
reason: IssueCloseReason;
|
|
207
|
+
/** False when the issue was already closed: nothing was posted. */
|
|
208
|
+
changed: boolean;
|
|
209
|
+
commentUrl: string | null;
|
|
210
|
+
/** posted now, already posted by this session (a retry), or none asked. */
|
|
211
|
+
comment: "posted" | "already_posted" | "none";
|
|
212
|
+
recorded: Recorded | null;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** Close issue `n` with a reason and an optional comment posted first; verified after. */
|
|
216
|
+
export function closeIssue(
|
|
217
|
+
cwd: string,
|
|
218
|
+
api: IssueApi,
|
|
219
|
+
input: {
|
|
220
|
+
number: number;
|
|
221
|
+
reason: IssueCloseReason;
|
|
222
|
+
comment: string | null;
|
|
223
|
+
actor: LedgerActor;
|
|
224
|
+
},
|
|
225
|
+
): ForgeResult<CloseOutcome> {
|
|
226
|
+
const allowed = grant(cwd, api);
|
|
227
|
+
if (!allowed.ok) return allowed;
|
|
228
|
+
const issue = api.view(input.number);
|
|
229
|
+
if (!issue.ok) return issue;
|
|
230
|
+
const base = {
|
|
231
|
+
number: input.number,
|
|
232
|
+
url: issue.data.url,
|
|
233
|
+
reason: input.reason,
|
|
234
|
+
};
|
|
235
|
+
if (issue.data.state === "closed")
|
|
236
|
+
return success({
|
|
237
|
+
...base,
|
|
238
|
+
changed: false,
|
|
239
|
+
commentUrl: null,
|
|
240
|
+
comment: "none",
|
|
241
|
+
recorded: null,
|
|
242
|
+
});
|
|
243
|
+
let commentUrl: string | null = null;
|
|
244
|
+
let comment: CloseOutcome["comment"] = "none";
|
|
245
|
+
if (input.comment !== null) {
|
|
246
|
+
const before = postedBefore(cwd, api, input.actor, input.number, input.comment);
|
|
247
|
+
if (before) {
|
|
248
|
+
commentUrl = before.url;
|
|
249
|
+
comment = "already_posted";
|
|
250
|
+
} else {
|
|
251
|
+
const posted = postComment(api, issue.data, input.comment);
|
|
252
|
+
if (!posted.ok) return posted;
|
|
253
|
+
commentUrl = posted.data.url;
|
|
254
|
+
comment = "posted";
|
|
255
|
+
// Recorded at once: a failed close below must not lose it, and a retry reads it.
|
|
256
|
+
recordComment(cwd, api, input.actor, input.number, input.comment, commentUrl);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// After a posted comment, a failure says so and the retry needs no --comment.
|
|
260
|
+
const stillOpen = (code: ForgeErrorCode, error: string): ForgeResult<CloseOutcome> =>
|
|
261
|
+
comment === "none"
|
|
262
|
+
? failure(code, error, `workit issue view ${input.number}`)
|
|
263
|
+
: failure(
|
|
264
|
+
code,
|
|
265
|
+
`${error}; the comment is posted${commentUrl ? ` (${commentUrl})` : ""} and issue #${input.number} is still open`,
|
|
266
|
+
`workit issue close ${input.number} --reason ${input.reason} # no --comment: it is posted; or workit issue view ${input.number}`,
|
|
267
|
+
);
|
|
268
|
+
const closed = api.close(input.number, input.reason);
|
|
269
|
+
if (!closed.ok) return stillOpen(closed.code, closed.error);
|
|
270
|
+
const after = api.view(input.number);
|
|
271
|
+
if (!after.ok) return after;
|
|
272
|
+
if (after.data.state !== "closed")
|
|
273
|
+
return stillOpen(
|
|
274
|
+
"failed",
|
|
275
|
+
`close_unverified: the forge accepted the close but issue #${input.number} still reads open`,
|
|
276
|
+
);
|
|
277
|
+
const recorded = record(cwd, api, input.actor, {
|
|
278
|
+
type: "issue.closed",
|
|
279
|
+
issue: input.number,
|
|
280
|
+
url: issue.data.url,
|
|
281
|
+
reason: input.reason,
|
|
282
|
+
commentUrl,
|
|
283
|
+
});
|
|
284
|
+
return success({ ...base, changed: true, commentUrl, comment, recorded });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type CreateOutcome = {
|
|
288
|
+
number: number;
|
|
289
|
+
url: string | null;
|
|
290
|
+
recorded: Recorded;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export function createIssue(
|
|
294
|
+
cwd: string,
|
|
295
|
+
api: IssueApi,
|
|
296
|
+
input: { title: string; body: string; labels: string[]; actor: LedgerActor },
|
|
297
|
+
): ForgeResult<CreateOutcome> {
|
|
298
|
+
if (!input.title.trim()) return failure("invalid_input", "an issue needs a title");
|
|
299
|
+
const allowed = grant(cwd, api);
|
|
300
|
+
if (!allowed.ok) return allowed;
|
|
301
|
+
const created = api.create({
|
|
302
|
+
title: input.title,
|
|
303
|
+
body: input.body,
|
|
304
|
+
labels: input.labels,
|
|
305
|
+
});
|
|
306
|
+
if (!created.ok) return created;
|
|
307
|
+
const recorded = record(cwd, api, input.actor, {
|
|
308
|
+
type: "issue.created",
|
|
309
|
+
issue: created.data.number,
|
|
310
|
+
url: created.data.url,
|
|
311
|
+
labels: input.labels,
|
|
312
|
+
});
|
|
313
|
+
return success({ ...created.data, recorded });
|
|
314
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ResolvedForge } from "./resolve";
|
|
2
|
+
import { failure, type ForgeCheck, type ForgeResult, type RefChecks } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Why the base is red, judged against the run before it: `flaky` when every
|
|
6
|
+
* failing check passed on the previous base commit, `broken` when one of them
|
|
7
|
+
* failed there too, `unknown` when the previous run could not tell.
|
|
8
|
+
*/
|
|
9
|
+
export type BaseRedCause =
|
|
10
|
+
| { kind: "flaky"; checks: ForgeCheck[] }
|
|
11
|
+
| { kind: "broken"; checks: ForgeCheck[] }
|
|
12
|
+
| { kind: "unknown"; note: string };
|
|
13
|
+
|
|
14
|
+
export function classifyBaseRed(
|
|
15
|
+
failing: readonly ForgeCheck[],
|
|
16
|
+
previous: ForgeResult<RefChecks>,
|
|
17
|
+
): BaseRedCause {
|
|
18
|
+
if (!previous.ok)
|
|
19
|
+
return { kind: "unknown", note: `the previous run could not be read (${previous.error})` };
|
|
20
|
+
const before = new Map(previous.data.checks.map((check) => [check.name, check.state]));
|
|
21
|
+
const broken = failing.filter((check) => before.get(check.name) === "failing");
|
|
22
|
+
if (broken.length) return { kind: "broken", checks: broken };
|
|
23
|
+
if (failing.every((check) => before.get(check.name) === "passing"))
|
|
24
|
+
return { kind: "flaky", checks: [...failing] };
|
|
25
|
+
return {
|
|
26
|
+
kind: "unknown",
|
|
27
|
+
note: "the previous run did not report every failing check, so a flake cannot be told apart",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The checks of the base commit before the head (`<base>~1`), the previous
|
|
33
|
+
* run on a squash-merged base. GitHub resolves the revision expression; GitLab
|
|
34
|
+
* reads pipelines by branch name only, so it reports no previous run.
|
|
35
|
+
*/
|
|
36
|
+
export function previousBaseChecks(resolved: ResolvedForge, base: string): ForgeResult<RefChecks> {
|
|
37
|
+
if (resolved.forge.kind !== "github")
|
|
38
|
+
return failure("unavailable", `${resolved.forge.kind} has no previous-run read`);
|
|
39
|
+
// Advisory only: a read that throws must never change the base_red refusal.
|
|
40
|
+
try {
|
|
41
|
+
return resolved.forge.branchChecks(`${base}~1`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return failure("failed", `the previous ${base} run read threw (${String(error)})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const quote = (name: string): string => `"${name.replaceAll('"', '\\"')}"`;
|
|
48
|
+
|
|
49
|
+
const named = (checks: readonly ForgeCheck[]): string =>
|
|
50
|
+
checks.map((check) => (check.url ? `${check.name} (${check.url})` : check.name)).join(", ");
|
|
51
|
+
|
|
52
|
+
/** The sentence the refusal adds and the next step it hands the lead. */
|
|
53
|
+
export function baseRedNextStep(
|
|
54
|
+
base: string,
|
|
55
|
+
cause: BaseRedCause,
|
|
56
|
+
override: string,
|
|
57
|
+
): { detail: string; unblock: string } {
|
|
58
|
+
if (cause.kind === "flaky")
|
|
59
|
+
return {
|
|
60
|
+
detail: `likely flaky: ${named(cause.checks)} passed on the previous ${base} run`,
|
|
61
|
+
unblock: `workit ci rerun --ref ${base} ${cause.checks.map((check) => `--check ${quote(check.name)}`).join(" ")} --reason flake # then merge once ${base} is green`,
|
|
62
|
+
};
|
|
63
|
+
if (cause.kind === "broken")
|
|
64
|
+
return {
|
|
65
|
+
detail: `${base} is broken: ${named(cause.checks)} also failed on the previous ${base} run, so a rerun will not help`,
|
|
66
|
+
unblock: `fix ${base} first; or, if the user accepts it: ${override}`,
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
detail: `flake or not is unknown: ${cause.note}`,
|
|
70
|
+
unblock: `workit ci rerun --ref ${base} --failed --reason flake # a flake on ${base}: merge once it is green; a real failure: fix ${base} first, or, if the user accepts it: ${override}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// PR/MR body issue linking (port of scripts/pr-create.sh --build-body), shared
|
|
2
|
+
// by the managed `hosting.pull_request` action (core/pr-create.ts re-exports
|
|
3
|
+
// these until S16) and `workit pr create` (S11), which passes typed input
|
|
4
|
+
// instead of environment variables.
|
|
5
|
+
import { vcsConfig } from "../core/vcs-config";
|
|
6
|
+
|
|
7
|
+
export function parseGhRepo(remote: string): string | null {
|
|
8
|
+
remote = (remote || "").trim().replace(/\/+$/, "");
|
|
9
|
+
if (!remote) return null;
|
|
10
|
+
if (remote.endsWith(".git")) remote = remote.slice(0, -4);
|
|
11
|
+
if (remote.includes(":")) remote = remote.split(":").pop() ?? ""; // drop git@host part (scp-style URL)
|
|
12
|
+
const parts = remote.split("/").filter(Boolean);
|
|
13
|
+
return parts.length >= 2 ? parts.slice(-2).join("/") : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseGhIssue(value: string): string {
|
|
17
|
+
const m = /issues\/(\d+)/.exec(value);
|
|
18
|
+
return m ? m[1] : String(value).trim().replace(/^#/, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// RL-03/CA-25/AR-08: a branch-derived numeric issue id must be a bare number at
|
|
22
|
+
// a segment or dash boundary — and never part of a date segment. Year-first
|
|
23
|
+
// (feature/2024-01-15/x) and day-first (feature/15-01-2024/x) dates are both
|
|
24
|
+
// skipped — a complete date anywhere in a segment (release-2024-01-15,
|
|
25
|
+
// v2-2024-01-15-fix) — so no date digit ever closes an issue. Deliberate
|
|
26
|
+
// numeric issue branches (feature/42-title, feature/2024-fix) keep linking.
|
|
27
|
+
export function deriveGhIssueFromBranch(branch: string): string {
|
|
28
|
+
for (const segment of branch.split("/")) {
|
|
29
|
+
if (/^\d{4}-\d/.test(segment)) continue; // year-first date-like segment (incl. year-month)
|
|
30
|
+
if (/\d{4}-\d{1,2}-\d{1,2}/.test(segment)) continue; // complete year-first date anywhere
|
|
31
|
+
if (/\d{1,2}-\d{1,2}-\d{4}/.test(segment)) continue; // complete day-first date anywhere
|
|
32
|
+
const m = /(?:^|-)(\d+)(?:-|$)/.exec(segment);
|
|
33
|
+
if (m) return m[1];
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A YouTrack id in the branch (feature/IRPT-740-x); anchored, 3+ digits so POSTGRES-16 or HTTP-3 never link. */
|
|
39
|
+
export function deriveYouTrackIssueFromBranch(branch: string): string {
|
|
40
|
+
return /(?:^|\/|-)([A-Z]{2,}-\d{3,})\b/.exec(branch)?.[1] ?? "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildBody(
|
|
44
|
+
body: string,
|
|
45
|
+
branch: string,
|
|
46
|
+
linkIssues: boolean,
|
|
47
|
+
baseUrl: string,
|
|
48
|
+
ytIssue: string,
|
|
49
|
+
ghLinkOnPr: boolean,
|
|
50
|
+
ghIssue: string,
|
|
51
|
+
ghRelation: string,
|
|
52
|
+
ghRepo: string | null,
|
|
53
|
+
): string {
|
|
54
|
+
let line: string | null = null;
|
|
55
|
+
if (linkIssues) {
|
|
56
|
+
const issue = ytIssue || deriveYouTrackIssueFromBranch(branch);
|
|
57
|
+
if (issue && baseUrl) line = `Related to: ${baseUrl.replace(/\/+$/, "")}/issue/${issue}`;
|
|
58
|
+
} else if (ghLinkOnPr) {
|
|
59
|
+
let issue = parseGhIssue(ghIssue);
|
|
60
|
+
if (!issue && branch) {
|
|
61
|
+
// pure-number issue id (feature/42-title -> 42); digits must be followed by a dash or end-of-string
|
|
62
|
+
// so version tokens (release/1.2.3, backport/8.0.1, lodash-4.17.21, 2024.1) never link
|
|
63
|
+
issue = deriveGhIssueFromBranch(branch);
|
|
64
|
+
}
|
|
65
|
+
if (issue) {
|
|
66
|
+
if (ghRelation === "related") {
|
|
67
|
+
line = `Related to #${issue}`;
|
|
68
|
+
if (ghRepo) line += ` — https://github.com/${ghRepo}/issues/${issue}`;
|
|
69
|
+
} else {
|
|
70
|
+
line = `Closes #${issue}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (line === null) return body;
|
|
75
|
+
return body ? `${body}\n\n${line}` : line;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The body `workit pr create` sends: the agent's text plus the workspace's
|
|
80
|
+
* issue link (YouTrack `link_issues`, or GitHub `issues.link_on_pr` with the
|
|
81
|
+
* issue number derived from the branch). Reads config, never writes it.
|
|
82
|
+
*/
|
|
83
|
+
export function prBodyFor(
|
|
84
|
+
cwd: string,
|
|
85
|
+
input: { body: string; branch: string; repo: string | null },
|
|
86
|
+
): string {
|
|
87
|
+
const resolved = vcsConfig("resolve", cwd);
|
|
88
|
+
if (resolved.ok === false) return input.body;
|
|
89
|
+
return buildBody(
|
|
90
|
+
input.body,
|
|
91
|
+
input.branch,
|
|
92
|
+
resolved.link_issues === true,
|
|
93
|
+
typeof resolved.youtrack_base_url === "string" ? resolved.youtrack_base_url : "",
|
|
94
|
+
"",
|
|
95
|
+
resolved.link_on_pr === true,
|
|
96
|
+
"",
|
|
97
|
+
"closes",
|
|
98
|
+
input.repo,
|
|
99
|
+
);
|
|
100
|
+
}
|