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
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
4
6
|
const {
|
|
5
7
|
assertProtectedWriteAllowed,
|
|
6
8
|
recordProtectedStateAuditEvent,
|
|
7
9
|
} = require('../lib/protected-state-surfaces');
|
|
8
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
authorizeAndConsumeProtectedStateWrites,
|
|
12
|
+
isValidGitObjectId,
|
|
13
|
+
} = require('../lib/protected-state-authority');
|
|
14
|
+
const { verifyBunLockfileRegeneration } = require('../lib/bun-lockfile-proof');
|
|
15
|
+
const { resolveBaseRemote, resolveBaseBranch } = require('../lib/base-remote');
|
|
16
|
+
const realStagedFiles = new Set();
|
|
17
|
+
const deletedFiles = new Set();
|
|
9
18
|
|
|
10
19
|
function parseNameStatus(output) {
|
|
11
20
|
const files = [];
|
|
@@ -15,8 +24,10 @@ function parseNameStatus(output) {
|
|
|
15
24
|
const parts = trimmed.split('\t').filter(Boolean);
|
|
16
25
|
const status = parts[0] || '';
|
|
17
26
|
if (/^[RC]/.test(status)) {
|
|
27
|
+
if (parts[1]) deletedFiles.add(parts[1]);
|
|
18
28
|
files.push(...parts.slice(1, 3));
|
|
19
29
|
} else {
|
|
30
|
+
if (status.startsWith('D') && parts[1]) deletedFiles.add(parts[1]);
|
|
20
31
|
files.push(parts[1]);
|
|
21
32
|
}
|
|
22
33
|
}
|
|
@@ -24,26 +35,45 @@ function parseNameStatus(output) {
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function getStagedFiles() {
|
|
38
|
+
let stagedFiles;
|
|
27
39
|
if (process.env.FORGE_PROTECTED_STATE_STAGED_NAME_STATUS !== undefined) {
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (process.env.FORGE_PROTECTED_STATE_STAGED_FILES !== undefined) {
|
|
32
|
-
return process.env.FORGE_PROTECTED_STATE_STAGED_FILES
|
|
40
|
+
stagedFiles = parseNameStatus(process.env.FORGE_PROTECTED_STATE_STAGED_NAME_STATUS);
|
|
41
|
+
} else if (process.env.FORGE_PROTECTED_STATE_STAGED_FILES !== undefined) {
|
|
42
|
+
stagedFiles = process.env.FORGE_PROTECTED_STATE_STAGED_FILES
|
|
33
43
|
.split(/\r?\n/)
|
|
34
44
|
.map(line => line.trim())
|
|
35
45
|
.filter(Boolean);
|
|
46
|
+
} else {
|
|
47
|
+
const output = execFileSync('git', ['diff', '--cached', '--name-status', '--diff-filter=ACMRDT'], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
});
|
|
51
|
+
stagedFiles = parseNameStatus(output);
|
|
52
|
+
for (const file of stagedFiles) realStagedFiles.add(file);
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
if (process.env.FORGE_PROTECTED_STATE_STAGED_NAME_STATUS !== undefined || process.env.FORGE_PROTECTED_STATE_STAGED_FILES !== undefined) {
|
|
56
|
+
try {
|
|
57
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
});
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
return [...new Set(stagedFiles)];
|
|
63
|
+
}
|
|
64
|
+
const realOutput = execFileSync('git', ['diff', '--cached', '--name-status', '--diff-filter=ACMRDT'], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
+
});
|
|
68
|
+
const indexedFiles = parseNameStatus(realOutput);
|
|
69
|
+
for (const file of indexedFiles) realStagedFiles.add(file);
|
|
70
|
+
stagedFiles.push(...indexedFiles);
|
|
71
|
+
}
|
|
72
|
+
return [...new Set(stagedFiles)];
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
function getStagedContent(file) {
|
|
46
|
-
if (process.env.FORGE_PROTECTED_STATE_STAGED_CONTENTS_JSON) {
|
|
76
|
+
if (!realStagedFiles.has(file) && process.env.FORGE_PROTECTED_STATE_STAGED_CONTENTS_JSON) {
|
|
47
77
|
const contents = JSON.parse(process.env.FORGE_PROTECTED_STATE_STAGED_CONTENTS_JSON);
|
|
48
78
|
return Object.prototype.hasOwnProperty.call(contents, file) ? contents[file] : null;
|
|
49
79
|
}
|
|
@@ -54,10 +84,370 @@ function getStagedContent(file) {
|
|
|
54
84
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
85
|
});
|
|
56
86
|
} catch (_error) {
|
|
87
|
+
if (!deletedFiles.has(file)) return null;
|
|
88
|
+
try {
|
|
89
|
+
return execFileSync('git', ['show', `HEAD:${file}`], {
|
|
90
|
+
encoding: null,
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
function getCurrentHead(execGit = execFileSync) {
|
|
101
|
+
let output;
|
|
102
|
+
try {
|
|
103
|
+
output = execGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
+
});
|
|
107
|
+
} catch (headError) {
|
|
108
|
+
let symbolicRef;
|
|
109
|
+
try {
|
|
110
|
+
symbolicRef = execGit('git', ['symbolic-ref', '-q', 'HEAD'], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
}).trim();
|
|
114
|
+
} catch {
|
|
115
|
+
throw headError;
|
|
116
|
+
}
|
|
117
|
+
if (!/^refs\/heads\/[^\0\r\n]+$/.test(symbolicRef)) throw headError;
|
|
118
|
+
try {
|
|
119
|
+
execGit('git', ['show-ref', '--verify', '--quiet', symbolicRef], {
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
});
|
|
123
|
+
} catch (refError) {
|
|
124
|
+
if (refError.status === 1) return null;
|
|
125
|
+
throw headError;
|
|
126
|
+
}
|
|
127
|
+
throw headError;
|
|
128
|
+
}
|
|
129
|
+
const head = output.trim();
|
|
130
|
+
if (!isValidGitObjectId(head)) throw new Error('Current HEAD did not resolve to one full Git object id.');
|
|
131
|
+
return head;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ABSENT_ENTRY = 'absent';
|
|
135
|
+
const LINE_SPLIT = /\r?\n/;
|
|
136
|
+
|
|
137
|
+
// Pathspec magic is only honoured when Git is not already in a forced pathspec
|
|
138
|
+
// mode. If the hook inherits GIT_LITERAL_PATHSPECS=1, `:(literal)<file>` is read
|
|
139
|
+
// as a filename spelled `:(literal)<file>`, every probe matches nothing, and the
|
|
140
|
+
// exemption predicate would compare absent against absent. The sibling GLOB /
|
|
141
|
+
// NOGLOB / ICASE switches distort matching the same way, so strip all four and
|
|
142
|
+
// keep the explicit `:(literal)` magic (paths with glob or colon characters stay
|
|
143
|
+
// safe). Keys are compared case-insensitively because Windows environment names
|
|
144
|
+
// are case-insensitive.
|
|
145
|
+
const PATHSPEC_ENV_KEYS = new Set([
|
|
146
|
+
'GIT_LITERAL_PATHSPECS',
|
|
147
|
+
'GIT_GLOB_PATHSPECS',
|
|
148
|
+
'GIT_NOGLOB_PATHSPECS',
|
|
149
|
+
'GIT_ICASE_PATHSPECS',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
function pathspecSafeEnv() {
|
|
153
|
+
const env = { ...process.env };
|
|
154
|
+
for (const key of Object.keys(env)) {
|
|
155
|
+
if (PATHSPEC_ENV_KEYS.has(key.toUpperCase())) delete env[key];
|
|
156
|
+
}
|
|
157
|
+
return env;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const GIT_PROBE_ENV = pathspecSafeEnv();
|
|
161
|
+
const REMOTE_PROBE_TIMEOUT_MS = 15_000;
|
|
162
|
+
|
|
163
|
+
function gitCapture(args) {
|
|
164
|
+
return execFileSync('git', args, {
|
|
165
|
+
encoding: 'utf8',
|
|
166
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
|
+
env: GIT_PROBE_ENV,
|
|
168
|
+
}).trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Resolve a commit-ish to a full object id, or null when it does not exist.
|
|
172
|
+
// Worktree-safe: plain `git rev-parse` already resolves per-worktree state.
|
|
173
|
+
function resolveCommit(revision) {
|
|
174
|
+
try {
|
|
175
|
+
const id = gitCapture(['rev-parse', '--verify', '--quiet', `${revision}^{commit}`]);
|
|
176
|
+
return isValidGitObjectId(id) ? id : null;
|
|
177
|
+
} catch {
|
|
57
178
|
return null;
|
|
58
179
|
}
|
|
59
180
|
}
|
|
60
181
|
|
|
182
|
+
// Best common ancestor of two commits, or null when there is none (unrelated
|
|
183
|
+
// histories) or the query fails. Used to tell "the merge side deleted this path"
|
|
184
|
+
// apart from "this path never existed on the merge side".
|
|
185
|
+
function resolveMergeBase(left, right) {
|
|
186
|
+
let base;
|
|
187
|
+
try {
|
|
188
|
+
base = gitCapture(['merge-base', left, right]).split(LINE_SPLIT)[0].trim();
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return isValidGitObjectId(base) ? base : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Git prints the mode but not the type for index entries; derive it so index and
|
|
196
|
+
// tree entries normalise to the same shape.
|
|
197
|
+
function objectTypeForMode(mode) {
|
|
198
|
+
if (mode === '160000') return 'commit';
|
|
199
|
+
if (mode === '040000' || mode === '40000') return 'tree';
|
|
200
|
+
return 'blob';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Normalised `<mode> <type> <oid>` for the staged index entry, ABSENT_ENTRY when
|
|
204
|
+
// the path is not in the index, or null when the answer is unknown. A conflicted
|
|
205
|
+
// path yields several stage lines and therefore null — fail closed.
|
|
206
|
+
// The mode matters: blob equality alone would exempt a real 100644 -> 100755
|
|
207
|
+
// index change on a protected path.
|
|
208
|
+
function stagedEntry(file) {
|
|
209
|
+
let output;
|
|
210
|
+
try {
|
|
211
|
+
output = gitCapture(['ls-files', '--stage', '--', `:(literal)${file}`]);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (output === '') return ABSENT_ENTRY;
|
|
216
|
+
const lines = output.split(LINE_SPLIT).filter(Boolean);
|
|
217
|
+
if (lines.length !== 1) return null;
|
|
218
|
+
const match = /^([0-7]{6}) ([0-9a-f]{40}|[0-9a-f]{64}) [0-3]\t/.exec(lines[0]);
|
|
219
|
+
if (!match) return null;
|
|
220
|
+
return `${match[1]} ${objectTypeForMode(match[1])} ${match[2]}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Same normalised shape for `<revision>:<file>`, so the comparison covers mode
|
|
224
|
+
// and object type as well as content.
|
|
225
|
+
function revisionEntry(revision, file) {
|
|
226
|
+
let output;
|
|
227
|
+
try {
|
|
228
|
+
output = gitCapture(['ls-tree', revision, '--', `:(literal)${file}`]);
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (output === '') return ABSENT_ENTRY;
|
|
233
|
+
const lines = output.split(LINE_SPLIT).filter(Boolean);
|
|
234
|
+
if (lines.length !== 1) return null;
|
|
235
|
+
const match = /^([0-7]{6}) (blob|tree|commit) ([0-9a-f]{40}|[0-9a-f]{64})\t/.exec(lines[0]);
|
|
236
|
+
if (!match) return null;
|
|
237
|
+
return `${match[1]} ${match[2]} ${match[3]}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Adapter so the shared base-remote resolver runs through this script's
|
|
241
|
+
// pathspec-scrubbed git probe. The resolver ignores the options object it is
|
|
242
|
+
// handed here; the env scrub and quiet stdio come from `gitCapture`.
|
|
243
|
+
function baseRemoteProbe(_command, args) {
|
|
244
|
+
return gitCapture(args);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Resolve the repository's single canonical upstream ref, deterministically:
|
|
248
|
+
// the *base* remote (`upstream` preferred over `origin`, the same resolution
|
|
249
|
+
// `/ship` uses to pick a PR base), then that remote's default branch
|
|
250
|
+
// (`refs/remotes/<remote>/HEAD`, falling back to the conventional default
|
|
251
|
+
// names). Deliberately NOT the current branch's tracking remote: in a
|
|
252
|
+
// fork-style checkout the feature branch tracks the contributor-owned fork, and
|
|
253
|
+
// trusting it would let a change merged from the fork's default branch pass as
|
|
254
|
+
// "already published on the base" when it never reached the official repo.
|
|
255
|
+
// Fail closed when the official `upstream` remote is configured but has no
|
|
256
|
+
// fetched tracking refs: falling through to `origin` would change trust domains
|
|
257
|
+
// and let fork-only content pass as already published upstream.
|
|
258
|
+
// Fail closed when the base branch is ambiguous: with no authoritative HEAD
|
|
259
|
+
// and both conventional defaults present, candidate order would trust main
|
|
260
|
+
// even when master is the real integration branch, letting content published
|
|
261
|
+
// only to the wrong branch pass as already published on the base.
|
|
262
|
+
// Returns the fully-qualified local ref plus its remote/branch identity, or null
|
|
263
|
+
// when the canonical upstream cannot be established.
|
|
264
|
+
function canonicalUpstream() {
|
|
265
|
+
const cwd = process.cwd();
|
|
266
|
+
const remote = resolveBaseRemote(baseRemoteProbe, cwd);
|
|
267
|
+
if (remote === 'origin') {
|
|
268
|
+
try {
|
|
269
|
+
gitCapture(['remote', 'get-url', 'upstream']);
|
|
270
|
+
return null;
|
|
271
|
+
} catch {
|
|
272
|
+
// No upstream configured; origin fallback is legitimate.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const remotes = gitCapture(['remote']).split(/\r?\n/).map(line => line.trim());
|
|
277
|
+
if (!remotes.includes(remote)) return null;
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const candidates = [];
|
|
283
|
+
let head = null;
|
|
284
|
+
try {
|
|
285
|
+
const resolved = gitCapture(['symbolic-ref', '--quiet', `refs/remotes/${remote}/HEAD`]);
|
|
286
|
+
if (resolved) head = resolved;
|
|
287
|
+
} catch {
|
|
288
|
+
// no remote HEAD recorded; fall through to the resolved default branch
|
|
289
|
+
}
|
|
290
|
+
if (head) {
|
|
291
|
+
candidates.push(head);
|
|
292
|
+
} else {
|
|
293
|
+
const hasMain = Boolean(resolveCommit(`refs/remotes/${remote}/main`));
|
|
294
|
+
const hasMaster = Boolean(resolveCommit(`refs/remotes/${remote}/master`));
|
|
295
|
+
if (hasMain && hasMaster) return null;
|
|
296
|
+
}
|
|
297
|
+
candidates.push(`refs/remotes/${remote}/${resolveBaseBranch(baseRemoteProbe, process.cwd(), remote)}`);
|
|
298
|
+
candidates.push(`refs/remotes/${remote}/main`, `refs/remotes/${remote}/master`);
|
|
299
|
+
|
|
300
|
+
const prefix = `refs/remotes/${remote}/`;
|
|
301
|
+
for (const ref of candidates) {
|
|
302
|
+
if (resolveCommit(ref) && ref.startsWith(prefix) && ref.length > prefix.length) {
|
|
303
|
+
return { ref, remote, branch: ref.slice(prefix.length) };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A local remote-tracking ref is only cached evidence. Confirm that the remote
|
|
310
|
+
// still advertises both the selected default branch and its frozen commit
|
|
311
|
+
// without mutating refs; unavailable or malformed remote evidence fails closed.
|
|
312
|
+
function isCurrentCanonicalCommit(canonical, expectedCommit) {
|
|
313
|
+
if (!canonical || !expectedCommit) return false;
|
|
314
|
+
const expectedRef = `refs/heads/${canonical.branch}`;
|
|
315
|
+
let output;
|
|
316
|
+
try {
|
|
317
|
+
output = execFileSync('git', ['ls-remote', '--symref', '--exit-code', canonical.remote, 'HEAD'], {
|
|
318
|
+
encoding: 'utf8',
|
|
319
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
320
|
+
env: { ...GIT_PROBE_ENV, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'Never' },
|
|
321
|
+
timeout: REMOTE_PROBE_TIMEOUT_MS,
|
|
322
|
+
}).trim();
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const lines = output.split(LINE_SPLIT).filter(Boolean);
|
|
327
|
+
if (lines.length !== 2) return false;
|
|
328
|
+
const symref = lines.map(line => /^ref: ([^\0\r\n\t]+)\tHEAD$/.exec(line)).find(Boolean);
|
|
329
|
+
const head = lines.map(line => /^([0-9a-f]{40}|[0-9a-f]{64})\tHEAD$/.exec(line)).find(Boolean);
|
|
330
|
+
return Boolean(symref && head && symref[1] === expectedRef && head[1] === expectedCommit);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// True when `commit` is contained in the canonical upstream line — not merely
|
|
334
|
+
// "some ref points at it". `--contains` over refs/remotes would accept an
|
|
335
|
+
// untrusted contributor remote or a hand-written `git update-ref
|
|
336
|
+
// refs/remotes/<anything>`; ancestry in the canonical ref is the property we
|
|
337
|
+
// actually want (these bytes are already published on the branch this repo
|
|
338
|
+
// integrates into, where the same gate ran).
|
|
339
|
+
//
|
|
340
|
+
// Honest limitation: refs under .git are locally writable, and anyone who can
|
|
341
|
+
// write .git can disable this hook outright. This gate defends against
|
|
342
|
+
// accidental and agent-authored protected edits and against merges from
|
|
343
|
+
// untrusted contributor remotes. It is NOT a security boundary against a local
|
|
344
|
+
// adversary — do not treat it as one.
|
|
345
|
+
function isCanonicalUpstreamAncestor(commit, canonicalCommit) {
|
|
346
|
+
if (!canonicalCommit) return false;
|
|
347
|
+
try {
|
|
348
|
+
execFileSync('git', ['merge-base', '--is-ancestor', commit, canonicalCommit], {
|
|
349
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
350
|
+
});
|
|
351
|
+
return true;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// A merge is in progress when the per-worktree MERGE_HEAD exists. `--git-path`
|
|
358
|
+
// resolves the correct (possibly linked-worktree) git dir. Returns null unless a
|
|
359
|
+
// merge is genuinely in progress and every recorded id resolves; `trustedSides`
|
|
360
|
+
// holds only those MERGE_HEAD commits contained in the canonical upstream commit.
|
|
361
|
+
function readMergeProvenance() {
|
|
362
|
+
let mergeHeadPath;
|
|
363
|
+
try {
|
|
364
|
+
mergeHeadPath = gitCapture(['rev-parse', '--git-path', 'MERGE_HEAD']);
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (!mergeHeadPath) return null;
|
|
369
|
+
let raw;
|
|
370
|
+
try {
|
|
371
|
+
raw = fs.readFileSync(path.resolve(process.cwd(), mergeHeadPath), 'utf8');
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const ids = raw
|
|
376
|
+
.split(/\r?\n/)
|
|
377
|
+
.map(line => line.trim())
|
|
378
|
+
.filter(Boolean);
|
|
379
|
+
if (ids.length === 0 || !ids.every(isValidGitObjectId)) return null;
|
|
380
|
+
const resolved = ids.map(id => resolveCommit(id));
|
|
381
|
+
if (resolved.some(commit => commit === null)) return null;
|
|
382
|
+
// HEAD anchors the "no net change" half of the predicate. If it cannot be
|
|
383
|
+
// resolved we know nothing about the commit being built, so there is no
|
|
384
|
+
// exemption context at all — fail closed rather than fall back to the sides.
|
|
385
|
+
const head = resolveCommit('HEAD');
|
|
386
|
+
if (!head) return null;
|
|
387
|
+
const canonical = canonicalUpstream();
|
|
388
|
+
const canonicalCommit = canonical ? resolveCommit(canonical.ref) : null;
|
|
389
|
+
return {
|
|
390
|
+
head,
|
|
391
|
+
canonical,
|
|
392
|
+
canonicalCommit,
|
|
393
|
+
trustedSides: resolved.filter(commit => isCanonicalUpstreamAncestor(commit, canonicalCommit)),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Exemption predicate. While a merge is in progress, a staged protected path is
|
|
398
|
+
// exempt only when the committer introduced no net change AND the content has
|
|
399
|
+
// trusted provenance:
|
|
400
|
+
// - staged entry (mode + object) === the entry at HEAD: no net change versus the branch being
|
|
401
|
+
// committed onto, so there is no provenance question at all; or
|
|
402
|
+
// - staged entry === both a MERGE_HEAD contained in the canonical upstream
|
|
403
|
+
// commit and that current canonical commit: the bytes are still published
|
|
404
|
+
// on the line this repo integrates into, where the same gate ran.
|
|
405
|
+
// A purely local merge side earns no exemption — otherwise anyone could smuggle a
|
|
406
|
+
// protected edit in on a local branch and merge it. The merge base is likewise not
|
|
407
|
+
// a permitted side: matching only the base means both sides were reverted, an edit.
|
|
408
|
+
// Anything else stays blocked, and any failed git query fails closed.
|
|
409
|
+
function createMergeExemption() {
|
|
410
|
+
const merge = readMergeProvenance();
|
|
411
|
+
if (!merge) return () => false;
|
|
412
|
+
let canonicalIsCurrent;
|
|
413
|
+
const hasCurrentCanonicalCommit = () => {
|
|
414
|
+
if (canonicalIsCurrent === undefined) {
|
|
415
|
+
canonicalIsCurrent = isCurrentCanonicalCommit(merge.canonical, merge.canonicalCommit);
|
|
416
|
+
}
|
|
417
|
+
return canonicalIsCurrent;
|
|
418
|
+
};
|
|
419
|
+
return file => {
|
|
420
|
+
const staged = stagedEntry(file);
|
|
421
|
+
if (staged === null) return false;
|
|
422
|
+
if (staged !== ABSENT_ENTRY && staged === revisionEntry(merge.head, file)) return true;
|
|
423
|
+
if (!merge.canonicalCommit || staged !== revisionEntry(merge.canonicalCommit, file)) return false;
|
|
424
|
+
if (staged === ABSENT_ENTRY) {
|
|
425
|
+
// Git just reported this path as staged, so an absent index entry is only
|
|
426
|
+
// honest for a staged deletion; otherwise the probe contradicts git and
|
|
427
|
+
// the answer is unknown.
|
|
428
|
+
if (!deletedFiles.has(file)) return false;
|
|
429
|
+
// Absent on a trusted side is not proof that side deleted anything — a
|
|
430
|
+
// path that only ever existed on HEAD is absent upstream too, so a manual
|
|
431
|
+
// `git rm` during a merge would otherwise exempt itself. Demand that the
|
|
432
|
+
// merge side actually removed it: present at the merge base, gone at the
|
|
433
|
+
// side. Fail closed when the base or either probe is unavailable.
|
|
434
|
+
const trustedDeletion = merge.trustedSides.some(side => {
|
|
435
|
+
if (revisionEntry(side, file) !== ABSENT_ENTRY) return false;
|
|
436
|
+
const base = resolveMergeBase(merge.head, side);
|
|
437
|
+
if (!base) return false;
|
|
438
|
+
const baseEntry = revisionEntry(base, file);
|
|
439
|
+
return baseEntry !== null && baseEntry !== ABSENT_ENTRY;
|
|
440
|
+
});
|
|
441
|
+
return trustedDeletion && hasCurrentCanonicalCommit();
|
|
442
|
+
}
|
|
443
|
+
const trustedCarryOver = merge.trustedSides.some(revision => {
|
|
444
|
+
const entry = revisionEntry(revision, file);
|
|
445
|
+
return entry !== null && entry === staged;
|
|
446
|
+
});
|
|
447
|
+
return trustedCarryOver && hasCurrentCanonicalCommit();
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
61
451
|
async function main() {
|
|
62
452
|
const actor =
|
|
63
453
|
process.env.FORGE_PROTECTED_STATE_ACTOR ||
|
|
@@ -65,10 +455,28 @@ async function main() {
|
|
|
65
455
|
process.env.USER ||
|
|
66
456
|
process.env.USERNAME ||
|
|
67
457
|
'unknown';
|
|
458
|
+
const sourceHead = getCurrentHead();
|
|
459
|
+
const isMergeCarryOver = createMergeExemption();
|
|
460
|
+
const mergeExempt = [];
|
|
68
461
|
const probes = getStagedFiles()
|
|
69
462
|
.map(file => {
|
|
70
463
|
const probe = assertProtectedWriteAllowed(file, { actor, operation: 'staged_edit' });
|
|
71
464
|
if (!probe.requiredSurface) return { probe };
|
|
465
|
+
// Deliberately after the lockfile branch: bun.lock keeps its own
|
|
466
|
+
// regeneration proof, which a merge must never skip.
|
|
467
|
+
if (probe.path !== 'bun.lock' && isMergeCarryOver(probe.path)) {
|
|
468
|
+
mergeExempt.push(probe.path);
|
|
469
|
+
return { probe: { ...probe, allowed: true, decision: 'allowed_merge_carry_over' } };
|
|
470
|
+
}
|
|
471
|
+
if (probe.path === 'bun.lock' && probe.requiredSurface === 'lockfiles') {
|
|
472
|
+
return {
|
|
473
|
+
probe,
|
|
474
|
+
directDecision: {
|
|
475
|
+
...probe,
|
|
476
|
+
...verifyBunLockfileRegeneration(process.cwd()),
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
72
480
|
|
|
73
481
|
const content = getStagedContent(probe.path);
|
|
74
482
|
if (content === null) return { probe };
|
|
@@ -79,7 +487,8 @@ async function main() {
|
|
|
79
487
|
surface: probe.requiredSurface,
|
|
80
488
|
path: probe.path,
|
|
81
489
|
content,
|
|
82
|
-
operation: 'staged_edit',
|
|
490
|
+
operation: deletedFiles.has(probe.path) ? 'staged_delete' : 'staged_edit',
|
|
491
|
+
sourceHead,
|
|
83
492
|
},
|
|
84
493
|
};
|
|
85
494
|
});
|
|
@@ -87,17 +496,23 @@ async function main() {
|
|
|
87
496
|
const authorization = await authorizeAndConsumeProtectedStateWrites(
|
|
88
497
|
process.cwd(),
|
|
89
498
|
protectedProbes.map(entry => entry.request),
|
|
499
|
+
{ sourceHead },
|
|
90
500
|
);
|
|
91
501
|
let authorizationIndex = 0;
|
|
92
502
|
const decisions = probes
|
|
93
503
|
.map(entry => {
|
|
504
|
+
if (entry.directDecision) return entry.directDecision;
|
|
94
505
|
if (!entry.request) return entry.probe;
|
|
95
506
|
const trustedDecision = authorization.decisions[authorizationIndex++];
|
|
507
|
+
if (!trustedDecision && authorization.batchDecision) return { ...entry.probe, allowed: true };
|
|
96
508
|
return trustedDecision.allowed
|
|
97
509
|
? trustedDecision
|
|
98
510
|
: { ...entry.probe, ...trustedDecision, repairHint: entry.probe.repairHint };
|
|
99
511
|
})
|
|
100
512
|
.filter(decision => !decision.allowed);
|
|
513
|
+
if (authorization.batchDecision && !authorization.batchDecision.allowed) {
|
|
514
|
+
decisions.push(authorization.batchDecision);
|
|
515
|
+
}
|
|
101
516
|
|
|
102
517
|
for (const decision of decisions) {
|
|
103
518
|
const audit = recordProtectedStateAuditEvent(decision, { cwd: process.cwd() });
|
|
@@ -106,6 +521,10 @@ async function main() {
|
|
|
106
521
|
}
|
|
107
522
|
}
|
|
108
523
|
|
|
524
|
+
for (const file of mergeExempt) {
|
|
525
|
+
console.log(`OK: ${file} matches a merge side unchanged (merge in progress); no net edit introduced.`);
|
|
526
|
+
}
|
|
527
|
+
|
|
109
528
|
if (decisions.length === 0) {
|
|
110
529
|
console.log('OK: No protected state edits detected.');
|
|
111
530
|
process.exit(0);
|
|
@@ -123,7 +542,11 @@ async function main() {
|
|
|
123
542
|
process.exit(1);
|
|
124
543
|
}
|
|
125
544
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
545
|
+
if (require.main === module) {
|
|
546
|
+
main().catch(error => {
|
|
547
|
+
console.error(`Protected state check failed: ${error.message}`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = { getCurrentHead };
|