@deftai/directive-core 0.98.1 → 0.100.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/authz/actions.d.ts +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +401 -75
- package/dist/authz/decompose-apply.d.ts +67 -0
- package/dist/authz/decompose-apply.js +302 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/consumer-check-contract/evaluate.d.ts +40 -0
- package/dist/consumer-check-contract/evaluate.js +188 -3
- package/dist/consumer-check-contract/index.d.ts +1 -1
- package/dist/consumer-check-contract/index.js +1 -1
- package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
- package/dist/content-contracts/skills/greptile-detector.js +202 -4
- package/dist/decision/index.d.ts +17 -0
- package/dist/decision/index.js +35 -0
- package/dist/decision/list.d.ts +47 -0
- package/dist/decision/list.js +250 -0
- package/dist/decision/schema.d.ts +88 -0
- package/dist/decision/schema.js +293 -0
- package/dist/decision/write.d.ts +82 -0
- package/dist/decision/write.js +427 -0
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +72 -79
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/eval/report.d.ts +29 -0
- package/dist/eval/report.js +69 -0
- package/dist/eval/run.d.ts +9 -0
- package/dist/eval/run.js +40 -4
- package/dist/eval/version-pin.d.ts +99 -0
- package/dist/eval/version-pin.js +181 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/intake/issue-ingest.js +29 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/platform/host-content-surface.d.ts +74 -0
- package/dist/platform/host-content-surface.js +214 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/ceremony-dial.d.ts +289 -0
- package/dist/policy/ceremony-dial.js +980 -0
- package/dist/policy/deft-directive-disable.js +12 -2
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +16 -1
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/evaluate.js +10 -0
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/mergeability.js +5 -0
- package/dist/pr-merge-readiness/output.js +2 -0
- package/dist/pr-merge-readiness/parse.js +4 -0
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-merge-readiness/types.d.ts +6 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/render/framework-commands.js +4 -0
- package/dist/scope/acceptance-evidence.d.ts +76 -0
- package/dist/scope/acceptance-evidence.js +348 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.js +125 -11
- package/dist/scope/effort-activate-gate.d.ts +28 -0
- package/dist/scope/effort-activate-gate.js +64 -0
- package/dist/scope/index.d.ts +4 -0
- package/dist/scope/index.js +4 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +69 -2
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/effort-budget.d.ts +130 -0
- package/dist/session/effort-budget.js +334 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/session-start.d.ts +30 -1
- package/dist/session/session-start.js +234 -26
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
- package/dist/swarm/pre-dispatch-cli.js +143 -0
- package/dist/swarm/pre-dispatch.d.ts +87 -0
- package/dist/swarm/pre-dispatch.js +373 -0
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/vbrief-activate/activate.js +6 -0
- package/dist/vbrief-validate/constants.d.ts +2 -0
- package/dist/vbrief-validate/constants.js +2 -0
- package/dist/vbrief-validate/schema.js +4 -1
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +19 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3215 — pin and record framework version per eval cell; refuse mixed-version aggregation.
|
|
3
|
+
* Extends #3081 empiricism; wires into the #1584 shared-benchmark manifest shape when present.
|
|
4
|
+
*/
|
|
5
|
+
/** Repo-relative path for the #1584 shared-benchmark manifest. */
|
|
6
|
+
export declare const SHARED_BENCHMARK_MANIFEST_REL = "evals/shared-benchmark.json";
|
|
7
|
+
/** How the framework version was resolved at eval run start. */
|
|
8
|
+
export type FrameworkVersionSource = "package.json" | "override";
|
|
9
|
+
/** Policy when a treatment cell contains disagreeing framework versions. */
|
|
10
|
+
export type MixedVersionPolicy = "refuse" | "flag";
|
|
11
|
+
/** Resolved framework version pin captured once at eval run start. */
|
|
12
|
+
export interface FrameworkVersionPin {
|
|
13
|
+
readonly frameworkVersion: string;
|
|
14
|
+
readonly source: FrameworkVersionSource;
|
|
15
|
+
readonly resolvedAt: string;
|
|
16
|
+
}
|
|
17
|
+
/** Minimal run identity for cell purity checks (version + treatment grouping). */
|
|
18
|
+
export interface VersionedEvalRun {
|
|
19
|
+
readonly frameworkVersion: string;
|
|
20
|
+
readonly treatment?: string;
|
|
21
|
+
readonly model?: string;
|
|
22
|
+
readonly harness?: string;
|
|
23
|
+
readonly runId?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Result of checking whether one treatment cell is version-pure. */
|
|
26
|
+
export interface CellVersionPurity {
|
|
27
|
+
readonly pure: boolean;
|
|
28
|
+
readonly frameworkVersion: string | null;
|
|
29
|
+
readonly versions: readonly string[];
|
|
30
|
+
readonly runCount: number;
|
|
31
|
+
readonly treatment: string;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}
|
|
34
|
+
export interface AggregateCellOptions {
|
|
35
|
+
readonly runs: readonly VersionedEvalRun[];
|
|
36
|
+
/** Treatment key when runs omit `treatment` (e.g. model or with_skill). */
|
|
37
|
+
readonly treatment?: string;
|
|
38
|
+
/** Default `refuse`: mixed cells block aggregation. `flag` allows with purity evidence. */
|
|
39
|
+
readonly policy?: MixedVersionPolicy;
|
|
40
|
+
}
|
|
41
|
+
export interface AggregateCellResult {
|
|
42
|
+
readonly purity: CellVersionPurity;
|
|
43
|
+
readonly allowed: boolean;
|
|
44
|
+
readonly policy: MixedVersionPolicy;
|
|
45
|
+
readonly frameworkVersion: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** Version block stamped onto a #1584-shaped shared-benchmark manifest. */
|
|
48
|
+
export interface SharedBenchmarkVersionBlock {
|
|
49
|
+
readonly frameworkVersion: string;
|
|
50
|
+
readonly frameworkVersionSource: FrameworkVersionSource;
|
|
51
|
+
readonly frameworkVersionResolvedAt: string;
|
|
52
|
+
readonly versionPurityGate: "#3215";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the directive framework version once at eval run start.
|
|
56
|
+
* Prefers an explicit override (tests / pinned doctor output); else package.json pin.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveFrameworkVersionPin(options?: {
|
|
59
|
+
readonly override?: string;
|
|
60
|
+
readonly now?: () => Date;
|
|
61
|
+
}): FrameworkVersionPin;
|
|
62
|
+
/**
|
|
63
|
+
* Cell-level purity: all runs in one treatment must share a single framework version.
|
|
64
|
+
* Empty run sets are vacuously pure.
|
|
65
|
+
*/
|
|
66
|
+
export declare function evaluateCellVersionPurity(runs: readonly VersionedEvalRun[], treatment?: string): CellVersionPurity;
|
|
67
|
+
/**
|
|
68
|
+
* Aggregate (or refuse) runs for one treatment under the mixed-version policy.
|
|
69
|
+
* `refuse` (default) sets `allowed=false` when versions disagree; `flag` allows with evidence.
|
|
70
|
+
*/
|
|
71
|
+
export declare function aggregateCellWithVersionPurity(options: AggregateCellOptions): AggregateCellResult;
|
|
72
|
+
/**
|
|
73
|
+
* Group runs by treatment (or model×harness fallback) and evaluate purity per cell.
|
|
74
|
+
*/
|
|
75
|
+
export declare function evaluateLedgerVersionPurity(runs: readonly VersionedEvalRun[]): {
|
|
76
|
+
readonly pure: boolean;
|
|
77
|
+
readonly cells: readonly CellVersionPurity[];
|
|
78
|
+
readonly summary: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Merge a framework version pin into a #1584-shaped shared-benchmark manifest object.
|
|
82
|
+
* Stamps top-level `frameworkVersion` plus metadata for external consumers.
|
|
83
|
+
*/
|
|
84
|
+
export declare function wireFrameworkVersionIntoManifest(manifest: Record<string, unknown>, pin: FrameworkVersionPin): Record<string, unknown>;
|
|
85
|
+
/** Load `evals/shared-benchmark.json` when present; otherwise null. */
|
|
86
|
+
export declare function loadSharedBenchmarkManifest(projectRoot: string): Record<string, unknown> | null;
|
|
87
|
+
/**
|
|
88
|
+
* When a #1584 shared-benchmark manifest exists under the project root, return it
|
|
89
|
+
* with the framework version pin wired in. When `persist` is true, write the
|
|
90
|
+
* wired manifest back to disk so subsequent consumers see the pin (#3215 Greptile).
|
|
91
|
+
*/
|
|
92
|
+
export declare function applyVersionPinToSharedBenchmark(projectRoot: string, pin: FrameworkVersionPin, options?: {
|
|
93
|
+
readonly persist?: boolean;
|
|
94
|
+
}): {
|
|
95
|
+
readonly applied: boolean;
|
|
96
|
+
readonly persisted: boolean;
|
|
97
|
+
readonly manifest: Record<string, unknown> | null;
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=version-pin.d.ts.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3215 — pin and record framework version per eval cell; refuse mixed-version aggregation.
|
|
3
|
+
* Extends #3081 empiricism; wires into the #1584 shared-benchmark manifest shape when present.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { readCorePackageVersion } from "../engine-version.js";
|
|
8
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
9
|
+
/** Repo-relative path for the #1584 shared-benchmark manifest. */
|
|
10
|
+
export const SHARED_BENCHMARK_MANIFEST_REL = "evals/shared-benchmark.json";
|
|
11
|
+
function toIsoZ(date) {
|
|
12
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the directive framework version once at eval run start.
|
|
16
|
+
* Prefers an explicit override (tests / pinned doctor output); else package.json pin.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveFrameworkVersionPin(options) {
|
|
19
|
+
const now = options?.now ?? (() => new Date());
|
|
20
|
+
const override = options?.override?.trim();
|
|
21
|
+
if (override !== undefined && override.length > 0) {
|
|
22
|
+
return {
|
|
23
|
+
frameworkVersion: override,
|
|
24
|
+
source: "override",
|
|
25
|
+
resolvedAt: toIsoZ(now()),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
frameworkVersion: readCorePackageVersion(),
|
|
30
|
+
source: "package.json",
|
|
31
|
+
resolvedAt: toIsoZ(now()),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Cell-level purity: all runs in one treatment must share a single framework version.
|
|
36
|
+
* Empty run sets are vacuously pure.
|
|
37
|
+
*/
|
|
38
|
+
export function evaluateCellVersionPurity(runs, treatment) {
|
|
39
|
+
const label = treatment?.trim() ||
|
|
40
|
+
runs.find((r) => typeof r.treatment === "string" && r.treatment.length > 0)?.treatment ||
|
|
41
|
+
"default";
|
|
42
|
+
const versions = [
|
|
43
|
+
...new Set(runs.map((r) => r.frameworkVersion.trim()).filter((v) => v.length > 0)),
|
|
44
|
+
].sort();
|
|
45
|
+
if (runs.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
pure: true,
|
|
48
|
+
frameworkVersion: null,
|
|
49
|
+
versions: [],
|
|
50
|
+
runCount: 0,
|
|
51
|
+
treatment: label,
|
|
52
|
+
message: `Cell "${label}": no runs (vacuously pure).`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (versions.length <= 1) {
|
|
56
|
+
const version = versions[0] ?? null;
|
|
57
|
+
return {
|
|
58
|
+
pure: true,
|
|
59
|
+
frameworkVersion: version,
|
|
60
|
+
versions,
|
|
61
|
+
runCount: runs.length,
|
|
62
|
+
treatment: label,
|
|
63
|
+
message: `Cell "${label}": version-pure (v${version ?? "unknown"}, ${runs.length} run(s)).`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
pure: false,
|
|
68
|
+
frameworkVersion: null,
|
|
69
|
+
versions,
|
|
70
|
+
runCount: runs.length,
|
|
71
|
+
treatment: label,
|
|
72
|
+
message: `Cell "${label}": mixed framework versions [${versions.join(", ")}] across ${runs.length} run(s) — mixed-version aggregation blocked (#3215).`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Aggregate (or refuse) runs for one treatment under the mixed-version policy.
|
|
77
|
+
* `refuse` (default) sets `allowed=false` when versions disagree; `flag` allows with evidence.
|
|
78
|
+
*/
|
|
79
|
+
export function aggregateCellWithVersionPurity(options) {
|
|
80
|
+
const policy = options.policy ?? "refuse";
|
|
81
|
+
const purity = evaluateCellVersionPurity(options.runs, options.treatment);
|
|
82
|
+
const allowed = purity.pure || policy === "flag";
|
|
83
|
+
return {
|
|
84
|
+
purity,
|
|
85
|
+
allowed,
|
|
86
|
+
policy,
|
|
87
|
+
frameworkVersion: purity.frameworkVersion,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Group runs by treatment (or model×harness fallback) and evaluate purity per cell.
|
|
92
|
+
*/
|
|
93
|
+
export function evaluateLedgerVersionPurity(runs) {
|
|
94
|
+
const groups = new Map();
|
|
95
|
+
for (const run of runs) {
|
|
96
|
+
const key = run.treatment?.trim() ||
|
|
97
|
+
[run.model ?? "", run.harness ?? ""].filter((p) => p.length > 0).join("@") ||
|
|
98
|
+
"default";
|
|
99
|
+
const list = groups.get(key) ?? [];
|
|
100
|
+
list.push(run);
|
|
101
|
+
groups.set(key, list);
|
|
102
|
+
}
|
|
103
|
+
const cells = [...groups.entries()]
|
|
104
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
105
|
+
.map(([key, group]) => evaluateCellVersionPurity(group, key));
|
|
106
|
+
const pure = cells.every((c) => c.pure);
|
|
107
|
+
const mixed = cells.filter((c) => !c.pure);
|
|
108
|
+
const summary = cells.length === 0
|
|
109
|
+
? "Version purity: no cells."
|
|
110
|
+
: pure
|
|
111
|
+
? `Version purity: all ${cells.length} cell(s) pure.`
|
|
112
|
+
: `Version purity: ${mixed.length}/${cells.length} cell(s) mixed — ${mixed.map((c) => c.treatment).join(", ")}.`;
|
|
113
|
+
return { pure, cells, summary };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Merge a framework version pin into a #1584-shaped shared-benchmark manifest object.
|
|
117
|
+
* Stamps top-level `frameworkVersion` plus metadata for external consumers.
|
|
118
|
+
*/
|
|
119
|
+
export function wireFrameworkVersionIntoManifest(manifest, pin) {
|
|
120
|
+
const priorMeta = typeof manifest.metadata === "object" &&
|
|
121
|
+
manifest.metadata !== null &&
|
|
122
|
+
!Array.isArray(manifest.metadata)
|
|
123
|
+
? { ...manifest.metadata }
|
|
124
|
+
: {};
|
|
125
|
+
const versionBlock = {
|
|
126
|
+
frameworkVersion: pin.frameworkVersion,
|
|
127
|
+
frameworkVersionSource: pin.source,
|
|
128
|
+
frameworkVersionResolvedAt: pin.resolvedAt,
|
|
129
|
+
versionPurityGate: "#3215",
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
...manifest,
|
|
133
|
+
frameworkVersion: pin.frameworkVersion,
|
|
134
|
+
metadata: {
|
|
135
|
+
...priorMeta,
|
|
136
|
+
...versionBlock,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Load `evals/shared-benchmark.json` when present; otherwise null. */
|
|
141
|
+
export function loadSharedBenchmarkManifest(projectRoot) {
|
|
142
|
+
const path = resolve(projectRoot, SHARED_BENCHMARK_MANIFEST_REL);
|
|
143
|
+
if (!existsSync(path)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
148
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* When a #1584 shared-benchmark manifest exists under the project root, return it
|
|
159
|
+
* with the framework version pin wired in. When `persist` is true, write the
|
|
160
|
+
* wired manifest back to disk so subsequent consumers see the pin (#3215 Greptile).
|
|
161
|
+
*/
|
|
162
|
+
export function applyVersionPinToSharedBenchmark(projectRoot, pin, options) {
|
|
163
|
+
const existing = loadSharedBenchmarkManifest(projectRoot);
|
|
164
|
+
if (existing === null) {
|
|
165
|
+
return { applied: false, persisted: false, manifest: null };
|
|
166
|
+
}
|
|
167
|
+
const manifest = wireFrameworkVersionIntoManifest(existing, pin);
|
|
168
|
+
const shouldPersist = options?.persist === true;
|
|
169
|
+
if (!shouldPersist) {
|
|
170
|
+
return { applied: true, persisted: false, manifest };
|
|
171
|
+
}
|
|
172
|
+
// #2980: product write sink routes through containedWrite.
|
|
173
|
+
containedWrite({
|
|
174
|
+
root: resolve(projectRoot),
|
|
175
|
+
target: SHARED_BENCHMARK_MANIFEST_REL,
|
|
176
|
+
data: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
177
|
+
mode: "replace",
|
|
178
|
+
});
|
|
179
|
+
return { applied: true, persisted: true, manifest };
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=version-pin.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - CLEAN + bot merge allowed + --merge → optional wait-mergeable-and-merge
|
|
8
8
|
* - Missing grants → fail closed BLOCKED
|
|
9
9
|
*/
|
|
10
|
+
import { type EnforceMergeApprovalHeadInput, type MergeApprovalHeadResult } from "../policy/merge-approval-head.js";
|
|
10
11
|
import { evaluateAgentMerge } from "../policy/require-human-merge.js";
|
|
11
12
|
import type { WatchOptions, WatchResult } from "../pr-watch/types.js";
|
|
12
13
|
import { type PrFinishLoopResult } from "./types.js";
|
|
@@ -20,13 +21,23 @@ export interface PrFinishLoopOptions {
|
|
|
20
21
|
/** Attempt merge when CLEAN and policy allows (default false). */
|
|
21
22
|
readonly merge?: boolean;
|
|
22
23
|
readonly skipGrantGate?: boolean;
|
|
24
|
+
/** Skip #3235 head-bound plan:approved gate (tests). */
|
|
25
|
+
readonly skipMergeApprovalHeadGate?: boolean;
|
|
23
26
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
24
27
|
readonly now?: Date;
|
|
25
28
|
/** Inject watch for tests. */
|
|
26
29
|
readonly watchFn?: (prNumber: number, repo: string | null, options?: WatchOptions) => WatchResult;
|
|
27
|
-
/**
|
|
28
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Inject merge cascade for tests; returns exit code.
|
|
32
|
+
* Optional `matchHeadCommit` pins the merge to the gated head (#3235).
|
|
33
|
+
*/
|
|
34
|
+
readonly mergeFn?: (prNumber: number, repo: string | null, options?: {
|
|
35
|
+
readonly matchHeadCommit?: string | null;
|
|
36
|
+
}) => number;
|
|
29
37
|
readonly agentMergeFn?: typeof evaluateAgentMerge;
|
|
38
|
+
readonly mergeApprovalHeadFn?: (input: EnforceMergeApprovalHeadInput) => MergeApprovalHeadResult;
|
|
39
|
+
/** Inject live HEAD fetch (tests); defaults to REST pulls head.sha. */
|
|
40
|
+
readonly fetchPrHeadShaFn?: (prNumber: number, repo: string | null) => string | null;
|
|
30
41
|
readonly writeProgress?: boolean;
|
|
31
42
|
readonly iteration?: number;
|
|
32
43
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - CLEAN + bot merge allowed + --merge → optional wait-mergeable-and-merge
|
|
8
8
|
* - Missing grants → fail closed BLOCKED
|
|
9
9
|
*/
|
|
10
|
+
import { disablePullRequestAutoMerge, enforceMergeApprovalHead, fetchPrHeadShaRest, } from "../policy/merge-approval-head.js";
|
|
10
11
|
import { evaluateAgentMerge } from "../policy/require-human-merge.js";
|
|
11
12
|
import { EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CLEAN, VERDICT_NEW_P0_P1, } from "../pr-watch/constants.js";
|
|
12
13
|
import { watch } from "../pr-watch/watch.js";
|
|
@@ -70,7 +71,9 @@ export function runPrFinishLoop(options) {
|
|
|
70
71
|
}
|
|
71
72
|
// --- pr:watch ---
|
|
72
73
|
const watchFn = options.watchFn ?? watch;
|
|
73
|
-
|
|
74
|
+
// Prefer explicit repo; GH_REPO next. Repo is required for head-bound approval
|
|
75
|
+
// scoping when plan:approved records exist (#3235 cross-repo collision).
|
|
76
|
+
const repo = options.repo ?? process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? null;
|
|
74
77
|
let watchResult;
|
|
75
78
|
try {
|
|
76
79
|
watchResult = watchFn(prNumber, repo, {
|
|
@@ -129,6 +132,61 @@ export function runPrFinishLoop(options) {
|
|
|
129
132
|
grantId: null,
|
|
130
133
|
};
|
|
131
134
|
}
|
|
135
|
+
// #3235: head-bound plan:approved vs LIVE PR HEAD (before merge path).
|
|
136
|
+
// Always re-fetch HEAD. Never fall back to the watch snapshot: a failed live
|
|
137
|
+
// read + snapshot A would retain auto-merge while GitHub head is B.
|
|
138
|
+
if (options.skipMergeApprovalHeadGate !== true) {
|
|
139
|
+
const fetchHead = options.fetchPrHeadShaFn ?? fetchPrHeadShaRest;
|
|
140
|
+
const liveHead = fetchHead(prNumber, repo);
|
|
141
|
+
if (liveHead === null || liveHead.trim() === "") {
|
|
142
|
+
disablePullRequestAutoMerge(prNumber, repo);
|
|
143
|
+
const message = `pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: cannot read live HEAD ` +
|
|
144
|
+
"after CLEAN; auto-merge disabled (fail closed, #3235).";
|
|
145
|
+
log("merge", "stale-merge-approval", message);
|
|
146
|
+
return {
|
|
147
|
+
exitCode: EXIT_ACTION_REQUIRED,
|
|
148
|
+
haltReason: "stale-merge-approval",
|
|
149
|
+
message,
|
|
150
|
+
prNumber,
|
|
151
|
+
watchVerdict: VERDICT_CLEAN,
|
|
152
|
+
mergeAttempted: false,
|
|
153
|
+
mergeSkippedReason: "stale-merge-approval",
|
|
154
|
+
grantId: null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const headGateFn = options.mergeApprovalHeadFn ?? enforceMergeApprovalHead;
|
|
158
|
+
const headGate = headGateFn({
|
|
159
|
+
prNumber,
|
|
160
|
+
repo,
|
|
161
|
+
projectRoot,
|
|
162
|
+
currentHeadSha: liveHead,
|
|
163
|
+
disableAutoMergeOnDeny: true,
|
|
164
|
+
});
|
|
165
|
+
if (!headGate.allowed) {
|
|
166
|
+
const message = [
|
|
167
|
+
`pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: stale or unbound merge approval.`,
|
|
168
|
+
headGate.message,
|
|
169
|
+
headGate.recovery ?? "",
|
|
170
|
+
]
|
|
171
|
+
.filter((line) => line.length > 0)
|
|
172
|
+
.join("\n");
|
|
173
|
+
log("merge", "stale-merge-approval", message, {
|
|
174
|
+
approved_head_sha: headGate.approved_head_sha,
|
|
175
|
+
current_head_sha: headGate.current_head_sha,
|
|
176
|
+
auto_merge_disabled: headGate.auto_merge_disabled,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
exitCode: EXIT_ACTION_REQUIRED,
|
|
180
|
+
haltReason: "stale-merge-approval",
|
|
181
|
+
message,
|
|
182
|
+
prNumber,
|
|
183
|
+
watchVerdict: VERDICT_CLEAN,
|
|
184
|
+
mergeAttempted: false,
|
|
185
|
+
mergeSkippedReason: "stale-merge-approval",
|
|
186
|
+
grantId: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
132
190
|
// CLEAN path
|
|
133
191
|
if (options.merge !== true) {
|
|
134
192
|
const message = `pr:finish-loop CLEAN on PR #${prNumber}. Merge not requested ` +
|
|
@@ -165,12 +223,62 @@ export function runPrFinishLoop(options) {
|
|
|
165
223
|
grantId: null,
|
|
166
224
|
};
|
|
167
225
|
}
|
|
168
|
-
// Optional merge when policy allows
|
|
226
|
+
// Optional merge when policy allows — re-fetch live HEAD (not watch snapshot)
|
|
227
|
+
// and pin matchHeadCommit (#3235 TOCTOU).
|
|
169
228
|
if (options.mergeFn !== undefined) {
|
|
170
|
-
const
|
|
229
|
+
const fetchHead = options.fetchPrHeadShaFn ?? fetchPrHeadShaRest;
|
|
230
|
+
const liveHead = fetchHead(prNumber, repo);
|
|
231
|
+
if (options.skipMergeApprovalHeadGate !== true) {
|
|
232
|
+
if (liveHead === null || liveHead.trim() === "") {
|
|
233
|
+
disablePullRequestAutoMerge(prNumber, repo);
|
|
234
|
+
const message = `pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: cannot read live HEAD ` +
|
|
235
|
+
"before merge; auto-merge disabled (fail closed, #3235).";
|
|
236
|
+
log("merge", "stale-merge-approval", message);
|
|
237
|
+
return {
|
|
238
|
+
exitCode: EXIT_ACTION_REQUIRED,
|
|
239
|
+
haltReason: "stale-merge-approval",
|
|
240
|
+
message,
|
|
241
|
+
prNumber,
|
|
242
|
+
watchVerdict: VERDICT_CLEAN,
|
|
243
|
+
mergeAttempted: false,
|
|
244
|
+
mergeSkippedReason: "stale-merge-approval",
|
|
245
|
+
grantId: null,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const headGateFn = options.mergeApprovalHeadFn ?? enforceMergeApprovalHead;
|
|
249
|
+
const recheck = headGateFn({
|
|
250
|
+
prNumber,
|
|
251
|
+
repo,
|
|
252
|
+
projectRoot,
|
|
253
|
+
currentHeadSha: liveHead,
|
|
254
|
+
disableAutoMergeOnDeny: true,
|
|
255
|
+
});
|
|
256
|
+
if (!recheck.allowed) {
|
|
257
|
+
const message = [
|
|
258
|
+
`pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: stale merge approval at merge time.`,
|
|
259
|
+
recheck.message,
|
|
260
|
+
recheck.recovery ?? "",
|
|
261
|
+
]
|
|
262
|
+
.filter((line) => line.length > 0)
|
|
263
|
+
.join("\n");
|
|
264
|
+
log("merge", "stale-merge-approval", message);
|
|
265
|
+
return {
|
|
266
|
+
exitCode: EXIT_ACTION_REQUIRED,
|
|
267
|
+
haltReason: "stale-merge-approval",
|
|
268
|
+
message,
|
|
269
|
+
prNumber,
|
|
270
|
+
watchVerdict: VERDICT_CLEAN,
|
|
271
|
+
mergeAttempted: false,
|
|
272
|
+
mergeSkippedReason: "stale-merge-approval",
|
|
273
|
+
grantId: null,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const pinnedHead = liveHead;
|
|
278
|
+
const rc = options.mergeFn(prNumber, repo, { matchHeadCommit: pinnedHead });
|
|
171
279
|
if (rc === 0) {
|
|
172
280
|
const message = `pr:finish-loop MERGED PR #${prNumber}`;
|
|
173
|
-
log("merge", "merged", message);
|
|
281
|
+
log("merge", "merged", message, { matchHeadCommit: pinnedHead });
|
|
174
282
|
return {
|
|
175
283
|
exitCode: EXIT_OK,
|
|
176
284
|
haltReason: "merged",
|
|
@@ -182,7 +290,10 @@ export function runPrFinishLoop(options) {
|
|
|
182
290
|
grantId: null,
|
|
183
291
|
};
|
|
184
292
|
}
|
|
185
|
-
|
|
293
|
+
// Pin mismatch / merge fail: revoke auto-merge so unapproved head cannot land.
|
|
294
|
+
disablePullRequestAutoMerge(prNumber, repo);
|
|
295
|
+
const message = `pr:finish-loop merge attempt failed exit=${rc} for PR #${prNumber}; ` +
|
|
296
|
+
"disabled auto-merge after pinned-merge failure (#3235).";
|
|
186
297
|
log("merge", "error", message);
|
|
187
298
|
return {
|
|
188
299
|
exitCode: EXIT_BLOCKED,
|
|
@@ -8,7 +8,7 @@ export declare const EXIT_OK = 0;
|
|
|
8
8
|
export declare const EXIT_ACTION_REQUIRED = 1;
|
|
9
9
|
/** BLOCKED (grant/gate), ERRORED, TIMEOUT, config. */
|
|
10
10
|
export declare const EXIT_BLOCKED = 2;
|
|
11
|
-
export type FinishLoopHaltReason = "empty-queue" | "grant-missing" | "grant-expired" | "grant-deny" | "gate-deny" | "max-iterations" | "require-human-merge" | "address-findings" | "agent-implement" | "clean" | "merged" | "error";
|
|
11
|
+
export type FinishLoopHaltReason = "empty-queue" | "grant-missing" | "grant-expired" | "grant-deny" | "gate-deny" | "max-iterations" | "require-human-merge" | "stale-merge-approval" | "address-findings" | "agent-implement" | "clean" | "merged" | "error";
|
|
12
12
|
export type FinishLoopPhase = "gate" | "queue-scan" | "implement" | "pr-open" | "pr-watch" | "address" | "merge" | "halt";
|
|
13
13
|
export interface FinishLoopProgressLine {
|
|
14
14
|
readonly schemaVersion: 1;
|
|
@@ -142,11 +142,15 @@ export declare function decideHook(input: HookDispatchInput, seams?: HookPolicyS
|
|
|
142
142
|
/**
|
|
143
143
|
* Render host-facing hook output.
|
|
144
144
|
*
|
|
145
|
-
* Cursor deposits use `failClosed: true
|
|
146
|
-
*
|
|
147
|
-
* `
|
|
148
|
-
*
|
|
149
|
-
*
|
|
145
|
+
* Cursor deposits use `failClosed: true` with a tool.before timeout above the
|
|
146
|
+
* gated-ritual / live agent-hook readiness budget
|
|
147
|
+
* (`CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS` in init-deposit/agent-hooks; #3246).
|
|
148
|
+
* Cursor treats empty/null stdout (or a host timeout kill) as a hook failure
|
|
149
|
+
* and blocks the tool — so Cursor allows must emit explicit
|
|
150
|
+
* `{"permission":"allow"}` within the deposit timeout. Other hosts keep empty
|
|
151
|
+
* allow so the host permission flow is unchanged — except session.start /
|
|
152
|
+
* session.compact soft re-bind injection (#3171), which surfaces checklist
|
|
153
|
+
* text without a write tool.
|
|
150
154
|
*
|
|
151
155
|
* Cursor stdout always includes `code` (stable machine-readable decision code)
|
|
152
156
|
* so agents can distinguish policy denials from host-integration failures
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -184,10 +184,10 @@ export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
|
|
|
184
184
|
export function isAssistScratchWrite(projectRoot, targetPath, payload, environ = process.env) {
|
|
185
185
|
if (!isAllowlistedAssistScratchPath(projectRoot, targetPath))
|
|
186
186
|
return false;
|
|
187
|
-
// Structural classification only — compose with #3080 ephemeral markers.
|
|
187
|
+
// Structural classification only — compose with #3080 / #3259 ephemeral markers.
|
|
188
188
|
if (isAssistPosture(payload, environ))
|
|
189
189
|
return true;
|
|
190
|
-
if (isEphemeralSpawn(payload))
|
|
190
|
+
if (isEphemeralSpawn(payload, environ))
|
|
191
191
|
return true;
|
|
192
192
|
return false;
|
|
193
193
|
}
|
|
@@ -656,14 +656,18 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
656
656
|
if (!outsideRoot || isSpawnTool(toolName)) {
|
|
657
657
|
let proposedPathHint;
|
|
658
658
|
if (isSpawnTool(toolName)) {
|
|
659
|
-
// Multi-path recovery for implement-class spawns (#3080 AC4).
|
|
659
|
+
// Multi-path recovery for implement-class spawns (#3080 AC4 / #3259 honesty).
|
|
660
|
+
// Structural markers only — free-text prompt brackets are not sufficient.
|
|
660
661
|
proposedPathHint =
|
|
661
662
|
" Recovery: (1) Product implementation — run `deft scope:activate -- <path>` " +
|
|
662
663
|
"for the approved xBRIEF, then re-run the pre-start_agent gate stack. " +
|
|
663
|
-
"(2) Read-only research — spawn with `subagent_type`/`worker_role` explore. " +
|
|
664
|
-
"(3) Ephemeral docs/
|
|
665
|
-
"
|
|
666
|
-
"
|
|
664
|
+
"(2) Read-only research — spawn with structural `subagent_type`/`worker_role` explore. " +
|
|
665
|
+
"(3) Ephemeral docs/local-dev — set structural tool fields " +
|
|
666
|
+
"`worker_role`/`subagent_type` ∈ {ephemeral, docs, assist} (hosts that support them), " +
|
|
667
|
+
"or set session assist (`DEFT_SESSION_POSTURE=assist` or `DEFT_HOOK_ASSIST=1`), " +
|
|
668
|
+
"or run local-dev Shell (`docker compose` / `pnpm dev`) in the parent without a " +
|
|
669
|
+
"lifecycle story. Free-text markers such as `[worker_role: ephemeral]` in the " +
|
|
670
|
+
"prompt are NOT sufficient. Do not invent a fake scope only to satisfy this gate.";
|
|
667
671
|
}
|
|
668
672
|
else if (options.proposedLifecycleExempt &&
|
|
669
673
|
relTarget !== null &&
|
|
@@ -895,9 +899,9 @@ export function decideHook(input, seams = {}) {
|
|
|
895
899
|
scopePath: null,
|
|
896
900
|
};
|
|
897
901
|
}
|
|
898
|
-
// Ephemeral/docs/assist:
|
|
899
|
-
// Does not authorize push/merge/deploy —
|
|
900
|
-
if (isEphemeralSpawn(input.payload)) {
|
|
902
|
+
// Ephemeral/docs/assist (+ session assist env #3259): non-lifecycle spawn;
|
|
903
|
+
// no active xBRIEF. Does not authorize push/merge/deploy — shell/MCP matchers.
|
|
904
|
+
if (isEphemeralSpawn(input.payload, environ)) {
|
|
901
905
|
return {
|
|
902
906
|
verdict: "allow",
|
|
903
907
|
code: "spawn-ephemeral-ready",
|
|
@@ -951,11 +955,15 @@ function softAgentsRebindWireText(decision) {
|
|
|
951
955
|
/**
|
|
952
956
|
* Render host-facing hook output.
|
|
953
957
|
*
|
|
954
|
-
* Cursor deposits use `failClosed: true
|
|
955
|
-
*
|
|
956
|
-
* `
|
|
957
|
-
*
|
|
958
|
-
*
|
|
958
|
+
* Cursor deposits use `failClosed: true` with a tool.before timeout above the
|
|
959
|
+
* gated-ritual / live agent-hook readiness budget
|
|
960
|
+
* (`CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS` in init-deposit/agent-hooks; #3246).
|
|
961
|
+
* Cursor treats empty/null stdout (or a host timeout kill) as a hook failure
|
|
962
|
+
* and blocks the tool — so Cursor allows must emit explicit
|
|
963
|
+
* `{"permission":"allow"}` within the deposit timeout. Other hosts keep empty
|
|
964
|
+
* allow so the host permission flow is unchanged — except session.start /
|
|
965
|
+
* session.compact soft re-bind injection (#3171), which surfaces checklist
|
|
966
|
+
* text without a write tool.
|
|
959
967
|
*
|
|
960
968
|
* Cursor stdout always includes `code` (stable machine-readable decision code)
|
|
961
969
|
* so agents can distinguish policy denials from host-integration failures
|
package/dist/hooks/readonly.d.ts
CHANGED
|
@@ -13,9 +13,12 @@ export declare function isExploreSpawn(payload: unknown): boolean;
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function isAssistPosture(payload: unknown, environ?: NodeJS.ProcessEnv): boolean;
|
|
15
15
|
/**
|
|
16
|
-
* Ephemeral / assist / docs spawns skip active-xBRIEF implementation gates (#3080).
|
|
16
|
+
* Ephemeral / assist / docs spawns skip active-xBRIEF implementation gates (#3080 / #3259).
|
|
17
17
|
* True only with an explicit allowlisted marker. Absent marker → false (fail closed).
|
|
18
|
-
*
|
|
18
|
+
* Markers: structural `worker_role`/`subagent_type` ∈ {ephemeral,docs,assist}, OR
|
|
19
|
+
* session assist env (`DEFT_SESSION_POSTURE` assist-set / `DEFT_HOOK_ASSIST=1`) for spawn.
|
|
20
|
+
* Free-text prompt strings are never classified. When a marker conflicts with implement
|
|
21
|
+
* envelope signals, implement wins.
|
|
19
22
|
*/
|
|
20
|
-
export declare function isEphemeralSpawn(payload: unknown): boolean;
|
|
23
|
+
export declare function isEphemeralSpawn(payload: unknown, environ?: NodeJS.ProcessEnv): boolean;
|
|
21
24
|
//# sourceMappingURL=readonly.d.ts.map
|