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/merge.js
CHANGED
|
@@ -46,6 +46,8 @@ const { evaluateMergeRules } = require('../merge-rules');
|
|
|
46
46
|
const { runIssueOperation } = require('../forge-issues');
|
|
47
47
|
const { PrStateAdapter } = require('../adapters/pr-state-adapter');
|
|
48
48
|
const { stripGlobalFlags } = require('../global-flags');
|
|
49
|
+
const gateCommand = require('./gate');
|
|
50
|
+
const { computeContentHash, validateContractStructure } = require('@forge/contracts');
|
|
49
51
|
|
|
50
52
|
const FULL_HEAD_SHA = /^[0-9a-f]{40}$/i;
|
|
51
53
|
const FORGE_ISSUE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -64,6 +66,37 @@ const REVIEW_ACTOR_TYPENAMES = new Set([
|
|
|
64
66
|
const REVIEW_STATES = new Set([
|
|
65
67
|
'APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING',
|
|
66
68
|
]);
|
|
69
|
+
const MERGE_GATE = 'gate.merge';
|
|
70
|
+
|
|
71
|
+
function uuidFromSeed(seed) {
|
|
72
|
+
const hash = computeContentHash({ seed });
|
|
73
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function contractEnvelope(schemaId, objectId, createdAt, actor, payload) {
|
|
77
|
+
const value = {
|
|
78
|
+
schema_id: schemaId,
|
|
79
|
+
schema_version: 1,
|
|
80
|
+
object_id: objectId,
|
|
81
|
+
created_at: createdAt,
|
|
82
|
+
producer: { product_id: 'forge', product_version: '0.1.0', instance_id: actor },
|
|
83
|
+
capabilities_used: [],
|
|
84
|
+
provenance: { source_kind: 'guarded-merge', actor_class: 'system', actor_id: actor },
|
|
85
|
+
payload,
|
|
86
|
+
extensions: {},
|
|
87
|
+
};
|
|
88
|
+
value.content_hash = computeContentHash(value);
|
|
89
|
+
const validation = validateContractStructure(value);
|
|
90
|
+
if (!validation.ok) throw new Error(`${schemaId} validation failed`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function currentTimestamp(now = Date.now) {
|
|
95
|
+
const value = typeof now === 'function' ? now() : now;
|
|
96
|
+
const date = new Date(value);
|
|
97
|
+
if (!Number.isFinite(date.getTime())) throw new Error('Merge decision clock is invalid.');
|
|
98
|
+
return date.toISOString();
|
|
99
|
+
}
|
|
67
100
|
|
|
68
101
|
function normalizeFullHeadSha(value) {
|
|
69
102
|
return typeof value === 'string' && FULL_HEAD_SHA.test(value) ? value.toLowerCase() : null;
|
|
@@ -84,6 +117,53 @@ function normalizeRepository(value) {
|
|
|
84
117
|
return typeof value === 'string' && REPOSITORY_NAME.test(value) ? value.toLowerCase() : null;
|
|
85
118
|
}
|
|
86
119
|
|
|
120
|
+
function defaultResolveLocalRepository({ projectRoot, git = execFileSync }) {
|
|
121
|
+
try {
|
|
122
|
+
const remote = git('git', ['remote', 'get-url', 'origin'], {
|
|
123
|
+
cwd: projectRoot,
|
|
124
|
+
encoding: 'utf8',
|
|
125
|
+
timeout: 3000,
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
windowsHide: true,
|
|
128
|
+
}).trim();
|
|
129
|
+
const scp = /^[A-Za-z]:/.test(remote) || /^(?:git|https?|ssh|file):/i.test(remote) ? null
|
|
130
|
+
: /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9][A-Za-z0-9._-]*:([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?$/.exec(remote);
|
|
131
|
+
if (scp) return normalizeRepository(scp[1]);
|
|
132
|
+
const parsed = new URL(remote);
|
|
133
|
+
if (!['git:', 'http:', 'https:', 'ssh:'].includes(parsed.protocol)
|
|
134
|
+
|| !parsed.hostname || parsed.password || parsed.search || parsed.hash
|
|
135
|
+
|| (['http:', 'https:'].includes(parsed.protocol) && parsed.username)) return null;
|
|
136
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
137
|
+
if (parts.length !== 2) return null;
|
|
138
|
+
return normalizeRepository(`${parts[0]}/${parts[1].replace(/\.git$/, '')}`);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function terminalEvidenceFromTrace(row, { issueId, repository, number, expectedHead }) {
|
|
145
|
+
const normalizedExpectedHead = normalizeFullHeadSha(expectedHead);
|
|
146
|
+
if (!normalizedExpectedHead || !Array.isArray(row?.iterations)) return { invalid: true };
|
|
147
|
+
const merged = row.iterations.filter(iteration => iteration?.type === 'pr.merged');
|
|
148
|
+
if (merged.length === 0) return null;
|
|
149
|
+
if (merged.length !== 1) return { invalid: true };
|
|
150
|
+
const exact = merged.filter(iteration => iteration.issue_id === issueId
|
|
151
|
+
&& Number.isInteger(iteration.issue_revision) && iteration.issue_revision >= 0
|
|
152
|
+
&& normalizeFullHeadSha(iteration.head_sha) === normalizedExpectedHead
|
|
153
|
+
&& typeof iteration.at === 'string' && Number.isFinite(Date.parse(iteration.at))
|
|
154
|
+
&& /^[0-9a-f]{64}$/.test(iteration.work_packet_hash)
|
|
155
|
+
&& /^[0-9a-f]{64}$/.test(iteration.run_receipt_hash));
|
|
156
|
+
if (exact.length !== 1) return { invalid: true };
|
|
157
|
+
const iteration = exact[0];
|
|
158
|
+
const decisionSeed = `${issueId}:${iteration.issue_revision}:${repository}:${number}:${normalizedExpectedHead}`;
|
|
159
|
+
return {
|
|
160
|
+
decisionId: uuidFromSeed(`merge-packet:${decisionSeed}`),
|
|
161
|
+
receiptId: uuidFromSeed(`merge-receipt:${iteration.work_packet_hash}`),
|
|
162
|
+
receiptHash: iteration.run_receipt_hash,
|
|
163
|
+
occurredAt: iteration.at,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
87
167
|
function parseMergeArgs(argv) {
|
|
88
168
|
const values = { auto: false, pr: null, expectedHead: null, issueId: null, error: null };
|
|
89
169
|
const seen = new Set();
|
|
@@ -168,76 +248,343 @@ async function defaultVerifyIssueOwnership({
|
|
|
168
248
|
if (!actor) return { owned: false, actor: null, error: 'FORGE_ACTOR or FORGE_SESSION_ID is required.' };
|
|
169
249
|
const frozenEnv = { ...env, FORGE_ACTOR: actor };
|
|
170
250
|
const result = await runIssue('owns', [issueId], projectRoot, { env: frozenEnv });
|
|
171
|
-
const data = result
|
|
172
|
-
const
|
|
251
|
+
const data = result?.data;
|
|
252
|
+
const ownershipMatches = result?.ok === true && data?.owned === true
|
|
173
253
|
&& data.expired === false && data.actor === actor && data.claimed_by === actor;
|
|
254
|
+
const expectedSessionId = typeof env.FORGE_SESSION_ID === 'string' && env.FORGE_SESSION_ID.trim()
|
|
255
|
+
? env.FORGE_SESSION_ID.trim()
|
|
256
|
+
: null;
|
|
257
|
+
let sessionId = null;
|
|
258
|
+
if (ownershipMatches && expectedSessionId) {
|
|
259
|
+
const claimsResult = await runIssue('claims', [], projectRoot, { env: frozenEnv });
|
|
260
|
+
const claims = claimsResult?.ok === true && Array.isArray(claimsResult.data?.claims)
|
|
261
|
+
? claimsResult.data.claims
|
|
262
|
+
: [];
|
|
263
|
+
const exactClaims = claims.filter(claim => claim?.issue_id === issueId
|
|
264
|
+
&& claim?.actor === actor && claim?.session_id === expectedSessionId);
|
|
265
|
+
sessionId = exactClaims.length === 1 ? exactClaims[0].session_id : null;
|
|
266
|
+
}
|
|
267
|
+
const owned = ownershipMatches && (!expectedSessionId || sessionId === expectedSessionId);
|
|
174
268
|
return {
|
|
175
269
|
owned: Boolean(owned),
|
|
176
270
|
actor,
|
|
177
|
-
claimedBy: data
|
|
178
|
-
|
|
179
|
-
|
|
271
|
+
claimedBy: data?.claimed_by,
|
|
272
|
+
sessionId,
|
|
273
|
+
expired: typeof data?.expired === 'boolean' ? data.expired : null,
|
|
274
|
+
error: result?.error,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function selectPrBindingRow(rows, repository, number, gitCommonDir = null) {
|
|
279
|
+
if (!Array.isArray(rows)) return { error: 'Kernel PR linkage is unreadable.' };
|
|
280
|
+
const repositoryRows = repository
|
|
281
|
+
? rows.filter(row => normalizeRepository(row?.repo) === repository)
|
|
282
|
+
: rows;
|
|
283
|
+
const candidateRows = gitCommonDir
|
|
284
|
+
? repositoryRows.filter(row => row?.git_common_dir === gitCommonDir)
|
|
285
|
+
: repositoryRows;
|
|
286
|
+
if (candidateRows.some(row => !normalizeKernelPrNumber(row.number))) {
|
|
287
|
+
return { error: 'Kernel PR linkage contains a malformed PR number.' };
|
|
288
|
+
}
|
|
289
|
+
const matches = candidateRows.filter(row => normalizeKernelPrNumber(row.number) === number);
|
|
290
|
+
if (matches.length === 0) return { missing: true, error: 'Kernel PR linkage is missing.' };
|
|
291
|
+
if (matches.length > 1) return { error: 'Kernel PR linkage is ambiguous.' };
|
|
292
|
+
if (!normalizeRepository(matches[0]?.repo)) return { error: 'Kernel PR repository identity is unreadable.' };
|
|
293
|
+
return { row: matches[0] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function openPrBindingBroker(projectRoot, buildBroker) {
|
|
297
|
+
if (buildBroker) {
|
|
298
|
+
const built = await buildBroker({ projectRoot });
|
|
299
|
+
return { built, ownsDriver: false };
|
|
300
|
+
}
|
|
301
|
+
const { resolveGitCommonDir } = require('../kernel/broker');
|
|
302
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
303
|
+
const gitCommonDir = resolveGitCommonDir(projectRoot);
|
|
304
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot, gitCommonDir });
|
|
305
|
+
return {
|
|
306
|
+
built: { gitCommonDir, broker: deps.kernelBroker, driver: deps.kernelDriver },
|
|
307
|
+
ownsDriver: true,
|
|
180
308
|
};
|
|
181
309
|
}
|
|
182
310
|
|
|
311
|
+
async function readPrBindingRows(built, allowRetired, { issueId, number, repository }) {
|
|
312
|
+
if (allowRetired) {
|
|
313
|
+
const target = {
|
|
314
|
+
issue_id: issueId,
|
|
315
|
+
pr_number: Number(number),
|
|
316
|
+
git_common_dir: built.gitCommonDir,
|
|
317
|
+
};
|
|
318
|
+
if (repository) target.repo = repository;
|
|
319
|
+
const read = await built.broker.readTrace(target);
|
|
320
|
+
return { rows: read?.pull_requests, gaps: read?.gaps };
|
|
321
|
+
}
|
|
322
|
+
return { rows: await built.broker.listOpenPrs(built.gitCommonDir), gaps: [] };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function terminalTraceGapKind(gap) {
|
|
326
|
+
if (typeof gap !== 'string') return null;
|
|
327
|
+
if (/^(?:issue|worktree|work_folder|pull_requests|(?:plan|tasks|decisions)(?::(?:outside_project|oversized))?)$/.test(gap)) {
|
|
328
|
+
return 'benign';
|
|
329
|
+
}
|
|
330
|
+
if (gap === 'pull_requests:overflow'
|
|
331
|
+
|| /^pull_requests:[^:]+:unlinked_issue$/.test(gap)
|
|
332
|
+
|| /^iterations:[^:]+:(?:missing|incomplete|overflow)$/.test(gap)) {
|
|
333
|
+
return 'authority';
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function terminalTraceIsIncomplete(gaps, row) {
|
|
339
|
+
if (!Array.isArray(gaps) || gaps.some(gap => terminalTraceGapKind(gap) === null)) return true;
|
|
340
|
+
const rowId = typeof row?.id === 'string' && row.id ? row.id : null;
|
|
341
|
+
return gaps.some((gap) => {
|
|
342
|
+
if (gap === 'pull_requests:overflow') return true;
|
|
343
|
+
if (rowId && gap === `pull_requests:${rowId}:unlinked_issue`) return true;
|
|
344
|
+
if (!/^iterations:[^:]+:(?:missing|incomplete|overflow)$/.test(gap)) return false;
|
|
345
|
+
return !rowId || ['missing', 'incomplete', 'overflow']
|
|
346
|
+
.some(suffix => gap === `iterations:${rowId}:${suffix}`);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function terminalTraceAbsenceIsClean(gaps) {
|
|
351
|
+
return Array.isArray(gaps) && gaps.every(gap => terminalTraceGapKind(gap) === 'benign');
|
|
352
|
+
}
|
|
353
|
+
|
|
183
354
|
async function defaultVerifyPrIssueBinding({
|
|
184
355
|
issueId,
|
|
185
356
|
pr,
|
|
186
357
|
projectRoot,
|
|
187
358
|
prContext,
|
|
188
359
|
buildBroker,
|
|
360
|
+
allowRetired = false,
|
|
189
361
|
}) {
|
|
190
362
|
const number = normalizePrNumber(String(pr));
|
|
191
|
-
|
|
192
|
-
if (!number || !
|
|
363
|
+
let repository = normalizeRepository(prContext?.repository);
|
|
364
|
+
if (!number || !prContext || prContext.number !== Number(number) || (!allowRetired && !repository)) {
|
|
193
365
|
return { bound: false, error: 'PR identity is unreadable or does not match the requested PR number.' };
|
|
194
366
|
}
|
|
195
367
|
|
|
196
368
|
let built;
|
|
197
|
-
|
|
369
|
+
let ownsDriver = false;
|
|
198
370
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const { resolveGitCommonDir } = require('../kernel/broker');
|
|
203
|
-
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
204
|
-
const gitCommonDir = resolveGitCommonDir(projectRoot);
|
|
205
|
-
const deps = await buildMigratedKernelIssueDeps({ projectRoot, gitCommonDir });
|
|
206
|
-
built = { gitCommonDir, broker: deps.kernelBroker, driver: deps.kernelDriver };
|
|
207
|
-
}
|
|
208
|
-
if (!built || !built.broker || typeof built.broker.listOpenPrs !== 'function'
|
|
371
|
+
({ built, ownsDriver } = await openPrBindingBroker(projectRoot, buildBroker));
|
|
372
|
+
const linkageReader = allowRetired ? built?.broker?.readTrace : built?.broker?.listOpenPrs;
|
|
373
|
+
if (!built?.broker || typeof linkageReader !== 'function'
|
|
209
374
|
|| typeof built.gitCommonDir !== 'string' || !built.gitCommonDir) {
|
|
210
375
|
return { bound: false, error: 'Kernel PR linkage reader is unavailable.' };
|
|
211
376
|
}
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
377
|
+
const linkage = await readPrBindingRows(built, allowRetired, { issueId, number, repository });
|
|
378
|
+
const { rows } = linkage;
|
|
379
|
+
const selection = selectPrBindingRow(
|
|
380
|
+
rows,
|
|
381
|
+
repository,
|
|
382
|
+
number,
|
|
383
|
+
allowRetired ? built.gitCommonDir : null,
|
|
384
|
+
);
|
|
385
|
+
if (!selection.row) {
|
|
386
|
+
const cleanRetiredAbsence = allowRetired && selection.missing === true
|
|
387
|
+
&& terminalTraceAbsenceIsClean(linkage.gaps);
|
|
388
|
+
return {
|
|
389
|
+
bound: false,
|
|
390
|
+
error: cleanRetiredAbsence
|
|
391
|
+
? selection.error
|
|
392
|
+
: (selection.missing === true && allowRetired
|
|
393
|
+
? 'Kernel merged PR evidence is incomplete or ambiguous.'
|
|
394
|
+
: selection.error),
|
|
395
|
+
...(cleanRetiredAbsence ? { missing: true } : {}),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const { row } = selection;
|
|
399
|
+
repository = repository || normalizeRepository(row.repo);
|
|
400
|
+
const bound = (allowRetired || row.state === 'open') && row.issue_id === issueId;
|
|
401
|
+
if (allowRetired && bound && terminalTraceIsIncomplete(linkage.gaps, row)) {
|
|
402
|
+
return { bound: false, error: 'Kernel merged PR evidence is incomplete or ambiguous.' };
|
|
217
403
|
}
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
404
|
+
const terminalEvidence = allowRetired && bound
|
|
405
|
+
? terminalEvidenceFromTrace(row, {
|
|
406
|
+
issueId,
|
|
407
|
+
repository,
|
|
408
|
+
number: Number(number),
|
|
409
|
+
expectedHead: normalizeFullHeadSha(prContext?.headSha),
|
|
410
|
+
})
|
|
411
|
+
: null;
|
|
412
|
+
if (terminalEvidence?.invalid) {
|
|
413
|
+
return { bound: false, error: 'Kernel merged PR evidence is incomplete or ambiguous.' };
|
|
222
414
|
}
|
|
223
|
-
const row = matches[0];
|
|
224
|
-
const bound = row.state === 'open' && row.issue_id === issueId;
|
|
225
415
|
return {
|
|
226
416
|
bound,
|
|
227
417
|
repository,
|
|
228
418
|
number: Number(number),
|
|
229
419
|
issueId: row.issue_id || null,
|
|
230
|
-
|
|
420
|
+
branch: row.branch || null,
|
|
421
|
+
url: row.url || null,
|
|
422
|
+
gitCommonDir: row.git_common_dir || built.gitCommonDir,
|
|
423
|
+
error: bound ? null : (allowRetired
|
|
424
|
+
? 'Kernel PR row is linked to a different issue.'
|
|
425
|
+
: 'Kernel PR row is not open or is linked to a different issue.'),
|
|
426
|
+
...(terminalEvidence ? { terminalEvidence } : {}),
|
|
231
427
|
};
|
|
232
428
|
} catch (err) {
|
|
233
429
|
return { bound: false, error: `Kernel PR linkage verification failed: ${err.message}` };
|
|
234
430
|
} finally {
|
|
235
|
-
if (ownsDriver &&
|
|
431
|
+
if (ownsDriver && typeof built?.driver?.close === 'function') {
|
|
236
432
|
try { await built.driver.close(); } catch { /* cleanup must not mask the linkage verdict */ }
|
|
237
433
|
}
|
|
238
434
|
}
|
|
239
435
|
}
|
|
240
436
|
|
|
437
|
+
async function defaultVerifyMergeGate({ issueId, projectRoot, now }) {
|
|
438
|
+
const result = await gateCommand.handler(
|
|
439
|
+
['check', issueId, MERGE_GATE],
|
|
440
|
+
{},
|
|
441
|
+
projectRoot,
|
|
442
|
+
{ now },
|
|
443
|
+
);
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function defaultPrepareMergeDecision({
|
|
448
|
+
issueId,
|
|
449
|
+
pr,
|
|
450
|
+
expectedHead,
|
|
451
|
+
repository,
|
|
452
|
+
binding,
|
|
453
|
+
actor,
|
|
454
|
+
sessionId,
|
|
455
|
+
requireMergeGate = true,
|
|
456
|
+
config,
|
|
457
|
+
projectRoot,
|
|
458
|
+
env = process.env,
|
|
459
|
+
now = Date.now,
|
|
460
|
+
runIssue = runIssueOperation,
|
|
461
|
+
}) {
|
|
462
|
+
if (requireMergeGate !== true) {
|
|
463
|
+
throw new Error('A disabled merge gate cannot produce broker-verifiable terminal authority.');
|
|
464
|
+
}
|
|
465
|
+
const timestamp = currentTimestamp(now);
|
|
466
|
+
const frozenEnv = { ...env, FORGE_ACTOR: actor };
|
|
467
|
+
if (sessionId) frozenEnv.FORGE_SESSION_ID = sessionId;
|
|
468
|
+
else delete frozenEnv.FORGE_SESSION_ID;
|
|
469
|
+
const issue = await runIssue('show', [issueId], projectRoot, {
|
|
470
|
+
env: frozenEnv,
|
|
471
|
+
});
|
|
472
|
+
const revision = issue?.data?.revision;
|
|
473
|
+
if (issue?.ok !== true || !Number.isInteger(revision) || revision < 0) {
|
|
474
|
+
throw new Error('Live issue revision is unavailable for the merge decision.');
|
|
475
|
+
}
|
|
476
|
+
if (!binding || typeof binding.branch !== 'string' || !binding.branch
|
|
477
|
+
|| typeof binding.gitCommonDir !== 'string' || !binding.gitCommonDir) {
|
|
478
|
+
throw new Error('Authoritative PR branch/worktree linkage is unavailable for the merge decision.');
|
|
479
|
+
}
|
|
480
|
+
const capabilityDigest = computeContentHash({ capability: 'forge.merge.exact-head.v1' });
|
|
481
|
+
const workflowConfigRevision = computeContentHash({ merge: config.merge?.auto ?? null });
|
|
482
|
+
const riskManifestDigest = computeContentHash({ rules: config.merge?.auto?.rules ?? [] });
|
|
483
|
+
const decisionSeed = `${issueId}:${revision}:${repository}:${pr}:${expectedHead}`;
|
|
484
|
+
const packet = contractEnvelope(
|
|
485
|
+
'forge.memory.work-packet.v1',
|
|
486
|
+
uuidFromSeed(`merge-packet:${decisionSeed}`),
|
|
487
|
+
timestamp,
|
|
488
|
+
actor,
|
|
489
|
+
{
|
|
490
|
+
issue_id: issueId,
|
|
491
|
+
expected_issue_revision: revision,
|
|
492
|
+
packet_id: `merge:${decisionSeed}`,
|
|
493
|
+
packet_revision: 1,
|
|
494
|
+
repository_id: repository,
|
|
495
|
+
target_head: expectedHead,
|
|
496
|
+
objective: `Guarded exact-head merge of ${repository}#${pr}`,
|
|
497
|
+
authority: { kind: 'kernel-live-lease', issue_revision: revision },
|
|
498
|
+
allowed_mutations: ['pr.merged'],
|
|
499
|
+
prohibited_actions: ['pr.opened', 'review-thread.resolve'],
|
|
500
|
+
workflow_config_revision: workflowConfigRevision,
|
|
501
|
+
capability_manifest_digest: capabilityDigest,
|
|
502
|
+
risk_manifest_digest: riskManifestDigest,
|
|
503
|
+
receipt_requirements: { gate_ids: requireMergeGate ? [MERGE_GATE] : [] },
|
|
504
|
+
target: {
|
|
505
|
+
pr_number: Number(pr),
|
|
506
|
+
branch: binding.branch,
|
|
507
|
+
git_common_dir: binding.gitCommonDir,
|
|
508
|
+
...(binding.url ? { url: binding.url } : {}),
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
);
|
|
512
|
+
return {
|
|
513
|
+
decisionId: packet.object_id,
|
|
514
|
+
packet,
|
|
515
|
+
occurredAt: timestamp,
|
|
516
|
+
actor,
|
|
517
|
+
sessionId: sessionId || null,
|
|
518
|
+
binding: { ...binding },
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function defaultRecordMergeDecision({ decision, mergeResult, pr, expectedHead, repository, projectRoot, buildBroker }) {
|
|
523
|
+
if (!decision?.packet || !decision?.binding) throw new Error('Merge decision evidence is unavailable.');
|
|
524
|
+
const recovered = mergeResult?.recovered === true;
|
|
525
|
+
const receipt = contractEnvelope(
|
|
526
|
+
'forge.memory.run-receipt.v1',
|
|
527
|
+
uuidFromSeed(`merge-receipt:${decision.packet.content_hash}`),
|
|
528
|
+
decision.occurredAt,
|
|
529
|
+
decision.actor,
|
|
530
|
+
{
|
|
531
|
+
packet_hash: decision.packet.content_hash,
|
|
532
|
+
run_id: `merge:${repository}:${pr}:${expectedHead}`,
|
|
533
|
+
attempt_id: `merge:${expectedHead}`,
|
|
534
|
+
exact_head: expectedHead,
|
|
535
|
+
packet_revision: decision.packet.payload.packet_revision,
|
|
536
|
+
manifest_digest: decision.packet.payload.capability_manifest_digest,
|
|
537
|
+
workflow_config_revision: decision.packet.payload.workflow_config_revision,
|
|
538
|
+
status: 'PASS',
|
|
539
|
+
executor: { product_id: 'forge', mode: recovered ? 'guarded-exact-head-recovery' : 'guarded-exact-head' },
|
|
540
|
+
started_at: decision.occurredAt,
|
|
541
|
+
ended_at: decision.occurredAt,
|
|
542
|
+
evidence_refs: [{ kind: 'pr', repository, pr_number: Number(pr), head_sha: expectedHead }],
|
|
543
|
+
validation: { status: 'PASS', decision_id: decision.decisionId },
|
|
544
|
+
cleanup: { status: 'NOT_REQUIRED' },
|
|
545
|
+
mutations_attempted: ['pr.merged'],
|
|
546
|
+
mutations_authorized: ['pr.merged'],
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
let built;
|
|
551
|
+
const ownsDriver = !buildBroker;
|
|
552
|
+
try {
|
|
553
|
+
if (buildBroker) built = await buildBroker({ projectRoot });
|
|
554
|
+
else {
|
|
555
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
556
|
+
const deps = await buildMigratedKernelIssueDeps({
|
|
557
|
+
projectRoot,
|
|
558
|
+
gitCommonDir: decision.binding.gitCommonDir,
|
|
559
|
+
});
|
|
560
|
+
built = { broker: deps.kernelBroker, driver: deps.kernelDriver };
|
|
561
|
+
}
|
|
562
|
+
if (!built?.broker) throw new Error('Kernel lifecycle provider is unavailable.');
|
|
563
|
+
const { createMemoryAuthorityProvider } = require('@forge/memory');
|
|
564
|
+
const provider = createMemoryAuthorityProvider({ broker: built.broker });
|
|
565
|
+
await provider.recordPrLinkage({
|
|
566
|
+
phase: 'merged',
|
|
567
|
+
git_common_dir: decision.binding.gitCommonDir,
|
|
568
|
+
repo: repository,
|
|
569
|
+
number: Number(pr),
|
|
570
|
+
branch: decision.binding.branch,
|
|
571
|
+
...(decision.binding.url ? { url: decision.binding.url } : {}),
|
|
572
|
+
occurred_at: decision.occurredAt,
|
|
573
|
+
work_packet: decision.packet,
|
|
574
|
+
run_receipt: receipt,
|
|
575
|
+
}, {
|
|
576
|
+
actor: decision.actor,
|
|
577
|
+
sessionId: decision.sessionId,
|
|
578
|
+
requireExactLifecycleOwnership: true,
|
|
579
|
+
});
|
|
580
|
+
return { receiptId: receipt.object_id, receiptHash: receipt.content_hash };
|
|
581
|
+
} finally {
|
|
582
|
+
if (ownsDriver && built?.driver && typeof built.driver.close === 'function') {
|
|
583
|
+
try { await built.driver.close(); } catch { /* terminal evidence outcome remains authoritative */ }
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
241
588
|
function strictCheckSuccess(check) {
|
|
242
589
|
if (!check || typeof check !== 'object') return false;
|
|
243
590
|
const hasStatus = Object.prototype.hasOwnProperty.call(check, 'status');
|
|
@@ -419,15 +766,121 @@ function mandatoryContextError(context, expectedHead) {
|
|
|
419
766
|
return protectedGate.allowed ? null : protectedGate.reason;
|
|
420
767
|
}
|
|
421
768
|
|
|
769
|
+
async function recoverMergedTerminal({
|
|
770
|
+
pr, parsed, root, config, prContext, ownershipActor, ownershipSessionId, deps, replayOnly = false,
|
|
771
|
+
}) {
|
|
772
|
+
if (normalizeFullHeadSha(prContext?.headSha) !== parsed.expectedHead) {
|
|
773
|
+
return { success: false, merged: true, error: 'Merged PR identity does not match the expected head and repository.' };
|
|
774
|
+
}
|
|
775
|
+
const verifyPrIssueBinding = deps.verifyPrIssueBinding || defaultVerifyPrIssueBinding;
|
|
776
|
+
const binding = await verifyPrIssueBinding({
|
|
777
|
+
issueId: parsed.issueId,
|
|
778
|
+
pr,
|
|
779
|
+
projectRoot: root,
|
|
780
|
+
prContext,
|
|
781
|
+
buildBroker: deps.buildPrBindingBroker,
|
|
782
|
+
allowRetired: true,
|
|
783
|
+
});
|
|
784
|
+
const repository = normalizeRepository(prContext?.repository) || normalizeRepository(binding?.repository);
|
|
785
|
+
if (binding?.bound !== true) {
|
|
786
|
+
if (replayOnly && binding?.missing === true) {
|
|
787
|
+
return {
|
|
788
|
+
success: false,
|
|
789
|
+
merged: true,
|
|
790
|
+
replayMissing: true,
|
|
791
|
+
error: 'Merged PR terminal evidence is not yet available for replay.',
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return { success: false, merged: true, error: 'Merged PR is not authoritatively linked to the supplied Forge issue.' };
|
|
795
|
+
}
|
|
796
|
+
if (binding.terminalEvidence) {
|
|
797
|
+
if (!repository) {
|
|
798
|
+
return { success: false, merged: true, error: 'Merged PR repository identity is unavailable.' };
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
success: true, merged: true, enabled: true, recovered: true,
|
|
802
|
+
...binding.terminalEvidence,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (replayOnly) {
|
|
806
|
+
return {
|
|
807
|
+
success: false,
|
|
808
|
+
merged: true,
|
|
809
|
+
replayMissing: true,
|
|
810
|
+
error: 'Merged PR terminal evidence is not yet available for replay.',
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (!repository) {
|
|
814
|
+
return { success: false, merged: true, error: 'Merged PR repository identity is unavailable.' };
|
|
815
|
+
}
|
|
816
|
+
const decisionAt = currentTimestamp(deps.now || Date.now);
|
|
817
|
+
const verifyMergeGate = deps.verifyMergeGate || defaultVerifyMergeGate;
|
|
818
|
+
const gateApproval = await verifyMergeGate({
|
|
819
|
+
issueId: parsed.issueId, projectRoot: root, actor: ownershipActor, now: decisionAt,
|
|
820
|
+
});
|
|
821
|
+
if (gateApproval !== true && gateApproval?.approved !== true && gateApproval?.disabled !== true) {
|
|
822
|
+
return { success: false, merged: true, error: `${MERGE_GATE} is not approved for this issue.` };
|
|
823
|
+
}
|
|
824
|
+
if (gateApproval?.disabled === true) {
|
|
825
|
+
return {
|
|
826
|
+
success: false,
|
|
827
|
+
merged: true,
|
|
828
|
+
error: 'The disabled merge gate cannot produce broker-verifiable terminal authority.',
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const prepareMergeDecision = deps.prepareMergeDecision || defaultPrepareMergeDecision;
|
|
832
|
+
const decision = await prepareMergeDecision({
|
|
833
|
+
issueId: parsed.issueId,
|
|
834
|
+
pr,
|
|
835
|
+
expectedHead: parsed.expectedHead,
|
|
836
|
+
repository,
|
|
837
|
+
binding,
|
|
838
|
+
actor: ownershipActor,
|
|
839
|
+
sessionId: ownershipSessionId,
|
|
840
|
+
env: deps.env || process.env,
|
|
841
|
+
requireMergeGate: gateApproval?.disabled !== true,
|
|
842
|
+
config,
|
|
843
|
+
projectRoot: root,
|
|
844
|
+
now: decisionAt,
|
|
845
|
+
});
|
|
846
|
+
if (!decision || typeof decision.decisionId !== 'string' || !decision.decisionId) {
|
|
847
|
+
return { success: false, merged: true, error: 'Merge recovery decision evidence is unavailable.' };
|
|
848
|
+
}
|
|
849
|
+
const recordMergeDecision = deps.recordMergeDecision || defaultRecordMergeDecision;
|
|
850
|
+
try {
|
|
851
|
+
const terminal = await recordMergeDecision({
|
|
852
|
+
decision,
|
|
853
|
+
mergeResult: { merged: true, recovered: true },
|
|
854
|
+
pr,
|
|
855
|
+
expectedHead: parsed.expectedHead,
|
|
856
|
+
repository,
|
|
857
|
+
projectRoot: root,
|
|
858
|
+
buildBroker: deps.buildMergeBroker,
|
|
859
|
+
});
|
|
860
|
+
if (!terminal || typeof terminal.receiptId !== 'string' || !terminal.receiptId) {
|
|
861
|
+
throw new Error('terminal receipt evidence is unavailable');
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
success: true, merged: true, enabled: true, recovered: true,
|
|
865
|
+
decisionId: decision.decisionId, ...terminal,
|
|
866
|
+
};
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return {
|
|
869
|
+
success: false, merged: true, enabled: true, decisionId: decision.decisionId,
|
|
870
|
+
error: `Merged PR terminal linkage recovery failed: ${error.message}`,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
422
875
|
/** Default `gh` runner. Only reached by the default fetch/merge seams (never in unit tests). */
|
|
423
876
|
function defaultGh(args, options = {}) {
|
|
424
877
|
return execFileSync('gh', args, { encoding: 'utf8', ...options }); // NOSONAR S4036 - hardcoded CLI (gh), args array (no shell), developer-tool context
|
|
425
878
|
}
|
|
426
879
|
|
|
427
880
|
/** Parse `gh ... --json` output, returning `null` on any failure (callers fail closed). */
|
|
428
|
-
function ghJson(gh, args) {
|
|
881
|
+
function ghJson(gh, args, options) {
|
|
429
882
|
try {
|
|
430
|
-
return JSON.parse(gh(args) || '{}');
|
|
883
|
+
return JSON.parse(gh(args, options) || '{}');
|
|
431
884
|
} catch (_err) {
|
|
432
885
|
return null;
|
|
433
886
|
}
|
|
@@ -523,9 +976,20 @@ function fetchCheckRunObservations(gh, { owner, repo, head }) {
|
|
|
523
976
|
*
|
|
524
977
|
* @returns {object} prContext
|
|
525
978
|
*/
|
|
526
|
-
async function defaultFetchPrContext({ pr, gh = defaultGh, now = Date.now() }) {
|
|
527
|
-
const
|
|
528
|
-
|
|
979
|
+
async function defaultFetchPrContext({ pr, projectRoot, gh = defaultGh, now = Date.now() }) {
|
|
980
|
+
const repoOptions = typeof projectRoot === 'string' && projectRoot ? { cwd: projectRoot } : undefined;
|
|
981
|
+
const repoIdentity = ghJson(gh, ['repo', 'view', '--json', 'owner,name,isFork,parent'], repoOptions);
|
|
982
|
+
const currentRepository = normalizeRepository(repoIdentity?.owner?.login && repoIdentity?.name
|
|
983
|
+
? `${repoIdentity.owner.login}/${repoIdentity.name}`
|
|
984
|
+
: null);
|
|
985
|
+
const repository = repoIdentity?.isFork === true
|
|
986
|
+
? normalizeRepository(repoIdentity?.parent?.nameWithOwner)
|
|
987
|
+
: (repoIdentity?.isFork === false ? currentRepository : null);
|
|
988
|
+
const [owner, repo] = repository ? repository.split('/') : [null, null];
|
|
989
|
+
const view = repository
|
|
990
|
+
? (ghJson(gh, ['pr', 'view', String(pr), '--repo', repository, '--json',
|
|
991
|
+
'number,headRefOid,baseRefName,state,isDraft,mergeable,mergeStateStatus,statusCheckRollup,comments,updatedAt']) || {})
|
|
992
|
+
: {};
|
|
529
993
|
|
|
530
994
|
const rollup = Array.isArray(view.statusCheckRollup) ? view.statusCheckRollup : null;
|
|
531
995
|
|
|
@@ -538,9 +1002,6 @@ async function defaultFetchPrContext({ pr, gh = defaultGh, now = Date.now() }) {
|
|
|
538
1002
|
}))
|
|
539
1003
|
: [];
|
|
540
1004
|
|
|
541
|
-
const repoIdentity = ghJson(gh, ['repo', 'view', '--json', 'owner,name']);
|
|
542
|
-
const owner = repoIdentity && repoIdentity.owner && repoIdentity.owner.login;
|
|
543
|
-
const repo = repoIdentity && repoIdentity.name;
|
|
544
1005
|
const adapter = owner && repo
|
|
545
1006
|
? new PrStateAdapter({ gh: (_cmd, adapterArgs) => gh(adapterArgs) })
|
|
546
1007
|
: null;
|
|
@@ -599,7 +1060,7 @@ async function defaultFetchPrContext({ pr, gh = defaultGh, now = Date.now() }) {
|
|
|
599
1060
|
|
|
600
1061
|
return {
|
|
601
1062
|
number: Number.isInteger(view.number) ? view.number : null,
|
|
602
|
-
repository
|
|
1063
|
+
repository,
|
|
603
1064
|
headSha: view.headRefOid || null,
|
|
604
1065
|
checks,
|
|
605
1066
|
providerObservations,
|
|
@@ -627,6 +1088,10 @@ function defaultMergePr({ pr, expectedHead, repository, gh = defaultGh }) {
|
|
|
627
1088
|
if (!head) throw new Error('A full 40-character expected PR head SHA is required.');
|
|
628
1089
|
if (!normalizePrNumber(String(pr)) || !repo) throw new Error('A canonical PR number and repository are required.');
|
|
629
1090
|
gh(['pr', 'merge', String(pr), '--repo', repo, '--squash', '--match-head-commit', head]);
|
|
1091
|
+
const confirmed = ghJson(gh, ['pr', 'view', String(pr), '--repo', repo, '--json', 'state,headRefOid']);
|
|
1092
|
+
if (confirmed?.state !== 'MERGED' || normalizeFullHeadSha(confirmed.headRefOid) !== head) {
|
|
1093
|
+
return { merged: false, reason: 'provider merge is not confirmed for the exact reviewed head' };
|
|
1094
|
+
}
|
|
630
1095
|
return { merged: true, method: 'squash' };
|
|
631
1096
|
}
|
|
632
1097
|
|
|
@@ -653,6 +1118,69 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
653
1118
|
};
|
|
654
1119
|
}
|
|
655
1120
|
|
|
1121
|
+
const verifyIssueOwnership = deps.verifyIssueOwnership || defaultVerifyIssueOwnership;
|
|
1122
|
+
const ownershipEnv = deps.env || process.env;
|
|
1123
|
+
const ownershipActor = resolveOwnershipActor(ownershipEnv);
|
|
1124
|
+
const ownershipSessionId = typeof ownershipEnv.FORGE_SESSION_ID === 'string'
|
|
1125
|
+
&& ownershipEnv.FORGE_SESSION_ID.trim()
|
|
1126
|
+
? ownershipEnv.FORGE_SESSION_ID.trim()
|
|
1127
|
+
: null;
|
|
1128
|
+
const ownershipInput = {
|
|
1129
|
+
issueId: parsed.issueId,
|
|
1130
|
+
projectRoot: root,
|
|
1131
|
+
actor: ownershipActor,
|
|
1132
|
+
env: { ...ownershipEnv },
|
|
1133
|
+
};
|
|
1134
|
+
const fetchPrContext = deps.fetchPrContext || defaultFetchPrContext;
|
|
1135
|
+
const mergePr = deps.mergePr || defaultMergePr;
|
|
1136
|
+
const verifyPrIssueBinding = deps.verifyPrIssueBinding || defaultVerifyPrIssueBinding;
|
|
1137
|
+
const gh = deps.githubContext?.bound ? deps.githubContext.runGh : (deps.gh || defaultGh);
|
|
1138
|
+
const exactMergeIdentity = !parsed.error && parsed.expectedHead && parsed.issueId;
|
|
1139
|
+
let prContext; let prState;
|
|
1140
|
+
const replayBeforeProvider = async () => {
|
|
1141
|
+
const resolveLocalRepository = deps.resolveLocalRepository || defaultResolveLocalRepository;
|
|
1142
|
+
let localRepository;
|
|
1143
|
+
try {
|
|
1144
|
+
localRepository = normalizeRepository(await resolveLocalRepository({ projectRoot: root }));
|
|
1145
|
+
} catch {
|
|
1146
|
+
localRepository = null;
|
|
1147
|
+
}
|
|
1148
|
+
if (!localRepository) {
|
|
1149
|
+
return { failure: {
|
|
1150
|
+
success: false,
|
|
1151
|
+
merged: false,
|
|
1152
|
+
error: 'Local repository identity is unavailable for terminal evidence replay.',
|
|
1153
|
+
} };
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const replay = await recoverMergedTerminal({
|
|
1157
|
+
pr,
|
|
1158
|
+
parsed,
|
|
1159
|
+
root,
|
|
1160
|
+
config: {},
|
|
1161
|
+
prContext: {
|
|
1162
|
+
number: Number(pr),
|
|
1163
|
+
headSha: parsed.expectedHead,
|
|
1164
|
+
repository: localRepository,
|
|
1165
|
+
},
|
|
1166
|
+
ownershipActor,
|
|
1167
|
+
ownershipSessionId,
|
|
1168
|
+
deps,
|
|
1169
|
+
replayOnly: true,
|
|
1170
|
+
});
|
|
1171
|
+
if (replay.replayMissing !== true) {
|
|
1172
|
+
return replay.success ? { replay } : { failure: { ...replay, merged: false } };
|
|
1173
|
+
}
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
return { failure: {
|
|
1176
|
+
success: false,
|
|
1177
|
+
merged: false,
|
|
1178
|
+
error: `Merged PR terminal evidence replay failed: ${error.message}`,
|
|
1179
|
+
} };
|
|
1180
|
+
}
|
|
1181
|
+
return {};
|
|
1182
|
+
};
|
|
1183
|
+
|
|
656
1184
|
const loadConfig = deps.loadConfig || loadRawConfig;
|
|
657
1185
|
let config;
|
|
658
1186
|
try {
|
|
@@ -675,7 +1203,6 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
675
1203
|
process.stdout.write(`${reason}\n`);
|
|
676
1204
|
return { success: true, merged: false, enabled: false, reason };
|
|
677
1205
|
}
|
|
678
|
-
|
|
679
1206
|
// Opted in but no rules → refuse (fail-closed): an empty ruleset is vacuously
|
|
680
1207
|
// "allowed", which would merge unconditionally. Treat that as misconfiguration.
|
|
681
1208
|
if (rules.length === 0) {
|
|
@@ -684,21 +1211,12 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
684
1211
|
return { success: false, merged: false, enabled: true, reason };
|
|
685
1212
|
}
|
|
686
1213
|
|
|
687
|
-
if (
|
|
1214
|
+
if (!exactMergeIdentity) {
|
|
688
1215
|
const reason = parsed.error
|
|
689
1216
|
|| 'Enabled auto-merge requires --expect-head <full 40-character SHA> and --issue <Forge issue ID>.';
|
|
690
1217
|
return { success: false, merged: false, enabled: true, error: reason };
|
|
691
1218
|
}
|
|
692
1219
|
|
|
693
|
-
const verifyIssueOwnership = deps.verifyIssueOwnership || defaultVerifyIssueOwnership;
|
|
694
|
-
const ownershipEnv = deps.env || process.env;
|
|
695
|
-
const ownershipActor = resolveOwnershipActor(ownershipEnv);
|
|
696
|
-
const ownershipInput = {
|
|
697
|
-
issueId: parsed.issueId,
|
|
698
|
-
projectRoot: root,
|
|
699
|
-
actor: ownershipActor,
|
|
700
|
-
env: { ...ownershipEnv },
|
|
701
|
-
};
|
|
702
1220
|
let ownership;
|
|
703
1221
|
try {
|
|
704
1222
|
ownership = await verifyIssueOwnership(ownershipInput);
|
|
@@ -707,35 +1225,55 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
707
1225
|
}
|
|
708
1226
|
if (!ownership || ownership.owned !== true || ownership.expired !== false
|
|
709
1227
|
|| typeof ownership.actor !== 'string' || ownership.actor !== ownershipActor
|
|
710
|
-
|| typeof ownership.claimedBy !== 'string' || ownership.claimedBy !== ownershipActor
|
|
1228
|
+
|| typeof ownership.claimedBy !== 'string' || ownership.claimedBy !== ownershipActor
|
|
1229
|
+
|| ownership.sessionId !== ownershipSessionId) {
|
|
711
1230
|
return {
|
|
712
1231
|
success: false,
|
|
713
1232
|
merged: false,
|
|
714
|
-
error: `Active Kernel ownership claim is required for issue ${parsed.issueId}; refusing to merge.`,
|
|
1233
|
+
error: `Active Kernel ownership claim with the exact session is required for issue ${parsed.issueId}; refusing to merge.`,
|
|
715
1234
|
};
|
|
716
1235
|
}
|
|
1236
|
+
const preProvider = await replayBeforeProvider();
|
|
1237
|
+
if (preProvider.failure) return preProvider.failure;
|
|
1238
|
+
if (preProvider.replay) return preProvider.replay;
|
|
717
1239
|
|
|
718
|
-
const fetchPrContext = deps.fetchPrContext || defaultFetchPrContext;
|
|
719
|
-
const mergePr = deps.mergePr || defaultMergePr;
|
|
720
|
-
const verifyPrIssueBinding = deps.verifyPrIssueBinding || defaultVerifyPrIssueBinding;
|
|
721
|
-
const gh = deps.gh || defaultGh;
|
|
722
|
-
|
|
723
|
-
let prContext;
|
|
724
1240
|
try {
|
|
725
1241
|
prContext = await fetchPrContext({ pr, projectRoot: root, gh, now: deps.now || Date.now() });
|
|
726
1242
|
} catch (err) {
|
|
727
1243
|
return { success: false, merged: false, error: `Failed to fetch PR context: ${err.message}` };
|
|
728
1244
|
}
|
|
1245
|
+
prState = prContext?.state ? String(prContext.state).toUpperCase() : '';
|
|
1246
|
+
if (prState === 'MERGED') {
|
|
1247
|
+
try {
|
|
1248
|
+
const replay = await recoverMergedTerminal({
|
|
1249
|
+
pr, parsed, root, config: {}, prContext, ownershipActor, ownershipSessionId, deps, replayOnly: true,
|
|
1250
|
+
});
|
|
1251
|
+
if (replay.replayMissing !== true) return replay;
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
return { success: false, merged: true, error: `Merged PR terminal evidence replay failed: ${error.message}` };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
729
1256
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1257
|
+
if (prState === 'MERGED') {
|
|
1258
|
+
if (!ownershipSessionId) {
|
|
1259
|
+
return { success: false, merged: true, error: 'An exact FORGE_SESSION_ID session identity is required to recover terminal merge evidence.' };
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
return await recoverMergedTerminal({
|
|
1263
|
+
pr, parsed, root, config, prContext, ownershipActor, ownershipSessionId, deps,
|
|
1264
|
+
});
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
return { success: false, merged: true, error: `Merged PR terminal linkage recovery failed: ${error.message}` };
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (prState === 'CLOSED') {
|
|
735
1270
|
const reason = `PR #${pr} is ${prState} (not OPEN) — nothing to merge. No action taken.`;
|
|
736
1271
|
process.stdout.write(`${reason}\n`);
|
|
737
1272
|
return { success: true, merged: false, enabled: true, state: prState, reason };
|
|
738
1273
|
}
|
|
1274
|
+
if (!ownershipSessionId) {
|
|
1275
|
+
return { success: false, merged: false, error: 'An exact FORGE_SESSION_ID session identity is required before merge.' };
|
|
1276
|
+
}
|
|
739
1277
|
|
|
740
1278
|
const mandatoryError = mandatoryContextError(prContext, parsed.expectedHead);
|
|
741
1279
|
if (mandatoryError) return { success: false, merged: false, error: mandatoryError };
|
|
@@ -784,7 +1322,16 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
784
1322
|
// Re-apply the terminal-state guard on the FRESH context: the PR may have been
|
|
785
1323
|
// merged or closed between the first fetch and now. Never merge a terminal PR.
|
|
786
1324
|
const freshState = freshContext && freshContext.state ? String(freshContext.state).toUpperCase() : '';
|
|
787
|
-
if (freshState === 'MERGED'
|
|
1325
|
+
if (freshState === 'MERGED') {
|
|
1326
|
+
try {
|
|
1327
|
+
return await recoverMergedTerminal({
|
|
1328
|
+
pr, parsed, root, config, prContext: freshContext, ownershipActor, ownershipSessionId, deps,
|
|
1329
|
+
});
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
return { success: false, merged: true, error: `Merged PR terminal linkage recovery failed: ${error.message}` };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (freshState === 'CLOSED') {
|
|
788
1335
|
const reason = `PR #${pr} became ${freshState} (not OPEN) before merge — nothing to merge. No action taken.`;
|
|
789
1336
|
process.stdout.write(`${reason}\n`);
|
|
790
1337
|
return { success: true, merged: false, enabled: true, state: freshState, reason };
|
|
@@ -828,12 +1375,62 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
828
1375
|
}
|
|
829
1376
|
if (!finalOwnership || finalOwnership.owned !== true || finalOwnership.expired !== false
|
|
830
1377
|
|| typeof finalOwnership.actor !== 'string' || finalOwnership.actor !== ownershipActor
|
|
831
|
-
|| typeof finalOwnership.claimedBy !== 'string' || finalOwnership.claimedBy !== ownershipActor
|
|
1378
|
+
|| typeof finalOwnership.claimedBy !== 'string' || finalOwnership.claimedBy !== ownershipActor
|
|
1379
|
+
|| finalOwnership.sessionId !== ownershipSessionId) {
|
|
832
1380
|
return { success: false, merged: false, error: 'Kernel ownership changed or expired before merge; refusing to merge.' };
|
|
833
1381
|
}
|
|
834
1382
|
|
|
1383
|
+
const decisionAt = currentTimestamp(deps.now || Date.now);
|
|
1384
|
+
const verifyMergeGate = deps.verifyMergeGate || defaultVerifyMergeGate;
|
|
1385
|
+
let gateApproval;
|
|
1386
|
+
try {
|
|
1387
|
+
gateApproval = await verifyMergeGate({
|
|
1388
|
+
issueId: parsed.issueId,
|
|
1389
|
+
projectRoot: root,
|
|
1390
|
+
actor: ownershipActor,
|
|
1391
|
+
now: decisionAt,
|
|
1392
|
+
});
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
return { success: false, merged: false, error: `Failed to verify ${MERGE_GATE}: ${err.message}` };
|
|
1395
|
+
}
|
|
1396
|
+
if (gateApproval !== true && gateApproval?.approved !== true && gateApproval?.disabled !== true) {
|
|
1397
|
+
return { success: false, merged: false, error: `${MERGE_GATE} is not approved for this issue.` };
|
|
1398
|
+
}
|
|
1399
|
+
if (gateApproval?.disabled === true) {
|
|
1400
|
+
return {
|
|
1401
|
+
success: false,
|
|
1402
|
+
merged: false,
|
|
1403
|
+
error: 'The disabled merge gate cannot produce broker-verifiable terminal authority; refusing to mutate GitHub.',
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const prepareMergeDecision = deps.prepareMergeDecision || defaultPrepareMergeDecision;
|
|
1408
|
+
let decision;
|
|
835
1409
|
try {
|
|
836
|
-
|
|
1410
|
+
decision = await prepareMergeDecision({
|
|
1411
|
+
issueId: parsed.issueId,
|
|
1412
|
+
pr,
|
|
1413
|
+
expectedHead: parsed.expectedHead,
|
|
1414
|
+
repository: leasedRepository,
|
|
1415
|
+
binding: freshBinding,
|
|
1416
|
+
actor: ownershipActor,
|
|
1417
|
+
sessionId: ownershipSessionId,
|
|
1418
|
+
env: ownershipEnv,
|
|
1419
|
+
requireMergeGate: gateApproval?.disabled !== true,
|
|
1420
|
+
config,
|
|
1421
|
+
projectRoot: root,
|
|
1422
|
+
now: decisionAt,
|
|
1423
|
+
});
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
return { success: false, merged: false, error: `Failed to prepare merge decision: ${err.message}` };
|
|
1426
|
+
}
|
|
1427
|
+
if (!decision || typeof decision.decisionId !== 'string' || !decision.decisionId) {
|
|
1428
|
+
return { success: false, merged: false, error: 'Merge decision evidence is unavailable.' };
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
let mergeResult;
|
|
1432
|
+
try {
|
|
1433
|
+
mergeResult = await mergePr({
|
|
837
1434
|
pr,
|
|
838
1435
|
expectedHead: parsed.expectedHead,
|
|
839
1436
|
repository: leasedRepository,
|
|
@@ -841,28 +1438,78 @@ async function handler(args, _flags, projectRoot, deps = {}) {
|
|
|
841
1438
|
projectRoot: root,
|
|
842
1439
|
gh,
|
|
843
1440
|
});
|
|
844
|
-
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
return { success: false, merged: false, decisionId: decision.decisionId, error: `Merge failed: ${err.message}` };
|
|
1443
|
+
}
|
|
1444
|
+
if (mergeResult?.merged !== true) {
|
|
1445
|
+
return {
|
|
1446
|
+
success: false,
|
|
1447
|
+
merged: false,
|
|
1448
|
+
decisionId: decision.decisionId,
|
|
1449
|
+
error: `Merge failed: ${mergeResult?.reason || 'provider did not confirm the merge'}`,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const recordMergeDecision = deps.recordMergeDecision || defaultRecordMergeDecision;
|
|
1454
|
+
let terminal;
|
|
1455
|
+
try {
|
|
1456
|
+
terminal = await recordMergeDecision({
|
|
1457
|
+
decision,
|
|
1458
|
+
mergeResult,
|
|
1459
|
+
pr,
|
|
1460
|
+
expectedHead: parsed.expectedHead,
|
|
1461
|
+
repository: leasedRepository,
|
|
1462
|
+
projectRoot: root,
|
|
1463
|
+
buildBroker: deps.buildMergeBroker,
|
|
1464
|
+
});
|
|
1465
|
+
} catch (err) {
|
|
845
1466
|
return {
|
|
846
|
-
success:
|
|
1467
|
+
success: false,
|
|
847
1468
|
merged: true,
|
|
848
1469
|
enabled: true,
|
|
849
1470
|
allowed: true,
|
|
850
|
-
|
|
851
|
-
|
|
1471
|
+
decisionId: decision.decisionId,
|
|
1472
|
+
error: `Merge succeeded but terminal linkage failed: ${err.message}`,
|
|
852
1473
|
};
|
|
853
|
-
} catch (err) {
|
|
854
|
-
return { success: false, merged: false, error: `Merge failed: ${err.message}` };
|
|
855
1474
|
}
|
|
1475
|
+
if (!terminal || typeof terminal.receiptId !== 'string' || !terminal.receiptId) {
|
|
1476
|
+
return {
|
|
1477
|
+
success: false,
|
|
1478
|
+
merged: true,
|
|
1479
|
+
enabled: true,
|
|
1480
|
+
allowed: true,
|
|
1481
|
+
decisionId: decision.decisionId,
|
|
1482
|
+
error: 'Merge succeeded but terminal receipt evidence is unavailable.',
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
process.stdout.write(`All ${rules.length} merge rule(s) passed — merged PR #${pr}.\n`);
|
|
1487
|
+
const providerFields = typeof mergeResult?.method === 'string' ? { method: mergeResult.method } : {};
|
|
1488
|
+
return {
|
|
1489
|
+
...providerFields,
|
|
1490
|
+
success: true,
|
|
1491
|
+
merged: true,
|
|
1492
|
+
enabled: true,
|
|
1493
|
+
allowed: true,
|
|
1494
|
+
reason: 'all merge rules passed',
|
|
1495
|
+
decisionId: decision.decisionId,
|
|
1496
|
+
...terminal,
|
|
1497
|
+
};
|
|
856
1498
|
}
|
|
857
1499
|
|
|
858
1500
|
module.exports = {
|
|
859
1501
|
name: 'merge',
|
|
1502
|
+
githubAuth: true,
|
|
860
1503
|
description: 'Opt-in conditional auto-merge: merge a PR only when all user-configured rules pass (OFF by default)',
|
|
861
1504
|
usage: 'Usage: forge merge --auto <pr> --expect-head <40-char-sha> --issue <issue-id>',
|
|
862
1505
|
handler,
|
|
863
1506
|
// Exported seams for testing / reuse.
|
|
864
1507
|
defaultFetchPrContext,
|
|
1508
|
+
defaultResolveLocalRepository,
|
|
865
1509
|
defaultMergePr,
|
|
1510
|
+
defaultPrepareMergeDecision,
|
|
1511
|
+
defaultRecordMergeDecision,
|
|
1512
|
+
defaultVerifyMergeGate,
|
|
866
1513
|
defaultVerifyPrIssueBinding,
|
|
867
1514
|
defaultVerifyIssueOwnership,
|
|
868
1515
|
evaluateProtectedRequiredChecks,
|