@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.
Files changed (84) hide show
  1. package/dist/doctor/checks.d.ts +6 -0
  2. package/dist/doctor/checks.js +139 -0
  3. package/dist/doctor/main.js +2 -1
  4. package/dist/hooks/dispatcher.d.ts +18 -3
  5. package/dist/hooks/dispatcher.js +173 -62
  6. package/dist/hooks/readonly.d.ts +7 -0
  7. package/dist/hooks/readonly.js +83 -0
  8. package/dist/hooks/tools.d.ts +7 -0
  9. package/dist/hooks/tools.js +16 -0
  10. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  11. package/dist/init-deposit/agent-hooks.js +77 -21
  12. package/dist/policy/index.d.ts +3 -0
  13. package/dist/policy/index.js +45 -1
  14. package/dist/policy/product-signal.d.ts +45 -0
  15. package/dist/policy/product-signal.js +210 -0
  16. package/dist/policy/runtime-authority.d.ts +54 -0
  17. package/dist/policy/runtime-authority.js +180 -0
  18. package/dist/policy/staleness-tickler.d.ts +20 -0
  19. package/dist/policy/staleness-tickler.js +151 -0
  20. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  21. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  22. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  23. package/dist/pr-merge-readiness/gh.js +10 -3
  24. package/dist/pr-merge-readiness/index.d.ts +2 -1
  25. package/dist/pr-merge-readiness/index.js +2 -1
  26. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  27. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  28. package/dist/pr-watch/constants.d.ts +5 -0
  29. package/dist/pr-watch/constants.js +6 -1
  30. package/dist/pr-watch/main.js +8 -0
  31. package/dist/pr-watch/probe.js +8 -0
  32. package/dist/pr-watch/types.d.ts +7 -0
  33. package/dist/pr-watch/watch.js +8 -1
  34. package/dist/product-signal/actor-name.d.ts +18 -0
  35. package/dist/product-signal/actor-name.js +94 -0
  36. package/dist/product-signal/consent.d.ts +30 -0
  37. package/dist/product-signal/consent.js +116 -0
  38. package/dist/product-signal/gates.d.ts +17 -0
  39. package/dist/product-signal/gates.js +66 -0
  40. package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
  41. package/dist/product-signal/github-private-sink-adapter.js +274 -0
  42. package/dist/product-signal/headless.d.ts +8 -0
  43. package/dist/product-signal/headless.js +32 -0
  44. package/dist/product-signal/install-context.d.ts +13 -0
  45. package/dist/product-signal/install-context.js +41 -0
  46. package/dist/product-signal/local-signal-summary.d.ts +7 -0
  47. package/dist/product-signal/local-signal-summary.js +173 -0
  48. package/dist/product-signal/payload.d.ts +90 -0
  49. package/dist/product-signal/payload.js +124 -0
  50. package/dist/product-signal/sink-bootstrap.d.ts +29 -0
  51. package/dist/product-signal/sink-bootstrap.js +108 -0
  52. package/dist/product-signal/submit-adapter.d.ts +14 -0
  53. package/dist/product-signal/submit-adapter.js +2 -0
  54. package/dist/product-signal/submit.d.ts +54 -0
  55. package/dist/product-signal/submit.js +290 -0
  56. package/dist/scm/gh-rest.d.ts +3 -1
  57. package/dist/scm/gh-rest.js +22 -0
  58. package/dist/scope/main.js +12 -0
  59. package/dist/session/release-availability.d.ts +21 -0
  60. package/dist/session/release-availability.js +109 -0
  61. package/dist/session/ritual-sentinel.d.ts +14 -0
  62. package/dist/session/ritual-sentinel.js +28 -0
  63. package/dist/session/session-start.d.ts +10 -0
  64. package/dist/session/session-start.js +18 -0
  65. package/dist/staleness-tickler/escalation.d.ts +14 -0
  66. package/dist/staleness-tickler/escalation.js +106 -0
  67. package/dist/staleness-tickler/idle.d.ts +18 -0
  68. package/dist/staleness-tickler/idle.js +47 -0
  69. package/dist/staleness-tickler/index.d.ts +8 -0
  70. package/dist/staleness-tickler/index.js +8 -0
  71. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  72. package/dist/staleness-tickler/probe-directive.js +106 -0
  73. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  74. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  75. package/dist/staleness-tickler/run.d.ts +31 -0
  76. package/dist/staleness-tickler/run.js +227 -0
  77. package/dist/staleness-tickler/state.d.ts +10 -0
  78. package/dist/staleness-tickler/state.js +46 -0
  79. package/dist/staleness-tickler/types.d.ts +85 -0
  80. package/dist/staleness-tickler/types.js +5 -0
  81. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  82. package/dist/vbrief-validate/plan-hooks.js +50 -0
  83. package/dist/verify-env/agent-hooks.js +4 -2
  84. package/package.json +3 -3
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ export const PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION = 1;
4
+ export const LOCAL_SIGNAL_SUMMARY_SCHEMA_VERSION = 1;
5
+ export const SKILLS_SUMMARY_SCHEMA_VERSION = 1;
6
+ export const MAX_PAYLOAD_JSON_BYTES = 64_000;
7
+ export const MAX_HUMAN_ANSWERS = 3;
8
+ export const MAX_ANSWER_LENGTH = 2_000;
9
+ export const MAX_FREE_TEXT_LENGTH = 4_000;
10
+ export const MAX_AGENT_NOTES_LENGTH = 2_000;
11
+ const SECRET_PATTERNS = [
12
+ /\bghp_[A-Za-z0-9]{20,}\b/,
13
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
14
+ /\bAKIA[0-9A-Z]{16}\b/,
15
+ /\bsk-[A-Za-z0-9]{20,}\b/,
16
+ /\bBearer\s+[A-Za-z0-9._-]{20,}\b/i,
17
+ /\bBEGIN (?:RSA |EC )?PRIVATE KEY\b/,
18
+ /\b\.env\b/,
19
+ ];
20
+ function containsSecretHygieneViolation(text) {
21
+ for (const pattern of SECRET_PATTERNS) {
22
+ if (pattern.test(text)) {
23
+ return true;
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+ function isValidNps(value) {
29
+ if (value === null) {
30
+ return true;
31
+ }
32
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 10;
33
+ }
34
+ /** Client-side payload validation before submit (#2693 D7). */
35
+ export function validateProductSignalPayload(payload) {
36
+ const errors = [];
37
+ if (payload.schemaVersion !== PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION) {
38
+ errors.push(`schemaVersion must be ${PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION}`);
39
+ }
40
+ if (payload.surface !== "pulse" && payload.surface !== "portrait") {
41
+ errors.push("surface must be pulse or portrait");
42
+ }
43
+ if (payload.installId.trim().length === 0) {
44
+ errors.push("installId is required");
45
+ }
46
+ if (payload.actorName.trim().length === 0) {
47
+ errors.push("actorName is required");
48
+ }
49
+ if (!isValidNps(payload.human.nps)) {
50
+ errors.push("human.nps must be null or integer 0-10");
51
+ }
52
+ if (payload.human.answers.length > MAX_HUMAN_ANSWERS) {
53
+ errors.push(`human.answers exceeds max ${MAX_HUMAN_ANSWERS}`);
54
+ }
55
+ for (const answer of payload.human.answers) {
56
+ if (answer.q.length === 0 || answer.a.length === 0) {
57
+ errors.push("human.answers entries require non-empty q and a");
58
+ }
59
+ if (answer.a.length > MAX_ANSWER_LENGTH) {
60
+ errors.push(`human answer exceeds max length ${MAX_ANSWER_LENGTH}`);
61
+ }
62
+ }
63
+ if (payload.human.freeText !== null && payload.human.freeText.length > MAX_FREE_TEXT_LENGTH) {
64
+ errors.push(`human.freeText exceeds max length ${MAX_FREE_TEXT_LENGTH}`);
65
+ }
66
+ if (payload.agentNotes !== null && payload.agentNotes.length > MAX_AGENT_NOTES_LENGTH) {
67
+ errors.push(`agentNotes exceeds max length ${MAX_AGENT_NOTES_LENGTH}`);
68
+ }
69
+ const serialized = JSON.stringify(payload);
70
+ if (Buffer.byteLength(serialized, "utf8") > MAX_PAYLOAD_JSON_BYTES) {
71
+ errors.push(`payload exceeds max size ${MAX_PAYLOAD_JSON_BYTES} bytes`);
72
+ }
73
+ if (containsSecretHygieneViolation(serialized)) {
74
+ errors.push("payload failed secret-hygiene scan");
75
+ }
76
+ return errors;
77
+ }
78
+ /** Optional #829 sidecar path (read-only attach; emit not implemented here). */
79
+ export const SKILLS_TELEMETRY_SIDECAR_REL = join(".deft-cache", "skills-telemetry.json");
80
+ /** Read minimized skillsSummary when local sidecar exists (#2693 D14). */
81
+ export function readSkillsSummarySidecar(projectRoot) {
82
+ const path = resolve(projectRoot, SKILLS_TELEMETRY_SIDECAR_REL);
83
+ if (!existsSync(path)) {
84
+ return null;
85
+ }
86
+ try {
87
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
88
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
89
+ return null;
90
+ }
91
+ const rec = parsed;
92
+ const topRaw = rec.top;
93
+ if (!Array.isArray(topRaw)) {
94
+ return null;
95
+ }
96
+ const top = [];
97
+ for (const item of topRaw.slice(0, 10)) {
98
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
99
+ continue;
100
+ }
101
+ const row = item;
102
+ const skill = typeof row.skill === "string" ? row.skill.trim() : "";
103
+ if (skill.length === 0) {
104
+ continue;
105
+ }
106
+ top.push({
107
+ skill,
108
+ useCount: typeof row.useCount === "number" ? row.useCount : 0,
109
+ viewCount: typeof row.viewCount === "number" ? row.viewCount : 0,
110
+ lastUsed: typeof row.lastUsed === "string" ? row.lastUsed : null,
111
+ });
112
+ }
113
+ const skillCount = typeof rec.skillCount === "number" ? rec.skillCount : top.length;
114
+ return {
115
+ schemaVersion: SKILLS_SUMMARY_SCHEMA_VERSION,
116
+ top,
117
+ skillCount,
118
+ };
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ //# sourceMappingURL=payload.js.map
@@ -0,0 +1,29 @@
1
+ import { type GhRestSeams } from "../scm/gh-rest.js";
2
+ /** D20 minimum label bootstrap set (#2693). */
3
+ export declare const PRODUCT_SIGNAL_BOOTSTRAP_LABELS: readonly {
4
+ readonly name: string;
5
+ readonly color: string;
6
+ readonly description: string;
7
+ }[];
8
+ export declare const PRODUCT_SIGNAL_SINK_README = "# deftai/product-signal\n\nInternal (org) operator inbox for consented Directive product-improvement signal (Phase 1 under epic #2603).\n\n## Standing threads\n\n- Thread key: `(installId, actorName)` \u2014 see issue body marker `<!-- deft:product-signal v1 ... -->`\n- **Portrait**: one open issue per key; body upserted in place (`surface:portrait`)\n- **Pulse**: dated comments appended on standing issue (`surface:pulse`)\n- **Gap notes**: `Gap:` comments on the pulse thread (`signal:gap` label when present)\n\n## Labels (bootstrapped)\n\n- `surface:pulse`, `surface:portrait`\n- `nps:promoter`, `nps:passive`, `nps:detractor`, `nps:none`\n- `signal:gap`\n- `directive:<major.minor>` \u2014 created on first use per directive version\n\nThis repo is an operator-readable inbox, not the long-term analytics warehouse (#2603).\n";
9
+ export interface BootstrapSinkResult {
10
+ readonly exitCode: 0 | 1 | 2;
11
+ readonly stdout: string;
12
+ readonly repo: string;
13
+ }
14
+ /** Bootstrap D20 labels (idempotent — ignores existing label 422). */
15
+ export declare function bootstrapProductSignalLabels(repo?: string, seams?: GhRestSeams): {
16
+ created: number;
17
+ skipped: number;
18
+ errors: string[];
19
+ };
20
+ /**
21
+ * Create sink repo + labels via gh CLI (#2693 D6/D20).
22
+ * Creates as private then sets visibility to **internal** (org-visible, not public).
23
+ */
24
+ export declare function bootstrapProductSignalSink(options?: {
25
+ repo?: string;
26
+ dryRun?: boolean;
27
+ seams?: GhRestSeams;
28
+ }): BootstrapSinkResult;
29
+ //# sourceMappingURL=sink-bootstrap.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { DEFAULT_PRODUCT_SIGNAL_SINK_REPO } from "../policy/product-signal.js";
3
+ import { GhRestError, restCreateLabel } from "../scm/gh-rest.js";
4
+ /** D20 minimum label bootstrap set (#2693). */
5
+ export const PRODUCT_SIGNAL_BOOTSTRAP_LABELS = [
6
+ { name: "surface:pulse", color: "1D76DB", description: "Standing pulse thread" },
7
+ { name: "surface:portrait", color: "5319E7", description: "Standing portrait thread" },
8
+ { name: "nps:promoter", color: "0E8A16", description: "NPS 9-10" },
9
+ { name: "nps:passive", color: "FBCA04", description: "NPS 7-8" },
10
+ { name: "nps:detractor", color: "D93F0B", description: "NPS 0-6" },
11
+ { name: "nps:none", color: "C5DEF5", description: "No NPS score" },
12
+ { name: "signal:gap", color: "B60205", description: "Gap comment on pulse thread" },
13
+ ];
14
+ export const PRODUCT_SIGNAL_SINK_README = `# deftai/product-signal
15
+
16
+ Internal (org) operator inbox for consented Directive product-improvement signal (Phase 1 under epic #2603).
17
+
18
+ ## Standing threads
19
+
20
+ - Thread key: \`(installId, actorName)\` — see issue body marker \`<!-- deft:product-signal v1 ... -->\`
21
+ - **Portrait**: one open issue per key; body upserted in place (\`surface:portrait\`)
22
+ - **Pulse**: dated comments appended on standing issue (\`surface:pulse\`)
23
+ - **Gap notes**: \`Gap:\` comments on the pulse thread (\`signal:gap\` label when present)
24
+
25
+ ## Labels (bootstrapped)
26
+
27
+ - \`surface:pulse\`, \`surface:portrait\`
28
+ - \`nps:promoter\`, \`nps:passive\`, \`nps:detractor\`, \`nps:none\`
29
+ - \`signal:gap\`
30
+ - \`directive:<major.minor>\` — created on first use per directive version
31
+
32
+ This repo is an operator-readable inbox, not the long-term analytics warehouse (#2603).
33
+ `;
34
+ /** Bootstrap D20 labels (idempotent — ignores existing label 422). */
35
+ export function bootstrapProductSignalLabels(repo = DEFAULT_PRODUCT_SIGNAL_SINK_REPO, seams = {}) {
36
+ let created = 0;
37
+ let skipped = 0;
38
+ const errors = [];
39
+ for (const label of PRODUCT_SIGNAL_BOOTSTRAP_LABELS) {
40
+ try {
41
+ restCreateLabel(repo, label.name, label.color, label.description, seams);
42
+ created += 1;
43
+ }
44
+ catch (err) {
45
+ if (err instanceof GhRestError) {
46
+ if (err.stderr.toLowerCase().includes("already_exists") || err.exitCode === 422) {
47
+ skipped += 1;
48
+ continue;
49
+ }
50
+ errors.push(`${label.name}: ${err.stderr || err.message}`);
51
+ }
52
+ else {
53
+ errors.push(`${label.name}: ${String(err)}`);
54
+ }
55
+ }
56
+ }
57
+ return { created, skipped, errors };
58
+ }
59
+ /**
60
+ * Create sink repo + labels via gh CLI (#2693 D6/D20).
61
+ * Creates as private then sets visibility to **internal** (org-visible, not public).
62
+ */
63
+ export function bootstrapProductSignalSink(options = {}) {
64
+ const repo = options.repo ?? DEFAULT_PRODUCT_SIGNAL_SINK_REPO;
65
+ if (options.dryRun) {
66
+ return {
67
+ exitCode: 0,
68
+ stdout: `[dry-run] would bootstrap internal sink ${repo} with README + ${PRODUCT_SIGNAL_BOOTSTRAP_LABELS.length} labels\n`,
69
+ repo,
70
+ };
71
+ }
72
+ const create = spawnSync("gh", [
73
+ "repo",
74
+ "create",
75
+ repo,
76
+ "--private",
77
+ "--description",
78
+ "Internal product-improvement signal inbox for Directive partners (Refs #2693)",
79
+ "--add-readme",
80
+ ], { encoding: "utf8" });
81
+ if (create.status !== 0 && !String(create.stderr).toLowerCase().includes("already exists")) {
82
+ return {
83
+ exitCode: 2,
84
+ stdout: `sink bootstrap failed: ${create.stderr || create.stdout}\n`,
85
+ repo,
86
+ };
87
+ }
88
+ // Org "internal" visibility: gh create has no --internal; set after create.
89
+ const visibility = spawnSync("gh", ["repo", "edit", repo, "--visibility", "internal", "--accept-visibility-change-consequences"], { encoding: "utf8" });
90
+ if (visibility.status !== 0) {
91
+ return {
92
+ exitCode: 2,
93
+ stdout: `sink repo created but failed to set visibility=internal: ${visibility.stderr || visibility.stdout}\n`,
94
+ repo,
95
+ };
96
+ }
97
+ const labelResult = bootstrapProductSignalLabels(repo, options.seams);
98
+ const lines = [
99
+ `product-signal sink ${repo} ready (visibility=internal).`,
100
+ `labels: created=${labelResult.created} skipped=${labelResult.skipped}`,
101
+ ];
102
+ if (labelResult.errors.length > 0) {
103
+ lines.push(`label warnings: ${labelResult.errors.join("; ")}`);
104
+ }
105
+ lines.push("", "Update repo README with standing-thread conventions if empty.");
106
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, repo };
107
+ }
108
+ //# sourceMappingURL=sink-bootstrap.js.map
@@ -0,0 +1,14 @@
1
+ import type { ProductSignalOutcome } from "./gates.js";
2
+ import type { ProductSignalPayload } from "./payload.js";
3
+ export interface SubmitResult {
4
+ readonly outcome: ProductSignalOutcome;
5
+ readonly issueUrl?: string | null;
6
+ readonly issueNumber?: number | null;
7
+ readonly message: string;
8
+ }
9
+ /** Transport-pluggable submit seam (#2693 D5). */
10
+ export interface SubmitAdapter {
11
+ readonly id: string;
12
+ submit(payload: ProductSignalPayload): Promise<SubmitResult>;
13
+ }
14
+ //# sourceMappingURL=submit-adapter.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=submit-adapter.js.map
@@ -0,0 +1,54 @@
1
+ import { type ProductSignalOutcome } from "./gates.js";
2
+ import { type ProductSignalHuman, type ProductSignalPayload, type ProductSignalSurface } from "./payload.js";
3
+ export declare const PRODUCT_SIGNAL_LAST_SUBMIT_REL: string;
4
+ export interface ProductSignalSubmitInput {
5
+ readonly surface: ProductSignalSurface;
6
+ readonly human?: Partial<ProductSignalHuman>;
7
+ readonly agentNotes?: string | null;
8
+ readonly gapText?: string | null;
9
+ }
10
+ export interface ProductSignalSubmitOptions extends ProductSignalSubmitInput {
11
+ readonly projectRoot?: string | null;
12
+ readonly dryRun?: boolean;
13
+ readonly json?: boolean;
14
+ readonly skipGates?: boolean;
15
+ }
16
+ export interface ProductSignalSubmitResult {
17
+ readonly outcome: ProductSignalOutcome;
18
+ readonly exitCode: 0 | 1 | 2;
19
+ readonly message: string;
20
+ readonly issueUrl?: string | null;
21
+ readonly payload?: ProductSignalPayload;
22
+ }
23
+ /** Assemble a versioned product-signal payload (#2693 D7). */
24
+ export declare function assembleProductSignalPayload(projectRoot: string, input: ProductSignalSubmitInput): ProductSignalPayload;
25
+ /** Submit product-signal through gates + adapter (#2693). */
26
+ export declare function submitProductSignal(options: ProductSignalSubmitOptions): Promise<ProductSignalSubmitResult>;
27
+ export declare function runProductSignalStatus(projectRoot: string | null): {
28
+ exitCode: 0;
29
+ text: string;
30
+ };
31
+ export declare function runProductSignalEnable(projectRoot: string | null, confirm: boolean): {
32
+ exitCode: 0 | 1 | 2;
33
+ text: string;
34
+ };
35
+ export declare function runProductSignalConsent(action: "grant" | "revoke"): {
36
+ exitCode: 0 | 1;
37
+ text: string;
38
+ };
39
+ export declare function runProductSignalBootstrapSink(dryRun: boolean): {
40
+ exitCode: 0 | 1 | 2;
41
+ text: string;
42
+ };
43
+ export declare function parseProductSignalSubmitArgs(argv: string[]): {
44
+ surface: ProductSignalSurface;
45
+ dryRun: boolean;
46
+ json: boolean;
47
+ projectRoot: string;
48
+ nps: number | null;
49
+ error?: string;
50
+ };
51
+ /** CLI module entrypoint for dispatch (#2693). */
52
+ export declare function productSignalMain(argv?: string[]): Promise<number>;
53
+ export declare function mainEntry(argv?: string[]): Promise<number>;
54
+ //# sourceMappingURL=submit.d.ts.map
@@ -0,0 +1,290 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
4
+ import { resolveProjectRoot } from "../scope/project-context.js";
5
+ import { resolveActorName } from "./actor-name.js";
6
+ import { grantProductSignalConsent, isProductSignalConsented, readProductSignalConsent, revokeProductSignalConsent, } from "./consent.js";
7
+ import { evaluateProductSignalGates } from "./gates.js";
8
+ import { GitHubPrivateSinkAdapter } from "./github-private-sink-adapter.js";
9
+ import { collectInstallContext } from "./install-context.js";
10
+ import { assembleLocalSignalSummary } from "./local-signal-summary.js";
11
+ import { PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION, readSkillsSummarySidecar, validateProductSignalPayload, } from "./payload.js";
12
+ import { bootstrapProductSignalSink } from "./sink-bootstrap.js";
13
+ export const PRODUCT_SIGNAL_LAST_SUBMIT_REL = join(".deft-cache", "product-signal-last-submit.json");
14
+ function recordLastSubmit(projectRoot, outcome, url) {
15
+ const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
16
+ try {
17
+ mkdirSync(join(path, ".."), { recursive: true });
18
+ appendFileSync(path, `${JSON.stringify({
19
+ outcome,
20
+ issueUrl: url,
21
+ at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
22
+ })}\n`, "utf8");
23
+ }
24
+ catch {
25
+ // observability only
26
+ }
27
+ }
28
+ function readLastSubmitSummary(projectRoot) {
29
+ const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
30
+ if (!existsSync(path)) {
31
+ return null;
32
+ }
33
+ try {
34
+ const lines = readFileSync(path, "utf8")
35
+ .split("\n")
36
+ .filter((l) => l.trim());
37
+ const last = lines[lines.length - 1];
38
+ if (!last) {
39
+ return null;
40
+ }
41
+ const rec = JSON.parse(last);
42
+ if (rec !== null && typeof rec === "object" && !Array.isArray(rec)) {
43
+ const at = rec.at;
44
+ const outcome = rec.outcome;
45
+ const url = rec.issueUrl;
46
+ return `last=${String(outcome)} at=${String(at)} url=${String(url ?? "none")}`;
47
+ }
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ return null;
53
+ }
54
+ /** Assemble a versioned product-signal payload (#2693 D7). */
55
+ export function assembleProductSignalPayload(projectRoot, input) {
56
+ const ctx = collectInstallContext(projectRoot);
57
+ const actor = resolveActorName({ projectRoot });
58
+ const consent = readProductSignalConsent();
59
+ const human = {
60
+ nps: input.human?.nps ?? null,
61
+ answers: input.human?.answers ?? [],
62
+ freeText: input.human?.freeText ?? null,
63
+ };
64
+ return {
65
+ schemaVersion: PRODUCT_SIGNAL_PAYLOAD_SCHEMA_VERSION,
66
+ surface: input.surface,
67
+ installId: ctx.installId,
68
+ actorName: actor.displayName,
69
+ actorNameSource: actor.actorNameSource,
70
+ directiveVersion: ctx.directiveVersion,
71
+ os: ctx.os,
72
+ osVersion: ctx.osVersion,
73
+ shell: ctx.shell,
74
+ harness: ctx.harness,
75
+ harnessVersion: ctx.harnessVersion,
76
+ consentTier: consent?.tier ?? "none",
77
+ consentSource: "user",
78
+ consentVersion: consent ? String(consent.consentVersion) : "0",
79
+ human,
80
+ agentNotes: input.agentNotes ?? null,
81
+ localSignalSummary: assembleLocalSignalSummary(projectRoot),
82
+ skillsSummary: readSkillsSummarySidecar(projectRoot),
83
+ collectedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
84
+ };
85
+ }
86
+ /** Submit product-signal through gates + adapter (#2693). */
87
+ export async function submitProductSignal(options) {
88
+ const root = resolveProjectRoot(options.projectRoot ?? undefined);
89
+ if (root === null) {
90
+ return { outcome: "error-config", exitCode: 2, message: "project root not found\n" };
91
+ }
92
+ if (!options.skipGates) {
93
+ const gates = evaluateProductSignalGates({ projectRoot: root });
94
+ if (!gates.allowed) {
95
+ return { outcome: gates.outcome, exitCode: 0, message: `${gates.message}\n` };
96
+ }
97
+ }
98
+ const payload = assembleProductSignalPayload(root, options);
99
+ const validationErrors = validateProductSignalPayload(payload);
100
+ if (validationErrors.length > 0) {
101
+ return {
102
+ outcome: "validation",
103
+ exitCode: 1,
104
+ message: `validation failed: ${validationErrors.join("; ")}\n`,
105
+ payload,
106
+ };
107
+ }
108
+ if (options.dryRun) {
109
+ return {
110
+ outcome: "dry-run",
111
+ exitCode: 0,
112
+ message: `[dry-run] payload valid for ${payload.surface}\n`,
113
+ payload,
114
+ };
115
+ }
116
+ const policy = resolveProductSignal(root);
117
+ const adapter = new GitHubPrivateSinkAdapter({ sinkRepo: policy.sinkRepo });
118
+ const result = await adapter.submit(payload, { gapText: options.gapText });
119
+ if (result.outcome === "submitted") {
120
+ recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
121
+ }
122
+ const urlLine = result.issueUrl ? ` url=${result.issueUrl}` : "";
123
+ return {
124
+ outcome: result.outcome,
125
+ exitCode: 0,
126
+ message: `${result.message}${urlLine}\n`,
127
+ issueUrl: result.issueUrl,
128
+ payload,
129
+ };
130
+ }
131
+ export function runProductSignalStatus(projectRoot) {
132
+ const root = resolveProjectRoot(projectRoot ?? undefined) ?? process.cwd();
133
+ const policy = resolveProductSignal(root);
134
+ const consented = isProductSignalConsented();
135
+ const last = readLastSubmitSummary(root);
136
+ const lines = [
137
+ formatProductSignalStatusLine(policy),
138
+ `[deft product-signal] consented=${String(consented)}`,
139
+ last ? `[deft product-signal] ${last}` : "[deft product-signal] last submit: none",
140
+ ];
141
+ return { exitCode: 0, text: `${lines.join("\n")}\n` };
142
+ }
143
+ export function runProductSignalEnable(projectRoot, confirm) {
144
+ const root = resolveProjectRoot(projectRoot ?? undefined);
145
+ if (root === null) {
146
+ return { exitCode: 2, text: "project root not found\n" };
147
+ }
148
+ const result = enableProductSignal(root, { confirm });
149
+ return { exitCode: result.exitCode, text: result.stdout };
150
+ }
151
+ export function runProductSignalConsent(action) {
152
+ if (action === "grant") {
153
+ const record = grantProductSignalConsent();
154
+ return {
155
+ exitCode: 0,
156
+ text: `product-signal consent granted (tier=${record.tier}, version=${record.consentVersion}).\n`,
157
+ };
158
+ }
159
+ const ok = revokeProductSignalConsent();
160
+ if (!ok) {
161
+ return { exitCode: 1, text: "no consent file to revoke\n" };
162
+ }
163
+ return { exitCode: 0, text: "product-signal consent revoked\n" };
164
+ }
165
+ export function runProductSignalBootstrapSink(dryRun) {
166
+ const result = bootstrapProductSignalSink({ dryRun });
167
+ return { exitCode: result.exitCode, text: result.stdout };
168
+ }
169
+ export function parseProductSignalSubmitArgs(argv) {
170
+ let surface = "pulse";
171
+ let dryRun = false;
172
+ let json = false;
173
+ let projectRoot = ".";
174
+ let nps = null;
175
+ for (let i = 0; i < argv.length; i += 1) {
176
+ const arg = argv[i];
177
+ if (arg === "--surface") {
178
+ const v = argv[i + 1];
179
+ if (v !== "pulse" && v !== "portrait") {
180
+ return { surface, dryRun, json, projectRoot, nps, error: "invalid --surface" };
181
+ }
182
+ surface = v;
183
+ i += 1;
184
+ }
185
+ else if (arg?.startsWith("--surface=")) {
186
+ const v = arg.slice("--surface=".length);
187
+ if (v !== "pulse" && v !== "portrait") {
188
+ return { surface, dryRun, json, projectRoot, nps, error: "invalid --surface" };
189
+ }
190
+ surface = v;
191
+ }
192
+ else if (arg === "--dry-run") {
193
+ dryRun = true;
194
+ }
195
+ else if (arg === "--json") {
196
+ json = true;
197
+ }
198
+ else if (arg === "--nps") {
199
+ const v = Number(argv[i + 1]);
200
+ if (!Number.isInteger(v) || v < 0 || v > 10) {
201
+ return { surface, dryRun, json, projectRoot, nps, error: "invalid --nps" };
202
+ }
203
+ nps = v;
204
+ i += 1;
205
+ }
206
+ else if (arg === "--project-root") {
207
+ projectRoot = argv[i + 1] ?? ".";
208
+ i += 1;
209
+ }
210
+ else if (arg?.startsWith("--project-root=")) {
211
+ projectRoot = arg.slice("--project-root=".length);
212
+ }
213
+ else {
214
+ return { surface, dryRun, json, projectRoot, nps, error: `unrecognized argument: ${arg}` };
215
+ }
216
+ }
217
+ return { surface, dryRun, json, projectRoot, nps };
218
+ }
219
+ /** CLI module entrypoint for dispatch (#2693). */
220
+ export async function productSignalMain(argv = process.argv.slice(2)) {
221
+ const sub = argv[0];
222
+ if (sub === "status") {
223
+ const rootIdx = argv.indexOf("--project-root");
224
+ const root = rootIdx >= 0
225
+ ? (argv[rootIdx + 1] ?? ".")
226
+ : (argv.find((a) => a.startsWith("--project-root="))?.split("=")[1] ?? ".");
227
+ const result = runProductSignalStatus(root);
228
+ process.stdout.write(result.text);
229
+ return result.exitCode;
230
+ }
231
+ if (sub === "enable") {
232
+ const confirm = argv.includes("--confirm");
233
+ const rootIdx = argv.indexOf("--project-root");
234
+ const root = rootIdx >= 0
235
+ ? (argv[rootIdx + 1] ?? ".")
236
+ : (argv.find((a) => a.startsWith("--project-root="))?.split("=")[1] ?? ".");
237
+ const result = runProductSignalEnable(root, confirm);
238
+ process.stdout.write(result.text);
239
+ return result.exitCode;
240
+ }
241
+ if (sub === "consent") {
242
+ const grant = argv.includes("--grant");
243
+ const revoke = argv.includes("--revoke");
244
+ if (grant === revoke) {
245
+ process.stderr.write("usage: product-signal consent -- --grant|--revoke\n");
246
+ return 1;
247
+ }
248
+ const result = runProductSignalConsent(grant ? "grant" : "revoke");
249
+ process.stdout.write(result.text);
250
+ return result.exitCode;
251
+ }
252
+ if (sub === "bootstrap-sink") {
253
+ const result = runProductSignalBootstrapSink(argv.includes("--dry-run"));
254
+ process.stdout.write(result.text);
255
+ return result.exitCode;
256
+ }
257
+ if (sub === "submit") {
258
+ const parsed = parseProductSignalSubmitArgs(argv.slice(1));
259
+ if (parsed.error) {
260
+ process.stderr.write(`${parsed.error}\n`);
261
+ return 1;
262
+ }
263
+ const result = await submitProductSignal({
264
+ projectRoot: parsed.projectRoot,
265
+ surface: parsed.surface,
266
+ dryRun: parsed.dryRun,
267
+ json: parsed.json,
268
+ human: parsed.nps !== null ? { nps: parsed.nps, answers: [], freeText: null } : undefined,
269
+ });
270
+ if (parsed.json) {
271
+ process.stdout.write(`${JSON.stringify({
272
+ outcome: result.outcome,
273
+ exit_code: result.exitCode,
274
+ message: result.message.trim(),
275
+ issue_url: result.issueUrl ?? null,
276
+ payload: result.payload ?? null,
277
+ }, null, 2)}\n`);
278
+ }
279
+ else {
280
+ process.stdout.write(result.message);
281
+ }
282
+ return result.exitCode;
283
+ }
284
+ process.stderr.write("usage: product-signal [status|enable|consent|submit|bootstrap-sink] ...\n");
285
+ return 1;
286
+ }
287
+ export async function mainEntry(argv = process.argv.slice(2)) {
288
+ return productSignalMain(argv);
289
+ }
290
+ //# sourceMappingURL=submit.js.map
@@ -53,9 +53,11 @@ export interface RestIssueListOptions {
53
53
  export declare function restIssueList(repo: string, options?: RestIssueListOptions, seams?: GhRestSeams): Record<string, unknown>[];
54
54
  export declare const REST_MAX_PER_PAGE = 100;
55
55
  export declare const REST_PAGINATION_MAX_PAGES = 100;
56
- export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
56
+ export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
57
57
  export declare function restCreateIssue(repo: string, title: string, body: string, labels?: readonly string[], seams?: GhRestSeams): Record<string, unknown>;
58
58
  export declare function restPostComment(repo: string, n: number, body: string, seams?: GhRestSeams): Record<string, unknown>;
59
+ export declare function restUpdateIssue(repo: string, n: number, patch: Record<string, unknown>, seams?: GhRestSeams): Record<string, unknown>;
60
+ export declare function restCreateLabel(repo: string, name: string, color: string, description: string, seams?: GhRestSeams): Record<string, unknown>;
59
61
  export declare function restCloseIssue(repo: string, n: number, reason?: string | null, seams?: GhRestSeams): Record<string, unknown>;
60
62
  export declare function restOpenPr(repo: string, head: string, base: string, title: string, body: string, options?: {
61
63
  draft?: boolean;
@@ -171,6 +171,8 @@ export const REST_PAGINATION_MAX_PAGES = 100;
171
171
  export const PUBLIC_HELPERS = [
172
172
  "restCreateIssue",
173
173
  "restPostComment",
174
+ "restUpdateIssue",
175
+ "restCreateLabel",
174
176
  "restCloseIssue",
175
177
  "restOpenPr",
176
178
  "restMergePr",
@@ -224,6 +226,26 @@ export function restPostComment(repo, n, body, seams = {}) {
224
226
  ...seams,
225
227
  });
226
228
  }
229
+ export function restUpdateIssue(repo, n, patch, seams = {}) {
230
+ const [owner, name] = splitRepo(repo);
231
+ const endpoint = `repos/${owner}/${name}/issues/${n}`;
232
+ return execMutation([endpoint, "--method", "PATCH"], {
233
+ endpoint,
234
+ payload: patch,
235
+ hint: "verify repo permissions and issue number; check gh auth status",
236
+ ...seams,
237
+ });
238
+ }
239
+ export function restCreateLabel(repo, name, color, description, seams = {}) {
240
+ const [owner, repoName] = splitRepo(repo);
241
+ const endpoint = `repos/${owner}/${repoName}/labels`;
242
+ return execMutation([endpoint, "--method", "POST"], {
243
+ endpoint,
244
+ payload: { name, color, description },
245
+ hint: "verify repo permissions; label may already exist (422 is acceptable for idempotent bootstrap)",
246
+ ...seams,
247
+ });
248
+ }
227
249
  export function restCloseIssue(repo, n, reason = "completed", seams = {}) {
228
250
  const [owner, name] = splitRepo(repo);
229
251
  const endpoint = `repos/${owner}/${name}/issues/${n}`;