@deftai/directive-core 0.81.0 → 0.83.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/content-contracts/skills/helpers.d.ts +6 -0
- package/dist/content-contracts/skills/helpers.js +47 -3
- package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
- package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -0
- package/dist/hooks/cursor-hooks.d.ts +23 -0
- package/dist/hooks/cursor-hooks.js +95 -0
- package/dist/hooks/dispatcher.d.ts +8 -1
- package/dist/hooks/dispatcher.js +23 -5
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -2
- package/dist/init-deposit/agent-hooks.js +137 -14
- package/dist/init-deposit/hygiene.js +1 -0
- package/dist/intake/github-body-cli.js +2 -0
- package/dist/intake/github-body.d.ts +11 -0
- package/dist/intake/github-body.js +95 -24
- package/dist/integration-e2e/helpers.js +1 -1
- package/dist/issue-sync/sync-from-xbrief.js +3 -0
- package/dist/lifecycle/events.d.ts +1 -0
- package/dist/lifecycle/events.js +59 -9
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +275 -0
- package/dist/orphan-active/index.d.ts +3 -0
- package/dist/orphan-active/index.js +3 -0
- package/dist/orphan-active/refs.d.ts +16 -0
- package/dist/orphan-active/refs.js +105 -0
- package/dist/policy/host-hooks.d.ts +21 -0
- package/dist/policy/host-hooks.js +96 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +18 -0
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +44 -73
- package/dist/pr-wait-mergeable/result.d.ts +2 -1
- package/dist/pr-wait-mergeable/result.js +4 -0
- package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
- package/dist/pr-wait-mergeable/semantic-green.js +202 -0
- package/dist/pr-wait-mergeable/types.d.ts +1 -0
- package/dist/pr-watch/constants.d.ts +4 -4
- package/dist/pr-watch/constants.js +4 -4
- package/dist/pr-watch/watch.js +5 -3
- package/dist/product-signal/consent.d.ts +20 -3
- package/dist/product-signal/consent.js +63 -6
- package/dist/product-signal/gates.d.ts +1 -1
- package/dist/product-signal/submit.d.ts +5 -1
- package/dist/product-signal/submit.js +42 -18
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scope/brief-io.d.ts +26 -0
- package/dist/scope/brief-io.js +77 -0
- package/dist/scope/decompose.js +2 -2
- package/dist/scope/decomposed-refs.js +3 -3
- package/dist/scope/demote.js +2 -2
- package/dist/scope/project-definition-sync.d.ts +2 -2
- package/dist/scope/project-definition-sync.js +15 -3
- package/dist/scope/registry-artifact-sync.d.ts +6 -1
- package/dist/scope/registry-artifact-sync.js +31 -13
- package/dist/scope/transition.js +23 -15
- package/dist/scope/undo.js +2 -2
- package/dist/scope/vbrief-json.d.ts +1 -2
- package/dist/scope/vbrief-json.js +1 -4
- package/dist/staleness-tickler/state.js +19 -2
- package/dist/triage/queue/cache.js +5 -0
- package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
- package/dist/vbrief-validate/plan-hooks.js +25 -0
- package/dist/verify-env/agent-hooks.d.ts +2 -1
- package/dist/verify-env/agent-hooks.js +3 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
- package/dist/verify-source/skill-external-fetch-gate.js +71 -0
- package/package.json +7 -3
|
@@ -16,22 +16,45 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
16
16
|
let capMinutes = 60;
|
|
17
17
|
const protectedValues = [];
|
|
18
18
|
let emitJson = false;
|
|
19
|
+
let cascadeMode = false;
|
|
20
|
+
let requireMasterCiGreen = false;
|
|
21
|
+
let baseBranch = null;
|
|
22
|
+
const baseReturn = () => ({
|
|
23
|
+
prNumber,
|
|
24
|
+
repo,
|
|
25
|
+
capMinutes,
|
|
26
|
+
protectedValues,
|
|
27
|
+
emitJson,
|
|
28
|
+
cascadeMode,
|
|
29
|
+
requireMasterCiGreen,
|
|
30
|
+
baseBranch,
|
|
31
|
+
});
|
|
19
32
|
for (let i = 0; i < argv.length; i += 1) {
|
|
20
33
|
const arg = argv[i];
|
|
21
34
|
if (arg === "--json") {
|
|
22
35
|
emitJson = true;
|
|
23
36
|
}
|
|
37
|
+
else if (arg === "--cascade") {
|
|
38
|
+
cascadeMode = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === "--require-master-ci-green") {
|
|
41
|
+
requireMasterCiGreen = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "--base-branch") {
|
|
44
|
+
const value = argv[i + 1];
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return { ...baseReturn(), error: "argument --base-branch: expected one argument" };
|
|
47
|
+
}
|
|
48
|
+
baseBranch = value;
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
else if (arg?.startsWith("--base-branch=")) {
|
|
52
|
+
baseBranch = arg.slice("--base-branch=".length);
|
|
53
|
+
}
|
|
24
54
|
else if (arg === "--repo") {
|
|
25
55
|
const value = argv[i + 1];
|
|
26
56
|
if (value === undefined) {
|
|
27
|
-
return {
|
|
28
|
-
prNumber,
|
|
29
|
-
repo,
|
|
30
|
-
capMinutes,
|
|
31
|
-
protectedValues,
|
|
32
|
-
emitJson,
|
|
33
|
-
error: "argument --repo: expected one argument",
|
|
34
|
-
};
|
|
57
|
+
return { ...baseReturn(), error: "argument --repo: expected one argument" };
|
|
35
58
|
}
|
|
36
59
|
repo = value;
|
|
37
60
|
i += 1;
|
|
@@ -42,25 +65,11 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
42
65
|
else if (arg === "--cap-minutes") {
|
|
43
66
|
const value = argv[i + 1];
|
|
44
67
|
if (value === undefined) {
|
|
45
|
-
return {
|
|
46
|
-
prNumber,
|
|
47
|
-
repo,
|
|
48
|
-
capMinutes,
|
|
49
|
-
protectedValues,
|
|
50
|
-
emitJson,
|
|
51
|
-
error: "argument --cap-minutes: expected one argument",
|
|
52
|
-
};
|
|
68
|
+
return { ...baseReturn(), error: "argument --cap-minutes: expected one argument" };
|
|
53
69
|
}
|
|
54
70
|
const parsed = Number(value);
|
|
55
71
|
if (!Number.isFinite(parsed)) {
|
|
56
|
-
return {
|
|
57
|
-
prNumber,
|
|
58
|
-
repo,
|
|
59
|
-
capMinutes,
|
|
60
|
-
protectedValues,
|
|
61
|
-
emitJson,
|
|
62
|
-
error: `invalid --cap-minutes value: ${value}`,
|
|
63
|
-
};
|
|
72
|
+
return { ...baseReturn(), error: `invalid --cap-minutes value: ${value}` };
|
|
64
73
|
}
|
|
65
74
|
capMinutes = parsed;
|
|
66
75
|
i += 1;
|
|
@@ -69,28 +78,14 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
69
78
|
const value = arg.slice("--cap-minutes=".length);
|
|
70
79
|
const parsed = Number(value);
|
|
71
80
|
if (!Number.isFinite(parsed)) {
|
|
72
|
-
return {
|
|
73
|
-
prNumber,
|
|
74
|
-
repo,
|
|
75
|
-
capMinutes,
|
|
76
|
-
protectedValues,
|
|
77
|
-
emitJson,
|
|
78
|
-
error: `invalid --cap-minutes value: ${value}`,
|
|
79
|
-
};
|
|
81
|
+
return { ...baseReturn(), error: `invalid --cap-minutes value: ${value}` };
|
|
80
82
|
}
|
|
81
83
|
capMinutes = parsed;
|
|
82
84
|
}
|
|
83
85
|
else if (arg === "--protected") {
|
|
84
86
|
const value = argv[i + 1];
|
|
85
87
|
if (value === undefined) {
|
|
86
|
-
return {
|
|
87
|
-
prNumber,
|
|
88
|
-
repo,
|
|
89
|
-
capMinutes,
|
|
90
|
-
protectedValues,
|
|
91
|
-
emitJson,
|
|
92
|
-
error: "argument --protected: expected one argument",
|
|
93
|
-
};
|
|
88
|
+
return { ...baseReturn(), error: "argument --protected: expected one argument" };
|
|
94
89
|
}
|
|
95
90
|
protectedValues.push(value);
|
|
96
91
|
i += 1;
|
|
@@ -99,51 +94,23 @@ export function parseWaitMergeableArgs(argv) {
|
|
|
99
94
|
protectedValues.push(arg.slice("--protected=".length));
|
|
100
95
|
}
|
|
101
96
|
else if (arg?.startsWith("-")) {
|
|
102
|
-
return {
|
|
103
|
-
prNumber,
|
|
104
|
-
repo,
|
|
105
|
-
capMinutes,
|
|
106
|
-
protectedValues,
|
|
107
|
-
emitJson,
|
|
108
|
-
error: `unrecognized arguments: ${arg}`,
|
|
109
|
-
};
|
|
97
|
+
return { ...baseReturn(), error: `unrecognized arguments: ${arg}` };
|
|
110
98
|
}
|
|
111
99
|
else if (prNumber === null) {
|
|
112
100
|
const n = Number(arg);
|
|
113
101
|
if (!Number.isInteger(n)) {
|
|
114
|
-
return {
|
|
115
|
-
prNumber,
|
|
116
|
-
repo,
|
|
117
|
-
capMinutes,
|
|
118
|
-
protectedValues,
|
|
119
|
-
emitJson,
|
|
120
|
-
error: `invalid PR number: ${arg}`,
|
|
121
|
-
};
|
|
102
|
+
return { ...baseReturn(), error: `invalid PR number: ${arg}` };
|
|
122
103
|
}
|
|
123
104
|
prNumber = n;
|
|
124
105
|
}
|
|
125
106
|
else {
|
|
126
|
-
return {
|
|
127
|
-
prNumber,
|
|
128
|
-
repo,
|
|
129
|
-
capMinutes,
|
|
130
|
-
protectedValues,
|
|
131
|
-
emitJson,
|
|
132
|
-
error: `unrecognized arguments: ${arg}`,
|
|
133
|
-
};
|
|
107
|
+
return { ...baseReturn(), error: `unrecognized arguments: ${arg}` };
|
|
134
108
|
}
|
|
135
109
|
}
|
|
136
110
|
if (prNumber === null) {
|
|
137
|
-
return {
|
|
138
|
-
prNumber,
|
|
139
|
-
repo,
|
|
140
|
-
capMinutes,
|
|
141
|
-
protectedValues,
|
|
142
|
-
emitJson,
|
|
143
|
-
error: "the following arguments are required: pr_number",
|
|
144
|
-
};
|
|
111
|
+
return { ...baseReturn(), error: "the following arguments are required: pr_number" };
|
|
145
112
|
}
|
|
146
|
-
return
|
|
113
|
+
return baseReturn();
|
|
147
114
|
}
|
|
148
115
|
function summaryLabelForExit(exitCode) {
|
|
149
116
|
switch (exitCode) {
|
|
@@ -183,6 +150,10 @@ export function runWaitMergeable(argv, options = {}) {
|
|
|
183
150
|
protectedFn: options.protectedFn,
|
|
184
151
|
monitorFn: options.monitorFn,
|
|
185
152
|
mergeFn: options.mergeFn,
|
|
153
|
+
semanticGreenFn: options.semanticGreenFn,
|
|
154
|
+
cascadeMode: args.cascadeMode,
|
|
155
|
+
requireMasterCiGreen: args.requireMasterCiGreen,
|
|
156
|
+
baseBranch: args.baseBranch,
|
|
186
157
|
});
|
|
187
158
|
const summaryLabel = summaryLabelForExit(result.exitCode);
|
|
188
159
|
process.stderr.write(`[pr_wait_mergeable] PR #${result.prNumber} repo=${result.repo} ` +
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { WaitMergeableResult } from "./types.js";
|
|
2
2
|
export declare function toResultDict(result: WaitMergeableResult): Record<string, unknown>;
|
|
3
|
-
export declare function makeResult(fields: Omit<WaitMergeableResult, "monitorResult" | "protectedCheck" | "mergeStdout" | "mergeStderr"> & {
|
|
3
|
+
export declare function makeResult(fields: Omit<WaitMergeableResult, "monitorResult" | "protectedCheck" | "semanticGreen" | "mergeStdout" | "mergeStderr"> & {
|
|
4
4
|
readonly monitorResult?: Record<string, unknown>;
|
|
5
5
|
readonly protectedCheck?: Record<string, unknown>;
|
|
6
|
+
readonly semanticGreen?: Record<string, unknown>;
|
|
6
7
|
readonly mergeStdout?: string;
|
|
7
8
|
readonly mergeStderr?: string;
|
|
8
9
|
}): WaitMergeableResult;
|
|
@@ -11,6 +11,9 @@ export function toResultDict(result) {
|
|
|
11
11
|
if (Object.keys(result.protectedCheck).length > 0) {
|
|
12
12
|
payload.protected_check = result.protectedCheck;
|
|
13
13
|
}
|
|
14
|
+
if (Object.keys(result.semanticGreen).length > 0) {
|
|
15
|
+
payload.semantic_green = result.semanticGreen;
|
|
16
|
+
}
|
|
14
17
|
if (result.mergeStdout.length > 0) {
|
|
15
18
|
payload.merge_stdout = result.mergeStdout;
|
|
16
19
|
}
|
|
@@ -26,6 +29,7 @@ export function makeResult(fields) {
|
|
|
26
29
|
return {
|
|
27
30
|
monitorResult: fields.monitorResult ?? {},
|
|
28
31
|
protectedCheck: fields.protectedCheck ?? {},
|
|
32
|
+
semanticGreen: fields.semanticGreen ?? {},
|
|
29
33
|
mergeStdout: fields.mergeStdout ?? "",
|
|
30
34
|
mergeStderr: fields.mergeStderr ?? "",
|
|
31
35
|
prNumber: fields.prNumber,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RunGhFn } from "../pr-merge-readiness/types.js";
|
|
2
|
+
export interface SemanticGreenOptions {
|
|
3
|
+
/** Enable merge-cascade semantic-green checks (#2385). */
|
|
4
|
+
readonly cascadeMode?: boolean;
|
|
5
|
+
/** After a prior cascade merge, require target-branch CI green at HEAD before the next merge. */
|
|
6
|
+
readonly requireMasterCiGreen?: boolean;
|
|
7
|
+
/** Target branch for stale-base comparison (defaults to PR base.ref). */
|
|
8
|
+
readonly baseBranch?: string | null;
|
|
9
|
+
readonly runGh?: RunGhFn;
|
|
10
|
+
}
|
|
11
|
+
export interface SemanticGreenPayload {
|
|
12
|
+
readonly pr_base_sha: string | null;
|
|
13
|
+
readonly target_branch: string | null;
|
|
14
|
+
readonly target_head_sha: string | null;
|
|
15
|
+
readonly master_ci: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface SemanticGreenResult {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly outcome: string | null;
|
|
20
|
+
readonly exitCode: number | null;
|
|
21
|
+
readonly error: string | null;
|
|
22
|
+
readonly payload: SemanticGreenPayload;
|
|
23
|
+
}
|
|
24
|
+
/** Pre-merge semantic-green gate for merge cascades (#2385). */
|
|
25
|
+
export declare function evaluateSemanticGreen(prNumber: number, repo: string, options?: SemanticGreenOptions): SemanticGreenResult;
|
|
26
|
+
export type SemanticGreenFn = typeof evaluateSemanticGreen;
|
|
27
|
+
//# sourceMappingURL=semantic-green.d.ts.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { buildCiSummaryLine, evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
|
|
2
|
+
import { defaultRunGh, fetchCheckRunsRest } from "../pr-merge-readiness/gh.js";
|
|
3
|
+
import { EXIT_CONFIG_ERROR, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
|
|
4
|
+
function emptyPayload() {
|
|
5
|
+
return {
|
|
6
|
+
pr_base_sha: null,
|
|
7
|
+
target_branch: null,
|
|
8
|
+
target_head_sha: null,
|
|
9
|
+
master_ci: {},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function fetchPrBase(prNumber, repo, runGh) {
|
|
13
|
+
const rc = runGh(["gh", "api", `repos/${repo}/pulls/${prNumber}`]);
|
|
14
|
+
if (rc.returncode !== 0) {
|
|
15
|
+
return {
|
|
16
|
+
baseRef: null,
|
|
17
|
+
baseSha: null,
|
|
18
|
+
error: `gh api /pulls/${prNumber} failed: ${rc.stderr.trim()}`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (!rc.stdout.trim()) {
|
|
22
|
+
return { baseRef: null, baseSha: null, error: "empty body from gh api /pulls/<N>" };
|
|
23
|
+
}
|
|
24
|
+
let payload;
|
|
25
|
+
try {
|
|
26
|
+
payload = JSON.parse(rc.stdout);
|
|
27
|
+
}
|
|
28
|
+
catch (exc) {
|
|
29
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
30
|
+
return { baseRef: null, baseSha: null, error: `could not parse PR JSON: ${message}` };
|
|
31
|
+
}
|
|
32
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
33
|
+
return { baseRef: null, baseSha: null, error: "unexpected PR JSON shape (not a dict)" };
|
|
34
|
+
}
|
|
35
|
+
const base = payload.base;
|
|
36
|
+
if (base === null || typeof base !== "object" || Array.isArray(base)) {
|
|
37
|
+
return { baseRef: null, baseSha: null, error: "PR JSON missing base object" };
|
|
38
|
+
}
|
|
39
|
+
const baseRecord = base;
|
|
40
|
+
const baseRef = typeof baseRecord.ref === "string" && baseRecord.ref.length > 0 ? baseRecord.ref : null;
|
|
41
|
+
const baseSha = typeof baseRecord.sha === "string" && baseRecord.sha.length > 0 ? baseRecord.sha : null;
|
|
42
|
+
if (baseRef === null || baseSha === null) {
|
|
43
|
+
return { baseRef, baseSha, error: "PR JSON missing base.ref or base.sha" };
|
|
44
|
+
}
|
|
45
|
+
return { baseRef, baseSha, error: null };
|
|
46
|
+
}
|
|
47
|
+
function fetchBranchHeadSha(repo, branch, runGh) {
|
|
48
|
+
const encoded = branch.split("/").map(encodeURIComponent).join("/");
|
|
49
|
+
const rc = runGh(["gh", "api", `repos/${repo}/git/ref/heads/${encoded}`]);
|
|
50
|
+
if (rc.returncode !== 0) {
|
|
51
|
+
return {
|
|
52
|
+
sha: null,
|
|
53
|
+
error: `gh api /git/ref/heads/${branch} failed: ${rc.stderr.trim()}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!rc.stdout.trim()) {
|
|
57
|
+
return { sha: null, error: `empty body from gh api /git/ref/heads/${branch}` };
|
|
58
|
+
}
|
|
59
|
+
let payload;
|
|
60
|
+
try {
|
|
61
|
+
payload = JSON.parse(rc.stdout);
|
|
62
|
+
}
|
|
63
|
+
catch (exc) {
|
|
64
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
65
|
+
return { sha: null, error: `could not parse branch ref JSON: ${message}` };
|
|
66
|
+
}
|
|
67
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
68
|
+
return { sha: null, error: "unexpected branch ref JSON shape (not a dict)" };
|
|
69
|
+
}
|
|
70
|
+
const object = payload.object;
|
|
71
|
+
if (object === null || typeof object !== "object" || Array.isArray(object)) {
|
|
72
|
+
return { sha: null, error: "branch ref JSON missing object.sha" };
|
|
73
|
+
}
|
|
74
|
+
const sha = object.sha;
|
|
75
|
+
if (typeof sha !== "string" || sha.length === 0) {
|
|
76
|
+
return { sha: null, error: "branch ref JSON missing object.sha" };
|
|
77
|
+
}
|
|
78
|
+
return { sha, error: null };
|
|
79
|
+
}
|
|
80
|
+
function evaluateTargetBranchCi(repo, headSha, runGh) {
|
|
81
|
+
const check = fetchCheckRunsRest(headSha, repo, runGh);
|
|
82
|
+
if (check.error.length > 0) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: "Required CI check-runs on target branch HEAD could not be fetched; " +
|
|
86
|
+
`fail closed (#2385). ${check.error}`,
|
|
87
|
+
masterCi: { ready_state: "blocked", error: check.error },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const ciResult = evaluateCiGate(check.checkRuns);
|
|
91
|
+
const masterCi = {
|
|
92
|
+
...ciResult.summary,
|
|
93
|
+
summary_line: buildCiSummaryLine(ciResult.summary),
|
|
94
|
+
};
|
|
95
|
+
if (ciResult.summary.ready_state !== "ready") {
|
|
96
|
+
const detail = ciResult.failures.length > 0
|
|
97
|
+
? ciResult.failures.join(" ")
|
|
98
|
+
: `target branch CI ready_state=${ciResult.summary.ready_state}`;
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: "Target branch CI is not green at current HEAD; block cascade merge until master is green (#2385). " +
|
|
102
|
+
detail,
|
|
103
|
+
masterCi,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, error: null, masterCi };
|
|
107
|
+
}
|
|
108
|
+
/** Pre-merge semantic-green gate for merge cascades (#2385). */
|
|
109
|
+
export function evaluateSemanticGreen(prNumber, repo, options = {}) {
|
|
110
|
+
if (options.cascadeMode !== true) {
|
|
111
|
+
return { ok: true, outcome: null, exitCode: null, error: null, payload: emptyPayload() };
|
|
112
|
+
}
|
|
113
|
+
const runGh = options.runGh ?? defaultRunGh;
|
|
114
|
+
const prBase = fetchPrBase(prNumber, repo, runGh);
|
|
115
|
+
if (prBase.error !== null) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
outcome: "config-error",
|
|
119
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
120
|
+
error: prBase.error,
|
|
121
|
+
payload: {
|
|
122
|
+
...emptyPayload(),
|
|
123
|
+
pr_base_sha: prBase.baseSha,
|
|
124
|
+
target_branch: prBase.baseRef,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const targetBranch = options.baseBranch ?? prBase.baseRef;
|
|
129
|
+
if (targetBranch === null || targetBranch.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
outcome: "config-error",
|
|
133
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
134
|
+
error: "Could not resolve target branch for semantic-green gate (--base-branch or PR base.ref).",
|
|
135
|
+
payload: {
|
|
136
|
+
pr_base_sha: prBase.baseSha,
|
|
137
|
+
target_branch: null,
|
|
138
|
+
target_head_sha: null,
|
|
139
|
+
master_ci: {},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const branchHead = fetchBranchHeadSha(repo, targetBranch, runGh);
|
|
144
|
+
if (branchHead.error !== null) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
outcome: "config-error",
|
|
148
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
149
|
+
error: branchHead.error,
|
|
150
|
+
payload: {
|
|
151
|
+
pr_base_sha: prBase.baseSha,
|
|
152
|
+
target_branch: targetBranch,
|
|
153
|
+
target_head_sha: null,
|
|
154
|
+
master_ci: {},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const payload = {
|
|
159
|
+
pr_base_sha: prBase.baseSha,
|
|
160
|
+
target_branch: targetBranch,
|
|
161
|
+
target_head_sha: branchHead.sha,
|
|
162
|
+
master_ci: {},
|
|
163
|
+
};
|
|
164
|
+
if (prBase.baseSha !== branchHead.sha) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
outcome: "semantic-stale-base",
|
|
168
|
+
exitCode: EXIT_TIMEOUT_OR_ESCALATION,
|
|
169
|
+
error: "PR base SHA is behind the current target branch HEAD (merge-tree-clean but semantically stale). " +
|
|
170
|
+
`Rebase/update-branch onto ${targetBranch} at ${branchHead.sha} and wait for fresh green CI before cascade merge (#2385). ` +
|
|
171
|
+
`pr_base=${prBase.baseSha} target_head=${branchHead.sha}`,
|
|
172
|
+
payload,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (options.requireMasterCiGreen === true) {
|
|
176
|
+
const ci = evaluateTargetBranchCi(repo, branchHead.sha, runGh);
|
|
177
|
+
if (!ci.ok) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
outcome: "master-ci-not-green",
|
|
181
|
+
exitCode: EXIT_TIMEOUT_OR_ESCALATION,
|
|
182
|
+
error: ci.error,
|
|
183
|
+
payload: {
|
|
184
|
+
...payload,
|
|
185
|
+
master_ci: ci.masterCi,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
outcome: null,
|
|
192
|
+
exitCode: null,
|
|
193
|
+
error: null,
|
|
194
|
+
payload: {
|
|
195
|
+
...payload,
|
|
196
|
+
master_ci: ci.masterCi,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, outcome: null, exitCode: null, error: null, payload };
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=semantic-green.js.map
|
|
@@ -5,6 +5,7 @@ export interface WaitMergeableResult {
|
|
|
5
5
|
readonly exitCode: number;
|
|
6
6
|
readonly monitorResult: Record<string, unknown>;
|
|
7
7
|
readonly protectedCheck: Record<string, unknown>;
|
|
8
|
+
readonly semanticGreen: Record<string, unknown>;
|
|
8
9
|
readonly mergeStdout: string;
|
|
9
10
|
readonly mergeStderr: string;
|
|
10
11
|
readonly error: string | null;
|
|
@@ -39,10 +39,10 @@ export declare const DEFAULT_POLL_SECONDS = 90;
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const WATCH_HELP: string;
|
|
41
41
|
/**
|
|
42
|
-
* Consecutive polls
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Consecutive polls where the CLEAN gate is wedged on HEAD (!has_blocking &&
|
|
43
|
+
* !is_clean with a holdout other than sha_match) before STALL (#1039). Stale-SHA
|
|
44
|
+
* reads (sha_match holdout) do NOT advance this counter — re-review in flight
|
|
45
|
+
* waits until max-wait cap (#2313 / #1259 INCOMPLETE_BUT_RATED).
|
|
46
46
|
*/
|
|
47
47
|
export declare const DEFAULT_STALL_THRESHOLD = 3;
|
|
48
48
|
/**
|
|
@@ -61,10 +61,10 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
|
|
|
61
61
|
" 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
|
|
62
62
|
" 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL | config / usage error\n";
|
|
63
63
|
/**
|
|
64
|
-
* Consecutive polls
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
64
|
+
* Consecutive polls where the CLEAN gate is wedged on HEAD (!has_blocking &&
|
|
65
|
+
* !is_clean with a holdout other than sha_match) before STALL (#1039). Stale-SHA
|
|
66
|
+
* reads (sha_match holdout) do NOT advance this counter — re-review in flight
|
|
67
|
+
* waits until max-wait cap (#2313 / #1259 INCOMPLETE_BUT_RATED).
|
|
68
68
|
*/
|
|
69
69
|
export const DEFAULT_STALL_THRESHOLD = 3;
|
|
70
70
|
/**
|
package/dist/pr-watch/watch.js
CHANGED
|
@@ -99,9 +99,11 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
99
99
|
if (!oneShot && ciBlockedStreak >= DEFAULT_CI_BLOCKED_THRESHOLD) {
|
|
100
100
|
return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
|
|
101
101
|
}
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
102
|
+
// STALL (#1039): wedged CLEAN-gate on HEAD — !has_blocking && !is_clean for N
|
|
103
|
+
// consecutive polls with a holdout OTHER than sha_match. Stale-SHA reads
|
|
104
|
+
// (clean_gate_holdout=sha_match) are INCOMPLETE for HEAD per #1259 / #2313 —
|
|
105
|
+
// keep polling until cap, not early STALL while re-review is in flight.
|
|
106
|
+
if (!probe.hasBlocking && !probe.isClean && probe.cleanGateHoldout !== "sha_match") {
|
|
105
107
|
stallStreak += 1;
|
|
106
108
|
}
|
|
107
109
|
else {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export declare const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
|
|
2
|
-
/** Consent record schema version (#2693 D2). */
|
|
3
|
-
export declare const PRODUCT_SIGNAL_CONSENT_VERSION =
|
|
2
|
+
/** Consent record schema version (#2693 D2, #2767 v2 sink binding). */
|
|
3
|
+
export declare const PRODUCT_SIGNAL_CONSENT_VERSION = 2;
|
|
4
4
|
/** Phase-1 consent tier permitting qualitative outbound (#2693 D2). */
|
|
5
5
|
export declare const PRODUCT_SIGNAL_CONSENT_TIER = "product-signal";
|
|
6
6
|
export interface ProductSignalConsentRecord {
|
|
7
7
|
readonly consentVersion: number;
|
|
8
8
|
readonly grantedAt: string;
|
|
9
9
|
readonly tier: string;
|
|
10
|
+
/** Normalized owner/repo sink authorized by v2 consent (#2767). */
|
|
11
|
+
readonly sinkRepo?: string;
|
|
10
12
|
readonly revokedAt?: string;
|
|
11
13
|
}
|
|
12
14
|
export interface ResolveConsentPathOptions {
|
|
@@ -14,16 +16,31 @@ export interface ResolveConsentPathOptions {
|
|
|
14
16
|
readonly env?: NodeJS.ProcessEnv;
|
|
15
17
|
readonly homeDir?: string;
|
|
16
18
|
}
|
|
19
|
+
export interface SinkAuthorizationResult {
|
|
20
|
+
readonly authorized: boolean;
|
|
21
|
+
readonly configuredSink: string;
|
|
22
|
+
readonly consentedSink: string | null;
|
|
23
|
+
readonly sinksMatch: boolean;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}
|
|
17
26
|
/** Platform-config consent path adjacent to USER.md (#2693 D2). */
|
|
18
27
|
export declare function resolveProductSignalConsentPath(options?: ResolveConsentPathOptions): string;
|
|
28
|
+
/** Normalize sinkRepo to lowercase owner/repo (#2767). */
|
|
29
|
+
export declare function normalizeProductSignalSinkRepo(raw: string): string;
|
|
30
|
+
/** Resolve the sink authorized by a consent record (#2767). */
|
|
31
|
+
export declare function resolveConsentedProductSignalSink(consent: ProductSignalConsentRecord | null): string | null;
|
|
32
|
+
/** Authorize configured sink against install consent (#2767). */
|
|
33
|
+
export declare function authorizeProductSignalSink(configuredSink: string, consent: ProductSignalConsentRecord | null): SinkAuthorizationResult;
|
|
19
34
|
/** Read consent file; returns null when absent, invalid, or revoked. */
|
|
20
35
|
export declare function readProductSignalConsent(options?: ResolveConsentPathOptions): ProductSignalConsentRecord | null;
|
|
21
36
|
/** True when a non-revoked consent grant exists. */
|
|
22
37
|
export declare function isProductSignalConsented(options?: ResolveConsentPathOptions): boolean;
|
|
23
38
|
export interface WriteConsentOptions extends ResolveConsentPathOptions {
|
|
24
39
|
readonly now?: Date;
|
|
40
|
+
/** Normalized sink to bind into v2 consent (#2767). Defaults to baked-in sink. */
|
|
41
|
+
readonly sinkRepo?: string;
|
|
25
42
|
}
|
|
26
|
-
/** Write a fresh consent grant (#2693 D17 yes path). */
|
|
43
|
+
/** Write a fresh consent grant (#2693 D17 yes path, #2767 v2 sink binding). */
|
|
27
44
|
export declare function grantProductSignalConsent(options?: WriteConsentOptions): ProductSignalConsentRecord;
|
|
28
45
|
/** Revoke consent by setting revokedAt (#2693 D2). */
|
|
29
46
|
export declare function revokeProductSignalConsent(options?: WriteConsentOptions): boolean;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { DEFAULT_PRODUCT_SIGNAL_SINK_REPO } from "../policy/product-signal.js";
|
|
4
5
|
import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
|
|
5
6
|
export const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
|
|
6
|
-
/** Consent record schema version (#2693 D2). */
|
|
7
|
-
export const PRODUCT_SIGNAL_CONSENT_VERSION =
|
|
7
|
+
/** Consent record schema version (#2693 D2, #2767 v2 sink binding). */
|
|
8
|
+
export const PRODUCT_SIGNAL_CONSENT_VERSION = 2;
|
|
9
|
+
/** Legacy consent schema — authorizes default sink only (#2767). */
|
|
10
|
+
const PRODUCT_SIGNAL_CONSENT_VERSION_V1 = 1;
|
|
8
11
|
/** Phase-1 consent tier permitting qualitative outbound (#2693 D2). */
|
|
9
12
|
export const PRODUCT_SIGNAL_CONSENT_TIER = "product-signal";
|
|
10
13
|
function resolveHomeDirForConsent(options) {
|
|
@@ -32,25 +35,76 @@ export function resolveProductSignalConsentPath(options = {}) {
|
|
|
32
35
|
const homeDir = resolveHomeDirForConsent(options);
|
|
33
36
|
return join(platformUserConfigDir(platform, env, homeDir), PRODUCT_SIGNAL_CONSENT_FILENAME);
|
|
34
37
|
}
|
|
38
|
+
/** Normalize sinkRepo to lowercase owner/repo (#2767). */
|
|
39
|
+
export function normalizeProductSignalSinkRepo(raw) {
|
|
40
|
+
const sink = raw.trim().replace(/^https?:\/\/github\.com\//i, "");
|
|
41
|
+
return sink.replace(/\/+$/, "").toLowerCase();
|
|
42
|
+
}
|
|
35
43
|
function parseConsentRecord(raw) {
|
|
36
44
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
37
45
|
return null;
|
|
38
46
|
}
|
|
39
47
|
const rec = raw;
|
|
40
|
-
if (typeof rec.consentVersion !== "number" ||
|
|
48
|
+
if (typeof rec.consentVersion !== "number" ||
|
|
49
|
+
typeof rec.grantedAt !== "string" ||
|
|
50
|
+
typeof rec.tier !== "string") {
|
|
41
51
|
return null;
|
|
42
52
|
}
|
|
43
|
-
|
|
53
|
+
const revokedAt = typeof rec.revokedAt === "string" ? rec.revokedAt : undefined;
|
|
54
|
+
let sinkRepo;
|
|
55
|
+
if (rec.consentVersion >= PRODUCT_SIGNAL_CONSENT_VERSION) {
|
|
56
|
+
if (typeof rec.sinkRepo !== "string" || rec.sinkRepo.trim().length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
sinkRepo = normalizeProductSignalSinkRepo(rec.sinkRepo);
|
|
60
|
+
if (sinkRepo.length === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (rec.consentVersion !== PRODUCT_SIGNAL_CONSENT_VERSION_V1) {
|
|
44
65
|
return null;
|
|
45
66
|
}
|
|
46
|
-
const revokedAt = typeof rec.revokedAt === "string" ? rec.revokedAt : undefined;
|
|
47
67
|
return {
|
|
48
68
|
consentVersion: rec.consentVersion,
|
|
49
69
|
grantedAt: rec.grantedAt,
|
|
50
70
|
tier: rec.tier,
|
|
71
|
+
sinkRepo,
|
|
51
72
|
revokedAt,
|
|
52
73
|
};
|
|
53
74
|
}
|
|
75
|
+
/** Resolve the sink authorized by a consent record (#2767). */
|
|
76
|
+
export function resolveConsentedProductSignalSink(consent) {
|
|
77
|
+
if (consent === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (consent.consentVersion >= PRODUCT_SIGNAL_CONSENT_VERSION) {
|
|
81
|
+
return consent.sinkRepo ?? null;
|
|
82
|
+
}
|
|
83
|
+
if (consent.consentVersion === PRODUCT_SIGNAL_CONSENT_VERSION_V1) {
|
|
84
|
+
return normalizeProductSignalSinkRepo(DEFAULT_PRODUCT_SIGNAL_SINK_REPO);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** Authorize configured sink against install consent (#2767). */
|
|
89
|
+
export function authorizeProductSignalSink(configuredSink, consent) {
|
|
90
|
+
const configured = normalizeProductSignalSinkRepo(configuredSink);
|
|
91
|
+
const consented = resolveConsentedProductSignalSink(consent);
|
|
92
|
+
const sinksMatch = consented !== null && configured === consented;
|
|
93
|
+
let message = "sink authorized";
|
|
94
|
+
if (!sinksMatch) {
|
|
95
|
+
message =
|
|
96
|
+
consented === null
|
|
97
|
+
? "product-signal requires consent (`task product-signal:consent -- --grant`)."
|
|
98
|
+
: `product-signal skipped (sink-unconsented): configured sink=${configured} does not match consented sink=${consented}. Re-run \`task product-signal:consent -- --grant\` after confirming the destination.`;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
authorized: sinksMatch,
|
|
102
|
+
configuredSink: configured,
|
|
103
|
+
consentedSink: consented,
|
|
104
|
+
sinksMatch,
|
|
105
|
+
message,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
54
108
|
/** Read consent file; returns null when absent, invalid, or revoked. */
|
|
55
109
|
export function readProductSignalConsent(options = {}) {
|
|
56
110
|
const path = resolveProductSignalConsentPath(options);
|
|
@@ -76,13 +130,16 @@ export function readProductSignalConsent(options = {}) {
|
|
|
76
130
|
export function isProductSignalConsented(options = {}) {
|
|
77
131
|
return readProductSignalConsent(options) !== null;
|
|
78
132
|
}
|
|
79
|
-
/** Write a fresh consent grant (#2693 D17 yes path). */
|
|
133
|
+
/** Write a fresh consent grant (#2693 D17 yes path, #2767 v2 sink binding). */
|
|
80
134
|
export function grantProductSignalConsent(options = {}) {
|
|
81
135
|
const now = options.now ?? new Date();
|
|
136
|
+
const normalizedSink = normalizeProductSignalSinkRepo((options.sinkRepo ?? DEFAULT_PRODUCT_SIGNAL_SINK_REPO).trim());
|
|
137
|
+
const sinkRepo = normalizedSink || normalizeProductSignalSinkRepo(DEFAULT_PRODUCT_SIGNAL_SINK_REPO);
|
|
82
138
|
const record = {
|
|
83
139
|
consentVersion: PRODUCT_SIGNAL_CONSENT_VERSION,
|
|
84
140
|
grantedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
85
141
|
tier: PRODUCT_SIGNAL_CONSENT_TIER,
|
|
142
|
+
sinkRepo,
|
|
86
143
|
};
|
|
87
144
|
const path = resolveProductSignalConsentPath(options);
|
|
88
145
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProductSignalOutcome = "submitted" | "dry-run" | "disabled" | "no-consent" | "no-network" | "non-interactive" | "sink-unreachable" | "sink-unauthorized" | "validation" | "error-config";
|
|
1
|
+
export type ProductSignalOutcome = "submitted" | "dry-run" | "disabled" | "no-consent" | "no-network" | "non-interactive" | "sink-unconsented" | "sink-unreachable" | "sink-unauthorized" | "validation" | "error-config";
|
|
2
2
|
export interface GateEvaluation {
|
|
3
3
|
readonly allowed: boolean;
|
|
4
4
|
readonly outcome: ProductSignalOutcome;
|