forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Pure, fail-closed reducer for merge evidence already normalized by adapters. */
|
|
4
|
+
|
|
5
|
+
const { types: { isProxy } } = require('node:util');
|
|
6
|
+
|
|
7
|
+
const FULL_SHA = /^[0-9a-f]{40}$/i;
|
|
8
|
+
const HEAD_SOURCES = new Set(['same-repository', 'fork', 'external']);
|
|
9
|
+
const REVIEW_DECISIONS = new Set(['NONE', 'APPROVED', 'REVIEW_REQUIRED', 'CHANGES_REQUESTED']);
|
|
10
|
+
|
|
11
|
+
const VERDICT_STATES = Object.freeze({
|
|
12
|
+
INCOMPLETE: 'INCOMPLETE',
|
|
13
|
+
STALE: 'STALE',
|
|
14
|
+
BLOCKED: 'BLOCKED',
|
|
15
|
+
MERGE_READY: 'MERGE_READY',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function normalizeSha(value) {
|
|
19
|
+
return typeof value === 'string' && FULL_SHA.test(value) ? value.toLowerCase() : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function object(value) {
|
|
23
|
+
if (!value || typeof value !== 'object') return null;
|
|
24
|
+
try {
|
|
25
|
+
return isProxy(value) || Array.isArray(value) ? null : value;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readDataProperty(value, key) {
|
|
32
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
33
|
+
return { ok: false, value: undefined };
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
if (isProxy(value)) return { ok: false, value: undefined };
|
|
37
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
38
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
39
|
+
return { ok: false, value: undefined };
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, value: descriptor.value };
|
|
42
|
+
} catch {
|
|
43
|
+
return { ok: false, value: undefined };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function data(value, key) {
|
|
48
|
+
return readDataProperty(value, key).value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readArrayItems(value, maxItems = 10_000) {
|
|
52
|
+
try {
|
|
53
|
+
if (isProxy(value) || !Array.isArray(value)) return null;
|
|
54
|
+
const lengthRead = readDataProperty(value, 'length');
|
|
55
|
+
const length = lengthRead.value;
|
|
56
|
+
if (!lengthRead.ok || !Number.isSafeInteger(length) || length < 0 || length > maxItems) return null;
|
|
57
|
+
const items = [];
|
|
58
|
+
for (let index = 0; index < length; index += 1) {
|
|
59
|
+
const item = readDataProperty(value, String(index));
|
|
60
|
+
if (!item.ok) return null;
|
|
61
|
+
items.push(item.value);
|
|
62
|
+
}
|
|
63
|
+
return items;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeRepository(value) {
|
|
70
|
+
if (typeof value !== 'string') return null;
|
|
71
|
+
const normalized = value.trim().toLowerCase();
|
|
72
|
+
const parts = normalized.split('/');
|
|
73
|
+
return parts.length === 2 && parts.every((part) => part && !/\s/.test(part)) ? normalized : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizePrNumber(value) {
|
|
77
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function reason(code, detail) {
|
|
81
|
+
return detail === undefined ? { code } : { code, detail };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addReason(groups, group, code, detail) {
|
|
85
|
+
groups[group].push(reason(code, detail));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function currentSurface(groups, surface, expectedHead, code) {
|
|
89
|
+
const head = normalizeSha(data(surface, 'headSha'));
|
|
90
|
+
if (!head) {
|
|
91
|
+
addReason(groups, 'incomplete', `${code}_head_invalid`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (expectedHead && head !== expectedHead) {
|
|
95
|
+
addReason(groups, 'stale', `${code}_stale`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function checkKey(entry) {
|
|
102
|
+
return `${entry.name}\u0000${entry.appId === null ? '*' : entry.appId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeCheckEntry(entry) {
|
|
106
|
+
const record = object(entry);
|
|
107
|
+
const name = data(record, 'name');
|
|
108
|
+
const rawAppId = data(record, 'appId');
|
|
109
|
+
if (!record || typeof name !== 'string' || !name.trim()) return null;
|
|
110
|
+
let appId = null;
|
|
111
|
+
if (rawAppId !== null) {
|
|
112
|
+
if (!Number.isSafeInteger(rawAppId) || rawAppId <= 0) return null;
|
|
113
|
+
appId = rawAppId;
|
|
114
|
+
}
|
|
115
|
+
return { name: name.trim(), appId };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addIdentityValidityReasons(identity, groups) {
|
|
119
|
+
if (!identity.expectedHead) addReason(groups, 'incomplete', 'invalid_expected_head');
|
|
120
|
+
if (!identity.expectedBase) addReason(groups, 'incomplete', 'invalid_expected_base');
|
|
121
|
+
if (!identity.expectedRepository) addReason(groups, 'incomplete', 'invalid_expected_repository');
|
|
122
|
+
if (!identity.expectedPrNumber) addReason(groups, 'incomplete', 'invalid_expected_pr_number');
|
|
123
|
+
if (!identity.prNumber) addReason(groups, 'incomplete', 'invalid_pr_number');
|
|
124
|
+
if (!identity.observedHead) addReason(groups, 'incomplete', 'invalid_observed_head');
|
|
125
|
+
if (!identity.baseSha) addReason(groups, 'incomplete', 'invalid_base');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function addIdentityDriftReasons(identity, groups) {
|
|
129
|
+
if (identity.expectedHead && identity.observedHead && identity.expectedHead !== identity.observedHead) {
|
|
130
|
+
addReason(groups, 'stale', 'head_mismatch');
|
|
131
|
+
}
|
|
132
|
+
if (identity.expectedBase && identity.baseSha && identity.expectedBase !== identity.baseSha) {
|
|
133
|
+
addReason(groups, 'stale', 'base_mismatch');
|
|
134
|
+
}
|
|
135
|
+
if (identity.expectedRepository && identity.repository
|
|
136
|
+
&& identity.expectedRepository !== identity.repository) {
|
|
137
|
+
addReason(groups, 'stale', 'repository_mismatch');
|
|
138
|
+
}
|
|
139
|
+
if (identity.expectedPrNumber && identity.prNumber
|
|
140
|
+
&& identity.expectedPrNumber !== identity.prNumber) {
|
|
141
|
+
addReason(groups, 'stale', 'pr_number_mismatch');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function evaluateHeadSource(head, headRepository, baseRepository, groups) {
|
|
146
|
+
if (!headRepository || !baseRepository) {
|
|
147
|
+
addReason(groups, 'incomplete', 'repository_identity_incomplete');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const source = data(head, 'source');
|
|
151
|
+
const sameRepository = headRepository === baseRepository;
|
|
152
|
+
if (!HEAD_SOURCES.has(source)) {
|
|
153
|
+
addReason(groups, 'incomplete', 'head_source_unknown');
|
|
154
|
+
} else if ((sameRepository && source !== 'same-repository')
|
|
155
|
+
|| (!sameRepository && source === 'same-repository')) {
|
|
156
|
+
addReason(groups, 'incomplete', 'head_source_conflict');
|
|
157
|
+
}
|
|
158
|
+
if (!sameRepository && data(head, 'acquired') !== true) {
|
|
159
|
+
addReason(groups, 'incomplete', 'external_head_not_acquired');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function evaluateIdentity(input, groups) {
|
|
164
|
+
const head = object(data(input, 'head'));
|
|
165
|
+
const base = object(data(input, 'base'));
|
|
166
|
+
const identity = {
|
|
167
|
+
expectedHead: normalizeSha(data(input, 'expectedHeadSha')),
|
|
168
|
+
expectedBase: normalizeSha(data(input, 'expectedBaseSha')),
|
|
169
|
+
expectedRepository: normalizeRepository(data(input, 'expectedRepository')),
|
|
170
|
+
expectedPrNumber: normalizePrNumber(data(input, 'expectedPrNumber')),
|
|
171
|
+
repository: normalizeRepository(data(base, 'repository')),
|
|
172
|
+
prNumber: normalizePrNumber(data(input, 'prNumber')),
|
|
173
|
+
observedHead: normalizeSha(data(head, 'sha')),
|
|
174
|
+
baseSha: normalizeSha(data(base, 'sha')),
|
|
175
|
+
};
|
|
176
|
+
const headRepository = normalizeRepository(data(head, 'repository'));
|
|
177
|
+
addIdentityValidityReasons(identity, groups);
|
|
178
|
+
addIdentityDriftReasons(identity, groups);
|
|
179
|
+
evaluateHeadSource(head, headRepository, identity.repository, groups);
|
|
180
|
+
return identity;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function evaluateAncestry(input, identity, groups) {
|
|
184
|
+
const ancestry = object(data(input, 'ancestry'));
|
|
185
|
+
if (!ancestry || data(ancestry, 'complete') !== true) {
|
|
186
|
+
addReason(groups, 'incomplete', 'ancestry_incomplete');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
currentSurface(groups, ancestry, identity.expectedHead, 'ancestry');
|
|
190
|
+
const ancestryBase = normalizeSha(data(ancestry, 'baseSha'));
|
|
191
|
+
if (!ancestryBase) addReason(groups, 'incomplete', 'ancestry_base_invalid');
|
|
192
|
+
else if (identity.baseSha && ancestryBase !== identity.baseSha) addReason(groups, 'stale', 'ancestry_base_stale');
|
|
193
|
+
else if (identity.expectedBase && ancestryBase !== identity.expectedBase) {
|
|
194
|
+
addReason(groups, 'stale', 'ancestry_base_stale');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const containsBase = data(ancestry, 'containsBase');
|
|
198
|
+
const behindBy = data(ancestry, 'behindBy');
|
|
199
|
+
const conflicting = data(ancestry, 'conflicting');
|
|
200
|
+
if (typeof containsBase !== 'boolean'
|
|
201
|
+
|| !Number.isSafeInteger(behindBy) || behindBy < 0
|
|
202
|
+
|| typeof conflicting !== 'boolean') {
|
|
203
|
+
addReason(groups, 'incomplete', 'ancestry_malformed');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!containsBase) addReason(groups, 'blocked', 'base_not_ancestor');
|
|
207
|
+
if (behindBy > 0) addReason(groups, 'blocked', 'head_behind_base', behindBy);
|
|
208
|
+
if (conflicting) addReason(groups, 'blocked', 'merge_conflict');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function evaluateRequiredPolicy(required, groups) {
|
|
212
|
+
const items = readArrayItems(required);
|
|
213
|
+
if (!items) {
|
|
214
|
+
addReason(groups, 'incomplete', 'required_policy_incomplete');
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const grouped = new Map();
|
|
218
|
+
const normalized = [];
|
|
219
|
+
for (const raw of items) {
|
|
220
|
+
const entry = normalizeCheckEntry(raw);
|
|
221
|
+
if (!entry) {
|
|
222
|
+
addReason(groups, 'incomplete', 'required_policy_malformed');
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!grouped.has(entry.name)) grouped.set(entry.name, []);
|
|
226
|
+
grouped.get(entry.name).push(entry);
|
|
227
|
+
}
|
|
228
|
+
for (const name of [...grouped.keys()].sort(compareCanonicalStrings)) {
|
|
229
|
+
const entries = grouped.get(name);
|
|
230
|
+
if (entries.length !== 1) addReason(groups, 'incomplete', 'required_policy_conflict', name);
|
|
231
|
+
else normalized.push(entries[0]);
|
|
232
|
+
}
|
|
233
|
+
return normalized;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function evaluateCheckObservations(observations, expectedHead, groups) {
|
|
237
|
+
const items = readArrayItems(observations);
|
|
238
|
+
if (!items) {
|
|
239
|
+
addReason(groups, 'incomplete', 'check_observations_incomplete');
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const grouped = new Map();
|
|
243
|
+
for (const raw of items) {
|
|
244
|
+
const entry = normalizeCheckEntry(raw);
|
|
245
|
+
const status = data(raw, 'status');
|
|
246
|
+
const conclusion = data(raw, 'conclusion');
|
|
247
|
+
if (!entry || typeof status !== 'string'
|
|
248
|
+
|| (conclusion !== null && typeof conclusion !== 'string')) {
|
|
249
|
+
addReason(groups, 'incomplete', 'check_observation_malformed');
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const head = normalizeSha(data(raw, 'headSha'));
|
|
253
|
+
if (!head) addReason(groups, 'incomplete', 'check_observation_head_invalid', entry.name);
|
|
254
|
+
else if (expectedHead && head !== expectedHead) addReason(groups, 'stale', 'check_observation_stale', entry.name);
|
|
255
|
+
const normalized = {
|
|
256
|
+
...entry,
|
|
257
|
+
status: status.toUpperCase(),
|
|
258
|
+
conclusion: String(conclusion || '').toUpperCase(),
|
|
259
|
+
};
|
|
260
|
+
const key = checkKey(normalized);
|
|
261
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
262
|
+
grouped.get(key).push(normalized);
|
|
263
|
+
}
|
|
264
|
+
const byKey = new Map();
|
|
265
|
+
const byName = new Map();
|
|
266
|
+
const conflicts = new Set();
|
|
267
|
+
const conflictNames = new Set();
|
|
268
|
+
for (const key of [...grouped.keys()].sort(compareCanonicalStrings)) {
|
|
269
|
+
const entries = grouped.get(key);
|
|
270
|
+
if (entries.length !== 1) {
|
|
271
|
+
conflicts.add(key);
|
|
272
|
+
conflictNames.add(entries[0].name);
|
|
273
|
+
addReason(groups, 'incomplete', 'check_observation_conflict', entries[0].name);
|
|
274
|
+
} else {
|
|
275
|
+
byKey.set(key, entries[0]);
|
|
276
|
+
if (!byName.has(entries[0].name)) byName.set(entries[0].name, []);
|
|
277
|
+
byName.get(entries[0].name).push(entries[0]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { byKey, byName, conflicts, conflictNames };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function evaluateChecks(input, expectedHead, groups) {
|
|
284
|
+
const checks = object(data(input, 'checks'));
|
|
285
|
+
if (!checks || data(checks, 'complete') !== true) {
|
|
286
|
+
addReason(groups, 'incomplete', 'checks_incomplete');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
currentSurface(groups, checks, expectedHead, 'checks');
|
|
290
|
+
const required = evaluateRequiredPolicy(data(checks, 'required'), groups);
|
|
291
|
+
const observations = evaluateCheckObservations(data(checks, 'observations'), expectedHead, groups);
|
|
292
|
+
if (!required || !observations) return;
|
|
293
|
+
|
|
294
|
+
for (const requirement of required) {
|
|
295
|
+
const key = checkKey(requirement);
|
|
296
|
+
if (observations.conflicts.has(key)
|
|
297
|
+
|| (requirement.appId === null && observations.conflictNames.has(requirement.name))) continue;
|
|
298
|
+
const matches = requirement.appId === null
|
|
299
|
+
? observations.byName.get(requirement.name) || []
|
|
300
|
+
: [observations.byKey.get(key)].filter(Boolean);
|
|
301
|
+
if (matches.length === 0) {
|
|
302
|
+
addReason(groups, 'blocked', 'required_check_missing', requirement.name);
|
|
303
|
+
} else if (!matches.some((entry) => entry.status === 'COMPLETED' && entry.conclusion === 'SUCCESS')) {
|
|
304
|
+
addReason(groups, 'blocked', 'required_check_not_successful', requirement.name);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function evaluateReview(input, expectedHead, groups) {
|
|
310
|
+
const review = object(data(input, 'review'));
|
|
311
|
+
if (!review || data(review, 'complete') !== true) {
|
|
312
|
+
addReason(groups, 'incomplete', 'review_incomplete');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
currentSurface(groups, review, expectedHead, 'review');
|
|
316
|
+
const required = data(review, 'required');
|
|
317
|
+
const rawDecision = data(review, 'decision');
|
|
318
|
+
const conflicting = data(review, 'conflicting');
|
|
319
|
+
if (typeof required !== 'boolean' || typeof rawDecision !== 'string'
|
|
320
|
+
|| typeof conflicting !== 'boolean') {
|
|
321
|
+
addReason(groups, 'incomplete', 'review_malformed');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const decision = rawDecision.toUpperCase();
|
|
325
|
+
if (!REVIEW_DECISIONS.has(decision)) {
|
|
326
|
+
addReason(groups, 'incomplete', 'review_decision_unknown');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (conflicting) {
|
|
330
|
+
addReason(groups, 'incomplete', 'review_state_conflict');
|
|
331
|
+
} else if (!required && decision === 'REVIEW_REQUIRED') {
|
|
332
|
+
addReason(groups, 'incomplete', 'review_state_conflict');
|
|
333
|
+
} else if (decision === 'CHANGES_REQUESTED') {
|
|
334
|
+
addReason(groups, 'blocked', 'changes_requested');
|
|
335
|
+
} else if (required && decision !== 'APPROVED') {
|
|
336
|
+
addReason(groups, 'blocked', 'approval_missing');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function evaluateThreads(input, expectedHead, groups) {
|
|
341
|
+
const threads = object(data(input, 'threads'));
|
|
342
|
+
if (!threads || data(threads, 'complete') !== true) {
|
|
343
|
+
addReason(groups, 'incomplete', 'threads_incomplete');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
currentSurface(groups, threads, expectedHead, 'threads');
|
|
347
|
+
const items = readArrayItems(data(threads, 'items'));
|
|
348
|
+
if (!items) {
|
|
349
|
+
addReason(groups, 'incomplete', 'threads_malformed');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const byId = new Map();
|
|
353
|
+
let unresolved = 0;
|
|
354
|
+
for (const thread of items) {
|
|
355
|
+
const id = data(thread, 'id');
|
|
356
|
+
const resolved = data(thread, 'resolved');
|
|
357
|
+
const outdated = data(thread, 'outdated');
|
|
358
|
+
if (!object(thread) || typeof id !== 'string' || !id.trim()
|
|
359
|
+
|| typeof resolved !== 'boolean' || typeof outdated !== 'boolean') {
|
|
360
|
+
addReason(groups, 'incomplete', 'thread_malformed');
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const normalizedId = id.trim();
|
|
364
|
+
if (!byId.has(normalizedId)) byId.set(normalizedId, []);
|
|
365
|
+
byId.get(normalizedId).push({ resolved, outdated });
|
|
366
|
+
}
|
|
367
|
+
for (const id of [...byId.keys()].sort(compareCanonicalStrings)) {
|
|
368
|
+
const states = byId.get(id);
|
|
369
|
+
if (states.length !== 1) {
|
|
370
|
+
addReason(groups, 'incomplete', 'thread_state_conflict', id);
|
|
371
|
+
} else if (!states[0].resolved && !states[0].outdated) {
|
|
372
|
+
unresolved += 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (unresolved > 0) addReason(groups, 'blocked', 'unresolved_threads', unresolved);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function verdictState(groups) {
|
|
379
|
+
if (groups.incomplete.length > 0) return VERDICT_STATES.INCOMPLETE;
|
|
380
|
+
if (groups.stale.length > 0) return VERDICT_STATES.STALE;
|
|
381
|
+
if (groups.blocked.length > 0) return VERDICT_STATES.BLOCKED;
|
|
382
|
+
return VERDICT_STATES.MERGE_READY;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function compareCanonicalStrings(left, right) {
|
|
386
|
+
if (left < right) return -1;
|
|
387
|
+
if (left > right) return 1;
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function compareReason(left, right) {
|
|
392
|
+
const codeOrder = compareCanonicalStrings(left.code, right.code);
|
|
393
|
+
if (codeOrder !== 0) return codeOrder;
|
|
394
|
+
const leftDetail = String(left.detail ?? '');
|
|
395
|
+
const rightDetail = String(right.detail ?? '');
|
|
396
|
+
return compareCanonicalStrings(leftDetail, rightDetail);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function canonicalReasons(groups) {
|
|
400
|
+
const seen = new Set();
|
|
401
|
+
return [...groups.incomplete, ...groups.stale, ...groups.blocked]
|
|
402
|
+
.sort(compareReason)
|
|
403
|
+
.filter((item) => {
|
|
404
|
+
const key = `${item.code}\u0000${String(item.detail ?? '')}`;
|
|
405
|
+
if (seen.has(key)) return false;
|
|
406
|
+
seen.add(key);
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function evaluateCurrentHeadVerdict(input = {}) {
|
|
412
|
+
const evidence = object(input) || {};
|
|
413
|
+
const groups = { incomplete: [], stale: [], blocked: [] };
|
|
414
|
+
const identity = evaluateIdentity(evidence, groups);
|
|
415
|
+
evaluateAncestry(evidence, identity, groups);
|
|
416
|
+
evaluateChecks(evidence, identity.expectedHead, groups);
|
|
417
|
+
evaluateReview(evidence, identity.expectedHead, groups);
|
|
418
|
+
evaluateThreads(evidence, identity.expectedHead, groups);
|
|
419
|
+
return {
|
|
420
|
+
state: verdictState(groups),
|
|
421
|
+
repository: identity.repository,
|
|
422
|
+
prNumber: identity.prNumber,
|
|
423
|
+
headSha: identity.observedHead,
|
|
424
|
+
baseSha: identity.baseSha,
|
|
425
|
+
reasons: canonicalReasons(groups),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
VERDICT_STATES,
|
|
431
|
+
evaluateCurrentHeadVerdict,
|
|
432
|
+
normalizePrNumber,
|
|
433
|
+
normalizeRepository,
|
|
434
|
+
normalizeSha,
|
|
435
|
+
object,
|
|
436
|
+
readArrayItems,
|
|
437
|
+
readDataProperty,
|
|
438
|
+
};
|
|
@@ -19,26 +19,60 @@
|
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
const { spawn, execFileSync } = require('node:child_process');
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const watchOwner = require('./watch-owner');
|
|
23
|
+
const { isCompiledBinary } = require('../package-root');
|
|
24
|
+
const { readGithubAccount } = require('../github-context');
|
|
25
|
+
|
|
26
|
+
const CANONICAL_REPOSITORY = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/;
|
|
27
|
+
|
|
28
|
+
async function launchGateComplete(owner = watchOwner, ownerOptions = {}) {
|
|
29
|
+
if (typeof owner?.readMigrationGate !== 'function') return false;
|
|
30
|
+
try {
|
|
31
|
+
const result = await owner.readMigrationGate({}, ownerOptions);
|
|
32
|
+
return result?.ok === true && result.gate?.state === 'complete';
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
|
|
24
38
|
/** Absolute path to the forge CLI entrypoint (this file is lib/pr-monitor/). */
|
|
25
39
|
function forgeBin() {
|
|
26
40
|
return path.join(__dirname, '..', '..', 'bin', 'forge.js');
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
function forgeArgs(args, opts = {}) {
|
|
44
|
+
return (opts.isCompiledBinary || isCompiledBinary)() ? [...args] : [forgeBin(), ...args];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Workers resolve their own account after re-entry. Preserve native unbound environments.
|
|
48
|
+
function githubWorkerEnvironment(projectRoot, opts = {}) {
|
|
49
|
+
let bound;
|
|
50
|
+
try {
|
|
51
|
+
bound = opts.githubContext ? opts.githubContext.bound
|
|
52
|
+
: Boolean((opts.readGithubAccount || readGithubAccount)(projectRoot));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error('Unable to read the worker GitHub account binding.');
|
|
55
|
+
}
|
|
56
|
+
if (!bound) return opts.env;
|
|
57
|
+
const env = { ...(opts.env || process.env) };
|
|
58
|
+
for (const key of Object.keys(env)) {
|
|
59
|
+
if (['gh_token', 'github_token', 'gh_host'].includes(key.toLowerCase())) delete env[key];
|
|
60
|
+
}
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
63
|
+
|
|
29
64
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* then falls through to spawn and relies on the watch loop's own de-dup.
|
|
65
|
+
* Parse a canonical `owner/repository` from an origin URL. Production launchers
|
|
66
|
+
* supply the provider-resolved base repository; this remains a bounded utility
|
|
67
|
+
* for diagnostics and compatibility callers.
|
|
34
68
|
*/
|
|
35
69
|
function defaultResolveSlug({ cwd, exec = execFileSync }) {
|
|
36
70
|
try {
|
|
37
71
|
const url = exec('git', ['remote', 'get-url', 'origin'], {
|
|
38
72
|
cwd, encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
|
|
39
73
|
}).trim();
|
|
40
|
-
const match = /[/:][
|
|
41
|
-
return match ? match[1] : null;
|
|
74
|
+
const match = /[/:]([^/:]+)\/([^/]+?)(?:\.git)?$/.exec(url);
|
|
75
|
+
return match ? `${match[1]}/${match[2]}`.toLowerCase() : null;
|
|
42
76
|
} catch {
|
|
43
77
|
return null;
|
|
44
78
|
}
|
|
@@ -51,45 +85,129 @@ function defaultResolveSlug({ cwd, exec = execFileSync }) {
|
|
|
51
85
|
* @param {string|number} opts.prNumber - the PR to watch.
|
|
52
86
|
* @param {string} [opts.cwd] - repo root (default process.cwd()).
|
|
53
87
|
* @param {Function} [opts.spawn] - child spawner (test injection).
|
|
54
|
-
* @param {
|
|
55
|
-
* @param {object} [opts.
|
|
56
|
-
* @param {
|
|
57
|
-
* @
|
|
88
|
+
* @param {string} opts.repository - canonical provider base repository.
|
|
89
|
+
* @param {object} [opts.reservation] - exact pre-reserved owner envelope.
|
|
90
|
+
* @param {object} [opts.owner] - watcher owner API (test injection).
|
|
91
|
+
* @param {object} [opts.ownerOptions] - Kernel driver and evidence verification.
|
|
92
|
+
* @returns {Promise<{ started: boolean, pid?: number|null, reason?: string }>} — never throws.
|
|
58
93
|
*/
|
|
59
|
-
function startPrWatcherDetached(opts = {}) {
|
|
94
|
+
async function startPrWatcherDetached(opts = {}) {
|
|
60
95
|
const { prNumber, cwd = process.cwd() } = opts;
|
|
61
96
|
const spawnFn = opts.spawn || spawn;
|
|
62
|
-
const
|
|
63
|
-
const
|
|
97
|
+
const owner = opts.owner || watchOwner;
|
|
98
|
+
const controllerPid = opts.controllerPid ?? process.pid;
|
|
99
|
+
const repository = typeof opts.repository === 'string' ? opts.repository.toLowerCase() : '';
|
|
100
|
+
const pr = Number(prNumber);
|
|
101
|
+
let reservation = null;
|
|
64
102
|
try {
|
|
65
|
-
if (!
|
|
103
|
+
if (!Number.isSafeInteger(pr) || pr <= 0) return { started: false, reason: 'no-pr' };
|
|
104
|
+
if (!CANONICAL_REPOSITORY.test(repository)) return { started: false, reason: 'repository-unavailable' };
|
|
105
|
+
if (!Number.isSafeInteger(controllerPid) || controllerPid <= 0
|
|
106
|
+
|| typeof owner.abortStarting !== 'function'
|
|
107
|
+
|| (opts.reservation == null && typeof owner.reserveStarting !== 'function')) {
|
|
108
|
+
return { started: false, reason: 'authority-unavailable' };
|
|
109
|
+
}
|
|
110
|
+
if (!await launchGateComplete(owner, opts.ownerOptions || {})) {
|
|
111
|
+
return { started: false, reason: 'migration-gate-incomplete' };
|
|
112
|
+
}
|
|
66
113
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
114
|
+
const env = githubWorkerEnvironment(cwd, opts);
|
|
115
|
+
const identity = { repo: repository, pr };
|
|
116
|
+
if (opts.reservation != null) {
|
|
117
|
+
reservation = opts.reservation;
|
|
118
|
+
const record = reservation?.record;
|
|
119
|
+
const startedAt = typeof record?.startedAt === 'string' ? new Date(record.startedAt) : null;
|
|
120
|
+
const exactReservation = reservation?.ok === true
|
|
121
|
+
&& record?.repo === repository
|
|
122
|
+
&& record?.pr === pr
|
|
123
|
+
&& record?.phase === 'starting'
|
|
124
|
+
&& record?.controllerPid === controllerPid
|
|
125
|
+
&& typeof record?.generation === 'string'
|
|
126
|
+
&& record.generation.length > 0
|
|
127
|
+
&& startedAt != null
|
|
128
|
+
&& !Number.isNaN(startedAt.getTime())
|
|
129
|
+
&& startedAt.toISOString() === record.startedAt;
|
|
130
|
+
if (!exactReservation) return { started: false, reason: 'invalid-reservation' };
|
|
131
|
+
} else {
|
|
132
|
+
reservation = await owner.reserveStarting(identity, { controllerPid }, opts.ownerOptions || {});
|
|
133
|
+
if (!reservation?.ok && reservation?.record?.phase === 'complete' && opts.providerEvidence
|
|
134
|
+
&& typeof owner.reserveReopened === 'function') {
|
|
135
|
+
reservation = await owner.reserveReopened(identity, {
|
|
136
|
+
generation: reservation.record.generation,
|
|
137
|
+
expectedReceiptId: reservation.record.terminalReceiptId,
|
|
138
|
+
controllerPid,
|
|
139
|
+
providerEvidence: opts.providerEvidence,
|
|
140
|
+
}, opts.ownerOptions || {});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!reservation?.ok || !reservation.record) {
|
|
144
|
+
return { started: false, reason: reservation?.reason || 'authority-unavailable' };
|
|
145
|
+
}
|
|
146
|
+
if (!await launchGateComplete(owner, opts.ownerOptions || {})) {
|
|
147
|
+
if (opts.reservation == null) {
|
|
148
|
+
await owner.abortStarting(identity, {
|
|
149
|
+
generation: reservation.record.generation, controllerPid,
|
|
150
|
+
}, opts.ownerOptions || {});
|
|
151
|
+
}
|
|
152
|
+
return { started: false, reason: 'migration-gate-incomplete' };
|
|
71
153
|
}
|
|
72
154
|
|
|
73
155
|
const child = spawnFn(
|
|
74
156
|
process.execPath,
|
|
75
|
-
[
|
|
76
|
-
|
|
157
|
+
forgeArgs([
|
|
158
|
+
'shepherd', 'watch', String(pr),
|
|
159
|
+
'--repo', repository,
|
|
160
|
+
'--generation', reservation.record.generation,
|
|
161
|
+
'--controller-pid', String(controllerPid),
|
|
162
|
+
'--started-at', reservation.record.startedAt,
|
|
163
|
+
], opts),
|
|
164
|
+
{ cwd, detached: true, stdio: 'ignore', windowsHide: true, ...(env ? { env } : {}) },
|
|
77
165
|
);
|
|
78
166
|
// spawn can emit an ASYNC 'error' (ENOENT/EACCES) AFTER returning; with no
|
|
79
167
|
// listener that becomes an unhandled exception that could crash ship. A no-op
|
|
80
|
-
// handler keeps a failed detached start best-effort
|
|
81
|
-
//
|
|
82
|
-
if (child && typeof child.on === 'function')
|
|
168
|
+
// handler keeps a failed detached start best-effort; the reservation is
|
|
169
|
+
// conditionally aborted so a later controller can recover immediately.
|
|
170
|
+
if (child && typeof child.on === 'function') {
|
|
171
|
+
child.on('error', () => {
|
|
172
|
+
void Promise.resolve()
|
|
173
|
+
.then(() => owner.abortStarting(identity, {
|
|
174
|
+
generation: reservation.record.generation, controllerPid,
|
|
175
|
+
}, opts.ownerOptions || {}))
|
|
176
|
+
.catch(() => { /* a stale starting row is recoverable by the daemon */ });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
83
179
|
if (child && typeof child.unref === 'function') child.unref();
|
|
84
|
-
|
|
85
|
-
|
|
180
|
+
if (!Number.isSafeInteger(child?.pid) || child.pid <= 0) {
|
|
181
|
+
await owner.abortStarting(identity, {
|
|
182
|
+
generation: reservation.record.generation, controllerPid,
|
|
183
|
+
}, opts.ownerOptions || {});
|
|
184
|
+
return { started: false, reason: 'spawn-pid-unavailable' };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
started: true,
|
|
188
|
+
pid: child.pid,
|
|
189
|
+
generation: reservation.record.generation,
|
|
190
|
+
startedAt: reservation.record.startedAt,
|
|
191
|
+
repository,
|
|
192
|
+
};
|
|
193
|
+
} catch {
|
|
86
194
|
// Lifecycle auto-start must never fail ship — degrade to "not started".
|
|
87
|
-
|
|
195
|
+
if (reservation?.record && CANONICAL_REPOSITORY.test(repository)) {
|
|
196
|
+
try {
|
|
197
|
+
await owner.abortStarting({ repo: repository, pr }, {
|
|
198
|
+
generation: reservation.record.generation, controllerPid,
|
|
199
|
+
}, opts.ownerOptions || {});
|
|
200
|
+
} catch { /* a stale starting row is recoverable by the daemon */ }
|
|
201
|
+
}
|
|
202
|
+
return { started: false, reason: 'watcher-launch-failed' };
|
|
88
203
|
}
|
|
89
204
|
}
|
|
90
205
|
|
|
91
206
|
module.exports = {
|
|
92
207
|
startPrWatcherDetached,
|
|
208
|
+
launchGateComplete,
|
|
93
209
|
defaultResolveSlug,
|
|
94
210
|
forgeBin,
|
|
211
|
+
forgeArgs,
|
|
212
|
+
githubWorkerEnvironment,
|
|
95
213
|
};
|