@deftai/directive-core 0.81.0 → 0.82.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.
Files changed (51) hide show
  1. package/dist/content-contracts/skills/helpers.d.ts +6 -0
  2. package/dist/content-contracts/skills/helpers.js +47 -3
  3. package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
  4. package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
  5. package/dist/eval/crud-telemetry.d.ts +2 -0
  6. package/dist/eval/crud-telemetry.js +30 -0
  7. package/dist/intake/github-body-cli.js +2 -0
  8. package/dist/intake/github-body.d.ts +11 -0
  9. package/dist/intake/github-body.js +95 -24
  10. package/dist/integration-e2e/helpers.js +1 -1
  11. package/dist/issue-sync/sync-from-xbrief.js +3 -0
  12. package/dist/orphan-active/evaluate.d.ts +25 -0
  13. package/dist/orphan-active/evaluate.js +275 -0
  14. package/dist/orphan-active/index.d.ts +3 -0
  15. package/dist/orphan-active/index.js +3 -0
  16. package/dist/orphan-active/refs.d.ts +16 -0
  17. package/dist/orphan-active/refs.js +105 -0
  18. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  19. package/dist/pr-wait-mergeable/cascade.js +18 -0
  20. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  21. package/dist/pr-wait-mergeable/main.js +44 -73
  22. package/dist/pr-wait-mergeable/result.d.ts +2 -1
  23. package/dist/pr-wait-mergeable/result.js +4 -0
  24. package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
  25. package/dist/pr-wait-mergeable/semantic-green.js +202 -0
  26. package/dist/pr-wait-mergeable/types.d.ts +1 -0
  27. package/dist/pr-watch/constants.d.ts +4 -4
  28. package/dist/pr-watch/constants.js +4 -4
  29. package/dist/pr-watch/watch.js +5 -3
  30. package/dist/release/native-steps.js +11 -4
  31. package/dist/render/framework-commands.js +6 -0
  32. package/dist/scope/brief-io.d.ts +26 -0
  33. package/dist/scope/brief-io.js +77 -0
  34. package/dist/scope/decompose.js +2 -2
  35. package/dist/scope/decomposed-refs.js +3 -3
  36. package/dist/scope/demote.js +2 -2
  37. package/dist/scope/project-definition-sync.d.ts +2 -2
  38. package/dist/scope/project-definition-sync.js +15 -3
  39. package/dist/scope/registry-artifact-sync.d.ts +6 -1
  40. package/dist/scope/registry-artifact-sync.js +31 -13
  41. package/dist/scope/transition.js +23 -15
  42. package/dist/scope/undo.js +2 -2
  43. package/dist/scope/vbrief-json.d.ts +1 -2
  44. package/dist/scope/vbrief-json.js +1 -4
  45. package/dist/staleness-tickler/state.js +19 -2
  46. package/dist/triage/queue/cache.js +5 -0
  47. package/dist/verify-source/index.d.ts +1 -0
  48. package/dist/verify-source/index.js +1 -0
  49. package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
  50. package/dist/verify-source/skill-external-fetch-gate.js +71 -0
  51. package/package.json +7 -3
