@deftai/directive-core 0.106.0 → 0.108.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/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.js +2 -0
- package/dist/check/named-cause.js +18 -3
- package/dist/codebase/provider.js +7 -1
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/design-critique/citation-grammar.d.ts +53 -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 +232 -0
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -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 +76 -0
- package/dist/hooks/classify/host-session-identity.js +470 -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 +7 -0
- package/dist/hooks/classify/paths.js +35 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +27 -0
- package/dist/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +69 -2
- package/dist/hooks/dispatcher.js +609 -45
- package/dist/hooks/fixtures/cases.d.ts +16 -2
- package/dist/hooks/fixtures/cases.js +175 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/intake/issue-ingest.js +62 -2
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/capture.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +75 -7
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +193 -128
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -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/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/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/value-feedback.js +9 -18
- package/dist/policy/write-fence.js +3 -0
- 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-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +7 -3
- 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 +11 -0
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/preflight-cache/evaluate.js +9 -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 +53 -12
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -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 +19 -0
- package/dist/release/types.d.ts +14 -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 +182 -1
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/scm/call.js +12 -3
- package/dist/scm/design-critique-chip.d.ts +38 -0
- package/dist/scm/design-critique-chip.js +187 -0
- package/dist/scm/gh-rest.d.ts +9 -1
- package/dist/scm/gh-rest.js +103 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +4 -1
- package/dist/scm/readiness.js +35 -5
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +3 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +5 -1
- package/dist/session/git.d.ts +42 -0
- package/dist/session/git.js +219 -3
- package/dist/session/occupancy.d.ts +239 -12
- package/dist/session/occupancy.js +820 -27
- 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 +17 -1
- package/dist/session/session-start.js +49 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-session-ritual.d.ts +33 -3
- package/dist/session/verify-session-ritual.js +89 -19
- 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 +77 -0
- package/dist/swarm/launch.js +248 -38
- 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/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +21 -7
- package/dist/triage/help/registry-data.js +53 -5
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- 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/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- 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-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -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/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -1
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- 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/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/xbrief-migrate/agents-header.js +69 -7
- package/package.json +7 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
1
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
3
2
|
import { valueFeedbackInstallForceOnSource } from "./org-force-on-migration.js";
|
|
4
3
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
5
4
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
@@ -219,14 +218,10 @@ export function enableValueFeedback(projectRoot, options) {
|
|
|
219
218
|
changed: false,
|
|
220
219
|
};
|
|
221
220
|
}
|
|
222
|
-
const
|
|
221
|
+
const _path = projectDefinitionPath(projectRoot);
|
|
223
222
|
try {
|
|
224
|
-
const { changed } =
|
|
225
|
-
const
|
|
226
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
227
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
228
|
-
}
|
|
229
|
-
const data = parsed;
|
|
223
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
224
|
+
const data = mutation.load();
|
|
230
225
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
231
226
|
if (data.plan === undefined) {
|
|
232
227
|
data.plan = {};
|
|
@@ -277,7 +272,7 @@ export function enableValueFeedback(projectRoot, options) {
|
|
|
277
272
|
legacyKeyMigrated;
|
|
278
273
|
policyBlock.valueFeedback = nextBlock;
|
|
279
274
|
if (changedFlag) {
|
|
280
|
-
|
|
275
|
+
mutation.persist(data);
|
|
281
276
|
}
|
|
282
277
|
const actor = options.actor ?? policyColonInvocation("enable-value-feedback");
|
|
283
278
|
const note = options.note ?? "";
|
|
@@ -317,14 +312,10 @@ export function enableValueFeedback(projectRoot, options) {
|
|
|
317
312
|
* updates will not re-force against intentional opt-out.
|
|
318
313
|
*/
|
|
319
314
|
export function clearValueFeedback(projectRoot, options = {}) {
|
|
320
|
-
const
|
|
315
|
+
const _path = projectDefinitionPath(projectRoot);
|
|
321
316
|
try {
|
|
322
|
-
const { changed } =
|
|
323
|
-
const
|
|
324
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
325
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
326
|
-
}
|
|
327
|
-
const data = parsed;
|
|
317
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
318
|
+
const data = mutation.load();
|
|
328
319
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
329
320
|
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
330
321
|
}
|
|
@@ -342,7 +333,7 @@ export function clearValueFeedback(projectRoot, options = {}) {
|
|
|
342
333
|
}
|
|
343
334
|
const previous = policyBlock.valueFeedback;
|
|
344
335
|
delete policyBlock.valueFeedback;
|
|
345
|
-
|
|
336
|
+
mutation.persist(data);
|
|
346
337
|
const actor = options.actor ?? policyColonInvocation("clear-value-feedback");
|
|
347
338
|
const note = options.note ?? "";
|
|
348
339
|
const parts = [
|
|
@@ -140,6 +140,9 @@ export function resolveWriteFence(projectPolicy, storyFileScope, options) {
|
|
|
140
140
|
allowPaths,
|
|
141
141
|
denyPaths,
|
|
142
142
|
scopes,
|
|
143
|
+
// Carry the project setting through: resolving a write fence must not
|
|
144
|
+
// silently enable Shell dest-form enforcement for a story (#3438 / #3594).
|
|
145
|
+
shellDestForms: projectPolicy.shellDestForms,
|
|
143
146
|
storyAllowPaths: storyAllow,
|
|
144
147
|
fenceSources: sources,
|
|
145
148
|
};
|
|
@@ -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,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).
|
|
@@ -2,12 +2,16 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { resolveBinary } from "../scm/binary.js";
|
|
3
3
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
4
4
|
import { GH_TIMEOUT_S } from "./constants.js";
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366).
|
|
7
|
+
* `binaryOverride` lets a caller pin the SCM binary (e.g. plain `gh` instead of
|
|
8
|
+
* the `ghx` cached proxy) when its guarantee depends on an uncached read (#3767).
|
|
9
|
+
*/
|
|
10
|
+
export function defaultRunGh(cmd, binaryOverride) {
|
|
7
11
|
if (cmd.length === 0 || cmd[0] !== "gh") {
|
|
8
12
|
return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
|
|
9
13
|
}
|
|
10
|
-
const binary = resolveBinary();
|
|
14
|
+
const binary = binaryOverride ?? resolveBinary();
|
|
11
15
|
const args = cmd.slice(1);
|
|
12
16
|
try {
|
|
13
17
|
const stdout = execFileSync(binary, args, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type EnforceMergeApprovalHeadInput, type MergeApprovalHeadResult } from "../policy/merge-approval-head.js";
|
|
2
2
|
import { type AgentMergeEvaluateResult } from "../policy/require-human-merge.js";
|
|
3
3
|
import { type SemanticGreenFn } from "./semantic-green.js";
|
|
4
|
-
import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
|
|
4
|
+
import type { CloseoutAttestableFn, MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
|
|
5
5
|
/** Post-merge umbrella checklist + current-shape refresh (#1649). */
|
|
6
6
|
export type UmbrellaReconcileFn = (projectRoot: string, repo: string) => void;
|
|
7
7
|
export interface WaitMergeableOptions {
|
|
@@ -32,6 +32,14 @@ export interface WaitMergeableOptions {
|
|
|
32
32
|
readonly skipMergeApprovalHeadGate?: boolean;
|
|
33
33
|
/** Inject head-bound approval enforcer (unit tests). */
|
|
34
34
|
readonly mergeApprovalHeadFn?: (input: EnforceMergeApprovalHeadInput) => MergeApprovalHeadResult;
|
|
35
|
+
/**
|
|
36
|
+
* When true, skip the merge-time closeout attestability gate (#3781).
|
|
37
|
+
* Default: same as skipHumanMergeGate, matching the head-gate convention —
|
|
38
|
+
* test harnesses that skip human-merge also skip this unless they opt in.
|
|
39
|
+
*/
|
|
40
|
+
readonly skipCloseoutAttestableGate?: boolean;
|
|
41
|
+
/** Inject the closeout attestability gate (unit tests). */
|
|
42
|
+
readonly closeoutAttestableFn?: CloseoutAttestableFn;
|
|
35
43
|
/** Inject PR HEAD fetch (unit tests; defaults to REST `pulls/<N>`). */
|
|
36
44
|
readonly fetchPrHeadShaFn?: (prNumber: number, repo: string | null) => string | null;
|
|
37
45
|
/**
|
|
@@ -6,7 +6,7 @@ import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
|
|
|
6
6
|
import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
|
|
7
7
|
import { makeResult } from "./result.js";
|
|
8
8
|
import { evaluateSemanticGreen } from "./semantic-green.js";
|
|
9
|
-
import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
|
|
9
|
+
import { runCloseoutAttestableCheck, runGhMerge, runMonitor, runProtectedCheck, } from "./wrappers.js";
|
|
10
10
|
/** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
|
|
11
11
|
function isProtectedCheckConfigFailure(stderr) {
|
|
12
12
|
const tail = stderr.trim();
|
|
@@ -228,6 +228,26 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
// #3781: last gate before the merge call. A PR may leave a brief unattested; it
|
|
232
|
+
// may not merge one whose issue it closes in the same act. Keyed on the PR's
|
|
233
|
+
// closing references, so it fires even when the brief is not in the branch diff.
|
|
234
|
+
const skipCloseoutGate = options.skipCloseoutAttestableGate === true ||
|
|
235
|
+
(options.skipCloseoutAttestableGate === undefined && options.skipHumanMergeGate === true);
|
|
236
|
+
if (!skipCloseoutGate) {
|
|
237
|
+
const closeoutFn = options.closeoutAttestableFn ?? runCloseoutAttestableCheck;
|
|
238
|
+
const [coRc, , coStderr] = closeoutFn(prNumber, repo, projectRoot);
|
|
239
|
+
if (coRc !== 0) {
|
|
240
|
+
return makeResult({
|
|
241
|
+
prNumber,
|
|
242
|
+
repo,
|
|
243
|
+
outcome: coRc === 1 ? "closeout-unattested" : "config-error",
|
|
244
|
+
exitCode: coRc === 1 ? EXIT_TIMEOUT_OR_ESCALATION : EXIT_CONFIG_ERROR,
|
|
245
|
+
monitorResult: monitorPayload,
|
|
246
|
+
protectedCheck: protectedCheckPayload,
|
|
247
|
+
error: coStderr.trim(),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
231
251
|
const [mergeRc, mergeStdout, mergeStderr] = mergeFn(prNumber, repo, {
|
|
232
252
|
matchHeadCommit,
|
|
233
253
|
});
|
|
@@ -18,4 +18,6 @@ export type MonitorFn = (prNumber: number, repo: string, capMinutes: number, opt
|
|
|
18
18
|
export type MergeFn = (prNumber: number, repo: string | null, options?: {
|
|
19
19
|
readonly matchHeadCommit?: string | null;
|
|
20
20
|
}) => SubprocessTriple;
|
|
21
|
+
/** Merge-time closeout attestability gate (#3781): 0 attestable / 1 unattested / 2 lookup. */
|
|
22
|
+
export type CloseoutAttestableFn = (prNumber: number, repo: string | null, projectRoot: string) => SubprocessTriple;
|
|
21
23
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -32,6 +32,12 @@ export interface RunProtectedCheckOptions {
|
|
|
32
32
|
}
|
|
33
33
|
/** Invoke pr-protected-issues CLI and return (returncode, stdout, stderr). */
|
|
34
34
|
export declare function runProtectedCheck(prNumber: number, repo: string | null, protectedIssues: readonly number[], options?: RunProtectedCheckOptions): SubprocessTriple;
|
|
35
|
+
/**
|
|
36
|
+
* Merge-time closeout attestability gate (#3781). In-process: the evaluator lives
|
|
37
|
+
* in this package, and a subprocess hop would only add a script-path failure mode
|
|
38
|
+
* to a gate that must fail closed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runCloseoutAttestableCheck(prNumber: number, repo: string | null, projectRoot: string): SubprocessTriple;
|
|
35
41
|
export interface RunMonitorOptions {
|
|
36
42
|
readonly nodeExecutable?: string;
|
|
37
43
|
readonly timeout?: number;
|
|
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { evaluate as evaluateCloseoutAttestable } from "../pr-closeout-attestable/evaluate.js";
|
|
6
7
|
import { resolveBinary } from "../scm/binary.js";
|
|
7
8
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
8
9
|
/** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
|
|
@@ -97,6 +98,16 @@ export function runProtectedCheck(prNumber, repo, protectedIssues, options = {})
|
|
|
97
98
|
});
|
|
98
99
|
return [result.returncode, result.stdout, result.stderr];
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Merge-time closeout attestability gate (#3781). In-process: the evaluator lives
|
|
103
|
+
* in this package, and a subprocess hop would only add a script-path failure mode
|
|
104
|
+
* to a gate that must fail closed.
|
|
105
|
+
*/
|
|
106
|
+
export function runCloseoutAttestableCheck(prNumber, repo, projectRoot) {
|
|
107
|
+
const result = evaluateCloseoutAttestable(projectRoot, prNumber, { repo });
|
|
108
|
+
const text = result.message.length > 0 ? `${result.message}\n` : "";
|
|
109
|
+
return result.code === 0 ? [0, text, ""] : [result.code, "", text];
|
|
110
|
+
}
|
|
100
111
|
/** Invoke pr-monitor CLI with --json and return (returncode, stdout, stderr). */
|
|
101
112
|
export function runMonitor(prNumber, repo, capMinutes, options = {}) {
|
|
102
113
|
const scriptPath = cliScriptPath("pr-monitor");
|
|
@@ -6,6 +6,19 @@ export declare const ACTIVE_FOLDER = "active";
|
|
|
6
6
|
export declare const ELIGIBLE_STATUS = "running";
|
|
7
7
|
/** Actionable redirect appended to every reject path (#810 / #2449). */
|
|
8
8
|
export declare const ACTIVATE_HINT = "Run `task scope:activate -- {path}` (or legacy `task vbrief:activate -- {path}`) before spawning an implementation agent.";
|
|
9
|
+
/**
|
|
10
|
+
* Recovery for the origin-freshness reject (#3828).
|
|
11
|
+
*
|
|
12
|
+
* Freshness is only reached after the active/ + running checks pass, so
|
|
13
|
+
* `ACTIVATE_HINT` names a transition that cannot apply here: `activate` accepts
|
|
14
|
+
* `pending/` alone and fails with `Invalid transition: 'activate' requires file
|
|
15
|
+
* in pending/`. `block` -> `unblock` is in place (`targetFolder: null`), requires
|
|
16
|
+
* and restores `running`, and stamps `plan.updated` plus `xBRIEFInfo.updated`,
|
|
17
|
+
* the field this reject compares. It is interim: the pair also records a
|
|
18
|
+
* `blocked` -> `running` transition for a brief that was never blocked, a
|
|
19
|
+
* tradeoff accepted knowingly. #3857 owns the dedicated acknowledge verb.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ORIGIN_FRESHNESS_HINT: string;
|
|
9
22
|
/** Lifecycle folder names eligible for implementation (#810). */
|
|
10
23
|
export declare const ELIGIBLE_LIFECYCLE_DIRS: readonly ["xbrief/active", "vbrief/active"];
|
|
11
24
|
export declare const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight -- xbrief/active/<story>.xbrief.json` (legacy: `task vbrief:preflight -- <path>`).";
|
|
@@ -32,6 +45,8 @@ export interface EvaluateOptions {
|
|
|
32
45
|
}
|
|
33
46
|
/** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
|
|
34
47
|
export declare function formatActivateHint(path: string): string;
|
|
48
|
+
/** Substitute every `{path}` without `$`-pattern expansion in user paths (#1721). */
|
|
49
|
+
export declare function formatOriginFreshnessHint(path: string): string;
|
|
35
50
|
/**
|
|
36
51
|
* Pure evaluator — returns `{ exitCode, message }`. Never throws; every error
|
|
37
52
|
* path collapses to exit 1 with an actionable message. Faithful to
|