forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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 +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- 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/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- 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 +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- 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-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- 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 +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- 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 +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- 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-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
package/lib/commands/shepherd.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* shepherd command — one bounded monitor pass over a pull request.
|
|
5
5
|
*
|
|
6
|
-
* `forge shepherd <pr> [--auto-rebase]`
|
|
7
|
-
* idempotent Tier-A action (rerun a flaky required
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* this command
|
|
6
|
+
* `forge shepherd <pr> [--auto-rebase]` runs one local review preflight, reads
|
|
7
|
+
* PR/CI state, takes at most one idempotent Tier-A action (rerun a flaky required
|
|
8
|
+
* check), persists bounded deltas/receipts, and exits. It NEVER merges and NEVER
|
|
9
|
+
* resolves review threads. Event-driven waiting belongs to the singleton/watch
|
|
10
|
+
* process; this command does not model-poll or loop in-process.
|
|
11
11
|
*
|
|
12
12
|
* `forge shepherd <pr> --bundle --json` instead prints the COMPLETE read-only
|
|
13
13
|
* PR-state bundle (all unresolved threads, merge state, CI, divergence,
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
* done IN CODE so an agent gets one payload instead of running those by hand. It
|
|
22
22
|
* still NEVER merges and NEVER resolves threads.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* Public Memory is durable authority; the local journal is compatibility delivery.
|
|
25
25
|
*
|
|
26
26
|
* @module commands/shepherd
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
const { execFileSync } = require('node:child_process');
|
|
30
|
+
const crypto = require('node:crypto');
|
|
30
31
|
|
|
31
32
|
const { runShepherdPass } = require('../pr-shepherd');
|
|
32
33
|
const { gatherPrBundle } = require('../pr-bundle');
|
|
@@ -34,16 +35,26 @@ const { gatherPullSignal, renderPullSummary } = require('../pr-pull');
|
|
|
34
35
|
const { PrStateAdapter } = require('../adapters/pr-state-adapter');
|
|
35
36
|
const { validatePrStateAdapter } = require('../pr-state-validator');
|
|
36
37
|
const { gatherMonitorSnapshot } = require('../pr-monitor/gather');
|
|
38
|
+
const { privacySafeIdentity } = require('../pr-monitor/flow-monitor');
|
|
37
39
|
const { pollEvents } = require('../pr-monitor/monitor');
|
|
38
40
|
const { watchLoop } = require('../pr-monitor/watch');
|
|
39
|
-
const { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
|
|
41
|
+
const { startPrWatcherDetached, launchGateComplete } = require('../pr-monitor/watch-lifecycle');
|
|
42
|
+
const watchOwner = require('../pr-monitor/watch-owner');
|
|
40
43
|
const reconcileExecutor = require('../pr-monitor/reconcile-executor');
|
|
41
44
|
const monitorJournal = require('../pr-monitor/journal');
|
|
42
45
|
const brokerMod = require('../kernel/broker');
|
|
43
46
|
const { EVENT_TYPES: T } = require('../pr-monitor/events');
|
|
44
47
|
const { autoShepherdRailEnabled } = require('./ship');
|
|
48
|
+
const { runLocalReviewPreflight } = require('../pr-monitor/review-preflight');
|
|
45
49
|
|
|
46
50
|
const DEFAULT_RERUN_BUDGET = 3;
|
|
51
|
+
const MAX_MONITOR_ID_LENGTH = 128;
|
|
52
|
+
|
|
53
|
+
function rootMonitorId(owner, repo, pr) {
|
|
54
|
+
const raw = privacySafeIdentity(`pr:${privacySafeIdentity(`${owner}/${repo}`)}:${pr}`);
|
|
55
|
+
if (raw.length <= MAX_MONITOR_ID_LENGTH) return raw;
|
|
56
|
+
return `pr:${crypto.createHash('sha256').update(raw).digest('hex')}`;
|
|
57
|
+
}
|
|
47
58
|
|
|
48
59
|
// windowsHide: true on EVERY spawn here is load-bearing, not cosmetic. The
|
|
49
60
|
// shepherd watcher runs detached in the background and re-polls every ~60s; on
|
|
@@ -52,14 +63,35 @@ const DEFAULT_RERUN_BUDGET = 3;
|
|
|
52
63
|
// preferred home is the harness's managed shell (hidden + reaped); a
|
|
53
64
|
// Forge-spawned detached watcher is the no-session fallback and must be
|
|
54
65
|
// COMPLETELY silent — no console window, ever. See kernel issue 931e7924.
|
|
55
|
-
const defaultGhRunner = (cmd, a) => execFileSync(cmd, a, {
|
|
66
|
+
const defaultGhRunner = (cmd, a, options = {}) => execFileSync(cmd, a, {
|
|
67
|
+
encoding: 'utf8', timeout: 30000, windowsHide: true, ...options,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function githubRunner(deps, projectRoot) {
|
|
71
|
+
return deps.githubContext?.bound
|
|
72
|
+
? (_cmd, args, options = {}) => deps.githubContext.runGh(args, {
|
|
73
|
+
encoding: 'utf8', timeout: 30000, windowsHide: true, cwd: projectRoot, ...options,
|
|
74
|
+
})
|
|
75
|
+
: (deps.gh || defaultGhRunner);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveTargetRepository(gh = defaultGhRunner) {
|
|
79
|
+
const repoJson = gh('gh', ['repo', 'view', '--json', 'nameWithOwner,parent']);
|
|
80
|
+
const repoInfo = JSON.parse(repoJson || '{}');
|
|
81
|
+
const repository = repoInfo.parent?.nameWithOwner || repoInfo.nameWithOwner;
|
|
82
|
+
if (typeof repository !== 'string'
|
|
83
|
+
|| !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
|
|
84
|
+
throw new Error('PR base repository is unavailable from the checkout');
|
|
85
|
+
}
|
|
86
|
+
return repository.toLowerCase();
|
|
87
|
+
}
|
|
56
88
|
|
|
57
89
|
/**
|
|
58
90
|
* Resolve owner/repo and base branch for the shepherd pass.
|
|
59
91
|
*
|
|
60
|
-
* The base
|
|
61
|
-
*
|
|
62
|
-
*
|
|
92
|
+
* The exact base commit is read from the PR itself (`gh pr view <pr> --json
|
|
93
|
+
* baseRefOid`) rather than a local remote, so fork checkouts and PRs targeting
|
|
94
|
+
* `release/*`/`develop` are evaluated against the provider's reviewed base.
|
|
63
95
|
* `owner`/`name` come from the repository the PR is queried in — that IS the
|
|
64
96
|
* base repository. `cwd` (the worktree root) is threaded through so divergence
|
|
65
97
|
* is computed against the right checkout.
|
|
@@ -67,21 +99,30 @@ const defaultGhRunner = (cmd, a) => execFileSync(cmd, a, { encoding: 'utf8', tim
|
|
|
67
99
|
* @param {object} deps
|
|
68
100
|
* @returns {Promise<{ pr: string, owner: string, repo: string, base: string, baseRef: string, cwd?: string }>}
|
|
69
101
|
*/
|
|
70
|
-
async function defaultBuildContext({ pr, gh, git, projectRoot }) {
|
|
71
|
-
const
|
|
102
|
+
async function defaultBuildContext({ pr, gh, git, projectRoot, repository }) {
|
|
103
|
+
const targetRepository = repository || resolveTargetRepository(gh);
|
|
104
|
+
const prJson = gh('gh', [
|
|
105
|
+
'pr', 'view', String(pr), '--repo', targetRepository,
|
|
106
|
+
'--json', 'baseRefName,baseRefOid,headRefOid,state',
|
|
107
|
+
]);
|
|
72
108
|
const prInfo = JSON.parse(prJson || '{}');
|
|
73
109
|
const base = prInfo.baseRefName || 'master';
|
|
110
|
+
if (typeof prInfo.baseRefOid !== 'string' || !/^[0-9a-f]{40}$/i.test(prInfo.baseRefOid)) {
|
|
111
|
+
throw new Error('PR base commit is unavailable from the provider');
|
|
112
|
+
}
|
|
113
|
+
const baseRef = prInfo.baseRefOid.toLowerCase();
|
|
114
|
+
const headSha = typeof prInfo.headRefOid === 'string' && /^[0-9a-f]{40}$/i.test(prInfo.headRefOid)
|
|
115
|
+
? prInfo.headRefOid.toLowerCase()
|
|
116
|
+
: null;
|
|
74
117
|
|
|
75
|
-
const
|
|
76
|
-
const repo = JSON.parse(repoJson || '{}');
|
|
77
|
-
const owner = repo.owner?.login || '';
|
|
78
|
-
const name = repo.name || '';
|
|
118
|
+
const [owner, name] = targetRepository.split('/');
|
|
79
119
|
|
|
80
|
-
let
|
|
120
|
+
let localHead = null;
|
|
81
121
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
122
|
+
const candidate = git('git', ['rev-parse', 'HEAD']).trim();
|
|
123
|
+
if (/^[0-9a-f]{40}$/i.test(candidate)) localHead = candidate.toLowerCase();
|
|
124
|
+
} catch {
|
|
125
|
+
/* a missing local checkout makes local review preflight not applicable */
|
|
85
126
|
}
|
|
86
127
|
|
|
87
128
|
return {
|
|
@@ -89,7 +130,10 @@ async function defaultBuildContext({ pr, gh, git, projectRoot }) {
|
|
|
89
130
|
owner,
|
|
90
131
|
repo: name,
|
|
91
132
|
base,
|
|
92
|
-
baseRef
|
|
133
|
+
baseRef,
|
|
134
|
+
headSha,
|
|
135
|
+
prState: typeof prInfo.state === 'string' ? prInfo.state.toUpperCase() : null,
|
|
136
|
+
localHead,
|
|
93
137
|
...(projectRoot ? { cwd: projectRoot } : {}),
|
|
94
138
|
};
|
|
95
139
|
}
|
|
@@ -122,6 +166,14 @@ function formatAction(action) {
|
|
|
122
166
|
}
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
function writePassSummary(pr, state, reason, actions) {
|
|
170
|
+
const passActions = Array.isArray(actions) ? actions : [];
|
|
171
|
+
const reasonSuffix = reason ? ` — ${reason}` : '';
|
|
172
|
+
process.stdout.write(`Shepherd pass — PR #${pr}: ${state}${reasonSuffix}\n`);
|
|
173
|
+
for (const action of passActions) process.stdout.write(` • ${formatAction(action)}\n`);
|
|
174
|
+
if (!passActions.length) process.stdout.write(' • no actions this pass\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
/**
|
|
126
178
|
* Build the DEFAULT `check.failed` enrichment hook for the events pull surface.
|
|
127
179
|
*
|
|
@@ -139,6 +191,14 @@ function formatAction(action) {
|
|
|
139
191
|
* @param {object} pullCtx - ctx forwarded to `gatherPull` (owner/repo/base/adapter/runGh/self).
|
|
140
192
|
* @returns {(records: object[]) => Promise<void>}
|
|
141
193
|
*/
|
|
194
|
+
const UNSAFE_MONITOR_EXCERPT = /(?:gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|sk_(?:live|test)_[a-z0-9]{16,}|sk-[a-z0-9]{16,}|AKIA[0-9A-Z]{16}|(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,}|(?:^|[\\/])(?:users|home|root)[\\/]\S+)/i;
|
|
195
|
+
|
|
196
|
+
function safeMonitorExcerpt(value) {
|
|
197
|
+
return typeof value === 'string' && value.length <= 16_384 && !UNSAFE_MONITOR_EXCERPT.test(value)
|
|
198
|
+
? value
|
|
199
|
+
: null;
|
|
200
|
+
}
|
|
201
|
+
|
|
142
202
|
function makeCheckFailureEnricher(pullCtx) {
|
|
143
203
|
const gatherPull = pullCtx.gatherPull || gatherPullSignal;
|
|
144
204
|
return async (records) => {
|
|
@@ -157,7 +217,8 @@ function makeCheckFailureEnricher(pullCtx) {
|
|
|
157
217
|
if (r.type !== T.CHECK_FAILED) continue;
|
|
158
218
|
const f = byName.get(r.data?.name);
|
|
159
219
|
if (!f) continue;
|
|
160
|
-
|
|
220
|
+
const excerpt = safeMonitorExcerpt(f.excerpt);
|
|
221
|
+
if (excerpt) r.data.excerpt = excerpt;
|
|
161
222
|
if (f.jobUrl) r.data.jobUrl = f.jobUrl;
|
|
162
223
|
}
|
|
163
224
|
};
|
|
@@ -170,6 +231,20 @@ function parseSince(args) {
|
|
|
170
231
|
return 0;
|
|
171
232
|
}
|
|
172
233
|
|
|
234
|
+
function booleanOwnerRunningProbe(probe) {
|
|
235
|
+
if (typeof probe !== 'function') return undefined;
|
|
236
|
+
return async () => {
|
|
237
|
+
const result = await probe();
|
|
238
|
+
if (result && typeof result === 'object' && result.ok === false) {
|
|
239
|
+
const error = new Error('Watcher authority is unavailable');
|
|
240
|
+
error.code = typeof result.reason === 'string' ? result.reason : 'authority_unavailable';
|
|
241
|
+
error.cause = result;
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
return result === true;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
173
248
|
/**
|
|
174
249
|
* Build the shared monitor context — journal `dir`, bounded `gather`, and the
|
|
175
250
|
* default `check.failed` enricher — that BOTH the `events` pull surface and the
|
|
@@ -184,16 +259,32 @@ function parseSince(args) {
|
|
|
184
259
|
async function buildMonitorContext(pr, projectRoot, deps) {
|
|
185
260
|
let dir = deps.dir;
|
|
186
261
|
let gather = deps.gather;
|
|
262
|
+
let repository = typeof deps.repository === 'string' ? deps.repository.toLowerCase() : null;
|
|
263
|
+
let prState = deps.prState || null;
|
|
264
|
+
let ownerAuthority = {
|
|
265
|
+
owner: deps.owner || watchOwner,
|
|
266
|
+
ownerOptions: deps.ownerOptions,
|
|
267
|
+
};
|
|
268
|
+
let authority = deps.store ? {
|
|
269
|
+
store: deps.store,
|
|
270
|
+
monitorId: deps.monitorId,
|
|
271
|
+
ownerRunId: deps.ownerRunId,
|
|
272
|
+
packetId: deps.packetId,
|
|
273
|
+
subjectId: deps.subjectId,
|
|
274
|
+
close: deps.close || (async () => {}),
|
|
275
|
+
} : {};
|
|
187
276
|
// enrich decorates newly-failed checks with log excerpts before the journal
|
|
188
277
|
// append. The caller MUST supply the default (not just forward an injected
|
|
189
278
|
// one), or a plain `forge shepherd events`/`watch` emits bare check.failed events.
|
|
190
279
|
let enrich = deps.enrich;
|
|
191
280
|
if (!gather || !dir) {
|
|
192
|
-
const gh = deps
|
|
193
|
-
const git = deps.git ||
|
|
281
|
+
const gh = githubRunner(deps, projectRoot);
|
|
282
|
+
const git = deps.git || defaultGhRunner;
|
|
194
283
|
const buildContext = deps.buildContext || defaultBuildContext;
|
|
195
|
-
const ctx = await buildContext({ pr, gh, git, projectRoot });
|
|
196
|
-
|
|
284
|
+
const ctx = await buildContext({ pr, gh, git, projectRoot, repository });
|
|
285
|
+
repository = `${ctx.owner}/${ctx.repo}`.toLowerCase();
|
|
286
|
+
prState = ctx.prState;
|
|
287
|
+
const adapter = deps.adapter || new PrStateAdapter({ gh, git, repository });
|
|
197
288
|
const validation = validatePrStateAdapter(adapter);
|
|
198
289
|
if (!validation.valid) {
|
|
199
290
|
return { error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
|
|
@@ -218,6 +309,45 @@ async function buildMonitorContext(pr, projectRoot, deps) {
|
|
|
218
309
|
runGh: (ghArgs) => gh('gh', ghArgs),
|
|
219
310
|
gatherPull: deps.gatherPull,
|
|
220
311
|
});
|
|
312
|
+
if (!deps.store) {
|
|
313
|
+
const buildKernelDeps = deps.buildKernelDeps
|
|
314
|
+
|| require('../kernel/cli-broker-factory').buildMigratedKernelIssueDeps;
|
|
315
|
+
const createMonitorStore = deps.createMonitorStore
|
|
316
|
+
|| require('@forge/memory').createMonitorStore;
|
|
317
|
+
let kernel;
|
|
318
|
+
try {
|
|
319
|
+
kernel = await buildKernelDeps({ projectRoot: projectRoot || process.cwd(), gitCommonDir });
|
|
320
|
+
const repositoryIdentity = privacySafeIdentity(`${ctx.owner}/${ctx.repo}`);
|
|
321
|
+
authority = {
|
|
322
|
+
store: createMonitorStore(kernel.kernelDriver),
|
|
323
|
+
monitorId: rootMonitorId(ctx.owner, ctx.repo, ctx.pr),
|
|
324
|
+
ownerRunId: privacySafeIdentity(`shepherd:${repositoryIdentity}:${ctx.pr}`),
|
|
325
|
+
packetId: privacySafeIdentity(`shepherd-packet:${repositoryIdentity}:${ctx.pr}`),
|
|
326
|
+
subjectId: privacySafeIdentity(`${repositoryIdentity}#${ctx.pr}`),
|
|
327
|
+
close: async () => kernel.kernelBroker?.close?.(),
|
|
328
|
+
};
|
|
329
|
+
ownerAuthority = {
|
|
330
|
+
owner: deps.owner || watchOwner,
|
|
331
|
+
ownerOptions: {
|
|
332
|
+
driver: kernel.kernelDriver,
|
|
333
|
+
verifyProviderEvidence: async (evidence, expected) => (
|
|
334
|
+
typeof evidence?.state === 'string'
|
|
335
|
+
&& expected.states.includes(evidence.state.toLowerCase())
|
|
336
|
+
),
|
|
337
|
+
verifyTerminalReceipt: async (receiptId) => {
|
|
338
|
+
const state = await authority.store.readDeliveryState(authority.monitorId);
|
|
339
|
+
return state?.terminal_receipt?.object_id === receiptId;
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
try { await kernel?.kernelBroker?.close?.(); } catch { /* fallback must remain available */ }
|
|
345
|
+
if (deps.forceAuthority) {
|
|
346
|
+
return { error: `Durable monitor authority is unavailable: ${error.message}` };
|
|
347
|
+
}
|
|
348
|
+
authority = {};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
221
351
|
} else if (!enrich && deps.gatherPull) {
|
|
222
352
|
// Injected gather (tests / programmatic callers) still gets the default
|
|
223
353
|
// enrichment when a pull-signal source is supplied.
|
|
@@ -225,7 +355,19 @@ async function buildMonitorContext(pr, projectRoot, deps) {
|
|
|
225
355
|
pr, adapter: deps.adapter, self: deps.self, runGh: deps.runGh, gatherPull: deps.gatherPull,
|
|
226
356
|
});
|
|
227
357
|
}
|
|
228
|
-
|
|
358
|
+
if (deps.forceAuthority && !authority.store) {
|
|
359
|
+
return { error: 'Durable monitor authority is unavailable.' };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
dir,
|
|
363
|
+
gather,
|
|
364
|
+
enrich,
|
|
365
|
+
repo: repository,
|
|
366
|
+
pr: Number(pr),
|
|
367
|
+
prState,
|
|
368
|
+
...authority,
|
|
369
|
+
...ownerAuthority,
|
|
370
|
+
};
|
|
229
371
|
}
|
|
230
372
|
|
|
231
373
|
/**
|
|
@@ -250,15 +392,31 @@ async function handleEvents(args, projectRoot, deps = {}) {
|
|
|
250
392
|
|
|
251
393
|
const built = await buildMonitorContext(pr, projectRoot, deps);
|
|
252
394
|
if (built.error) return { success: false, error: built.error };
|
|
253
|
-
const { dir, gather, enrich } = built;
|
|
254
|
-
|
|
255
395
|
const poll = deps.pollEvents || pollEvents;
|
|
256
|
-
|
|
396
|
+
let result;
|
|
397
|
+
try {
|
|
398
|
+
result = await poll({
|
|
399
|
+
...built,
|
|
400
|
+
since,
|
|
401
|
+
now: deps.now,
|
|
402
|
+
isOwnerRunning: booleanOwnerRunningProbe(deps.isOwnerRunning),
|
|
403
|
+
});
|
|
404
|
+
} finally {
|
|
405
|
+
await built.close?.();
|
|
406
|
+
}
|
|
257
407
|
// `output` is the agent-agnostic pull surface: NDJSON, one event per line. The
|
|
258
408
|
// registry CLI dispatch prints `result.output` (same contract as --pull/--bundle),
|
|
259
409
|
// so this handler does NOT write to stdout itself (that would double-print).
|
|
260
|
-
const
|
|
261
|
-
|
|
410
|
+
const overflow = result.overflow === true;
|
|
411
|
+
const overflowRecord = overflow ? {
|
|
412
|
+
type: 'monitor.overflow',
|
|
413
|
+
since: result.since,
|
|
414
|
+
firstAvailableSeq: result.events[0]?.seq ?? null,
|
|
415
|
+
action: 'restart-from-checkpoint',
|
|
416
|
+
} : null;
|
|
417
|
+
const outputRecords = overflowRecord ? [overflowRecord, ...result.events] : result.events;
|
|
418
|
+
const output = outputRecords.map((event) => JSON.stringify(event)).join('\n');
|
|
419
|
+
return { success: true, events: result.events, since: result.since, overflow, output };
|
|
262
420
|
}
|
|
263
421
|
|
|
264
422
|
/**
|
|
@@ -299,9 +457,11 @@ function wireSignals() {
|
|
|
299
457
|
* @param {Function} [exec] - gh runner (test injection).
|
|
300
458
|
* @returns {number[]}
|
|
301
459
|
*/
|
|
302
|
-
function defaultListOpenPrs(exec = execFileSync) {
|
|
460
|
+
function defaultListOpenPrs(repository, exec = execFileSync) {
|
|
303
461
|
try {
|
|
304
|
-
const out = exec('gh', [
|
|
462
|
+
const out = exec('gh', [
|
|
463
|
+
'pr', 'list', '--repo', repository, '--state', 'open', '--json', 'number', '-q', '.[].number',
|
|
464
|
+
], {
|
|
305
465
|
encoding: 'utf8', timeout: 20000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
|
|
306
466
|
});
|
|
307
467
|
return String(out)
|
|
@@ -325,7 +485,7 @@ function defaultListOpenPrs(exec = execFileSync) {
|
|
|
325
485
|
* @param {object} [deps]
|
|
326
486
|
* @returns {{ success: true, adopted: number[], total: number, reason?: string }}
|
|
327
487
|
*/
|
|
328
|
-
function handleAdopt(projectRoot, deps = {}) {
|
|
488
|
+
async function handleAdopt(projectRoot, deps = {}) {
|
|
329
489
|
const railEnabled = deps.railEnabled || autoShepherdRailEnabled;
|
|
330
490
|
// Gate BEFORE listing PRs / spawning watchers: a disabled rail must not spawn
|
|
331
491
|
// detached watchers. No-op result mirrors the fail-open (empty) adoption shape.
|
|
@@ -333,48 +493,145 @@ function handleAdopt(projectRoot, deps = {}) {
|
|
|
333
493
|
return { success: true, adopted: [], total: 0, reason: 'rail.auto_shepherd disabled' };
|
|
334
494
|
}
|
|
335
495
|
const listOpenPrs = deps.listOpenPrs || defaultListOpenPrs;
|
|
496
|
+
const gh = githubRunner(deps, projectRoot);
|
|
336
497
|
const startWatcher = deps.startWatcher || startPrWatcherDetached;
|
|
498
|
+
const resolveRepository = deps.resolveRepository
|
|
499
|
+
|| (() => resolveTargetRepository(gh));
|
|
337
500
|
let prs;
|
|
501
|
+
let repository;
|
|
502
|
+
let kernel;
|
|
503
|
+
let ownerOptions = deps.ownerOptions;
|
|
338
504
|
try {
|
|
339
|
-
|
|
505
|
+
repository = resolveRepository();
|
|
506
|
+
if (!ownerOptions) {
|
|
507
|
+
const buildKernelDeps = deps.buildKernelDeps
|
|
508
|
+
|| require('../kernel/cli-broker-factory').buildMigratedKernelIssueDeps;
|
|
509
|
+
kernel = await buildKernelDeps({ projectRoot: projectRoot || process.cwd() });
|
|
510
|
+
ownerOptions = {
|
|
511
|
+
driver: kernel.kernelDriver,
|
|
512
|
+
verifyProviderEvidence: async (evidence, expected) => (
|
|
513
|
+
evidence?.state === 'OPEN' && expected.states.includes('open')
|
|
514
|
+
),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
prs = listOpenPrs(repository, gh);
|
|
340
518
|
} catch {
|
|
341
519
|
prs = [];
|
|
342
520
|
}
|
|
343
521
|
if (!Array.isArray(prs)) prs = [];
|
|
344
522
|
const adopted = [];
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
523
|
+
try {
|
|
524
|
+
for (const pr of prs) {
|
|
525
|
+
try {
|
|
526
|
+
const owner = deps.owner || watchOwner;
|
|
527
|
+
if (!await launchGateComplete(owner, ownerOptions)) continue;
|
|
528
|
+
const res = await startWatcher({
|
|
529
|
+
prNumber: pr,
|
|
530
|
+
cwd: projectRoot,
|
|
531
|
+
repository,
|
|
532
|
+
owner,
|
|
533
|
+
ownerOptions,
|
|
534
|
+
providerEvidence: { state: 'OPEN', repository, pr },
|
|
535
|
+
});
|
|
536
|
+
if (res?.started) adopted.push(pr);
|
|
537
|
+
} catch { /* fail-open per PR: one bad arm never blocks the rest */ }
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
try { await kernel?.kernelBroker?.close?.(); } catch { /* best-effort handle cleanup */ }
|
|
350
541
|
}
|
|
351
542
|
return { success: true, adopted, total: prs.length };
|
|
352
543
|
}
|
|
353
544
|
|
|
545
|
+
function watchOption(args, name) {
|
|
546
|
+
const index = args.indexOf(name);
|
|
547
|
+
return index < 0 ? null : args[index + 1];
|
|
548
|
+
}
|
|
549
|
+
|
|
354
550
|
async function handleWatch(args, projectRoot, deps = {}) {
|
|
355
551
|
const rawArgs = args || [];
|
|
356
552
|
// `--adopt` (no PR arg): arm a detached watcher for every open PR.
|
|
357
553
|
if (rawArgs.includes('--adopt')) {
|
|
358
554
|
return handleAdopt(projectRoot, deps);
|
|
359
555
|
}
|
|
360
|
-
const
|
|
556
|
+
const optionIndexes = new Set();
|
|
557
|
+
for (const name of ['--repo', '--generation', '--controller-pid', '--started-at']) {
|
|
558
|
+
const index = rawArgs.indexOf(name);
|
|
559
|
+
if (index >= 0) optionIndexes.add(index + 1);
|
|
560
|
+
}
|
|
561
|
+
const pr = rawArgs.find((a, index) => !String(a).startsWith('--') && a !== 'watch' && !optionIndexes.has(index));
|
|
361
562
|
if (!pr) {
|
|
362
563
|
return { success: false, error: 'Usage: forge shepherd watch <pr> | forge shepherd watch --adopt' };
|
|
363
564
|
}
|
|
364
565
|
|
|
365
|
-
const
|
|
566
|
+
const repository = watchOption(rawArgs, '--repo');
|
|
567
|
+
const suppliedGeneration = watchOption(rawArgs, '--generation');
|
|
568
|
+
const suppliedControllerPid = Number(watchOption(rawArgs, '--controller-pid'));
|
|
569
|
+
const built = await buildMonitorContext(pr, projectRoot, { ...deps, repository: repository || deps.repository });
|
|
366
570
|
if (built.error) return { success: false, error: built.error };
|
|
367
|
-
const {
|
|
368
|
-
|
|
571
|
+
const identity = { repo: built.repo, pr: Number(pr) };
|
|
572
|
+
const owner = built.owner || deps.owner || watchOwner;
|
|
573
|
+
const ownerOptions = built.ownerOptions || deps.ownerOptions || {};
|
|
574
|
+
let generation = suppliedGeneration;
|
|
575
|
+
let controllerPid = suppliedGeneration ? suppliedControllerPid : process.pid;
|
|
576
|
+
let reservedHere = false;
|
|
577
|
+
if (suppliedGeneration && (!Number.isSafeInteger(suppliedControllerPid) || suppliedControllerPid <= 0)) {
|
|
578
|
+
await built.close?.();
|
|
579
|
+
return { success: false, error: 'Watcher controller identity is invalid' };
|
|
580
|
+
}
|
|
581
|
+
if (!generation) {
|
|
582
|
+
if (!await launchGateComplete(owner, ownerOptions)) {
|
|
583
|
+
await built.close?.();
|
|
584
|
+
return {
|
|
585
|
+
success: true, started: false, passes: 0, stopped: false,
|
|
586
|
+
reason: 'migration-gate-incomplete',
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
let reserved = await owner.reserveStarting(identity, { controllerPid }, ownerOptions);
|
|
590
|
+
if (!reserved?.ok && reserved?.record?.phase === 'complete' && built.prState === 'OPEN') {
|
|
591
|
+
reserved = await owner.reserveReopened(identity, {
|
|
592
|
+
generation: reserved.record.generation,
|
|
593
|
+
expectedReceiptId: reserved.record.terminalReceiptId,
|
|
594
|
+
controllerPid,
|
|
595
|
+
providerEvidence: { state: built.prState },
|
|
596
|
+
}, ownerOptions);
|
|
597
|
+
}
|
|
598
|
+
if (!reserved?.ok || !reserved.record) {
|
|
599
|
+
await built.close?.();
|
|
600
|
+
return {
|
|
601
|
+
success: true,
|
|
602
|
+
started: false,
|
|
603
|
+
passes: 0,
|
|
604
|
+
stopped: false,
|
|
605
|
+
reason: reserved?.reason || 'authority-unavailable',
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
generation = reserved.record.generation;
|
|
609
|
+
reservedHere = true;
|
|
610
|
+
}
|
|
611
|
+
if (!await launchGateComplete(owner, ownerOptions)) {
|
|
612
|
+
if (reservedHere) {
|
|
613
|
+
await owner.abortStarting(identity, { generation, controllerPid }, ownerOptions);
|
|
614
|
+
}
|
|
615
|
+
await built.close?.();
|
|
616
|
+
return {
|
|
617
|
+
success: true, started: false, passes: 0, stopped: false,
|
|
618
|
+
reason: 'migration-gate-incomplete',
|
|
619
|
+
};
|
|
620
|
+
}
|
|
369
621
|
const loop = deps.watchLoop || watchLoop;
|
|
370
622
|
// Injected signal (tests) suppresses real process handlers; otherwise wire them.
|
|
371
623
|
const wired = deps.signal ? { signal: deps.signal, cleanup: () => {} } : wireSignals();
|
|
372
624
|
let result;
|
|
373
625
|
try {
|
|
374
626
|
result = await loop({
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
627
|
+
...built,
|
|
628
|
+
repo: identity.repo,
|
|
629
|
+
pr: identity.pr,
|
|
630
|
+
generation,
|
|
631
|
+
controllerPid,
|
|
632
|
+
pid: process.pid,
|
|
633
|
+
owner,
|
|
634
|
+
ownerOptions,
|
|
378
635
|
now: deps.now,
|
|
379
636
|
emit: deps.emit,
|
|
380
637
|
sleep: deps.sleep,
|
|
@@ -383,12 +640,10 @@ async function handleWatch(args, projectRoot, deps = {}) {
|
|
|
383
640
|
maxPasses: deps.maxPasses,
|
|
384
641
|
lockOpts: deps.lockOpts,
|
|
385
642
|
signal: wired.signal,
|
|
386
|
-
watcherRunning: deps.watcherRunning,
|
|
387
|
-
writePid: deps.writePid,
|
|
388
|
-
removePid: deps.removePid,
|
|
389
643
|
});
|
|
390
644
|
} finally {
|
|
391
645
|
wired.cleanup();
|
|
646
|
+
await built.close?.();
|
|
392
647
|
}
|
|
393
648
|
|
|
394
649
|
return {
|
|
@@ -414,7 +669,12 @@ async function handleWatch(args, projectRoot, deps = {}) {
|
|
|
414
669
|
* @returns {Promise<object>} result envelope.
|
|
415
670
|
*/
|
|
416
671
|
async function handleDaemon(projectRoot, deps = {}) {
|
|
417
|
-
const res = await reconcileExecutor.runDaemon(projectRoot, {
|
|
672
|
+
const res = await reconcileExecutor.runDaemon(projectRoot, {
|
|
673
|
+
...deps,
|
|
674
|
+
...(deps.githubContext?.bound ? {
|
|
675
|
+
runGh: args => deps.githubContext.runGh(args, { cwd: projectRoot, timeout: 30000 }),
|
|
676
|
+
} : {}),
|
|
677
|
+
});
|
|
418
678
|
if (!res.ok) {
|
|
419
679
|
// A live foreign daemon owns the lease — this invocation is a clean no-op.
|
|
420
680
|
return { success: true, started: false, reason: res.reason || 'foreign-lease' };
|
|
@@ -422,6 +682,78 @@ async function handleDaemon(projectRoot, deps = {}) {
|
|
|
422
682
|
return { success: true, started: true };
|
|
423
683
|
}
|
|
424
684
|
|
|
685
|
+
async function collectConvergenceEvidence({ args, pr, projectRoot, context, adapter, deps }) {
|
|
686
|
+
const built = await buildMonitorContext(pr, projectRoot, {
|
|
687
|
+
...deps,
|
|
688
|
+
adapter,
|
|
689
|
+
buildContext: async () => context,
|
|
690
|
+
forceAuthority: true,
|
|
691
|
+
});
|
|
692
|
+
if (built.error) throw new Error(built.error);
|
|
693
|
+
try {
|
|
694
|
+
const polled = await (deps.pollEvents || pollEvents)({
|
|
695
|
+
...built,
|
|
696
|
+
since: parseSince(args),
|
|
697
|
+
now: deps.now,
|
|
698
|
+
isOwnerRunning: async () => false,
|
|
699
|
+
});
|
|
700
|
+
const current = await adapter.readState(pr);
|
|
701
|
+
const exactHead = typeof current?.headSha === 'string' && /^[0-9a-f]{40}$/i.test(current.headSha)
|
|
702
|
+
? current.headSha.toLowerCase()
|
|
703
|
+
: null;
|
|
704
|
+
const expectedHead = typeof context.headSha === 'string' && /^[0-9a-f]{40}$/i.test(context.headSha)
|
|
705
|
+
? context.headSha.toLowerCase()
|
|
706
|
+
: null;
|
|
707
|
+
if (!exactHead || exactHead !== expectedHead) {
|
|
708
|
+
throw new Error('PR head changed after the shepherd pass; rerun convergence on the current head');
|
|
709
|
+
}
|
|
710
|
+
// The inline pass is FORCED here precisely because this evidence gates a
|
|
711
|
+
// MERGE_READY handoff. When the migration gate is quarantined/conflicting or
|
|
712
|
+
// the authority is unreadable, pollEvents deliberately suppresses that pass and
|
|
713
|
+
// reports `ranPass: false`; the journal deltas it still returns are stale by
|
|
714
|
+
// construction. Treating them as convergence evidence would hand off a merge
|
|
715
|
+
// that was never authoritatively converged, so fail closed with the reason.
|
|
716
|
+
if (polled.ranPass !== true) {
|
|
717
|
+
throw new Error(`the authoritative convergence pass did not run (${
|
|
718
|
+
polled.migrationGateIncomplete === true
|
|
719
|
+
? 'watcher migration gate incomplete'
|
|
720
|
+
: (polled.authorityUnavailable === true ? 'watcher authority unavailable' : 'pass suppressed')
|
|
721
|
+
})`);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
deltas: Array.isArray(polled.events) ? polled.events : [],
|
|
725
|
+
deltaOverflow: polled.overflow === true,
|
|
726
|
+
receiptIds: Array.isArray(polled.receiptIds) ? polled.receiptIds : [],
|
|
727
|
+
continuationPending: polled.continuationPending === true,
|
|
728
|
+
...(polled.terminalReceiptId ? { terminalReceiptId: polled.terminalReceiptId } : {}),
|
|
729
|
+
exactHead,
|
|
730
|
+
};
|
|
731
|
+
} finally {
|
|
732
|
+
await built.close?.();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function convergenceHandoff(state, pr, evidence) {
|
|
737
|
+
if (state === 'MERGE_READY') {
|
|
738
|
+
return {
|
|
739
|
+
next: 'merge',
|
|
740
|
+
command: `forge merge --auto ${pr} --expect-head ${evidence.exactHead || '<exact-head>'} --issue <issue-id>`,
|
|
741
|
+
humanApprovalRequired: true,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (state === 'MERGED') {
|
|
745
|
+
return {
|
|
746
|
+
next: 'verify',
|
|
747
|
+
command: 'forge verify',
|
|
748
|
+
terminalReceiptId: evidence.terminalReceiptId || null,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (state === 'NEEDS_REVIEW') {
|
|
752
|
+
return { next: 'review', command: 'forge review', resolvesThreads: false };
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
|
|
425
757
|
/**
|
|
426
758
|
* Command handler.
|
|
427
759
|
*
|
|
@@ -453,8 +785,8 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
453
785
|
return { success: false, error: 'Usage: forge shepherd <pr> [--auto-rebase] [--bundle --json] [--pull --json]' };
|
|
454
786
|
}
|
|
455
787
|
|
|
456
|
-
const gh = deps
|
|
457
|
-
const git = deps.git ||
|
|
788
|
+
const gh = githubRunner(deps, projectRoot);
|
|
789
|
+
const git = deps.git || defaultGhRunner;
|
|
458
790
|
const buildContext = deps.buildContext || defaultBuildContext;
|
|
459
791
|
const runPass = deps.runPass || runShepherdPass;
|
|
460
792
|
const gatherBundle = deps.gatherBundle || gatherPrBundle;
|
|
@@ -467,7 +799,11 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
467
799
|
|
|
468
800
|
const ctx = await buildContext({ pr, gh, git, projectRoot });
|
|
469
801
|
|
|
470
|
-
const adapter = deps.adapter || new PrStateAdapter({
|
|
802
|
+
const adapter = deps.adapter || new PrStateAdapter({
|
|
803
|
+
gh,
|
|
804
|
+
git,
|
|
805
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
806
|
+
});
|
|
471
807
|
const validation = validatePrStateAdapter(adapter);
|
|
472
808
|
if (!validation.valid) {
|
|
473
809
|
return { success: false, error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
|
|
@@ -500,39 +836,206 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
500
836
|
return { success: true, bundle, output: JSON.stringify(bundle, null, 2) };
|
|
501
837
|
}
|
|
502
838
|
|
|
839
|
+
const runPreflight = deps.runLocalPreflight || runLocalReviewPreflight;
|
|
840
|
+
const preflightHead = ctx.headSha;
|
|
841
|
+
const cleanTree = isWorkingTreeClean(git);
|
|
842
|
+
let localPreflight;
|
|
843
|
+
try {
|
|
844
|
+
localPreflight = await runPreflight({
|
|
845
|
+
projectRoot: projectRoot || process.cwd(),
|
|
846
|
+
base: ctx.base,
|
|
847
|
+
baseRef: ctx.baseRef,
|
|
848
|
+
pr: String(pr),
|
|
849
|
+
expectedHead: ctx.headSha,
|
|
850
|
+
localHead: ctx.localHead,
|
|
851
|
+
cleanTree,
|
|
852
|
+
});
|
|
853
|
+
} catch (error) {
|
|
854
|
+
localPreflight = {
|
|
855
|
+
status: 'FAIL',
|
|
856
|
+
blocking: true,
|
|
857
|
+
providers: {},
|
|
858
|
+
findings: [{ provider: 'local-preflight', detail: error?.message || String(error) }],
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (localPreflight.blocking !== true) {
|
|
863
|
+
let postPreflightHead = null;
|
|
864
|
+
try {
|
|
865
|
+
const candidate = git('git', ['rev-parse', 'HEAD']).trim();
|
|
866
|
+
if (/^[0-9a-f]{40}$/i.test(candidate)) postPreflightHead = candidate.toLowerCase();
|
|
867
|
+
} catch {
|
|
868
|
+
/* unreadable mutable checkout fails the fence below */
|
|
869
|
+
}
|
|
870
|
+
const expectedLocalHead = typeof ctx.localHead === 'string' ? ctx.localHead.toLowerCase() : null;
|
|
871
|
+
if (!postPreflightHead || postPreflightHead !== expectedLocalHead || !isWorkingTreeClean(git)) {
|
|
872
|
+
localPreflight = {
|
|
873
|
+
...localPreflight,
|
|
874
|
+
status: 'INCOMPLETE',
|
|
875
|
+
blocking: true,
|
|
876
|
+
findings: [
|
|
877
|
+
...(Array.isArray(localPreflight.findings) ? localPreflight.findings : []),
|
|
878
|
+
{ provider: 'local-preflight', detail: 'Local checkout changed during local review; rerun on the exact clean PR head.' },
|
|
879
|
+
],
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
503
884
|
const result = await runPass({
|
|
504
885
|
...ctx,
|
|
505
886
|
adapter,
|
|
506
887
|
autoRebase,
|
|
507
|
-
cleanTree: autoRebase ?
|
|
888
|
+
cleanTree: autoRebase ? cleanTree : false,
|
|
508
889
|
rerunBudget: deps.rerunBudget || DEFAULT_RERUN_BUDGET,
|
|
509
890
|
rerunsUsed: deps.rerunsUsed || 0,
|
|
891
|
+
dryRun: localPreflight.blocking === true,
|
|
510
892
|
});
|
|
511
893
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
894
|
+
const collectEvidence = deps.collectConvergenceEvidence || collectConvergenceEvidence;
|
|
895
|
+
let evidence;
|
|
896
|
+
try {
|
|
897
|
+
const evidenceContext = result.expectedHead ? { ...ctx, headSha: result.expectedHead } : ctx;
|
|
898
|
+
evidence = await collectEvidence({ args, pr, projectRoot, context: evidenceContext, adapter, deps });
|
|
899
|
+
} catch (error) {
|
|
900
|
+
const reason = `Durable convergence evidence is unavailable: ${error.message}`;
|
|
901
|
+
writePassSummary(pr, 'INCOMPLETE', reason, result.actions);
|
|
902
|
+
return {
|
|
903
|
+
success: false,
|
|
904
|
+
state: 'INCOMPLETE',
|
|
905
|
+
remoteState: result.state,
|
|
906
|
+
reason,
|
|
907
|
+
actions: result.actions || [],
|
|
908
|
+
localPreflight,
|
|
909
|
+
deltas: [],
|
|
910
|
+
deltaOverflow: false,
|
|
911
|
+
receiptIds: [],
|
|
912
|
+
};
|
|
519
913
|
}
|
|
520
|
-
|
|
521
|
-
|
|
914
|
+
|
|
915
|
+
const normalizedPreflightHead = typeof preflightHead === 'string' ? preflightHead.toLowerCase() : null;
|
|
916
|
+
const normalizedResultHead = typeof result.expectedHead === 'string' ? result.expectedHead.toLowerCase() : null;
|
|
917
|
+
if (localPreflight.blocking !== true && result.state === 'MERGE_READY'
|
|
918
|
+
&& normalizedPreflightHead && normalizedResultHead && normalizedResultHead !== normalizedPreflightHead) {
|
|
919
|
+
return {
|
|
920
|
+
success: false,
|
|
921
|
+
state: 'INCOMPLETE',
|
|
922
|
+
remoteState: result.state,
|
|
923
|
+
reason: 'Local preflight head changed; rerun convergence on the current head',
|
|
924
|
+
actions: result.actions || [],
|
|
925
|
+
localPreflight,
|
|
926
|
+
deltas: evidence.deltas,
|
|
927
|
+
deltaOverflow: evidence.deltaOverflow,
|
|
928
|
+
receiptIds: evidence.receiptIds,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
let confirmedResult = result;
|
|
933
|
+
if (localPreflight.blocking !== true && result.state === 'MERGE_READY'
|
|
934
|
+
&& evidence.continuationPending !== true) {
|
|
935
|
+
const confirmation = await runPass({
|
|
936
|
+
...ctx,
|
|
937
|
+
adapter,
|
|
938
|
+
autoRebase: false,
|
|
939
|
+
cleanTree: false,
|
|
940
|
+
rerunBudget: deps.rerunBudget || DEFAULT_RERUN_BUDGET,
|
|
941
|
+
rerunsUsed: deps.rerunsUsed || 0,
|
|
942
|
+
dryRun: true,
|
|
943
|
+
});
|
|
944
|
+
confirmedResult = {
|
|
945
|
+
...confirmation,
|
|
946
|
+
actions: [...(result.actions || []), ...(confirmation.actions || [])],
|
|
947
|
+
};
|
|
948
|
+
const confirmedHead = typeof confirmedResult.expectedHead === 'string'
|
|
949
|
+
? confirmedResult.expectedHead.toLowerCase()
|
|
950
|
+
: null;
|
|
951
|
+
if (confirmedResult.state === 'MERGE_READY' && confirmedHead !== evidence.exactHead) {
|
|
952
|
+
return {
|
|
953
|
+
success: false,
|
|
954
|
+
state: 'INCOMPLETE',
|
|
955
|
+
remoteState: confirmedResult.state,
|
|
956
|
+
reason: 'PR head changed while confirming mutable merge-readiness evidence; rerun convergence on the current head',
|
|
957
|
+
actions: confirmedResult.actions,
|
|
958
|
+
localPreflight,
|
|
959
|
+
deltas: evidence.deltas,
|
|
960
|
+
deltaOverflow: evidence.deltaOverflow,
|
|
961
|
+
receiptIds: evidence.receiptIds,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (confirmedResult.state !== result.state) {
|
|
965
|
+
try {
|
|
966
|
+
const evidenceContext = confirmedResult.expectedHead
|
|
967
|
+
? { ...ctx, headSha: confirmedResult.expectedHead }
|
|
968
|
+
: ctx;
|
|
969
|
+
evidence = await collectEvidence({ args, pr, projectRoot, context: evidenceContext, adapter, deps });
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return {
|
|
972
|
+
success: false,
|
|
973
|
+
state: 'INCOMPLETE',
|
|
974
|
+
remoteState: confirmedResult.state,
|
|
975
|
+
reason: `Confirmed convergence evidence is unavailable: ${error.message}`,
|
|
976
|
+
actions: confirmedResult.actions,
|
|
977
|
+
localPreflight,
|
|
978
|
+
deltas: evidence.deltas,
|
|
979
|
+
deltaOverflow: evidence.deltaOverflow,
|
|
980
|
+
receiptIds: evidence.receiptIds,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (confirmedResult.state === 'MERGED' && !evidence.terminalReceiptId) {
|
|
984
|
+
return {
|
|
985
|
+
success: false,
|
|
986
|
+
state: 'INCOMPLETE',
|
|
987
|
+
remoteState: confirmedResult.state,
|
|
988
|
+
reason: 'Confirmed merged state has no durable terminal receipt',
|
|
989
|
+
actions: confirmedResult.actions,
|
|
990
|
+
localPreflight,
|
|
991
|
+
deltas: evidence.deltas,
|
|
992
|
+
deltaOverflow: evidence.deltaOverflow,
|
|
993
|
+
receiptIds: evidence.receiptIds,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
522
997
|
}
|
|
523
998
|
|
|
999
|
+
const effectiveState = evidence.continuationPending === true
|
|
1000
|
+
? 'PENDING'
|
|
1001
|
+
: localPreflight.blocking === true && confirmedResult.state === 'MERGE_READY'
|
|
1002
|
+
? (localPreflight.status === 'INCOMPLETE' ? 'INCOMPLETE' : 'PENDING')
|
|
1003
|
+
: confirmedResult.state;
|
|
1004
|
+
const effectiveReason = effectiveState !== confirmedResult.state
|
|
1005
|
+
? (evidence.continuationPending === true
|
|
1006
|
+
? 'Durable convergence work remains; run another bounded shepherd pass.'
|
|
1007
|
+
: effectiveState === 'INCOMPLETE'
|
|
1008
|
+
? 'Local review preflight is incomplete; merge readiness is not established.'
|
|
1009
|
+
: 'Local review preflight is blocking merge readiness.')
|
|
1010
|
+
: confirmedResult.reason;
|
|
1011
|
+
|
|
1012
|
+
// Surface the pass outcome so the monitor is legible when run interactively or
|
|
1013
|
+
// tailed by a scheduler (the bounded state machine is otherwise silent).
|
|
1014
|
+
writePassSummary(pr, effectiveState, effectiveReason, confirmedResult.actions);
|
|
1015
|
+
|
|
524
1016
|
return {
|
|
525
|
-
success:
|
|
526
|
-
state:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1017
|
+
success: effectiveState !== 'HARD_STOP' && effectiveState !== 'INCOMPLETE',
|
|
1018
|
+
state: effectiveState,
|
|
1019
|
+
...(effectiveState !== confirmedResult.state ? { remoteState: confirmedResult.state } : {}),
|
|
1020
|
+
reason: effectiveReason,
|
|
1021
|
+
actions: confirmedResult.actions || [],
|
|
1022
|
+
localPreflight,
|
|
1023
|
+
deltas: evidence.deltas,
|
|
1024
|
+
deltaOverflow: evidence.deltaOverflow,
|
|
1025
|
+
receiptIds: evidence.receiptIds,
|
|
1026
|
+
continuationPending: evidence.continuationPending === true,
|
|
1027
|
+
...(evidence.terminalReceiptId ? { terminalReceiptId: evidence.terminalReceiptId } : {}),
|
|
1028
|
+
...(convergenceHandoff(effectiveState, pr, evidence)
|
|
1029
|
+
? { handoff: convergenceHandoff(effectiveState, pr, evidence) }
|
|
1030
|
+
: {}),
|
|
1031
|
+
...(confirmedResult.authClass ? { authClass: confirmedResult.authClass } : {}),
|
|
1032
|
+
...(confirmedResult.retryAfter ? { retryAfter: confirmedResult.retryAfter } : {}),
|
|
531
1033
|
};
|
|
532
1034
|
}
|
|
533
1035
|
|
|
534
1036
|
module.exports = {
|
|
535
1037
|
name: 'shepherd',
|
|
1038
|
+
githubAuth: true,
|
|
536
1039
|
description: 'Run one bounded monitor pass over a PR (rerun flaky checks, escalate, hand off — never merges)',
|
|
537
1040
|
usage: 'Usage: forge shepherd <pr> [--auto-rebase] [--bundle --json] [--pull --json] | forge shepherd events <pr> --since <seq> [--json] | forge shepherd watch <pr> | forge shepherd watch --adopt',
|
|
538
1041
|
handler,
|
|
@@ -541,6 +1044,9 @@ module.exports = {
|
|
|
541
1044
|
buildMonitorContext,
|
|
542
1045
|
makeCheckFailureEnricher,
|
|
543
1046
|
parseSince,
|
|
1047
|
+
collectConvergenceEvidence,
|
|
1048
|
+
convergenceHandoff,
|
|
544
1049
|
defaultBuildContext,
|
|
1050
|
+
defaultListOpenPrs,
|
|
545
1051
|
isWorkingTreeClean,
|
|
546
1052
|
};
|