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
|
Binary file
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { normalizeEvidenceText } = require('../review-adapter');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* PR-monitor Actions-summary renderer. It turns one read-only
|
|
5
7
|
* `gatherPrBundle` result (lib/pr-bundle.js) into deterministic Markdown for
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
* how it is displayed, so there is no second verdict ladder to drift.
|
|
20
22
|
*/
|
|
21
23
|
const VERDICT_HEADLINE = {
|
|
24
|
+
INCOMPLETE: '⚪ **Verdict: `INCOMPLETE`** — bounded review/check evidence is incomplete; merge readiness blocked (fail-closed).',
|
|
22
25
|
UNKNOWN: '⚪ **Verdict: `unknown`** — a signal was unreadable; state unconfirmed (fail-closed).',
|
|
23
26
|
'BLOCKED-CONFLICT': '🔀 **Verdict: `blocked-conflict`** — branch conflicts with base; rebase/merge and resolve.',
|
|
24
27
|
BEHIND: '⬇️ **Verdict: `behind`** — branch is behind base; update/rebase (protection requires up-to-date).',
|
|
@@ -28,6 +31,13 @@ const VERDICT_HEADLINE = {
|
|
|
28
31
|
'CLEAN-MERGEABLE': '🟢 **Verdict: `clean-mergeable`** — green + zero unresolved threads; ready for a human to merge.',
|
|
29
32
|
};
|
|
30
33
|
|
|
34
|
+
const MAX_SUMMARY_THREADS = 64;
|
|
35
|
+
const MAX_SUMMARY_CHECKS = 20;
|
|
36
|
+
const MAX_SUMMARY_AUTHORS = 12;
|
|
37
|
+
const MAX_SUMMARY_TEXT_CHARS = 256;
|
|
38
|
+
const MAX_SUMMARY_SIGNALS = 16;
|
|
39
|
+
const MAX_SUMMARY_CHARS = 32768;
|
|
40
|
+
|
|
31
41
|
/** Render the one-line headline for a canonical verdict, failing closed. */
|
|
32
42
|
function verdictHeadline(verdict) {
|
|
33
43
|
return VERDICT_HEADLINE[String(verdict || '').toUpperCase()] || VERDICT_HEADLINE.UNKNOWN;
|
|
@@ -42,7 +52,7 @@ function verdictHeadline(verdict) {
|
|
|
42
52
|
* one line per diagnostic.
|
|
43
53
|
*/
|
|
44
54
|
function mdCode(value) {
|
|
45
|
-
const text =
|
|
55
|
+
const text = normalizeEvidenceText(value, { maxChars: MAX_SUMMARY_TEXT_CHARS });
|
|
46
56
|
let longestRun = 0;
|
|
47
57
|
let currentRun = 0;
|
|
48
58
|
for (const character of text) {
|
|
@@ -65,19 +75,38 @@ const MAX_THREADS_PER_AUTHOR = 8;
|
|
|
65
75
|
function groupByAuthor(comments) {
|
|
66
76
|
const byAuthor = new Map();
|
|
67
77
|
for (const comment of (Array.isArray(comments) ? comments : [])) {
|
|
68
|
-
const author =
|
|
78
|
+
const author = normalizeEvidenceText(comment.author || 'unknown', { maxChars: MAX_SUMMARY_TEXT_CHARS });
|
|
69
79
|
if (!byAuthor.has(author)) byAuthor.set(author, []);
|
|
70
80
|
byAuthor.get(author).push(comment);
|
|
71
81
|
}
|
|
72
|
-
return [...byAuthor.entries()].sort(
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
return [...byAuthor.entries()].sort((a, b) => {
|
|
83
|
+
if (b[1].length !== a[1].length) return b[1].length - a[1].length;
|
|
84
|
+
if (a[0] < b[0]) return -1;
|
|
85
|
+
if (a[0] > b[0]) return 1;
|
|
86
|
+
return 0;
|
|
87
|
+
});
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
/** One-line locator for a thread: `path:line` when known, else its id. */
|
|
78
91
|
function threadLocator(thread) {
|
|
79
|
-
if (thread.path)
|
|
80
|
-
|
|
92
|
+
if (thread.path) {
|
|
93
|
+
const path = normalizeEvidenceText(thread.path, { maxChars: MAX_SUMMARY_TEXT_CHARS });
|
|
94
|
+
return thread.line != null ? `${path}:${thread.line}` : path;
|
|
95
|
+
}
|
|
96
|
+
return normalizeEvidenceText(thread.threadId || '(thread)', { maxChars: MAX_SUMMARY_TEXT_CHARS });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validThread(thread) {
|
|
100
|
+
if (!thread || typeof thread !== 'object' || Array.isArray(thread)
|
|
101
|
+
|| typeof thread.author !== 'string' || !thread.author.trim()) return false;
|
|
102
|
+
const hasPath = typeof thread.path === 'string' && thread.path.trim();
|
|
103
|
+
const hasId = typeof thread.threadId === 'string' && thread.threadId.trim();
|
|
104
|
+
return Boolean(hasPath || hasId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validCheck(check) {
|
|
108
|
+
return Boolean(check && typeof check === 'object' && !Array.isArray(check)
|
|
109
|
+
&& typeof check.name === 'string' && check.name.trim());
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
/** Render unresolved review threads, preserving fail-closed availability. */
|
|
@@ -89,22 +118,34 @@ function renderThreads(bundle, lines) {
|
|
|
89
118
|
lines.push('### Review threads');
|
|
90
119
|
lines.push(`⚠️ Review threads were **unreadable** this pass (${mdCode(why)}) — not treated as zero. Re-run once the read recovers.`);
|
|
91
120
|
lines.push('');
|
|
92
|
-
return;
|
|
121
|
+
return 'INCOMPLETE';
|
|
93
122
|
}
|
|
94
123
|
|
|
95
|
-
|
|
124
|
+
if (!Array.isArray(bundle.unresolvedComments)
|
|
125
|
+
|| bundle.unresolvedComments.slice(0, MAX_SUMMARY_THREADS + 1).some((thread) => !validThread(thread))) {
|
|
126
|
+
lines.push(
|
|
127
|
+
'### Review threads',
|
|
128
|
+
'⚠️ Review thread evidence was **malformed** this pass — not treated as zero.',
|
|
129
|
+
''
|
|
130
|
+
);
|
|
131
|
+
return 'INCOMPLETE';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const comments = bundle.unresolvedComments;
|
|
135
|
+
const incomplete = comments.length > MAX_SUMMARY_THREADS;
|
|
96
136
|
if (comments.length === 0) {
|
|
97
137
|
lines.push('### Review threads');
|
|
98
138
|
lines.push('✅ No unresolved review threads.');
|
|
99
139
|
lines.push('');
|
|
100
|
-
return;
|
|
140
|
+
return 'ZERO';
|
|
101
141
|
}
|
|
102
142
|
|
|
103
|
-
const
|
|
143
|
+
const boundedComments = comments.slice(0, MAX_SUMMARY_THREADS);
|
|
144
|
+
const groups = groupByAuthor(boundedComments).slice(0, MAX_SUMMARY_AUTHORS);
|
|
104
145
|
lines.push(`### Unresolved review threads (${comments.length})`);
|
|
105
146
|
lines.push('');
|
|
106
147
|
for (const [author, threads] of groups) {
|
|
107
|
-
lines.push(`- **${author}** — ${threads.length}`);
|
|
148
|
+
lines.push(`- **${mdCode(author)}** — ${threads.length}`);
|
|
108
149
|
for (const thread of threads.slice(0, MAX_THREADS_PER_AUTHOR)) {
|
|
109
150
|
lines.push(` - ${mdCode(threadLocator(thread))}`);
|
|
110
151
|
}
|
|
@@ -112,7 +153,15 @@ function renderThreads(bundle, lines) {
|
|
|
112
153
|
lines.push(` - …and ${threads.length - MAX_THREADS_PER_AUTHOR} more`);
|
|
113
154
|
}
|
|
114
155
|
}
|
|
156
|
+
if (comments.length > boundedComments.length) {
|
|
157
|
+
lines.push(`- …and ${comments.length - boundedComments.length} more thread(s) omitted by the bounded summary`);
|
|
158
|
+
}
|
|
159
|
+
const representedThreads = groups.reduce((total, [, threads]) => total + threads.length, 0);
|
|
160
|
+
if (representedThreads < boundedComments.length) {
|
|
161
|
+
lines.push(`- …and ${boundedComments.length - representedThreads} thread(s) from additional authors`);
|
|
162
|
+
}
|
|
115
163
|
lines.push('');
|
|
164
|
+
return incomplete ? 'INCOMPLETE' : 'OPEN';
|
|
116
165
|
}
|
|
117
166
|
|
|
118
167
|
/** Render failing and pending checks, preserving fail-closed availability. */
|
|
@@ -123,24 +172,43 @@ function renderChecks(bundle, lines) {
|
|
|
123
172
|
lines.push('### Checks');
|
|
124
173
|
lines.push('⚠️ Checks were **unreadable** this pass — not treated as green. Re-run once the read recovers.');
|
|
125
174
|
lines.push('');
|
|
126
|
-
return;
|
|
175
|
+
return false;
|
|
127
176
|
}
|
|
128
177
|
|
|
129
178
|
const ci = bundle.ci || {};
|
|
130
|
-
const
|
|
131
|
-
|
|
179
|
+
const checkArrays = [ci.checks, ci.failing, ci.pending];
|
|
180
|
+
if (checkArrays.some((checks) => !Array.isArray(checks))
|
|
181
|
+
|| checkArrays.some((checks) => checks.slice(0, MAX_SUMMARY_CHECKS + 1)
|
|
182
|
+
.some((check) => !validCheck(check)))) {
|
|
183
|
+
lines.push(
|
|
184
|
+
'### Checks',
|
|
185
|
+
'⚠️ Check evidence was **malformed** this pass — not treated as green.',
|
|
186
|
+
''
|
|
187
|
+
);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const failing = ci.failing;
|
|
191
|
+
const pending = ci.pending;
|
|
192
|
+
const incomplete = checkArrays.some((checks) => checks.length > MAX_SUMMARY_CHECKS);
|
|
132
193
|
lines.push('### Checks');
|
|
133
194
|
if (failing.length === 0 && pending.length === 0) {
|
|
134
195
|
lines.push('✅ No failing or pending checks.');
|
|
135
196
|
} else {
|
|
136
197
|
if (failing.length > 0) {
|
|
137
|
-
lines.push(`- ❌ **Failing (${failing.length}):** ${failing.map((check) => mdCode(check.name || '?')).join(', ')}`);
|
|
198
|
+
lines.push(`- ❌ **Failing (${failing.length}):** ${failing.slice(0, MAX_SUMMARY_CHECKS).map((check) => mdCode(check.name || '?')).join(', ')}`);
|
|
199
|
+
if (failing.length > MAX_SUMMARY_CHECKS) {
|
|
200
|
+
lines.push(` - …and ${failing.length - MAX_SUMMARY_CHECKS} more failing check(s)`);
|
|
201
|
+
}
|
|
138
202
|
}
|
|
139
203
|
if (pending.length > 0) {
|
|
140
|
-
lines.push(`- ⏳ **Pending (${pending.length}):** ${pending.map((check) => mdCode(check.name || '?')).join(', ')}`);
|
|
204
|
+
lines.push(`- ⏳ **Pending (${pending.length}):** ${pending.slice(0, MAX_SUMMARY_CHECKS).map((check) => mdCode(check.name || '?')).join(', ')}`);
|
|
205
|
+
if (pending.length > MAX_SUMMARY_CHECKS) {
|
|
206
|
+
lines.push(` - …and ${pending.length - MAX_SUMMARY_CHECKS} more pending check(s)`);
|
|
207
|
+
}
|
|
141
208
|
}
|
|
142
209
|
}
|
|
143
210
|
lines.push('');
|
|
211
|
+
return !incomplete;
|
|
144
212
|
}
|
|
145
213
|
|
|
146
214
|
/**
|
|
@@ -159,8 +227,11 @@ function renderSummary(bundle = {}, opts = {}) {
|
|
|
159
227
|
const lines = ['## 🔭 Forge PR Monitor', ''];
|
|
160
228
|
const verdict = String(opts.verdict || '').toUpperCase();
|
|
161
229
|
const isUnknown = verdict === 'UNKNOWN' || !VERDICT_HEADLINE[verdict];
|
|
162
|
-
const unreadable = Array.isArray(opts.unreadable)
|
|
230
|
+
const unreadable = Array.isArray(opts.unreadable)
|
|
231
|
+
? opts.unreadable.filter(Boolean).slice(0, MAX_SUMMARY_SIGNALS)
|
|
232
|
+
: [];
|
|
163
233
|
|
|
234
|
+
const headlineIndex = lines.length;
|
|
164
235
|
lines.push(verdictHeadline(opts.verdict));
|
|
165
236
|
if (isUnknown && unreadable.length > 0) {
|
|
166
237
|
lines.push('');
|
|
@@ -170,8 +241,8 @@ function renderSummary(bundle = {}, opts = {}) {
|
|
|
170
241
|
lines.push('_Surfaces open review + check state so async feedback never rots. This monitor **does not merge** and never resolves review threads — a human merges in the GitHub UI._');
|
|
171
242
|
lines.push('');
|
|
172
243
|
|
|
173
|
-
renderThreads(bundle, lines);
|
|
174
|
-
renderChecks(bundle, lines);
|
|
244
|
+
let threadState = renderThreads(bundle, lines);
|
|
245
|
+
const checksComplete = renderChecks(bundle, lines);
|
|
175
246
|
|
|
176
247
|
const branch = bundle.branch || {};
|
|
177
248
|
if ((branch.behind || 0) > 0) {
|
|
@@ -180,14 +251,40 @@ function renderSummary(bundle = {}, opts = {}) {
|
|
|
180
251
|
}
|
|
181
252
|
|
|
182
253
|
const pr = opts.pr || bundle.pr || '<pr>';
|
|
183
|
-
|
|
184
|
-
lines.push(
|
|
185
|
-
|
|
254
|
+
const detailCommand = mdCode(`forge shepherd ${pr} --pull --json`);
|
|
255
|
+
lines.push(
|
|
256
|
+
'---',
|
|
257
|
+
`Detailed JSON: ${detailCommand}`,
|
|
258
|
+
`_Updated ${now.toISOString()} · summary-only monitor · labels state, never merges, never resolves threads._`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
let evidenceStatus = threadState === 'INCOMPLETE' || !checksComplete ? 'INCOMPLETE' : 'COMPLETE';
|
|
262
|
+
if (evidenceStatus === 'INCOMPLETE') lines[headlineIndex] = verdictHeadline('INCOMPLETE');
|
|
263
|
+
let body = lines.join('\n');
|
|
264
|
+
if (body.length > MAX_SUMMARY_CHARS) {
|
|
265
|
+
threadState = 'INCOMPLETE';
|
|
266
|
+
evidenceStatus = 'INCOMPLETE';
|
|
267
|
+
body = [
|
|
268
|
+
'## 🔭 Forge PR Monitor',
|
|
269
|
+
'',
|
|
270
|
+
verdictHeadline('INCOMPLETE'),
|
|
271
|
+
'',
|
|
272
|
+
'⚠️ Summary evidence exceeded the bounded rendering limit; inspect the detailed JSON.',
|
|
273
|
+
'',
|
|
274
|
+
'_This monitor does not merge and never resolves review threads._',
|
|
275
|
+
'',
|
|
276
|
+
`Detailed JSON: ${detailCommand}`,
|
|
277
|
+
`_Updated ${now.toISOString()} · summary-only monitor · labels state, never merges, never resolves threads._`,
|
|
278
|
+
].join('\n');
|
|
279
|
+
}
|
|
186
280
|
|
|
187
|
-
return { body
|
|
281
|
+
return { body, evidenceStatus, threadState };
|
|
188
282
|
}
|
|
189
283
|
|
|
190
284
|
module.exports = {
|
|
285
|
+
MAX_SUMMARY_CHARS,
|
|
286
|
+
MAX_SUMMARY_CHECKS,
|
|
287
|
+
MAX_SUMMARY_THREADS,
|
|
191
288
|
renderSummary,
|
|
192
289
|
verdictHeadline,
|
|
193
290
|
groupByAuthor,
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const preflightCommand = require('../commands/preflight');
|
|
6
|
+
|
|
7
|
+
const MAX_FINDINGS = 20;
|
|
8
|
+
const MAX_FINDING_CHARS = 500;
|
|
9
|
+
|
|
10
|
+
function boundedLines(value) {
|
|
11
|
+
return String(value || '')
|
|
12
|
+
.split(/\r?\n/)
|
|
13
|
+
.map(line => line.trim())
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.slice(0, MAX_FINDINGS)
|
|
16
|
+
.map(line => line.slice(0, MAX_FINDING_CHARS));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function defaultProbeCodeRabbit({ projectRoot }, exec = execFileSync) {
|
|
20
|
+
try {
|
|
21
|
+
exec('coderabbit', ['--version'], {
|
|
22
|
+
cwd: projectRoot,
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
timeout: 10_000,
|
|
25
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
});
|
|
28
|
+
return { available: true };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const reason = error?.code === 'ENOENT' ? 'CLI not installed' : 'CLI unavailable or unauthenticated';
|
|
31
|
+
return { available: false, reason };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseCodeRabbitEvents(output) {
|
|
36
|
+
const rawLines = String(output || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
37
|
+
const events = [];
|
|
38
|
+
let malformed = rawLines.length === 0;
|
|
39
|
+
for (const line of rawLines) {
|
|
40
|
+
try {
|
|
41
|
+
const event = JSON.parse(line);
|
|
42
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) malformed = true;
|
|
43
|
+
else events.push(event);
|
|
44
|
+
} catch {
|
|
45
|
+
malformed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { events, malformed };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function defaultRunCodeRabbit({ projectRoot, base, baseCommit }, exec = execFileSync) {
|
|
52
|
+
try {
|
|
53
|
+
const output = exec('coderabbit', ['review', '--agent', '--base', baseCommit || base], {
|
|
54
|
+
cwd: projectRoot,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: 180_000,
|
|
57
|
+
maxBuffer: 1024 * 1024,
|
|
58
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
});
|
|
61
|
+
const { events, malformed } = parseCodeRabbitEvents(output);
|
|
62
|
+
const terminal = events.at(-1);
|
|
63
|
+
const terminalValid = terminal?.type === 'complete'
|
|
64
|
+
&& ['review_completed', 'review_skipped'].includes(terminal.status)
|
|
65
|
+
&& Number.isSafeInteger(terminal.findings) && terminal.findings >= 0;
|
|
66
|
+
const findingEvents = events.filter(event => event.type === 'finding');
|
|
67
|
+
const errorEvents = events.filter(event => event.type === 'error');
|
|
68
|
+
const clean = !malformed && terminalValid && terminal.findings === 0
|
|
69
|
+
&& findingEvents.length === 0 && errorEvents.length === 0;
|
|
70
|
+
const findings = findingEvents.slice(0, MAX_FINDINGS).map(event => String(
|
|
71
|
+
event.codegenInstructions || event.message || 'CodeRabbit finding',
|
|
72
|
+
).slice(0, MAX_FINDING_CHARS));
|
|
73
|
+
if (!clean && findings.length === 0) {
|
|
74
|
+
let detail = `CodeRabbit reported ${terminal?.findings || errorEvents.length} finding(s)`;
|
|
75
|
+
if (malformed || !terminalValid) {
|
|
76
|
+
detail = 'CodeRabbit agent output was malformed or incomplete';
|
|
77
|
+
}
|
|
78
|
+
findings.push(detail);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: clean,
|
|
82
|
+
summary: clean ? 'local review completed with no findings' : `${findings.length} local review finding(s)`,
|
|
83
|
+
findings: clean ? [] : findings,
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const output = `${error?.stdout || ''}\n${error?.stderr || ''}`;
|
|
87
|
+
const findings = boundedLines(output);
|
|
88
|
+
if (/unauthori[sz]ed|authentication|not authenticated|log in|login required/i.test(output)) {
|
|
89
|
+
return { ok: false, unavailable: true, summary: 'CLI authentication unavailable', findings: [] };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
summary: findings[0] || 'local review failed',
|
|
94
|
+
findings,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function defaultRunDeterministic({ projectRoot, base, baseCommit, baseRef: resolvedBaseRef }, exec = execFileSync) {
|
|
100
|
+
const baseRef = baseCommit || resolvedBaseRef || (String(base || '').startsWith('origin/')
|
|
101
|
+
? String(base)
|
|
102
|
+
: `origin/${String(base || '')}`);
|
|
103
|
+
return preflightCommand.handler([], {}, projectRoot, {
|
|
104
|
+
log: () => {},
|
|
105
|
+
resolveChangeSet: ({ runAll }) => preflightCommand.resolveChangeSet(exec, { runAll, baseRef }),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function defaultResolveBaseCommit({ projectRoot, base, baseRef }, exec = execFileSync) {
|
|
110
|
+
const ref = baseRef || (String(base || '').startsWith('origin/')
|
|
111
|
+
? String(base)
|
|
112
|
+
: `origin/${String(base || '')}`);
|
|
113
|
+
const resolved = String(exec('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
114
|
+
cwd: projectRoot,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
timeout: 10_000,
|
|
117
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
}) || '').trim().toLowerCase();
|
|
120
|
+
if (!/^[0-9a-f]{40}$/.test(resolved)) throw new Error(`PR base commit is unavailable for ${ref}`);
|
|
121
|
+
return resolved;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function providerResult(status, ok, summary, findings = []) {
|
|
125
|
+
return { status, ok, summary, findings: findings.slice(0, MAX_FINDINGS) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function deterministicFindings(results) {
|
|
129
|
+
return (Array.isArray(results) ? results : [])
|
|
130
|
+
.filter(result => result?.ok === false && !result.skipped)
|
|
131
|
+
.slice(0, MAX_FINDINGS)
|
|
132
|
+
.map(result => ({
|
|
133
|
+
provider: String(result.name || 'deterministic-gate').slice(0, 80),
|
|
134
|
+
detail: String(result.summary || 'gate failed').slice(0, MAX_FINDING_CHARS),
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function notApplicableResult(context) {
|
|
139
|
+
const expectedHead = typeof context.expectedHead === 'string'
|
|
140
|
+
&& /^[0-9a-f]{40}$/i.test(context.expectedHead) ? context.expectedHead.toLowerCase() : null;
|
|
141
|
+
const localHead = typeof context.localHead === 'string' ? context.localHead.toLowerCase() : null;
|
|
142
|
+
const exactHeadAvailable = expectedHead !== null;
|
|
143
|
+
const exactHeadCheckedOut = exactHeadAvailable && localHead === expectedHead;
|
|
144
|
+
if (exactHeadCheckedOut && context.cleanTree === true) return null;
|
|
145
|
+
const dirtyCheckout = exactHeadCheckedOut && context.cleanTree !== true;
|
|
146
|
+
const summary = dirtyCheckout
|
|
147
|
+
? 'local checkout has uncommitted changes'
|
|
148
|
+
: (exactHeadAvailable ? 'local checkout is not the PR head' : 'PR head is unavailable');
|
|
149
|
+
return {
|
|
150
|
+
status: 'INCOMPLETE',
|
|
151
|
+
blocking: true,
|
|
152
|
+
providers: {
|
|
153
|
+
coderabbit: providerResult(
|
|
154
|
+
'NOT_APPLICABLE',
|
|
155
|
+
false,
|
|
156
|
+
summary,
|
|
157
|
+
),
|
|
158
|
+
deterministic: { status: 'NOT_APPLICABLE', ok: false, results: [] },
|
|
159
|
+
},
|
|
160
|
+
findings: [{ provider: 'local-preflight', detail: summary }],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function codeRabbitResult(context, probeCodeRabbit, runCodeRabbit) {
|
|
165
|
+
const probe = await probeCodeRabbit(context);
|
|
166
|
+
if (probe?.available !== true) {
|
|
167
|
+
return providerResult('UNAVAILABLE', false, probe?.reason || 'CLI unavailable');
|
|
168
|
+
}
|
|
169
|
+
const review = await runCodeRabbit(context);
|
|
170
|
+
if (review?.unavailable === true) {
|
|
171
|
+
return providerResult('UNAVAILABLE', false, review.summary || 'CLI unavailable');
|
|
172
|
+
}
|
|
173
|
+
return providerResult(
|
|
174
|
+
review?.ok === true ? 'PASS' : 'FAIL',
|
|
175
|
+
review?.ok === true,
|
|
176
|
+
review?.summary || 'local review completed',
|
|
177
|
+
Array.isArray(review?.findings) ? review.findings : [],
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function deterministicResult(context, runDeterministic) {
|
|
182
|
+
try {
|
|
183
|
+
return await runDeterministic(context);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return { success: false, results: [], reason: error?.message || String(error) };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runLocalReviewPreflight(context, deps = {}) {
|
|
190
|
+
const probeCodeRabbit = deps.probeCodeRabbit || defaultProbeCodeRabbit;
|
|
191
|
+
const runCodeRabbit = deps.runCodeRabbit || defaultRunCodeRabbit;
|
|
192
|
+
const runDeterministic = deps.runDeterministic || defaultRunDeterministic;
|
|
193
|
+
|
|
194
|
+
const notApplicable = notApplicableResult(context);
|
|
195
|
+
if (notApplicable) return notApplicable;
|
|
196
|
+
|
|
197
|
+
let reviewContext = context;
|
|
198
|
+
const suppliedBaseCommit = typeof context.baseCommit === 'string'
|
|
199
|
+
&& /^[0-9a-f]{40}$/i.test(context.baseCommit);
|
|
200
|
+
const shouldResolveBase = deps.resolveBaseCommit !== false && !suppliedBaseCommit;
|
|
201
|
+
if (shouldResolveBase) {
|
|
202
|
+
const resolveBaseCommit = typeof deps.resolveBaseCommit === 'function'
|
|
203
|
+
? deps.resolveBaseCommit
|
|
204
|
+
: defaultResolveBaseCommit;
|
|
205
|
+
try {
|
|
206
|
+
const baseCommit = String(await resolveBaseCommit(context) || '').trim().toLowerCase();
|
|
207
|
+
if (!/^[0-9a-f]{40}$/.test(baseCommit)) throw new Error('PR base commit resolver returned an invalid commit');
|
|
208
|
+
reviewContext = { ...context, baseCommit };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const detail = error?.message || String(error);
|
|
211
|
+
return {
|
|
212
|
+
status: 'INCOMPLETE',
|
|
213
|
+
blocking: true,
|
|
214
|
+
providers: {
|
|
215
|
+
coderabbit: providerResult('NOT_APPLICABLE', false, detail),
|
|
216
|
+
deterministic: { status: 'NOT_APPLICABLE', ok: false, results: [] },
|
|
217
|
+
},
|
|
218
|
+
findings: [{ provider: 'local-preflight', detail }],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
} else if (suppliedBaseCommit) {
|
|
222
|
+
reviewContext = { ...context, baseCommit: context.baseCommit.toLowerCase() };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const coderabbit = await codeRabbitResult(reviewContext, probeCodeRabbit, runCodeRabbit);
|
|
226
|
+
const deterministic = await deterministicResult(reviewContext, runDeterministic);
|
|
227
|
+
const gateResults = Array.isArray(deterministic?.results) ? deterministic.results : [];
|
|
228
|
+
const deterministicOk = deterministic?.success === true;
|
|
229
|
+
const findings = [
|
|
230
|
+
...coderabbit.findings.map(detail => ({ provider: 'coderabbit', detail })),
|
|
231
|
+
...deterministicFindings(gateResults),
|
|
232
|
+
].slice(0, MAX_FINDINGS);
|
|
233
|
+
if (!deterministicOk && findings.length === 0) {
|
|
234
|
+
findings.push({
|
|
235
|
+
provider: 'deterministic-gates',
|
|
236
|
+
detail: String(deterministic?.reason || 'preflight failed').slice(0, MAX_FINDING_CHARS),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const failed = !deterministicOk || coderabbit.status === 'FAIL';
|
|
241
|
+
const incomplete = coderabbit.status === 'UNAVAILABLE';
|
|
242
|
+
const blocking = failed || incomplete;
|
|
243
|
+
let status = 'PASS';
|
|
244
|
+
if (failed) status = 'FAIL';
|
|
245
|
+
else if (incomplete) status = 'INCOMPLETE';
|
|
246
|
+
return {
|
|
247
|
+
status,
|
|
248
|
+
blocking,
|
|
249
|
+
providers: {
|
|
250
|
+
coderabbit,
|
|
251
|
+
deterministic: {
|
|
252
|
+
status: deterministicOk ? 'PASS' : 'FAIL',
|
|
253
|
+
ok: deterministicOk,
|
|
254
|
+
results: gateResults,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
findings,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
MAX_FINDINGS,
|
|
263
|
+
boundedLines,
|
|
264
|
+
defaultProbeCodeRabbit,
|
|
265
|
+
defaultResolveBaseCommit,
|
|
266
|
+
defaultRunCodeRabbit,
|
|
267
|
+
defaultRunDeterministic,
|
|
268
|
+
runLocalReviewPreflight,
|
|
269
|
+
};
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* one lock. This module is the lease PRIMITIVE only: pure fs + I/O, no spawned
|
|
17
17
|
* process and no reconcile loop (the daemon wires those up later).
|
|
18
18
|
*
|
|
19
|
-
* Payload JSON: `{ pid, token, startedAt, heartbeatAt
|
|
19
|
+
* Payload JSON: `{ pid, token, startedAt, heartbeatAt }`.
|
|
20
|
+
* Per-PR watcher authority lives only in Kernel owner rows.
|
|
20
21
|
*
|
|
21
22
|
* ## Ownership is by TOKEN, not pid
|
|
22
23
|
* Each successful `acquire` mints a unique `token`. Every mutating op
|
|
23
|
-
* (`stamp`/`
|
|
24
|
+
* (`stamp`/`release`) verifies that token against the on-disk
|
|
24
25
|
* lock before writing. A pid can be reused after a crash/reboot, and a wedged
|
|
25
26
|
* owner can revive after its lease was reclaimed — in both cases the token no
|
|
26
27
|
* longer matches, so the superseded holder can never resurrect or mutate a lease
|
|
@@ -128,6 +129,9 @@ function tryExclusiveCreate(file, payload) {
|
|
|
128
129
|
* -> { ok:true, file, token, reclaimed } stale lock (dead/wedged owner) reclaimed
|
|
129
130
|
* -> { ok:false, held } a LIVE, FRESH foreign owner holds it
|
|
130
131
|
* -> { ok:false, held } we lost the atomic takeover race
|
|
132
|
+
* -> { ok:false, held:null, legacyMigrationPending:true, reason:'legacy-lease-unreadable' }
|
|
133
|
+
* the lock exists but cannot be parsed —
|
|
134
|
+
* never reclaimed, never deleted here
|
|
131
135
|
*
|
|
132
136
|
* `pid`/`isAlive`/`now`/`token` are injectable for testing. `onBeforeTakeover` is
|
|
133
137
|
* a test seam invoked AFTER the stale lock is removed and BEFORE our exclusive
|
|
@@ -141,11 +145,12 @@ function acquire(projectRoot, {
|
|
|
141
145
|
now = () => Date.now(),
|
|
142
146
|
token = crypto.randomUUID(),
|
|
143
147
|
onBeforeTakeover = null,
|
|
148
|
+
preserveLegacy = false,
|
|
144
149
|
} = {}) {
|
|
145
150
|
const file = lockFilePath(projectRoot, { gitCommonDir });
|
|
146
151
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
147
152
|
const iso = new Date(now()).toISOString();
|
|
148
|
-
const payload = { pid, token, startedAt: iso, heartbeatAt: iso
|
|
153
|
+
const payload = { pid, token, startedAt: iso, heartbeatAt: iso };
|
|
149
154
|
|
|
150
155
|
// Fast path: atomic exclusive create. Only ONE caller can win O_EXCL.
|
|
151
156
|
if (tryExclusiveCreate(file, payload)) {
|
|
@@ -158,15 +163,33 @@ function acquire(projectRoot, {
|
|
|
158
163
|
return { ok: false, held };
|
|
159
164
|
}
|
|
160
165
|
|
|
166
|
+
// NO caller may discard an unreadable lease: its bytes may be the only
|
|
167
|
+
// evidence of legacy watchers, and the daemon acquires HERE but migrates
|
|
168
|
+
// LATER — so an opt-in guard would still delete that evidence before
|
|
169
|
+
// migration could read it. This is unconditional at the shared acquisition
|
|
170
|
+
// boundary: fail closed, leave the bytes exactly as found, and surface a
|
|
171
|
+
// reason a caller can act on. Recovery belongs to migration, never to
|
|
172
|
+
// acquisition.
|
|
173
|
+
if (fs.existsSync(file) && !held) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false, held: null, legacyMigrationPending: true, reason: 'legacy-lease-unreadable',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (preserveLegacy && Array.isArray(held?.watchers)) {
|
|
180
|
+
return { ok: false, held, legacyMigrationPending: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
// Stale (dead/wedged owner, unreadable, or already ours). Take over ATOMICALLY:
|
|
162
184
|
// remove the stale lock, then re-create it with O_EXCL. If a competitor
|
|
163
185
|
// recreated it first, our exclusive create fails (EEXIST) and we back off — so
|
|
164
186
|
// two racers reclaiming the same stale lock can NEVER both win. A wedged owner
|
|
165
187
|
// that revives after we delete its lock is stopped by the per-lease `token`
|
|
166
|
-
// guard on stamp()/
|
|
188
|
+
// guard on stamp()/release(), never able to resurrect it here.
|
|
167
189
|
fs.rmSync(file, { force: true });
|
|
168
190
|
if (typeof onBeforeTakeover === 'function') onBeforeTakeover();
|
|
169
|
-
|
|
191
|
+
const takeoverPayload = Array.isArray(held?.watchers) ? { ...payload, watchers: held.watchers } : payload;
|
|
192
|
+
if (!tryExclusiveCreate(file, takeoverPayload)) {
|
|
170
193
|
return { ok: false, held: readLock(file) };
|
|
171
194
|
}
|
|
172
195
|
return { ok: true, file, token, reclaimed: true };
|
|
@@ -203,19 +226,6 @@ function stopHeartbeat(timer) {
|
|
|
203
226
|
if (timer) clearInterval(timer);
|
|
204
227
|
}
|
|
205
228
|
|
|
206
|
-
/**
|
|
207
|
-
* Rewrite the `watchers[]` array on OUR lock. Returns false when the lock is
|
|
208
|
-
* missing or NOT ours by `token`.
|
|
209
|
-
*/
|
|
210
|
-
function updateWatchers(projectRoot, prNumbers, { gitCommonDir, token, pid = process.pid } = {}) {
|
|
211
|
-
const file = lockFilePath(projectRoot, { gitCommonDir });
|
|
212
|
-
const held = readLock(file);
|
|
213
|
-
if (!held || !ownsLock(held, { token, pid })) return false;
|
|
214
|
-
held.watchers = Array.isArray(prNumbers) ? prNumbers : [];
|
|
215
|
-
writeLock(file, held);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
229
|
/**
|
|
220
230
|
* Release the lease — delete the lock ONLY when it is ours by `token`, so a
|
|
221
231
|
* foreign or already-reclaimed lock is never removed out from under its owner
|
|
@@ -246,7 +256,6 @@ module.exports = {
|
|
|
246
256
|
stamp,
|
|
247
257
|
startHeartbeat,
|
|
248
258
|
stopHeartbeat,
|
|
249
|
-
updateWatchers,
|
|
250
259
|
release,
|
|
251
260
|
owns,
|
|
252
261
|
};
|