@deftai/directive-core 0.80.0 → 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/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/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/package.json +3 -3
|
@@ -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
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { GhRestError, restCreateIssue, restIssueListPaginated, restPostComment, restUpdateIssue, } from "../scm/gh-rest.js";
|
|
2
|
+
import { normalizeActorKey } from "./actor-name.js";
|
|
3
|
+
import { classifySinkError } from "./gates.js";
|
|
4
|
+
export const PRODUCT_SIGNAL_MARKER_PREFIX = "<!-- deft:product-signal v1";
|
|
5
|
+
export const SURFACE_LABELS = {
|
|
6
|
+
pulse: "surface:pulse",
|
|
7
|
+
portrait: "surface:portrait",
|
|
8
|
+
};
|
|
9
|
+
/** Build HTML comment marker for standing-thread lookup (#2693 D8). */
|
|
10
|
+
export function buildThreadMarker(installId, actorName, surface) {
|
|
11
|
+
return (`${PRODUCT_SIGNAL_MARKER_PREFIX} ` +
|
|
12
|
+
`installId=${installId} actorKey=${normalizeActorKey(actorName)} surface=${surface} -->`);
|
|
13
|
+
}
|
|
14
|
+
function npsLabels(nps) {
|
|
15
|
+
if (nps === null) {
|
|
16
|
+
return ["nps:none"];
|
|
17
|
+
}
|
|
18
|
+
if (nps >= 9) {
|
|
19
|
+
return ["nps:promoter"];
|
|
20
|
+
}
|
|
21
|
+
if (nps >= 7) {
|
|
22
|
+
return ["nps:passive"];
|
|
23
|
+
}
|
|
24
|
+
return ["nps:detractor"];
|
|
25
|
+
}
|
|
26
|
+
function directiveVersionLabel(version) {
|
|
27
|
+
const match = version.match(/^(\d+\.\d+)/);
|
|
28
|
+
const minor = match?.[1] ?? "0.0";
|
|
29
|
+
return `directive:${minor}`;
|
|
30
|
+
}
|
|
31
|
+
function labelNamesFromIssue(issue) {
|
|
32
|
+
const labels = issue.labels;
|
|
33
|
+
if (!Array.isArray(labels)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const names = [];
|
|
37
|
+
for (const label of labels) {
|
|
38
|
+
if (typeof label === "string" && label.trim().length > 0) {
|
|
39
|
+
names.push(label.trim());
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (label !== null && typeof label === "object" && !Array.isArray(label)) {
|
|
43
|
+
const name = label.name;
|
|
44
|
+
if (typeof name === "string" && name.trim().length > 0) {
|
|
45
|
+
names.push(name.trim());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return names;
|
|
50
|
+
}
|
|
51
|
+
/** Merge labels for PATCH updates (GitHub replaces the full label set). */
|
|
52
|
+
export function mergeIssueLabels(existing, next) {
|
|
53
|
+
const merged = new Set(labelNamesFromIssue(existing));
|
|
54
|
+
for (const label of next) {
|
|
55
|
+
if (label.trim().length > 0) {
|
|
56
|
+
merged.add(label.trim());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...merged];
|
|
60
|
+
}
|
|
61
|
+
function formatPayloadBody(payload) {
|
|
62
|
+
const marker = buildThreadMarker(payload.installId, payload.actorName, payload.surface);
|
|
63
|
+
const humanLines = [];
|
|
64
|
+
if (payload.human.nps !== null) {
|
|
65
|
+
humanLines.push(`- NPS: ${payload.human.nps}`);
|
|
66
|
+
}
|
|
67
|
+
for (const answer of payload.human.answers) {
|
|
68
|
+
humanLines.push(`- **${answer.q}**: ${answer.a}`);
|
|
69
|
+
}
|
|
70
|
+
if (payload.human.freeText !== null && payload.human.freeText.trim().length > 0) {
|
|
71
|
+
humanLines.push(`- Free text: ${payload.human.freeText.trim()}`);
|
|
72
|
+
}
|
|
73
|
+
const summaryJson = payload.localSignalSummary !== null
|
|
74
|
+
? `\n<details><summary>localSignalSummary</summary>\n\n\`\`\`json\n${JSON.stringify(payload.localSignalSummary, null, 2)}\n\`\`\`\n</details>\n`
|
|
75
|
+
: "";
|
|
76
|
+
const skillsJson = payload.skillsSummary !== null
|
|
77
|
+
? `\n<details><summary>skillsSummary</summary>\n\n\`\`\`json\n${JSON.stringify(payload.skillsSummary, null, 2)}\n\`\`\`\n</details>\n`
|
|
78
|
+
: "";
|
|
79
|
+
return [
|
|
80
|
+
marker,
|
|
81
|
+
"",
|
|
82
|
+
`## ${payload.surface === "portrait" ? "Portrait" : "Pulse"} — ${payload.actorName}`,
|
|
83
|
+
"",
|
|
84
|
+
`- installId: \`${payload.installId}\``,
|
|
85
|
+
`- actorNameSource: ${payload.actorNameSource}`,
|
|
86
|
+
`- directive: ${payload.directiveVersion}`,
|
|
87
|
+
`- harness: ${payload.harness}${payload.harnessVersion ? ` (${payload.harnessVersion})` : ""}`,
|
|
88
|
+
`- os: ${payload.os} / shell: ${payload.shell}`,
|
|
89
|
+
`- collectedAt: ${payload.collectedAt}`,
|
|
90
|
+
"",
|
|
91
|
+
"### Human",
|
|
92
|
+
"",
|
|
93
|
+
humanLines.length > 0 ? humanLines.join("\n") : "_(no human answers)_",
|
|
94
|
+
payload.agentNotes ? `\n### Agent notes\n\n${payload.agentNotes}` : "",
|
|
95
|
+
summaryJson,
|
|
96
|
+
skillsJson,
|
|
97
|
+
"",
|
|
98
|
+
"_Submitted via product-signal (Refs #2693)._",
|
|
99
|
+
"",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
function formatPulseComment(payload) {
|
|
103
|
+
const date = payload.collectedAt.slice(0, 10);
|
|
104
|
+
const humanLines = [];
|
|
105
|
+
if (payload.human.nps !== null) {
|
|
106
|
+
humanLines.push(`NPS ${payload.human.nps}`);
|
|
107
|
+
}
|
|
108
|
+
for (const answer of payload.human.answers) {
|
|
109
|
+
humanLines.push(`${answer.q}: ${answer.a}`);
|
|
110
|
+
}
|
|
111
|
+
if (payload.human.freeText) {
|
|
112
|
+
humanLines.push(payload.human.freeText);
|
|
113
|
+
}
|
|
114
|
+
return [`**Pulse ${date}**`, "", ...humanLines, ""].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function findStandingIssue(repo, installId, actorName, surface, seams) {
|
|
117
|
+
const needleKey = normalizeActorKey(actorName);
|
|
118
|
+
const markerNeedle = `installId=${installId} actorKey=${needleKey} surface=${surface}`;
|
|
119
|
+
const issues = restIssueListPaginated(repo, { state: "open", labels: [SURFACE_LABELS[surface]], limit: 200 }, seams);
|
|
120
|
+
for (const issue of issues) {
|
|
121
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
122
|
+
if (body.includes(markerNeedle)) {
|
|
123
|
+
return issue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function issueUrlFromRecord(issue) {
|
|
129
|
+
const url = issue.html_url;
|
|
130
|
+
return typeof url === "string" ? url : null;
|
|
131
|
+
}
|
|
132
|
+
function issueNumberFromRecord(issue) {
|
|
133
|
+
const num = issue.number;
|
|
134
|
+
return typeof num === "number" ? num : null;
|
|
135
|
+
}
|
|
136
|
+
/** Phase-1 GitHub private sink adapter (#2693 D5/D8). */
|
|
137
|
+
export class GitHubPrivateSinkAdapter {
|
|
138
|
+
id = "github-private-sink";
|
|
139
|
+
repo;
|
|
140
|
+
seams;
|
|
141
|
+
constructor(options) {
|
|
142
|
+
this.repo = options.sinkRepo;
|
|
143
|
+
this.seams = options.seams ?? {};
|
|
144
|
+
}
|
|
145
|
+
async submit(payload, extras) {
|
|
146
|
+
try {
|
|
147
|
+
const labels = [
|
|
148
|
+
SURFACE_LABELS[payload.surface],
|
|
149
|
+
...npsLabels(payload.human.nps),
|
|
150
|
+
directiveVersionLabel(payload.directiveVersion),
|
|
151
|
+
];
|
|
152
|
+
let result;
|
|
153
|
+
if (payload.surface === "portrait") {
|
|
154
|
+
result = this.submitPortrait(payload, labels);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
result = this.submitPulse(payload, labels);
|
|
158
|
+
}
|
|
159
|
+
if (result.outcome === "submitted" &&
|
|
160
|
+
extras?.gapText !== undefined &&
|
|
161
|
+
extras.gapText !== null &&
|
|
162
|
+
extras.gapText.trim().length > 0) {
|
|
163
|
+
this.appendGapCommentOnPulse(payload, extras.gapText.trim());
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err instanceof GhRestError) {
|
|
169
|
+
const outcome = classifySinkError(err.stderr, err.exitCode);
|
|
170
|
+
return {
|
|
171
|
+
outcome,
|
|
172
|
+
message: `product-signal soft-skip (${outcome}): ${err.stderr || err.message}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
return {
|
|
177
|
+
outcome: "sink-unreachable",
|
|
178
|
+
message: `product-signal soft-skip (sink-unreachable): ${message}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
submitPortrait(payload, labels) {
|
|
183
|
+
const body = formatPayloadBody(payload);
|
|
184
|
+
const title = `[portrait] ${payload.actorName} @ ${payload.installId.slice(0, 8)}`;
|
|
185
|
+
const existing = findStandingIssue(this.repo, payload.installId, payload.actorName, "portrait", this.seams);
|
|
186
|
+
if (existing !== null) {
|
|
187
|
+
const num = issueNumberFromRecord(existing);
|
|
188
|
+
if (num === null) {
|
|
189
|
+
return { outcome: "sink-unreachable", message: "standing portrait issue missing number" };
|
|
190
|
+
}
|
|
191
|
+
const updated = restUpdateIssue(this.repo, num, { body, labels: [...labels] }, this.seams);
|
|
192
|
+
return {
|
|
193
|
+
outcome: "submitted",
|
|
194
|
+
issueUrl: issueUrlFromRecord(updated),
|
|
195
|
+
issueNumber: issueNumberFromRecord(updated),
|
|
196
|
+
message: `portrait upserted on #${num}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const created = restCreateIssue(this.repo, title, body, labels, this.seams);
|
|
200
|
+
return {
|
|
201
|
+
outcome: "submitted",
|
|
202
|
+
issueUrl: issueUrlFromRecord(created),
|
|
203
|
+
issueNumber: issueNumberFromRecord(created),
|
|
204
|
+
message: "portrait standing issue created",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
submitPulse(payload, labels) {
|
|
208
|
+
const existing = findStandingIssue(this.repo, payload.installId, payload.actorName, "pulse", this.seams);
|
|
209
|
+
let issueNumber;
|
|
210
|
+
let issueUrl;
|
|
211
|
+
if (existing === null) {
|
|
212
|
+
const title = `[pulse] ${payload.actorName} @ ${payload.installId.slice(0, 8)}`;
|
|
213
|
+
const body = formatPayloadBody(payload);
|
|
214
|
+
const created = restCreateIssue(this.repo, title, body, labels, this.seams);
|
|
215
|
+
issueNumber = issueNumberFromRecord(created);
|
|
216
|
+
issueUrl = issueUrlFromRecord(created);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
issueNumber = issueNumberFromRecord(existing);
|
|
220
|
+
issueUrl = issueUrlFromRecord(existing);
|
|
221
|
+
if (issueNumber !== null) {
|
|
222
|
+
restUpdateIssue(this.repo, issueNumber, { labels: mergeIssueLabels(existing, labels) }, this.seams);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (issueNumber === null) {
|
|
226
|
+
return { outcome: "sink-unreachable", message: "pulse standing issue missing number" };
|
|
227
|
+
}
|
|
228
|
+
restPostComment(this.repo, issueNumber, formatPulseComment(payload), this.seams);
|
|
229
|
+
return {
|
|
230
|
+
outcome: "submitted",
|
|
231
|
+
issueUrl,
|
|
232
|
+
issueNumber,
|
|
233
|
+
message: `pulse comment appended on #${issueNumber}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Append a Gap: comment on the standing pulse thread (#2693 D19). */
|
|
237
|
+
appendGapCommentOnPulse(payload, gapText) {
|
|
238
|
+
try {
|
|
239
|
+
const existing = findStandingIssue(this.repo, payload.installId, payload.actorName, "pulse", this.seams);
|
|
240
|
+
if (existing === null) {
|
|
241
|
+
return {
|
|
242
|
+
outcome: "sink-unreachable",
|
|
243
|
+
message: "no standing pulse issue for gap comment",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const num = issueNumberFromRecord(existing);
|
|
247
|
+
if (num === null) {
|
|
248
|
+
return { outcome: "sink-unreachable", message: "pulse issue missing number" };
|
|
249
|
+
}
|
|
250
|
+
const date = payload.collectedAt.slice(0, 10);
|
|
251
|
+
const body = `**Gap ${date}**\n\nGap: ${gapText}\n`;
|
|
252
|
+
restPostComment(this.repo, num, body, this.seams);
|
|
253
|
+
restUpdateIssue(this.repo, num, { labels: mergeIssueLabels(existing, [SURFACE_LABELS.pulse, "signal:gap"]) }, this.seams);
|
|
254
|
+
return {
|
|
255
|
+
outcome: "submitted",
|
|
256
|
+
issueUrl: issueUrlFromRecord(existing),
|
|
257
|
+
issueNumber: num,
|
|
258
|
+
message: `gap comment appended on pulse #${num}`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
if (err instanceof GhRestError) {
|
|
263
|
+
const outcome = classifySinkError(err.stderr, err.exitCode);
|
|
264
|
+
return {
|
|
265
|
+
outcome,
|
|
266
|
+
message: `gap comment soft-skip (${outcome}): ${err.stderr || err.message}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
270
|
+
return { outcome: "sink-unreachable", message };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=github-private-sink-adapter.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Headless / non-interactive detection (#2693 D16). */
|
|
2
|
+
export interface HeadlessDetectionOptions {
|
|
3
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
4
|
+
readonly stdinIsTTY?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** True when the session should fail-open without interactive prompts (#2693 D16). */
|
|
7
|
+
export declare function isHeadlessSession(options?: HeadlessDetectionOptions): boolean;
|
|
8
|
+
//# sourceMappingURL=headless.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Headless / non-interactive detection (#2693 D16). */
|
|
2
|
+
const CI_MARKERS = [
|
|
3
|
+
"CI",
|
|
4
|
+
"CONTINUOUS_INTEGRATION",
|
|
5
|
+
"GITHUB_ACTIONS",
|
|
6
|
+
"GITLAB_CI",
|
|
7
|
+
"BUILDKITE",
|
|
8
|
+
"CIRCLECI",
|
|
9
|
+
"JENKINS_URL",
|
|
10
|
+
"TF_BUILD",
|
|
11
|
+
"CURSOR_CLOUD_AGENT",
|
|
12
|
+
"DEFT_CLOUD_AGENT",
|
|
13
|
+
];
|
|
14
|
+
/** True when the session should fail-open without interactive prompts (#2693 D16). */
|
|
15
|
+
export function isHeadlessSession(options = {}) {
|
|
16
|
+
const env = options.env ?? process.env;
|
|
17
|
+
if (env.DEFT_HEADLESS === "1" || env.DEFT_HEADLESS === "true") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
for (const key of CI_MARKERS) {
|
|
21
|
+
const val = env[key];
|
|
22
|
+
if (val !== undefined && val !== "" && val !== "0" && val !== "false") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
|
|
27
|
+
if (stdinIsTTY === false) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=headless.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ProductSignalHarness = "cursor" | "cli" | "codex" | "opencode" | "other";
|
|
2
|
+
export interface InstallContext {
|
|
3
|
+
readonly installId: string;
|
|
4
|
+
readonly directiveVersion: string;
|
|
5
|
+
readonly os: string;
|
|
6
|
+
readonly osVersion: string;
|
|
7
|
+
readonly shell: string;
|
|
8
|
+
readonly harness: ProductSignalHarness;
|
|
9
|
+
readonly harnessVersion: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** Collect shared install context for product-signal payloads (#2693 D7). */
|
|
12
|
+
export declare function collectInstallContext(projectRoot: string): InstallContext;
|
|
13
|
+
//# sourceMappingURL=install-context.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { release } from "node:os";
|
|
2
|
+
import { readCorePackageVersion } from "../engine-version.js";
|
|
3
|
+
import { resolveInstallId } from "../events/attribution-enrichment.js";
|
|
4
|
+
import { detectEnvironmentContext } from "../platform/shell-context.js";
|
|
5
|
+
function detectHarness(env = process.env) {
|
|
6
|
+
const explicit = env.DEFT_HARNESS?.trim().toLowerCase();
|
|
7
|
+
if (explicit === "cursor" ||
|
|
8
|
+
explicit === "cli" ||
|
|
9
|
+
explicit === "codex" ||
|
|
10
|
+
explicit === "opencode" ||
|
|
11
|
+
explicit === "other") {
|
|
12
|
+
return explicit;
|
|
13
|
+
}
|
|
14
|
+
if (env.CURSOR_SESSION_ID || env.CURSOR_TRACE_ID) {
|
|
15
|
+
return "cursor";
|
|
16
|
+
}
|
|
17
|
+
if (env.CODEX_HOME || env.CODEX_SESSION) {
|
|
18
|
+
return "codex";
|
|
19
|
+
}
|
|
20
|
+
if (env.OPENCODE) {
|
|
21
|
+
return "opencode";
|
|
22
|
+
}
|
|
23
|
+
return "cli";
|
|
24
|
+
}
|
|
25
|
+
/** Collect shared install context for product-signal payloads (#2693 D7). */
|
|
26
|
+
export function collectInstallContext(projectRoot) {
|
|
27
|
+
const envCtx = detectEnvironmentContext();
|
|
28
|
+
const installId = resolveInstallId(projectRoot) ?? "unknown";
|
|
29
|
+
const harness = detectHarness();
|
|
30
|
+
const harnessVersion = process.env.DEFT_HARNESS_VERSION?.trim() || process.env.CURSOR_VERSION?.trim() || null;
|
|
31
|
+
return {
|
|
32
|
+
installId,
|
|
33
|
+
directiveVersion: readCorePackageVersion(),
|
|
34
|
+
os: envCtx.hostPlatform,
|
|
35
|
+
osVersion: release(),
|
|
36
|
+
shell: envCtx.shell.name,
|
|
37
|
+
harness,
|
|
38
|
+
harnessVersion,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=install-context.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LocalSignalSummary } from "./payload.js";
|
|
2
|
+
export declare const DEFAULT_LOCAL_SIGNAL_WINDOW = "30d";
|
|
3
|
+
/** Assemble minimized local ledger summaries (#2693 D15). */
|
|
4
|
+
export declare function assembleLocalSignalSummary(projectRoot: string, options?: {
|
|
5
|
+
window?: string;
|
|
6
|
+
}): LocalSignalSummary;
|
|
7
|
+
//# sourceMappingURL=local-signal-summary.d.ts.map
|