@deftai/directive-core 0.80.0 → 0.82.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/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +139 -0
- package/dist/doctor/main.js +2 -1
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -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/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/index.d.ts +1 -0
- package/dist/policy/index.js +19 -5
- package/dist/policy/product-signal.d.ts +45 -0
- package/dist/policy/product-signal.js +210 -0
- 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/actor-name.d.ts +18 -0
- package/dist/product-signal/actor-name.js +94 -0
- package/dist/product-signal/consent.d.ts +30 -0
- package/dist/product-signal/consent.js +116 -0
- package/dist/product-signal/gates.d.ts +17 -0
- package/dist/product-signal/gates.js +66 -0
- package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
- package/dist/product-signal/github-private-sink-adapter.js +274 -0
- package/dist/product-signal/headless.d.ts +8 -0
- package/dist/product-signal/headless.js +32 -0
- package/dist/product-signal/install-context.d.ts +13 -0
- package/dist/product-signal/install-context.js +41 -0
- package/dist/product-signal/local-signal-summary.d.ts +7 -0
- package/dist/product-signal/local-signal-summary.js +173 -0
- package/dist/product-signal/payload.d.ts +90 -0
- package/dist/product-signal/payload.js +124 -0
- package/dist/product-signal/sink-bootstrap.d.ts +29 -0
- package/dist/product-signal/sink-bootstrap.js +108 -0
- package/dist/product-signal/submit-adapter.d.ts +14 -0
- package/dist/product-signal/submit-adapter.js +2 -0
- package/dist/product-signal/submit.d.ts +54 -0
- package/dist/product-signal/submit.js +290 -0
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scm/gh-rest.d.ts +3 -1
- package/dist/scm/gh-rest.js +22 -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/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
|
@@ -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 {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type RunGhApiFn } from "../scm/gh-rest.js";
|
|
2
|
+
export type ActorNameSource = "user-md" | "gh-login" | "unnamed";
|
|
3
|
+
export interface ResolvedActorName {
|
|
4
|
+
readonly actorName: string;
|
|
5
|
+
readonly actorNameSource: ActorNameSource;
|
|
6
|
+
readonly displayName: string;
|
|
7
|
+
}
|
|
8
|
+
/** Normalize for thread-key matching (#2693 D8). */
|
|
9
|
+
export declare function normalizeActorKey(name: string): string;
|
|
10
|
+
/** Parse addressing-name from USER.md Personal section (#2693 D8). */
|
|
11
|
+
export declare function parseAddressingNameFromUserMd(text: string): string | null;
|
|
12
|
+
export interface ResolveActorNameOptions {
|
|
13
|
+
readonly projectRoot?: string;
|
|
14
|
+
readonly runGhApiFn?: RunGhApiFn;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve actorName with USER.md -> gh-login -> unnamed precedence (#2693 D8). */
|
|
17
|
+
export declare function resolveActorName(options?: ResolveActorNameOptions): ResolvedActorName;
|
|
18
|
+
//# sourceMappingURL=actor-name.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { runGhApi } from "../scm/gh-rest.js";
|
|
3
|
+
import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
|
|
4
|
+
/** Normalize for thread-key matching (#2693 D8). */
|
|
5
|
+
export function normalizeActorKey(name) {
|
|
6
|
+
return name.trim().replace(/\s+/g, " ").toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/** Parse addressing-name from USER.md Personal section (#2693 D8). */
|
|
9
|
+
export function parseAddressingNameFromUserMd(text) {
|
|
10
|
+
const lines = text.split(/\r?\n/);
|
|
11
|
+
let inPersonal = false;
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (/^##\s+personal\b/i.test(trimmed)) {
|
|
15
|
+
inPersonal = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (inPersonal && /^##\s+/.test(trimmed)) {
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
const nameMatch = trimmed.match(/^(?:-\s*)?(?:\*\*)?Name(?:\*\*)?\s*:\s*(.+)$/i) ??
|
|
22
|
+
trimmed.match(/^(?:-\s*)?(?:\*\*)?addressing-name(?:\*\*)?\s*:\s*(.+)$/i);
|
|
23
|
+
if (nameMatch?.[1] !== undefined) {
|
|
24
|
+
const value = nameMatch[1].replace(/\*\*/g, "").trim();
|
|
25
|
+
if (value.length > 0) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!inPersonal) {
|
|
30
|
+
const topMatch = trimmed.match(/^(?:\*\*)?Name(?:\*\*)?\s*:\s*(.+)$/i);
|
|
31
|
+
if (topMatch?.[1] !== undefined) {
|
|
32
|
+
const value = topMatch[1].replace(/\*\*/g, "").trim();
|
|
33
|
+
if (value.length > 0) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function readGhLogin(runGhApiFn) {
|
|
42
|
+
try {
|
|
43
|
+
const result = (runGhApiFn ?? runGhApi)(["user"], { timeout: 15 });
|
|
44
|
+
if (result.returncode !== 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
48
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
49
|
+
const login = parsed.login;
|
|
50
|
+
if (typeof login === "string" && login.trim().length > 0) {
|
|
51
|
+
return login.trim();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// best-effort
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/** Resolve actorName with USER.md -> gh-login -> unnamed precedence (#2693 D8). */
|
|
61
|
+
export function resolveActorName(options = {}) {
|
|
62
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
63
|
+
const userMd = resolveUserMdPath({ projectRoot });
|
|
64
|
+
if (userMd.found) {
|
|
65
|
+
try {
|
|
66
|
+
const text = readFileSync(userMd.path, "utf8");
|
|
67
|
+
const fromUser = parseAddressingNameFromUserMd(text);
|
|
68
|
+
if (fromUser !== null) {
|
|
69
|
+
return {
|
|
70
|
+
actorName: fromUser,
|
|
71
|
+
actorNameSource: "user-md",
|
|
72
|
+
displayName: fromUser,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// fall through
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const ghLogin = readGhLogin(options.runGhApiFn);
|
|
81
|
+
if (ghLogin !== null) {
|
|
82
|
+
return {
|
|
83
|
+
actorName: ghLogin,
|
|
84
|
+
actorNameSource: "gh-login",
|
|
85
|
+
displayName: ghLogin,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
actorName: "unnamed",
|
|
90
|
+
actorNameSource: "unnamed",
|
|
91
|
+
displayName: "unnamed",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=actor-name.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
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 = 1;
|
|
4
|
+
/** Phase-1 consent tier permitting qualitative outbound (#2693 D2). */
|
|
5
|
+
export declare const PRODUCT_SIGNAL_CONSENT_TIER = "product-signal";
|
|
6
|
+
export interface ProductSignalConsentRecord {
|
|
7
|
+
readonly consentVersion: number;
|
|
8
|
+
readonly grantedAt: string;
|
|
9
|
+
readonly tier: string;
|
|
10
|
+
readonly revokedAt?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolveConsentPathOptions {
|
|
13
|
+
readonly platform?: NodeJS.Platform;
|
|
14
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
15
|
+
readonly homeDir?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Platform-config consent path adjacent to USER.md (#2693 D2). */
|
|
18
|
+
export declare function resolveProductSignalConsentPath(options?: ResolveConsentPathOptions): string;
|
|
19
|
+
/** Read consent file; returns null when absent, invalid, or revoked. */
|
|
20
|
+
export declare function readProductSignalConsent(options?: ResolveConsentPathOptions): ProductSignalConsentRecord | null;
|
|
21
|
+
/** True when a non-revoked consent grant exists. */
|
|
22
|
+
export declare function isProductSignalConsented(options?: ResolveConsentPathOptions): boolean;
|
|
23
|
+
export interface WriteConsentOptions extends ResolveConsentPathOptions {
|
|
24
|
+
readonly now?: Date;
|
|
25
|
+
}
|
|
26
|
+
/** Write a fresh consent grant (#2693 D17 yes path). */
|
|
27
|
+
export declare function grantProductSignalConsent(options?: WriteConsentOptions): ProductSignalConsentRecord;
|
|
28
|
+
/** Revoke consent by setting revokedAt (#2693 D2). */
|
|
29
|
+
export declare function revokeProductSignalConsent(options?: WriteConsentOptions): boolean;
|
|
30
|
+
//# sourceMappingURL=consent.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
|
|
5
|
+
export const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
|
|
6
|
+
/** Consent record schema version (#2693 D2). */
|
|
7
|
+
export const PRODUCT_SIGNAL_CONSENT_VERSION = 1;
|
|
8
|
+
/** Phase-1 consent tier permitting qualitative outbound (#2693 D2). */
|
|
9
|
+
export const PRODUCT_SIGNAL_CONSENT_TIER = "product-signal";
|
|
10
|
+
function resolveHomeDirForConsent(options) {
|
|
11
|
+
if (options.homeDir !== undefined) {
|
|
12
|
+
return options.homeDir;
|
|
13
|
+
}
|
|
14
|
+
const env = options.env ?? process.env;
|
|
15
|
+
const platform = options.platform ?? process.platform;
|
|
16
|
+
if (platform === "win32") {
|
|
17
|
+
const userProfile = env.USERPROFILE?.trim();
|
|
18
|
+
if (userProfile) {
|
|
19
|
+
return userProfile;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const home = env.HOME?.trim();
|
|
23
|
+
if (home) {
|
|
24
|
+
return home;
|
|
25
|
+
}
|
|
26
|
+
return homedir();
|
|
27
|
+
}
|
|
28
|
+
/** Platform-config consent path adjacent to USER.md (#2693 D2). */
|
|
29
|
+
export function resolveProductSignalConsentPath(options = {}) {
|
|
30
|
+
const platform = options.platform ?? process.platform;
|
|
31
|
+
const env = options.env ?? process.env;
|
|
32
|
+
const homeDir = resolveHomeDirForConsent(options);
|
|
33
|
+
return join(platformUserConfigDir(platform, env, homeDir), PRODUCT_SIGNAL_CONSENT_FILENAME);
|
|
34
|
+
}
|
|
35
|
+
function parseConsentRecord(raw) {
|
|
36
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const rec = raw;
|
|
40
|
+
if (typeof rec.consentVersion !== "number" || typeof rec.grantedAt !== "string") {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (typeof rec.tier !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const revokedAt = typeof rec.revokedAt === "string" ? rec.revokedAt : undefined;
|
|
47
|
+
return {
|
|
48
|
+
consentVersion: rec.consentVersion,
|
|
49
|
+
grantedAt: rec.grantedAt,
|
|
50
|
+
tier: rec.tier,
|
|
51
|
+
revokedAt,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Read consent file; returns null when absent, invalid, or revoked. */
|
|
55
|
+
export function readProductSignalConsent(options = {}) {
|
|
56
|
+
const path = resolveProductSignalConsentPath(options);
|
|
57
|
+
if (!existsSync(path)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
62
|
+
const record = parseConsentRecord(parsed);
|
|
63
|
+
if (record === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (record.revokedAt !== undefined && record.revokedAt.trim().length > 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return record;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** True when a non-revoked consent grant exists. */
|
|
76
|
+
export function isProductSignalConsented(options = {}) {
|
|
77
|
+
return readProductSignalConsent(options) !== null;
|
|
78
|
+
}
|
|
79
|
+
/** Write a fresh consent grant (#2693 D17 yes path). */
|
|
80
|
+
export function grantProductSignalConsent(options = {}) {
|
|
81
|
+
const now = options.now ?? new Date();
|
|
82
|
+
const record = {
|
|
83
|
+
consentVersion: PRODUCT_SIGNAL_CONSENT_VERSION,
|
|
84
|
+
grantedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
85
|
+
tier: PRODUCT_SIGNAL_CONSENT_TIER,
|
|
86
|
+
};
|
|
87
|
+
const path = resolveProductSignalConsentPath(options);
|
|
88
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
89
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
90
|
+
return record;
|
|
91
|
+
}
|
|
92
|
+
/** Revoke consent by setting revokedAt (#2693 D2). */
|
|
93
|
+
export function revokeProductSignalConsent(options = {}) {
|
|
94
|
+
const path = resolveProductSignalConsentPath(options);
|
|
95
|
+
if (!existsSync(path)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const now = options.now ?? new Date();
|
|
99
|
+
let existing = null;
|
|
100
|
+
try {
|
|
101
|
+
existing = parseConsentRecord(JSON.parse(readFileSync(path, "utf8")));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (existing === null) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const revoked = {
|
|
110
|
+
...existing,
|
|
111
|
+
revokedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
112
|
+
};
|
|
113
|
+
writeFileSync(path, `${JSON.stringify(revoked, null, 2)}\n`, "utf8");
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ProductSignalOutcome = "submitted" | "dry-run" | "disabled" | "no-consent" | "no-network" | "non-interactive" | "sink-unreachable" | "sink-unauthorized" | "validation" | "error-config";
|
|
2
|
+
export interface GateEvaluation {
|
|
3
|
+
readonly allowed: boolean;
|
|
4
|
+
readonly outcome: ProductSignalOutcome;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EvaluateGatesOptions {
|
|
8
|
+
readonly projectRoot: string;
|
|
9
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
10
|
+
readonly requireConsent?: boolean;
|
|
11
|
+
readonly stdinIsTTY?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Pre-submit gate evaluation (#2693 D11 soft-skip matrix). */
|
|
14
|
+
export declare function evaluateProductSignalGates(options: EvaluateGatesOptions): GateEvaluation;
|
|
15
|
+
/** Map GhRestError-like conditions to soft-skip outcomes (#2693 D18). */
|
|
16
|
+
export declare function classifySinkError(stderr: string, exitCode: number): ProductSignalOutcome;
|
|
17
|
+
//# sourceMappingURL=gates.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolveProductSignal } from "../policy/product-signal.js";
|
|
2
|
+
import { isProductSignalConsented } from "./consent.js";
|
|
3
|
+
import { isHeadlessSession } from "./headless.js";
|
|
4
|
+
/** Pre-submit gate evaluation (#2693 D11 soft-skip matrix). */
|
|
5
|
+
export function evaluateProductSignalGates(options) {
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
const policy = resolveProductSignal(options.projectRoot);
|
|
8
|
+
if (!policy.enabled) {
|
|
9
|
+
return {
|
|
10
|
+
allowed: false,
|
|
11
|
+
outcome: "disabled",
|
|
12
|
+
message: "product-signal disabled (plan.policy.productSignal.enabled=false).",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (env.DEFT_NO_NETWORK === "1") {
|
|
16
|
+
return {
|
|
17
|
+
allowed: false,
|
|
18
|
+
outcome: "no-network",
|
|
19
|
+
message: "product-signal skipped (DEFT_NO_NETWORK=1).",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (isHeadlessSession({ env, stdinIsTTY: options.stdinIsTTY })) {
|
|
23
|
+
if (options.requireConsent !== false && !isProductSignalConsented({ env })) {
|
|
24
|
+
return {
|
|
25
|
+
allowed: false,
|
|
26
|
+
outcome: "non-interactive",
|
|
27
|
+
message: "product-signal skipped in headless/non-interactive session (no consent on file).",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (options.requireConsent !== false && !isProductSignalConsented({ env })) {
|
|
32
|
+
return {
|
|
33
|
+
allowed: false,
|
|
34
|
+
outcome: "no-consent",
|
|
35
|
+
message: "product-signal requires consent (`task product-signal:consent -- --grant`).",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (policy.error !== null) {
|
|
39
|
+
return {
|
|
40
|
+
allowed: false,
|
|
41
|
+
outcome: "error-config",
|
|
42
|
+
message: `product-signal policy error: ${policy.error}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
allowed: true,
|
|
47
|
+
outcome: "submitted",
|
|
48
|
+
message: "gates passed",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Map GhRestError-like conditions to soft-skip outcomes (#2693 D18). */
|
|
52
|
+
export function classifySinkError(stderr, exitCode) {
|
|
53
|
+
const text = stderr.toLowerCase();
|
|
54
|
+
if (text.includes("401") ||
|
|
55
|
+
text.includes("403") ||
|
|
56
|
+
text.includes("404") ||
|
|
57
|
+
text.includes("not found") ||
|
|
58
|
+
text.includes("permission")) {
|
|
59
|
+
return "sink-unauthorized";
|
|
60
|
+
}
|
|
61
|
+
if (exitCode !== 0) {
|
|
62
|
+
return "sink-unreachable";
|
|
63
|
+
}
|
|
64
|
+
return "sink-unreachable";
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=gates.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type GhRestSeams } from "../scm/gh-rest.js";
|
|
2
|
+
import type { ProductSignalPayload, ProductSignalSurface } from "./payload.js";
|
|
3
|
+
import type { SubmitAdapter, SubmitResult } from "./submit-adapter.js";
|
|
4
|
+
export declare const PRODUCT_SIGNAL_MARKER_PREFIX = "<!-- deft:product-signal v1";
|
|
5
|
+
export declare const SURFACE_LABELS: Record<ProductSignalSurface, string>;
|
|
6
|
+
/** Build HTML comment marker for standing-thread lookup (#2693 D8). */
|
|
7
|
+
export declare function buildThreadMarker(installId: string, actorName: string, surface: ProductSignalSurface): string;
|
|
8
|
+
/** Merge labels for PATCH updates (GitHub replaces the full label set). */
|
|
9
|
+
export declare function mergeIssueLabels(existing: Record<string, unknown>, next: readonly string[]): string[];
|
|
10
|
+
export interface GitHubPrivateSinkAdapterOptions {
|
|
11
|
+
readonly sinkRepo: string;
|
|
12
|
+
readonly seams?: GhRestSeams;
|
|
13
|
+
}
|
|
14
|
+
/** Phase-1 GitHub private sink adapter (#2693 D5/D8). */
|
|
15
|
+
export declare class GitHubPrivateSinkAdapter implements SubmitAdapter {
|
|
16
|
+
readonly id = "github-private-sink";
|
|
17
|
+
private readonly repo;
|
|
18
|
+
private readonly seams;
|
|
19
|
+
constructor(options: GitHubPrivateSinkAdapterOptions);
|
|
20
|
+
submit(payload: ProductSignalPayload, extras?: {
|
|
21
|
+
readonly gapText?: string | null;
|
|
22
|
+
}): Promise<SubmitResult>;
|
|
23
|
+
private submitPortrait;
|
|
24
|
+
private submitPulse;
|
|
25
|
+
/** Append a Gap: comment on the standing pulse thread (#2693 D19). */
|
|
26
|
+
appendGapCommentOnPulse(payload: ProductSignalPayload, gapText: string): SubmitResult;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=github-private-sink-adapter.d.ts.map
|