@@ -0,0 +1,275 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
+ import { defaultRunGh } from "../pr-protected-issues/gh.js";
5
+ import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "../triage/queue/constants.js";
6
+ import { resolveRepo } from "../triage/queue/repo.js";
7
+ import { collectGithubRefs } from "./refs.js";
8
+ function readJson(path) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
11
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
12
+ ? parsed
13
+ : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function planOf(data) {
20
+ const plan = data?.plan;
21
+ return typeof plan === "object" && plan !== null && !Array.isArray(plan)
22
+ ? plan
23
+ : null;
24
+ }
25
+ function relBriefPath(path, projectRoot) {
26
+ try {
27
+ return relative(resolve(projectRoot), resolve(path)).replace(/\\/g, "/");
28
+ }
29
+ catch {
30
+ return path.replace(/\\/g, "/");
31
+ }
32
+ }
33
+ function listActiveRunningBriefs(projectRoot) {
34
+ let lifecycleRoot;
35
+ try {
36
+ lifecycleRoot = resolveLifecycleRoot(projectRoot);
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ const activeDir = join(lifecycleRoot, "active");
42
+ if (!existsSync(activeDir)) {
43
+ return [];
44
+ }
45
+ const out = [];
46
+ for (const entry of readdirSync(activeDir, { withFileTypes: true })) {
47
+ if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
48
+ continue;
49
+ }
50
+ const path = join(activeDir, entry.name);
51
+ const data = readJson(path);
52
+ const plan = planOf(data);
53
+ if (plan === null) {
54
+ continue;
55
+ }
56
+ if (String(plan.status ?? "").toLowerCase() !== "running") {
57
+ continue;
58
+ }
59
+ out.push({ path, plan });
60
+ }
61
+ return out.sort((a, b) => a.path.localeCompare(b.path));
62
+ }
63
+ function readCachedIssueState(projectRoot, ref) {
64
+ const [owner, name] = ref.repo.split("/", 2);
65
+ if (!owner || !name) {
66
+ return null;
67
+ }
68
+ const rawPath = join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, owner, name, String(ref.number), "raw.json");
69
+ if (!existsSync(rawPath)) {
70
+ return null;
71
+ }
72
+ const raw = readJson(rawPath);
73
+ if (raw === null) {
74
+ return null;
75
+ }
76
+ const state = typeof raw.state === "string" ? raw.state.toLowerCase() : "";
77
+ if (state === "closed") {
78
+ return "closed";
79
+ }
80
+ if (state === "open") {
81
+ return "open";
82
+ }
83
+ return null;
84
+ }
85
+ function fetchIssueStateLive(ref, runGh) {
86
+ const path = `repos/${ref.repo}/issues/${ref.number}`;
87
+ const result = runGh(["gh", "api", path]);
88
+ if (result.returncode !== 0) {
89
+ return null;
90
+ }
91
+ try {
92
+ const payload = JSON.parse(result.stdout);
93
+ if (payload === null || typeof payload !== "object") {
94
+ return null;
95
+ }
96
+ const state = payload.state;
97
+ if (state === "closed") {
98
+ return "closed";
99
+ }
100
+ if (state === "open") {
101
+ return "open";
102
+ }
103
+ return null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ function resolveIssueState(ref, projectRoot, runGh, skipGh) {
110
+ const cached = readCachedIssueState(projectRoot, ref);
111
+ if (cached !== null) {
112
+ return cached;
113
+ }
114
+ if (skipGh) {
115
+ return null;
116
+ }
117
+ return fetchIssueStateLive(ref, runGh);
118
+ }
119
+ function fetchPrMerged(ref, runGh) {
120
+ const path = `repos/${ref.repo}/pulls/${ref.number}`;
121
+ const result = runGh(["gh", "api", path]);
122
+ if (result.returncode !== 0) {
123
+ return null;
124
+ }
125
+ try {
126
+ const payload = JSON.parse(result.stdout);
127
+ if (payload === null || typeof payload !== "object") {
128
+ return null;
129
+ }
130
+ const mergedAt = payload.merged_at;
131
+ if (mergedAt === null) {
132
+ return false;
133
+ }
134
+ return typeof mergedAt === "string" && mergedAt.length > 0;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh) {
141
+ for (const pr of prRefs) {
142
+ if (skipGh) {
143
+ continue;
144
+ }
145
+ const merged = fetchPrMerged(pr, runGh);
146
+ if (merged === true) {
147
+ return { orphaned: true, reason: `linked PR #${pr.number} is merged` };
148
+ }
149
+ }
150
+ if (issueRefs.length === 0) {
151
+ return { orphaned: false, reason: null };
152
+ }
153
+ let resolved = 0;
154
+ for (const ref of issueRefs) {
155
+ const state = resolveIssueState(ref, projectRoot, runGh, skipGh);
156
+ if (state === null) {
157
+ return { orphaned: false, reason: null };
158
+ }
159
+ resolved += 1;
160
+ if (state !== "closed") {
161
+ return { orphaned: false, reason: null };
162
+ }
163
+ }
164
+ if (resolved > 0) {
165
+ return { orphaned: true, reason: "all referenced issues are closed" };
166
+ }
167
+ return { orphaned: false, reason: null };
168
+ }
169
+ function formatRefusal(orphans, projectRoot) {
170
+ const lines = [
171
+ `verify:orphan-active: ${orphans.length} active/running xBRIEF${orphans.length === 1 ? "" : "s"} look shipped but still consume WIP (project_root=${projectRoot}).`,
172
+ " Remediation: move each brief out of active/ with lifecycle ownership:",
173
+ " task scope:complete -- xbrief/active/<file>.xbrief.json",
174
+ " task scope:cancel -- xbrief/active/<file>.xbrief.json # when abandoning",
175
+ " For stop-at:pr-open workers the orchestrator owns post-merge complete/cancel;",
176
+ " for drive-to:merge-ready workers scope:complete is part of the worker unit (#2321).",
177
+ " Or run task swarm:finalize-cohort / task swarm:complete-cohort after cohort merge.",
178
+ " Offending briefs:",
179
+ ];
180
+ for (const orphan of orphans) {
181
+ lines.push(` - ${orphan.path} (${orphan.reason})`);
182
+ }
183
+ return lines.join("\n");
184
+ }
185
+ /**
186
+ * Pure evaluator for orphaned active/running xBRIEF detection (#2321).
187
+ * Fails when active/ briefs with plan.status==running reference only closed
188
+ * issues and/or a merged PR — the stop-at:pr-open orphan signature.
189
+ */
190
+ export function evaluate(projectRoot, options = {}) {
191
+ const root = resolve(projectRoot);
192
+ const quiet = options.quiet ?? false;
193
+ const skipGh = options.skipGh ?? false;
194
+ const runGh = options.runGh ?? defaultRunGh;
195
+ const defaultRepo = resolveRepo(options.repo, root);
196
+ if (!existsSync(root)) {
197
+ return {
198
+ code: 2,
199
+ message: `verify:orphan-active: project root does not exist: ${root}`,
200
+ stream: "stderr",
201
+ orphans: [],
202
+ };
203
+ }
204
+ let lifecycleRoot;
205
+ try {
206
+ lifecycleRoot = resolveLifecycleRoot(root);
207
+ }
208
+ catch (err) {
209
+ const message = err instanceof Error ? err.message : String(err);
210
+ // Consumer/greenfield fixtures may still use legacy vbrief/ only (#2112).
211
+ // Orphan detection applies only to xbrief/active/ — skip cleanly, not config fail.
212
+ if (message.includes("No xbrief/ layout found")) {
213
+ if (quiet) {
214
+ return { code: 0, message: "", stream: "none", orphans: [] };
215
+ }
216
+ return {
217
+ code: 0,
218
+ message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
219
+ stream: "stdout",
220
+ orphans: [],
221
+ };
222
+ }
223
+ return {
224
+ code: 2,
225
+ message: `verify:orphan-active: ${message}`,
226
+ stream: "stderr",
227
+ orphans: [],
228
+ };
229
+ }
230
+ if (!existsSync(lifecycleRoot)) {
231
+ if (quiet) {
232
+ return { code: 0, message: "", stream: "none", orphans: [] };
233
+ }
234
+ return {
235
+ code: 0,
236
+ message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
237
+ stream: "stdout",
238
+ orphans: [],
239
+ };
240
+ }
241
+ const orphans = [];
242
+ for (const brief of listActiveRunningBriefs(root)) {
243
+ const { issues, prs } = collectGithubRefs(brief.plan, defaultRepo);
244
+ if (issues.length === 0 && prs.length === 0) {
245
+ continue;
246
+ }
247
+ const assessment = assessOrphanSignature(issues, prs, root, runGh, skipGh);
248
+ if (assessment.orphaned && assessment.reason !== null) {
249
+ orphans.push({
250
+ path: relBriefPath(brief.path, root),
251
+ reason: assessment.reason,
252
+ });
253
+ }
254
+ }
255
+ if (orphans.length > 0) {
256
+ return {
257
+ code: 1,
258
+ message: formatRefusal(orphans, root),
259
+ stream: "stderr",
260
+ orphans,
261
+ };
262
+ }
263
+ if (quiet) {
264
+ return { code: 0, message: "", stream: "none", orphans: [] };
265
+ }
266
+ const scanned = listActiveRunningBriefs(root).length;
267
+ return {
268
+ code: 0,
269
+ message: `verify:orphan-active: no orphaned active/running xBRIEFs ` +
270
+ `(scanned ${scanned} running brief${scanned === 1 ? "" : "s"} in active/).`,
271
+ stream: "stdout",
272
+ orphans: [],
273
+ };
274
+ }
275
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./evaluate.js";
2
+ export * from "./refs.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from "./evaluate.js";
2
+ export * from "./refs.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ export interface IssueRef {
2
+ readonly repo: string;
3
+ readonly number: number;
4
+ }
5
+ export interface PrRef {
6
+ readonly repo: string;
7
+ readonly number: number;
8
+ }
9
+ /** Parse (repo, pr_number) from a github-pr reference URI. */
10
+ export declare function parseGithubPrUri(uri: unknown): [string | null, number | null];
11
+ /** Collect GitHub issue and PR refs from a scope plan object. */
12
+ export declare function collectGithubRefs(plan: Record<string, unknown>, defaultRepo: string | null): {
13
+ issues: IssueRef[];
14
+ prs: PrRef[];
15
+ };
16
+ //# sourceMappingURL=refs.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { referenceTypeMatches } from "@deftai/directive-types";
2
+ import { parseGithubIssueUri } from "../triage/reconcile/parse-uri.js";
3
+ /** Parse (repo, pr_number) from a github-pr reference URI. */
4
+ export function parseGithubPrUri(uri) {
5
+ if (typeof uri !== "string") {
6
+ return [null, null];
7
+ }
8
+ const cleaned = uri.trim().replace(/\/$/, "");
9
+ if (!cleaned) {
10
+ return [null, null];
11
+ }
12
+ const noScheme = cleaned.includes("://") ? cleaned.split("://").slice(1).join("://") : cleaned;
13
+ const parts = noScheme.split("/").filter(Boolean);
14
+ if (parts.length >= 4 && parts[parts.length - 2] === "pull") {
15
+ const tail = parts[parts.length - 1] ?? "";
16
+ if (/^\d+$/.test(tail)) {
17
+ const owner = parts[parts.length - 4];
18
+ const repo = parts[parts.length - 3];
19
+ if (owner && repo) {
20
+ return [`${owner}/${repo}`, Number(tail)];
21
+ }
22
+ }
23
+ }
24
+ const tail = parts[parts.length - 1] ?? "";
25
+ if (/^\d+$/.test(tail)) {
26
+ return [null, Number(tail)];
27
+ }
28
+ return [null, null];
29
+ }
30
+ function parseTrackingIssue(value) {
31
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
32
+ return value;
33
+ }
34
+ if (typeof value !== "string") {
35
+ return null;
36
+ }
37
+ const match = value.match(/#(\d+)/);
38
+ return match ? Number(match[1]) : null;
39
+ }
40
+ function addIssueRef(out, seen, repo, number, defaultRepo) {
41
+ if (number === null) {
42
+ return;
43
+ }
44
+ const resolvedRepo = repo ?? defaultRepo;
45
+ if (resolvedRepo === null || resolvedRepo.length === 0) {
46
+ return;
47
+ }
48
+ const key = `${resolvedRepo}:${number}`;
49
+ if (seen.has(key)) {
50
+ return;
51
+ }
52
+ seen.add(key);
53
+ out.push({ repo: resolvedRepo, number });
54
+ }
55
+ function addPrRef(out, seen, repo, number, defaultRepo) {
56
+ if (number === null) {
57
+ return;
58
+ }
59
+ const resolvedRepo = repo ?? defaultRepo;
60
+ if (resolvedRepo === null || resolvedRepo.length === 0) {
61
+ return;
62
+ }
63
+ const key = `${resolvedRepo}:${number}`;
64
+ if (seen.has(key)) {
65
+ return;
66
+ }
67
+ seen.add(key);
68
+ out.push({ repo: resolvedRepo, number });
69
+ }
70
+ /** Collect GitHub issue and PR refs from a scope plan object. */
71
+ export function collectGithubRefs(plan, defaultRepo) {
72
+ const issues = [];
73
+ const prs = [];
74
+ const seenIssues = new Set();
75
+ const seenPrs = new Set();
76
+ const refs = plan.references;
77
+ if (Array.isArray(refs)) {
78
+ for (const ref of refs) {
79
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
80
+ continue;
81
+ }
82
+ const typed = ref;
83
+ const type = String(typed.type ?? "");
84
+ if (referenceTypeMatches(type, "github-issue")) {
85
+ const [repo, number] = parseGithubIssueUri(typed.uri);
86
+ addIssueRef(issues, seenIssues, repo, number, defaultRepo);
87
+ }
88
+ else if (referenceTypeMatches(type, "github-pr")) {
89
+ const [repo, number] = parseGithubPrUri(typed.uri);
90
+ addPrRef(prs, seenPrs, repo, number, defaultRepo);
91
+ }
92
+ }
93
+ }
94
+ const metadata = plan.metadata;
95
+ if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
96
+ const tracking = metadata["x-tracking"];
97
+ if (typeof tracking === "object" && tracking !== null && !Array.isArray(tracking)) {
98
+ const t = tracking;
99
+ addIssueRef(issues, seenIssues, defaultRepo, parseTrackingIssue(t.parent_issue), defaultRepo);
100
+ addIssueRef(issues, seenIssues, defaultRepo, parseTrackingIssue(t.decomposition_origin), defaultRepo);
101
+ }
102
+ }
103
+ return { issues, prs };
104
+ }
105
+ //# sourceMappingURL=refs.js.map
@@ -1,8 +1,16 @@
1
+ import { type SemanticGreenFn } from "./semantic-green.js";
1
2
  import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
