@deftai/directive-core 0.107.0 → 0.109.0
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/dist/architecture/sor-preflight.js +2 -0
- package/dist/cache/operations.js +1 -1
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/codebase/provider.js +7 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +60 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +277 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +89 -0
- package/dist/hooks/classify/host-session-identity.js +467 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +9 -0
- package/dist/hooks/classify/paths.js +39 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +94 -1
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +58 -3
- package/dist/hooks/dispatcher.js +762 -73
- package/dist/hooks/fixtures/cases.d.ts +22 -2
- package/dist/hooks/fixtures/cases.js +228 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +83 -4
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +52 -0
- package/dist/orphan-active/evaluate.js +250 -128
- package/dist/orphan-active/index.d.ts +2 -0
- package/dist/orphan-active/index.js +2 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +42 -11
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +14 -3
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +23 -9
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +74 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +186 -4
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +48 -9
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +34 -13
- package/dist/scm/gh-rest.js +168 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +259 -13
- package/dist/session/occupancy.js +877 -33
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +7 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge-time closeout attestability gate (#3781).
|
|
3
|
+
*
|
|
4
|
+
* A pull request may leave a brief unattested; it may not MERGE one whose issue
|
|
5
|
+
* it closes in the same act. The trigger is the PR's structured closing
|
|
6
|
+
* references, never the branch diff: CI runs before the merge and the issue
|
|
7
|
+
* closes on it, so at check time no orphan exists yet and a diff-keyed gate can
|
|
8
|
+
* never fail the PR that introduces the problem (PR #3786's own merge gate
|
|
9
|
+
* returned success while creating one). The brief also need not be in the diff —
|
|
10
|
+
* for #3598 it landed on master seventeen hours before its closing PR.
|
|
11
|
+
*
|
|
12
|
+
* The rule itself is not restated here. `evaluateAcceptanceEvidenceGate` is the
|
|
13
|
+
* single decision procedure `scope:complete` enforces, and this gate calls it so
|
|
14
|
+
* the two cannot drift.
|
|
15
|
+
*/
|
|
16
|
+
import { type GateRunner } from "../orphan-active/issue-state.js";
|
|
17
|
+
import type { RunGhFn } from "../pr-protected-issues/types.js";
|
|
18
|
+
import { type StrictAcceptanceAxis } from "../scope/acceptance-evidence.js";
|
|
19
|
+
export type OutputStream = "stdout" | "stderr" | "none";
|
|
20
|
+
/** One acceptance criterion the merge would strand without evidence or disposition. */
|
|
21
|
+
export interface UnattestedCriterion {
|
|
22
|
+
/** Plan-item path, e.g. `items[3]` or `items[1].subItems[0]`. */
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
/** Verbatim detail from `evaluateAcceptanceEvidenceGate` — the reason it refused. */
|
|
26
|
+
readonly detail: string;
|
|
27
|
+
/**
|
|
28
|
+
* Strict axes inferred for this criterion (#3240). Non-empty means `merge` and
|
|
29
|
+
* `review` evidence cannot satisfy it, so the message must say which kind can.
|
|
30
|
+
*/
|
|
31
|
+
readonly requiredAxes: readonly StrictAcceptanceAxis[];
|
|
32
|
+
}
|
|
33
|
+
/** An active/running brief the PR's closing reference would orphan on merge. */
|
|
34
|
+
export interface CloseoutFinding {
|
|
35
|
+
/** Project-root-relative brief path. */
|
|
36
|
+
readonly briefPath: string;
|
|
37
|
+
/** The closing-referenced issue this brief tracks. */
|
|
38
|
+
readonly issue: number;
|
|
39
|
+
readonly unattested: readonly UnattestedCriterion[];
|
|
40
|
+
}
|
|
41
|
+
export interface PrCloseoutAttestableResult {
|
|
42
|
+
/** 0 attestable / 1 unattested closeout / 2 config or lookup error. */
|
|
43
|
+
readonly code: 0 | 1 | 2;
|
|
44
|
+
readonly message: string;
|
|
45
|
+
readonly stream: OutputStream;
|
|
46
|
+
readonly prNumber: number;
|
|
47
|
+
/** Structured closing-issue references read from the forge. */
|
|
48
|
+
readonly closingIssues: readonly number[];
|
|
49
|
+
readonly findings: readonly CloseoutFinding[];
|
|
50
|
+
/** True when the closing-reference read resolved through `ghx`, a cached GET proxy. */
|
|
51
|
+
readonly proxied: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Reads the PR's structured closing-issue references; `null` on lookup failure. */
|
|
54
|
+
export type FetchClosingIssuesFn = (prNumber: number, repo: string | null, runGh: RunGhFn) => number[] | null;
|
|
55
|
+
export interface EvaluateOptions {
|
|
56
|
+
readonly repo?: string | null;
|
|
57
|
+
/**
|
|
58
|
+
* SCM read seam plus its freshness basis. Defaults to `makeGateRunner()`, which
|
|
59
|
+
* pins plain `gh` when present so a cached `ghx` GET cannot fail this gate open.
|
|
60
|
+
*/
|
|
61
|
+
readonly runner?: GateRunner;
|
|
62
|
+
readonly quiet?: boolean;
|
|
63
|
+
/** Closing-reference seam so tests do not need a forge. */
|
|
64
|
+
readonly fetchClosingIssues?: FetchClosingIssuesFn;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fail closed when merging `prNumber` would close an issue whose brief is still
|
|
68
|
+
* `running` in `active/` with acceptance criteria carrying neither
|
|
69
|
+
* `x-directive/evidence` nor `x-directive/disposition`.
|
|
70
|
+
*
|
|
71
|
+
* Exit contract: 0 attestable (including "this PR closes nothing") / 1 unattested
|
|
72
|
+
* closeout / 2 config or closing-reference lookup error. A lookup that cannot be
|
|
73
|
+
* resolved is 2, not 0 — the gate never green-lights a merge it could not check.
|
|
74
|
+
*
|
|
75
|
+
* The brief is read from `projectRoot`'s working tree, which at merge time is the
|
|
76
|
+
* PR head checkout. That is the tree the merge lands, and it is the same
|
|
77
|
+
* working-tree basis `verify:orphan-active` uses.
|
|
78
|
+
*/
|
|
79
|
+
export declare function evaluate(projectRoot: string, prNumber: number, options?: EvaluateOptions): PrCloseoutAttestableResult;
|
|
80
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge-time closeout attestability gate (#3781).
|
|
3
|
+
*
|
|
4
|
+
* A pull request may leave a brief unattested; it may not MERGE one whose issue
|
|
5
|
+
* it closes in the same act. The trigger is the PR's structured closing
|
|
6
|
+
* references, never the branch diff: CI runs before the merge and the issue
|
|
7
|
+
* closes on it, so at check time no orphan exists yet and a diff-keyed gate can
|
|
8
|
+
* never fail the PR that introduces the problem (PR #3786's own merge gate
|
|
9
|
+
* returned success while creating one). The brief also need not be in the diff —
|
|
10
|
+
* for #3598 it landed on master seventeen hours before its closing PR.
|
|
11
|
+
*
|
|
12
|
+
* The rule itself is not restated here. `evaluateAcceptanceEvidenceGate` is the
|
|
13
|
+
* single decision procedure `scope:complete` enforces, and this gate calls it so
|
|
14
|
+
* the two cannot drift.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join, relative, resolve } from "node:path";
|
|
18
|
+
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
19
|
+
import { makeGateRunner } from "../orphan-active/issue-state.js";
|
|
20
|
+
import { collectGithubRefs } from "../orphan-active/refs.js";
|
|
21
|
+
import { fetchClosingIssuesReferences } from "../pr-protected-issues/gh.js";
|
|
22
|
+
import { ACCEPTANCE_DISPOSITION_KEY, ACCEPTANCE_DISPOSITIONS, ACCEPTANCE_EVIDENCE_KEY, ACCEPTANCE_EVIDENCE_KINDS, evaluateAcceptanceEvidenceGate, inferRequiredStrictAxes, } from "../scope/acceptance-evidence.js";
|
|
23
|
+
import { resolveRepo } from "../triage/queue/repo.js";
|
|
24
|
+
function readJson(path) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
27
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
28
|
+
? parsed
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function planOf(data) {
|
|
36
|
+
const plan = data?.plan;
|
|
37
|
+
return typeof plan === "object" && plan !== null && !Array.isArray(plan)
|
|
38
|
+
? plan
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
function relBriefPath(path, projectRoot) {
|
|
42
|
+
try {
|
|
43
|
+
return relative(resolve(projectRoot), resolve(path)).replace(/\\/g, "/");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return path.replace(/\\/g, "/");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function listActiveRunningBriefs(lifecycleRoot) {
|
|
50
|
+
const activeDir = join(lifecycleRoot, "active");
|
|
51
|
+
if (!existsSync(activeDir)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const entry of readdirSync(activeDir, { withFileTypes: true })) {
|
|
56
|
+
if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const path = join(activeDir, entry.name);
|
|
60
|
+
const plan = planOf(readJson(path));
|
|
61
|
+
if (plan === null || String(plan.status ?? "").toLowerCase() !== "running") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
out.push({ path, plan });
|
|
65
|
+
}
|
|
66
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Index plan items by the same path the acceptance gate reports, so a blocker can
|
|
70
|
+
* be mapped back to its item for axis inference. Traversal order mirrors
|
|
71
|
+
* `walkItems` in `scope/acceptance-evidence.ts`; the attestation rule stays there.
|
|
72
|
+
*/
|
|
73
|
+
function indexPlanItems(items, pathPrefix, out) {
|
|
74
|
+
if (!Array.isArray(items)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
items.forEach((item, index) => {
|
|
78
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const obj = item;
|
|
82
|
+
const path = `${pathPrefix}[${index}]`;
|
|
83
|
+
out.set(path, obj);
|
|
84
|
+
indexPlanItems(obj.subItems, `${path}.subItems`, out);
|
|
85
|
+
indexPlanItems(obj.items, `${path}.items`, out);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const DISPOSITION_SHAPE = `${ACCEPTANCE_DISPOSITION_KEY} {disposition: ${ACCEPTANCE_DISPOSITIONS.join("|")}, ` +
|
|
89
|
+
`reason, provenance {kind: operator-cli|operator-session|human-event, actor: <non-agent>}, recorded_at}`;
|
|
90
|
+
function evidenceShape(kinds) {
|
|
91
|
+
return `${ACCEPTANCE_EVIDENCE_KEY} {kind: ${kinds}, pointer, recorded_at, recorded_by}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Name the exact shape this criterion needs. A single evidence.kind covers one
|
|
95
|
+
* axis, so two inferred axes cannot be satisfied by any kind — say that instead of
|
|
96
|
+
* printing a kind list that would still be refused (#3240 suitability rule).
|
|
97
|
+
*/
|
|
98
|
+
function renderCriterion(criterion) {
|
|
99
|
+
const axes = criterion.requiredAxes;
|
|
100
|
+
const lines = [
|
|
101
|
+
` - ${criterion.path} "${criterion.title}"`,
|
|
102
|
+
` why: ${criterion.detail}`,
|
|
103
|
+
];
|
|
104
|
+
if (axes.length > 1) {
|
|
105
|
+
lines.push(` axis: this criterion requires ${axes.join(" + ")}; no single evidence.kind covers`, " two axes, so evidence cannot satisfy it as written", ` needs: pin one axis with "requires": "${axes[0]}" then ` +
|
|
106
|
+
`${evidenceShape(axes[0])},`, " or split the criterion one axis per item,", ` or ${DISPOSITION_SHAPE}`);
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
const kinds = axes.length === 1 ? axes[0] : ACCEPTANCE_EVIDENCE_KINDS.join("|");
|
|
110
|
+
lines.push(` needs: ${evidenceShape(kinds)}`, ` or ${DISPOSITION_SHAPE}`);
|
|
111
|
+
if (axes.length === 1) {
|
|
112
|
+
lines.push(` axis: this criterion requires ${axes[0]} — ` +
|
|
113
|
+
"merge and review evidence cannot satisfy it (#3240)");
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* `gh` was absent so the closing-reference read went through `ghx`, a cached GET
|
|
119
|
+
* proxy this gate cannot inspect. Say so rather than implying a fresh read
|
|
120
|
+
* (#3767 / #3737).
|
|
121
|
+
*/
|
|
122
|
+
const PROXIED_CAVEAT = " Note: `gh` was not on PATH, so the closing-reference read resolved through `ghx`, a cached\n" +
|
|
123
|
+
" GET proxy; freshness is bounded by that proxy, which this gate cannot inspect (#3737).";
|
|
124
|
+
function formatRefusal(prNumber, findings, projectRoot, proxied) {
|
|
125
|
+
const criteria = findings.reduce((sum, f) => sum + f.unattested.length, 0);
|
|
126
|
+
const briefNoun = findings.length === 1 ? "brief" : "briefs";
|
|
127
|
+
const criterionNoun = criteria === 1 ? "criterion" : "criteria";
|
|
128
|
+
const issues = findings.map((f) => `#${f.issue}`).join(", ");
|
|
129
|
+
const lines = [
|
|
130
|
+
`verify:pr-closeout-attestable: PR #${prNumber} closes ${issues}, leaving ${findings.length} ` +
|
|
131
|
+
`active/running ${briefNoun} with ${criteria} unattested acceptance ${criterionNoun} ` +
|
|
132
|
+
`(project_root=${projectRoot}).`,
|
|
133
|
+
" Merging now strands the brief on master: the issue closes, the brief stays running in",
|
|
134
|
+
" active/, and scope:complete then refuses it. A PR may leave a brief unattested; it may",
|
|
135
|
+
" not merge one whose issue is closing in the same act (#3781).",
|
|
136
|
+
" Unattested criteria:",
|
|
137
|
+
];
|
|
138
|
+
for (const finding of findings) {
|
|
139
|
+
lines.push(` ${finding.briefPath} (closes #${finding.issue})`);
|
|
140
|
+
for (const criterion of finding.unattested) {
|
|
141
|
+
lines.push(...renderCriterion(criterion));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lines.push(" Evidence is not authenticated — recorded_by accepts any non-empty string. Record what you", " actually did: a pointer must be the artifact its kind names (a test run for test, this PR's", " merge for merge, a deployment for deploy). An agent may evidence a criterion; only", " human-origin provenance may waive one (#3240 / #2944).", " Remediation (performable by this PR's author): stamp the criteria above on the brief in this", " branch, commit, push, then re-run:", ` task verify:pr-closeout-attestable -- --pr ${prNumber}`, " Trigger is the PR's closing references, not the branch diff. A PR that leaves an unattested", " brief without closing its issue is unaffected.");
|
|
145
|
+
if (proxied) {
|
|
146
|
+
lines.push(PROXIED_CAVEAT);
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
function configError(prNumber, message, proxied = false) {
|
|
151
|
+
return {
|
|
152
|
+
code: 2,
|
|
153
|
+
message: `verify:pr-closeout-attestable: ${message}`,
|
|
154
|
+
stream: "stderr",
|
|
155
|
+
prNumber,
|
|
156
|
+
closingIssues: [],
|
|
157
|
+
findings: [],
|
|
158
|
+
proxied,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Fail closed when merging `prNumber` would close an issue whose brief is still
|
|
163
|
+
* `running` in `active/` with acceptance criteria carrying neither
|
|
164
|
+
* `x-directive/evidence` nor `x-directive/disposition`.
|
|
165
|
+
*
|
|
166
|
+
* Exit contract: 0 attestable (including "this PR closes nothing") / 1 unattested
|
|
167
|
+
* closeout / 2 config or closing-reference lookup error. A lookup that cannot be
|
|
168
|
+
* resolved is 2, not 0 — the gate never green-lights a merge it could not check.
|
|
169
|
+
*
|
|
170
|
+
* The brief is read from `projectRoot`'s working tree, which at merge time is the
|
|
171
|
+
* PR head checkout. That is the tree the merge lands, and it is the same
|
|
172
|
+
* working-tree basis `verify:orphan-active` uses.
|
|
173
|
+
*/
|
|
174
|
+
export function evaluate(projectRoot, prNumber, options = {}) {
|
|
175
|
+
const root = resolve(projectRoot);
|
|
176
|
+
const quiet = options.quiet ?? false;
|
|
177
|
+
if (!existsSync(root)) {
|
|
178
|
+
return configError(prNumber, `project root does not exist: ${root}`);
|
|
179
|
+
}
|
|
180
|
+
let lifecycleRoot;
|
|
181
|
+
try {
|
|
182
|
+
lifecycleRoot = resolveLifecycleRoot(root);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
186
|
+
// Consumers may still be on a legacy vbrief/-only layout (#2112). Closeout
|
|
187
|
+
// attestability applies to xbrief/active/ only — skip cleanly, not config fail.
|
|
188
|
+
if (message.includes("No xbrief/ layout found")) {
|
|
189
|
+
return {
|
|
190
|
+
code: 0,
|
|
191
|
+
message: quiet
|
|
192
|
+
? ""
|
|
193
|
+
: "verify:pr-closeout-attestable: no xbrief/ lifecycle root; nothing to check.",
|
|
194
|
+
stream: quiet ? "none" : "stdout",
|
|
195
|
+
prNumber,
|
|
196
|
+
closingIssues: [],
|
|
197
|
+
findings: [],
|
|
198
|
+
proxied: false,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return configError(prNumber, message);
|
|
202
|
+
}
|
|
203
|
+
if (!existsSync(lifecycleRoot)) {
|
|
204
|
+
return {
|
|
205
|
+
code: 0,
|
|
206
|
+
message: quiet
|
|
207
|
+
? ""
|
|
208
|
+
: "verify:pr-closeout-attestable: no xbrief/ lifecycle root; nothing to check.",
|
|
209
|
+
stream: quiet ? "none" : "stdout",
|
|
210
|
+
prNumber,
|
|
211
|
+
closingIssues: [],
|
|
212
|
+
findings: [],
|
|
213
|
+
proxied: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Pin plain `gh` when it exists: `ghx` is a cached GET proxy and a stale
|
|
217
|
+
// closing-reference read would fail this gate open (#3767 / #3737).
|
|
218
|
+
const runner = options.runner ?? makeGateRunner();
|
|
219
|
+
const fetchClosing = options.fetchClosingIssues ?? fetchClosingIssuesReferences;
|
|
220
|
+
const repo = resolveRepo(options.repo, root);
|
|
221
|
+
if (repo === null || repo.length === 0) {
|
|
222
|
+
// Closing references are repository-scoped. Without the slug this gate could
|
|
223
|
+
// only compare bare numbers, and a same-numbered issue in an unrelated
|
|
224
|
+
// repository would block a valid merge.
|
|
225
|
+
return configError(prNumber, "cannot resolve OWNER/REPO for the closing-reference read. Pass --repo OWNER/REPO, " +
|
|
226
|
+
"set $GH_REPO, or run inside a checkout with a GitHub origin remote.", runner.proxied);
|
|
227
|
+
}
|
|
228
|
+
const linked = fetchClosing(prNumber, repo, runner.runGh);
|
|
229
|
+
if (linked === null) {
|
|
230
|
+
return configError(prNumber, `could not read closing-issue references for PR #${prNumber}` +
|
|
231
|
+
` (repo=${repo}). ` +
|
|
232
|
+
"Refusing to certify the merge on an unverified lookup — retry after fixing gh auth, " +
|
|
233
|
+
"rate limit, or network.", runner.proxied);
|
|
234
|
+
}
|
|
235
|
+
const closingIssues = [...new Set(linked)].sort((a, b) => a - b);
|
|
236
|
+
if (closingIssues.length === 0) {
|
|
237
|
+
return {
|
|
238
|
+
code: 0,
|
|
239
|
+
message: quiet
|
|
240
|
+
? ""
|
|
241
|
+
: `verify:pr-closeout-attestable: PR #${prNumber} closes no issue; closeout attestability does not apply.`,
|
|
242
|
+
stream: quiet ? "none" : "stdout",
|
|
243
|
+
prNumber,
|
|
244
|
+
closingIssues,
|
|
245
|
+
findings: [],
|
|
246
|
+
proxied: runner.proxied,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const closingSet = new Set(closingIssues);
|
|
250
|
+
const findings = [];
|
|
251
|
+
for (const brief of listActiveRunningBriefs(lifecycleRoot)) {
|
|
252
|
+
const { issues } = collectGithubRefs(brief.plan, repo);
|
|
253
|
+
// Match on (repo, number). Closing references are scoped to the PR's repository,
|
|
254
|
+
// so a bare-number match would let an unrelated brief tracking the same number in
|
|
255
|
+
// another repository block this merge. Refs with no repo of their own inherit the
|
|
256
|
+
// PR's repo from collectGithubRefs, which is the correct reading of a bare number.
|
|
257
|
+
// GitHub slugs are case-insensitive; a case-sensitive compare would let
|
|
258
|
+
// DeftAI/Directive vs deftai/directive miss and fail the gate open.
|
|
259
|
+
const issue = issues.find((ref) => ref.repo.toLowerCase() === repo.toLowerCase() && closingSet.has(ref.number))?.number;
|
|
260
|
+
if (issue === undefined) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const gate = evaluateAcceptanceEvidenceGate(brief.plan);
|
|
264
|
+
if (gate.ok) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const itemsByPath = new Map();
|
|
268
|
+
indexPlanItems(brief.plan.items, "items", itemsByPath);
|
|
269
|
+
const unattested = gate.reports
|
|
270
|
+
.filter((report) => report.outcome === "missing" || report.outcome === "invalid")
|
|
271
|
+
.map((report) => {
|
|
272
|
+
const item = itemsByPath.get(report.path);
|
|
273
|
+
return {
|
|
274
|
+
path: report.path,
|
|
275
|
+
title: report.title,
|
|
276
|
+
detail: report.detail,
|
|
277
|
+
requiredAxes: item === undefined ? [] : inferRequiredStrictAxes(item),
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
findings.push({ briefPath: relBriefPath(brief.path, root), issue, unattested });
|
|
281
|
+
}
|
|
282
|
+
if (findings.length > 0) {
|
|
283
|
+
return {
|
|
284
|
+
code: 1,
|
|
285
|
+
message: formatRefusal(prNumber, findings, root, runner.proxied),
|
|
286
|
+
stream: "stderr",
|
|
287
|
+
prNumber,
|
|
288
|
+
closingIssues,
|
|
289
|
+
findings,
|
|
290
|
+
proxied: runner.proxied,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const issueList = closingIssues.map((n) => `#${n}`).join(", ");
|
|
294
|
+
const pass = `verify:pr-closeout-attestable: PR #${prNumber} closes ${issueList}; ` +
|
|
295
|
+
"no active/running brief for those issues has unattested acceptance criteria.";
|
|
296
|
+
return {
|
|
297
|
+
code: 0,
|
|
298
|
+
message: quiet ? "" : runner.proxied ? `${pass}\n${PROXIED_CAVEAT}` : pass,
|
|
299
|
+
stream: quiet ? "none" : "stdout",
|
|
300
|
+
prNumber,
|
|
301
|
+
closingIssues,
|
|
302
|
+
findings: [],
|
|
303
|
+
proxied: runner.proxied,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { defaultWhich } from "../scm/binary.js";
|
|
3
|
+
import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
|
|
4
|
+
import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
|
|
3
5
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
4
6
|
import { GH_TIMEOUT_S } from "./constants.js";
|
|
5
7
|
/** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
|
|
@@ -7,18 +9,39 @@ export function defaultRunGh(cmd) {
|
|
|
7
9
|
if (cmd.length === 0 || cmd[0] !== "gh") {
|
|
8
10
|
return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
|
|
9
11
|
}
|
|
10
|
-
const binary = resolveBinary();
|
|
11
12
|
const args = cmd.slice(1);
|
|
13
|
+
const binary = resolveBinaryForArgv(args[0] ?? "", args.slice(1));
|
|
14
|
+
const execOpts = {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
timeout: GH_TIMEOUT_S * 1000,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
19
|
+
};
|
|
20
|
+
const runOnce = (bin) => execFileSync(bin, args, execOpts);
|
|
12
21
|
try {
|
|
13
|
-
const stdout =
|
|
14
|
-
encoding: "utf8",
|
|
15
|
-
timeout: GH_TIMEOUT_S * 1000,
|
|
16
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
-
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
18
|
-
});
|
|
22
|
+
const stdout = runOnce(binary);
|
|
19
23
|
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
20
24
|
}
|
|
21
|
-
catch (
|
|
25
|
+
catch (caught) {
|
|
26
|
+
let err = caught;
|
|
27
|
+
const first = err;
|
|
28
|
+
const retry = ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
|
|
29
|
+
status: typeof first.status === "number" ? first.status : null,
|
|
30
|
+
error: first.code === "ENOENT" || first.code === "ETIMEDOUT" || typeof first.status !== "number"
|
|
31
|
+
? { code: first.code, message: first.message }
|
|
32
|
+
: undefined,
|
|
33
|
+
stdout: typeof first.stdout === "string" ? first.stdout : "",
|
|
34
|
+
stderr: typeof first.stderr === "string" ? first.stderr : "",
|
|
35
|
+
});
|
|
36
|
+
if (retry !== null) {
|
|
37
|
+
try {
|
|
38
|
+
const stdout = runOnce(retry);
|
|
39
|
+
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
40
|
+
}
|
|
41
|
+
catch (retryErr) {
|
|
42
|
+
err = retryErr;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
const e = err;
|
|
23
46
|
if (e.code === "ENOENT") {
|
|
24
47
|
return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
|
|
@@ -3,6 +3,7 @@ export declare function parseAllowList(values: readonly string[]): Set<number>;
|
|
|
3
3
|
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
4
4
|
export interface RunOptions {
|
|
5
5
|
readonly runGh?: RunGhFn;
|
|
6
|
+
readonly runGit?: RunGhFn;
|
|
6
7
|
}
|
|
7
8
|
export declare function run(argv: readonly string[], options?: RunOptions): number;
|
|
8
9
|
export declare function cmdPrCheckClosingKeywords(argv: readonly string[], options?: RunOptions): number;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
1
3
|
import { EXIT_CONFIG_ERROR, EXIT_HITS_FOUND, EXIT_OK } from "./constants.js";
|
|
2
4
|
import { findAllClosingKeywordHits, findHits, renderHit } from "./detect.js";
|
|
3
5
|
import { defaultRunGh, fetchPrBody, fetchPrCommitMessages } from "./gh.js";
|
|
@@ -23,6 +25,7 @@ function emptyParsed(error) {
|
|
|
23
25
|
pr: null,
|
|
24
26
|
bodyFile: null,
|
|
25
27
|
commitsFile: null,
|
|
28
|
+
fromGitRange: null,
|
|
26
29
|
repo: null,
|
|
27
30
|
allowKnownFalsePositives: [],
|
|
28
31
|
allowClose: [],
|
|
@@ -34,6 +37,7 @@ export function parseArgs(argv) {
|
|
|
34
37
|
let pr = null;
|
|
35
38
|
let bodyFile = null;
|
|
36
39
|
let commitsFile = null;
|
|
40
|
+
let fromGitRange = null;
|
|
37
41
|
let repo = null;
|
|
38
42
|
let mode = "both";
|
|
39
43
|
const allowKnownFalsePositives = [];
|
|
@@ -82,6 +86,17 @@ export function parseArgs(argv) {
|
|
|
82
86
|
else if (arg?.startsWith("--commits-file=")) {
|
|
83
87
|
commitsFile = arg.slice("--commits-file=".length);
|
|
84
88
|
}
|
|
89
|
+
else if (arg === "--from-git-range") {
|
|
90
|
+
const value = argv[i + 1];
|
|
91
|
+
if (value === undefined) {
|
|
92
|
+
return emptyParsed("argument --from-git-range: expected one argument");
|
|
93
|
+
}
|
|
94
|
+
fromGitRange = value;
|
|
95
|
+
i += 1;
|
|
96
|
+
}
|
|
97
|
+
else if (arg?.startsWith("--from-git-range=")) {
|
|
98
|
+
fromGitRange = arg.slice("--from-git-range=".length);
|
|
99
|
+
}
|
|
85
100
|
else if (arg === "--repo") {
|
|
86
101
|
const value = argv[i + 1];
|
|
87
102
|
if (value === undefined) {
|
|
@@ -140,7 +155,50 @@ export function parseArgs(argv) {
|
|
|
140
155
|
return emptyParsed(`unrecognized arguments: ${arg}`);
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
pr,
|
|
160
|
+
bodyFile,
|
|
161
|
+
commitsFile,
|
|
162
|
+
fromGitRange,
|
|
163
|
+
repo,
|
|
164
|
+
allowKnownFalsePositives,
|
|
165
|
+
allowClose,
|
|
166
|
+
mode,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function defaultRunGit(cmd) {
|
|
170
|
+
try {
|
|
171
|
+
const stdout = execFileSync("git", [...cmd], {
|
|
172
|
+
encoding: "utf8",
|
|
173
|
+
timeout: 30_000,
|
|
174
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
175
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
176
|
+
});
|
|
177
|
+
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
const e = err;
|
|
181
|
+
return {
|
|
182
|
+
returncode: typeof e.status === "number" ? e.status : 1,
|
|
183
|
+
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
|
184
|
+
stderr: typeof e.stderr === "string" ? e.stderr : String(e.message ?? ""),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function readGitRange(range, runGit) {
|
|
189
|
+
const result = runGit(["log", range, "--format=%B%n--END--"]);
|
|
190
|
+
if (result.returncode !== 0) {
|
|
191
|
+
process.stderr.write("Error: git log " +
|
|
192
|
+
range +
|
|
193
|
+
" failed: " +
|
|
194
|
+
result.stderr.trim() +
|
|
195
|
+
". Recovery: git fetch origin master.\n");
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return result.stdout
|
|
199
|
+
.split("\n--END--\n")
|
|
200
|
+
.map((part) => part.trim())
|
|
201
|
+
.filter((part) => part.length > 0);
|
|
144
202
|
}
|
|
145
203
|
function filterHits(hits, allowList) {
|
|
146
204
|
return hits.filter((h) => !allowList.has(h.issueNumber));
|
|
@@ -220,8 +278,8 @@ export function run(argv, options = {}) {
|
|
|
220
278
|
commitMessages = msgs;
|
|
221
279
|
}
|
|
222
280
|
else {
|
|
223
|
-
if (args.bodyFile === null && args.commitsFile === null) {
|
|
224
|
-
process.stderr.write("Error: must specify --pr OR --body-file / --commits-file.\n");
|
|
281
|
+
if (args.bodyFile === null && args.commitsFile === null && args.fromGitRange === null) {
|
|
282
|
+
process.stderr.write("Error: must specify --pr OR --body-file / --commits-file / --from-git-range.\n");
|
|
225
283
|
return EXIT_CONFIG_ERROR;
|
|
226
284
|
}
|
|
227
285
|
if (args.bodyFile !== null) {
|
|
@@ -238,6 +296,13 @@ export function run(argv, options = {}) {
|
|
|
238
296
|
}
|
|
239
297
|
commitMessages = msgs;
|
|
240
298
|
}
|
|
299
|
+
if (args.fromGitRange !== null) {
|
|
300
|
+
const msgs = readGitRange(args.fromGitRange, options.runGit ?? defaultRunGit);
|
|
301
|
+
if (msgs === null) {
|
|
302
|
+
return EXIT_CONFIG_ERROR;
|
|
303
|
+
}
|
|
304
|
+
commitMessages = [...commitMessages, ...msgs];
|
|
305
|
+
}
|
|
241
306
|
}
|
|
242
307
|
const runFp = args.mode === "fp" || args.mode === "both";
|
|
243
308
|
const runIntent = args.mode === "intent" || args.mode === "both";
|
|
@@ -18,6 +18,8 @@ export interface ParsedArgs {
|
|
|
18
18
|
readonly pr: number | null;
|
|
19
19
|
readonly bodyFile: string | null;
|
|
20
20
|
readonly commitsFile: string | null;
|
|
21
|
+
/** Offline git range (e.g. origin/master..HEAD) for check-graph wiring (#3969). */
|
|
22
|
+
readonly fromGitRange: string | null;
|
|
21
23
|
readonly repo: string | null;
|
|
22
24
|
readonly allowKnownFalsePositives: readonly string[];
|
|
23
25
|
/** Intent-mode allowlist of issue numbers permitted to use real closing keywords (#3015). */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { defaultWhich } from "../scm/binary.js";
|
|
3
|
+
import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
|
|
4
|
+
import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
|
|
3
5
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
4
6
|
import { GH_TIMEOUT_S, GREPTILE_LOGIN } from "./constants.js";
|
|
5
7
|
/** Display / inventory key for a required context (includes app binding when set). */
|
|
@@ -47,18 +49,39 @@ export function defaultRunGh(cmd) {
|
|
|
47
49
|
if (cmd.length === 0 || cmd[0] !== "gh") {
|
|
48
50
|
return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
|
|
49
51
|
}
|
|
50
|
-
const binary = resolveBinary();
|
|
51
52
|
const args = cmd.slice(1);
|
|
53
|
+
const binary = resolveBinaryForArgv(args[0] ?? "", args.slice(1));
|
|
54
|
+
const execOpts = {
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
timeout: GH_TIMEOUT_S * 1000,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
59
|
+
};
|
|
60
|
+
const runOnce = (bin) => execFileSync(bin, args, execOpts);
|
|
52
61
|
try {
|
|
53
|
-
const stdout =
|
|
54
|
-
encoding: "utf8",
|
|
55
|
-
timeout: GH_TIMEOUT_S * 1000,
|
|
56
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
-
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
58
|
-
});
|
|
62
|
+
const stdout = runOnce(binary);
|
|
59
63
|
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
60
64
|
}
|
|
61
|
-
catch (
|
|
65
|
+
catch (caught) {
|
|
66
|
+
let err = caught;
|
|
67
|
+
const first = err;
|
|
68
|
+
const retry = ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
|
|
69
|
+
status: typeof first.status === "number" ? first.status : null,
|
|
70
|
+
error: first.code === "ENOENT" || first.code === "ETIMEDOUT" || typeof first.status !== "number"
|
|
71
|
+
? { code: first.code, message: first.message }
|
|
72
|
+
: undefined,
|
|
73
|
+
stdout: typeof first.stdout === "string" ? first.stdout : "",
|
|
74
|
+
stderr: typeof first.stderr === "string" ? first.stderr : "",
|
|
75
|
+
});
|
|
76
|
+
if (retry !== null) {
|
|
77
|
+
try {
|
|
78
|
+
const stdout = runOnce(retry);
|
|
79
|
+
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
80
|
+
}
|
|
81
|
+
catch (retryErr) {
|
|
82
|
+
err = retryErr;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
62
85
|
const e = err;
|
|
63
86
|
if (e.code === "ENOENT") {
|
|
64
87
|
return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { RunGhFn, RunGhResult } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366).
|
|
4
|
+
* `binaryOverride` lets a caller pin the SCM binary (e.g. plain `gh` instead of
|
|
5
|
+
* the `ghx` cached proxy) when its guarantee depends on an uncached read (#3767).
|
|
6
|
+
*/
|
|
7
|
+
export declare function defaultRunGh(cmd: readonly string[], binaryOverride?: string): RunGhResult;
|
|
4
8
|
/**
|
|
5
9
|
* Run ``gh pr view <N> --json closingIssuesReferences`` and return linked issue
|
|
6
10
|
* numbers. Returns ``null`` on external error (caller maps to EXIT_EXTERNAL_ERROR).
|