@deftai/directive-core 0.79.4 → 0.81.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/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +139 -0
- package/dist/doctor/main.js +2 -1
- package/dist/hooks/dispatcher.d.ts +18 -3
- package/dist/hooks/dispatcher.js +173 -62
- package/dist/hooks/readonly.d.ts +7 -0
- package/dist/hooks/readonly.js +83 -0
- package/dist/hooks/tools.d.ts +7 -0
- package/dist/hooks/tools.js +16 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -1
- package/dist/init-deposit/agent-hooks.js +77 -21
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +45 -1
- package/dist/policy/product-signal.d.ts +45 -0
- package/dist/policy/product-signal.js +210 -0
- package/dist/policy/runtime-authority.d.ts +54 -0
- package/dist/policy/runtime-authority.js +180 -0
- package/dist/policy/staleness-tickler.d.ts +20 -0
- package/dist/policy/staleness-tickler.js +151 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
- package/dist/pr-merge-readiness/ci-gate.js +38 -5
- package/dist/pr-merge-readiness/gh.d.ts +4 -0
- package/dist/pr-merge-readiness/gh.js +10 -3
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +2 -1
- package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
- package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
- package/dist/pr-watch/constants.d.ts +5 -0
- package/dist/pr-watch/constants.js +6 -1
- package/dist/pr-watch/main.js +8 -0
- package/dist/pr-watch/probe.js +8 -0
- package/dist/pr-watch/types.d.ts +7 -0
- package/dist/pr-watch/watch.js +8 -1
- 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/scm/gh-rest.d.ts +3 -1
- package/dist/scm/gh-rest.js +22 -0
- package/dist/scope/main.js +12 -0
- package/dist/session/release-availability.d.ts +21 -0
- package/dist/session/release-availability.js +109 -0
- package/dist/session/ritual-sentinel.d.ts +14 -0
- package/dist/session/ritual-sentinel.js +28 -0
- package/dist/session/session-start.d.ts +10 -0
- package/dist/session/session-start.js +18 -0
- package/dist/staleness-tickler/escalation.d.ts +14 -0
- package/dist/staleness-tickler/escalation.js +106 -0
- package/dist/staleness-tickler/idle.d.ts +18 -0
- package/dist/staleness-tickler/idle.js +47 -0
- package/dist/staleness-tickler/index.d.ts +8 -0
- package/dist/staleness-tickler/index.js +8 -0
- package/dist/staleness-tickler/probe-directive.d.ts +32 -0
- package/dist/staleness-tickler/probe-directive.js +106 -0
- package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
- package/dist/staleness-tickler/probe-xbrief.js +103 -0
- package/dist/staleness-tickler/run.d.ts +31 -0
- package/dist/staleness-tickler/run.js +227 -0
- package/dist/staleness-tickler/state.d.ts +10 -0
- package/dist/staleness-tickler/state.js +46 -0
- package/dist/staleness-tickler/types.d.ts +85 -0
- package/dist/staleness-tickler/types.js +5 -0
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +50 -0
- package/dist/verify-env/agent-hooks.js +4 -2
- package/package.json +3 -3
package/dist/pr-watch/probe.js
CHANGED
|
@@ -15,6 +15,8 @@ function errorProbe(headSha, message) {
|
|
|
15
15
|
errored: false,
|
|
16
16
|
ciFailures: 0,
|
|
17
17
|
ciFailedChecks: [],
|
|
18
|
+
ciReadyState: null,
|
|
19
|
+
ciCapacityStalledChecks: [],
|
|
18
20
|
terminalCheckRun: false,
|
|
19
21
|
isClean: false,
|
|
20
22
|
cleanGateHoldout: null,
|
|
@@ -63,6 +65,8 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
|
|
|
63
65
|
// still owns the hard CI gate via pr:merge-ready (#796).
|
|
64
66
|
let ciFailures = 0;
|
|
65
67
|
let ciFailedChecks = [];
|
|
68
|
+
let ciReadyState = null;
|
|
69
|
+
let ciCapacityStalledChecks = [];
|
|
66
70
|
let terminalCheckRun = true;
|
|
67
71
|
if (repo !== null) {
|
|
68
72
|
const check = fetchCheckRunsRest(headSha, repo, runGh);
|
|
@@ -70,6 +74,8 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
|
|
|
70
74
|
const ci = evaluateCiGate(check.checkRuns, {});
|
|
71
75
|
ciFailedChecks = ci.summary.failed_required;
|
|
72
76
|
ciFailures = ciFailedChecks.length;
|
|
77
|
+
ciReadyState = ci.summary.ready_state;
|
|
78
|
+
ciCapacityStalledChecks = ci.summary.capacity_stalled_required;
|
|
73
79
|
terminalCheckRun = ci.summary.pending_required.length === 0;
|
|
74
80
|
}
|
|
75
81
|
}
|
|
@@ -95,6 +101,8 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
|
|
|
95
101
|
errored,
|
|
96
102
|
ciFailures,
|
|
97
103
|
ciFailedChecks,
|
|
104
|
+
ciReadyState,
|
|
105
|
+
ciCapacityStalledChecks,
|
|
98
106
|
terminalCheckRun,
|
|
99
107
|
isClean,
|
|
100
108
|
cleanGateHoldout,
|
package/dist/pr-watch/types.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface WatchProbe {
|
|
|
15
15
|
readonly ciFailures: number;
|
|
16
16
|
/** Failed required check identities (name + conclusion), when CI was probed. */
|
|
17
17
|
readonly ciFailedChecks: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* CI gate ready_state from evaluateCiGate (#2169 / #2672).
|
|
20
|
+
* Includes `runner_capacity_stall` distinct from `not_ready_yet`.
|
|
21
|
+
*/
|
|
22
|
+
readonly ciReadyState: string | null;
|
|
23
|
+
/** Required checks classified as capacity-stalled (#2672). */
|
|
24
|
+
readonly ciCapacityStalledChecks: readonly string[];
|
|
18
25
|
/** All required CI check-runs have a terminal conclusion (none pending). */
|
|
19
26
|
readonly terminalCheckRun: boolean;
|
|
20
27
|
readonly isClean: boolean;
|
package/dist/pr-watch/watch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultRunGh } from "../pr-merge-readiness/gh.js";
|
|
2
|
-
import { DEFAULT_CI_BLOCKED_THRESHOLD, DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CI_BLOCKED, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
|
|
2
|
+
import { DEFAULT_CI_BLOCKED_THRESHOLD, DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CI_BLOCKED, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_RUNNER_CAPACITY_STALL, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
|
|
3
3
|
import { probeOnce } from "./probe.js";
|
|
4
4
|
const systemMonotonicClock = {
|
|
5
5
|
now() {
|
|
@@ -80,6 +80,11 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
80
80
|
if (probe.errored) {
|
|
81
81
|
return build(VERDICT_ERRORED, EXIT_TERMINAL_ERROR, probe, poll);
|
|
82
82
|
}
|
|
83
|
+
// #2672: capacity stall is distinct from ordinary not_ready_yet — surface
|
|
84
|
+
// immediately (exit 2) so agents wait for auto-failover instead of --skip-ci.
|
|
85
|
+
if (probe.ciReadyState === "runner_capacity_stall") {
|
|
86
|
+
return build(VERDICT_RUNNER_CAPACITY_STALL, EXIT_TERMINAL_ERROR, probe, poll);
|
|
87
|
+
}
|
|
83
88
|
// #2688: Greptile side satisfied on HEAD but CI red — fail loud toward a
|
|
84
89
|
// fix loop instead of burning max-wait-minutes on idle Greptile polls.
|
|
85
90
|
if (probe.cleanGateHoldout === "ci_failures") {
|
|
@@ -128,6 +133,8 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
128
133
|
errored: false,
|
|
129
134
|
ciFailures: 0,
|
|
130
135
|
ciFailedChecks: [],
|
|
136
|
+
ciReadyState: null,
|
|
137
|
+
ciCapacityStalledChecks: [],
|
|
131
138
|
terminalCheckRun: false,
|
|
132
139
|
isClean: false,
|
|
133
140
|
cleanGateHoldout: null,
|
|
@@ -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
|