2
3
  export interface WaitMergeableOptions {
3
4
  readonly protectedFn?: ProtectedCheckFn;
4
5
  readonly monitorFn?: MonitorFn;
5
6
  readonly mergeFn?: MergeFn;
7
+ readonly semanticGreenFn?: SemanticGreenFn;
8
+ /** Enable merge-cascade semantic-green gate (#2385). */
9
+ readonly cascadeMode?: boolean;
10
+ /** After a prior cascade merge, require target-branch CI green at HEAD. */
11
+ readonly requireMasterCiGreen?: boolean;
12
+ /** Target branch override for stale-base comparison (defaults to PR base.ref). */
13
+ readonly baseBranch?: string | null;
6
14
  }
7
15
  /** Run protected-check -> wait -> merge cascade (#1369). */
8
16
  export declare function waitMergeableAndMerge(prNumber: number, repo: string, options: {
@@ -1,6 +1,7 @@
1
1
  import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
2
2
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
3
3
  import { makeResult } from "./result.js";
4
+ import { evaluateSemanticGreen } from "./semantic-green.js";
4
5
  import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
5
6
  /** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
6
7
  function isProtectedCheckConfigFailure(stderr) {
@@ -14,6 +15,7 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
14
15
  const protectedFn = options.protectedFn ?? runProtectedCheck;
15
16
  const monitorFn = options.monitorFn ?? runMonitor;
16
17
  const mergeFn = options.mergeFn ?? runGhMerge;
18
+ const semanticGreenFn = options.semanticGreenFn ?? evaluateSemanticGreen;
17
19
  const protectedIssues = options.protected;
18
20
  let protectedCheckPayload = {};
19
21
  if (protectedIssues.length > 0) {
@@ -47,6 +49,22 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
47
49
  });
48
50
  }
49
51
  }
52
+ const semanticGreen = semanticGreenFn(prNumber, repo, {
53
+ cascadeMode: options.cascadeMode,
54
+ requireMasterCiGreen: options.requireMasterCiGreen,
55
+ baseBranch: options.baseBranch,
56
+ });
57
+ if (!semanticGreen.ok) {
58
+ return makeResult({
59
+ prNumber,
60
+ repo,
61
+ outcome: semanticGreen.outcome ?? "semantic-green-blocked",
62
+ exitCode: semanticGreen.exitCode ?? EXIT_TIMEOUT_OR_ESCALATION,
63
+ protectedCheck: protectedCheckPayload,
64
+ semanticGreen: { ...semanticGreen.payload },
65
+ error: semanticGreen.error,
66
+ });
67
+ }
50
68
  const [monRc, monStdout, monStderr] = monitorFn(prNumber, repo, options.capMinutes);
51
69
  const monitorPayload = parseMonitorPayload(monStdout);
52
70
  const [outcome, monitorExit] = classifyMonitorOutcome(monRc, monitorPayload);
@@ -5,6 +5,9 @@ export interface ParsedWaitMergeableArgs {
5
5
  readonly capMinutes: number;
6
6
  readonly protectedValues: readonly string[];
7
7
  readonly emitJson: boolean;
8
+ readonly cascadeMode: boolean;
9
+ readonly requireMasterCiGreen: boolean;
10
+ readonly baseBranch: string | null;
8
11
  readonly error?: string;
9
12
  }
10
13
  export declare function parseWaitMergeableArgs(argv: readonly string[]): ParsedWaitMergeableArgs;
@@ -12,6 +15,7 @@ export interface RunWaitMergeableOptions {
12
15
  readonly protectedFn?: Parameters<typeof waitMergeableAndMerge>[2]["protectedFn"];
13
16
  readonly monitorFn?: Parameters<typeof waitMergeableAndMerge>[2]["monitorFn"];
14
17
  readonly mergeFn?: Parameters<typeof waitMergeableAndMerge>[2]["mergeFn"];
18
+ readonly semanticGreenFn?: Parameters<typeof waitMergeableAndMerge>[2]["semanticGreenFn"];
15
19
  }
16
20
  export declare function runWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
17
21
  export declare function cmdPrWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
@@ -16,22 +16,45 @@ export function parseWaitMergeableArgs(argv) {
16
16
  let capMinutes = 60;
17
17
  const protectedValues = [];
18
18
  let emitJson = false;
19
+ let cascadeMode = false;
20
+ let requireMasterCiGreen = false;
21
+ let baseBranch = null;
22
+ const baseReturn = () => ({
23
+ prNumber,
24
+ repo,
25
+ capMinutes,
26
+ protectedValues,
27
+ emitJson,
28
+ cascadeMode,
29
+ requireMasterCiGreen,
30
+ baseBranch,
31
+ });
19
32
  for (let i = 0; i < argv.length; i += 1) {
20
33
  const arg = argv[i];
21
34
  if (arg === "--json") {
22
35
  emitJson = true;
23
36
  }
37
+ else if (arg === "--cascade") {
38
+ cascadeMode = true;
39
+ }
40
+ else if (arg === "--require-master-ci-green") {
41
+ requireMasterCiGreen = true;
42
+ }
43
+ else if (arg === "--base-branch") {
44
+ const value = argv[i + 1];
45
+ if (value === undefined) {
46
+ return { ...baseReturn(), error: "argument --base-branch: expected one argument" };
47
+ }
48
+ baseBranch = value;
49
+ i += 1;
50
+ }
51
+ else if (arg?.startsWith("--base-branch=")) {
52
+ baseBranch = arg.slice("--base-branch=".length);
53
+ }
24
54
  else if (arg === "--repo") {
25
55
  const value = argv[i + 1];
26
56
  if (value === undefined) {
27
- return {
28
- prNumber,
29
- repo,
30
- capMinutes,
31
- protectedValues,
32
- emitJson,
33
- error: "argument --repo: expected one argument",
34
- };
57
+ return { ...baseReturn(), error: "argument --repo: expected one argument" };
35
58
  }
36
59
  repo = value;
37
60
  i += 1;
@@ -42,25 +65,11 @@ export function parseWaitMergeableArgs(argv) {
42
65
  else if (arg === "--cap-minutes") {
43
66
  const value = argv[i + 1];
44
67
  if (value === undefined) {
45
- return {
46
- prNumber,
47
- repo,
48
- capMinutes,
49
- protectedValues,
50
- emitJson,
51
- error: "argument --cap-minutes: expected one argument",
52
- };
68
+ return { ...baseReturn(), error: "argument --cap-minutes: expected one argument" };
53
69
  }
54
70
  const parsed = Number(value);
55
71
  if (!Number.isFinite(parsed)) {
56
- return {
57
- prNumber,
58
- repo,
59
- capMinutes,
60
- protectedValues,
61
- emitJson,
62
- error: `invalid --cap-minutes value: ${value}`,
63
- };
72
+ return { ...baseReturn(), error: `invalid --cap-minutes value: ${value}` };
64
73
  }
65
74
  capMinutes = parsed;
66
75
  i += 1;
@@ -69,28 +78,14 @@ export function parseWaitMergeableArgs(argv) {
69
78
  const value = arg.slice("--cap-minutes=".length);
70
79
  const parsed = Number(value);
71
80
  if (!Number.isFinite(parsed)) {
72
- return {
73
- prNumber,
74
- repo,
75
- capMinutes,
76
- protectedValues,
77
- emitJson,
78
- error: `invalid --cap-minutes value: ${value}`,
79
- };
81
+ return { ...baseReturn(), error: `invalid --cap-minutes value: ${value}` };
80
82
  }
81
83
  capMinutes = parsed;
82
84
  }
83
85
  else if (arg === "--protected") {
84
86
  const value = argv[i + 1];
85
87
  if (value === undefined) {
86
- return {
87
- prNumber,
88
- repo,
89
- capMinutes,
90
- protectedValues,
91
- emitJson,
92
- error: "argument --protected: expected one argument",
93
- };
88
+ return { ...baseReturn(), error: "argument --protected: expected one argument" };
94
89
  }
95
90
  protectedValues.push(value);
96
91
  i += 1;
@@ -99,51 +94,23 @@ export function parseWaitMergeableArgs(argv) {
99
94
  protectedValues.push(arg.slice("--protected=".length));
100
95
  }
101
96
  else if (arg?.startsWith("-")) {
102
- return {
103
- prNumber,
104
- repo,
105
- capMinutes,
106
- protectedValues,
107
- emitJson,
108
- error: `unrecognized arguments: ${arg}`,
109
- };
97
+ return { ...baseReturn(), error: `unrecognized arguments: ${arg}` };
110
98
  }
111
99
  else if (prNumber === null) {
112
100
  const n = Number(arg);
113
101
  if (!Number.isInteger(n)) {
114
- return {
115
- prNumber,
116
- repo,
117
- capMinutes,
118
- protectedValues,
119
- emitJson,
120
- error: `invalid PR number: ${arg}`,
121
- };
102
+ return { ...baseReturn(), error: `invalid PR number: ${arg}` };
122
103
  }
123
104
  prNumber = n;
124
105
  }
125
106
  else {
126
- return {
127
- prNumber,
128
- repo,
129
- capMinutes,
130
- protectedValues,
131
- emitJson,
132
- error: `unrecognized arguments: ${arg}`,
133
- };
107
+ return { ...baseReturn(), error: `unrecognized arguments: ${arg}` };
134
108
  }
135
109
  }
136
110
  if (prNumber === null) {
137
- return {
138
- prNumber,
139
- repo,
140
- capMinutes,
141
- protectedValues,
142
- emitJson,
143
- error: "the following arguments are required: pr_number",
144
- };
111
+ return { ...baseReturn(), error: "the following arguments are required: pr_number" };
145
112
  }
146
- return { prNumber, repo, capMinutes, protectedValues, emitJson };
113
+ return baseReturn();
147
114
  }
148
115
  function summaryLabelForExit(exitCode) {
149
116
  switch (exitCode) {
@@ -183,6 +150,10 @@ export function runWaitMergeable(argv, options = {}) {
183
150
  protectedFn: options.protectedFn,
184
151
  monitorFn: options.monitorFn,
185
152
  mergeFn: options.mergeFn,
153
+ semanticGreenFn: options.semanticGreenFn,
154
+ cascadeMode: args.cascadeMode,
155
+ requireMasterCiGreen: args.requireMasterCiGreen,
156
+ baseBranch: args.baseBranch,
186
157
  });
187
158
  const summaryLabel = summaryLabelForExit(result.exitCode);
188
159
  process.stderr.write(`[pr_wait_mergeable] PR #${result.prNumber} repo=${result.repo} ` +
@@ -1,8 +1,9 @@
1
1
  import type { WaitMergeableResult } from "./types.js";
2
2
  export declare function toResultDict(result: WaitMergeableResult): Record<string, unknown>;
3
- export declare function makeResult(fields: Omit<WaitMergeableResult, "monitorResult" | "protectedCheck" | "mergeStdout" | "mergeStderr"> & {
3
+ export declare function makeResult(fields: Omit<WaitMergeableResult, "monitorResult" | "protectedCheck" | "semanticGreen" | "mergeStdout" | "mergeStderr"> & {
4
4
  readonly monitorResult?: Record<string, unknown>;
5
5
  readonly protectedCheck?: Record<string, unknown>;
6
+ readonly semanticGreen?: Record<string, unknown>;
6
7
  readonly mergeStdout?: string;
7
8
  readonly mergeStderr?: string;
8
9
  }): WaitMergeableResult;