@papi-ai/server 0.7.41 → 0.7.42

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.
@@ -82,7 +82,12 @@ Your output is **not done** if ANY of these is true (these are falsifiable — c
82
82
  - [ ] Gradient text or gradient fill used decoratively (the generic-AI look).
83
83
  - [ ] Your brand accent used as a glow / dark-mode-only sheen instead of solid, intentional colour.
84
84
  - [ ] An "earned" signal colour (insight/intelligence/premium) used as a plain decorative fill.
85
- - [ ] No single dominant above-the-fold hero, or two elements competing for dominance.
85
+ - [ ] Two elements competing for dominance (both fight for the eye). NOTE: a page with NO hero is
86
+ fine when nothing must lead — do not manufacture one to satisfy this line; only *competing*
87
+ dominance fails (see The core principle).
88
+ - [ ] Guidance, state, or a next action explained in a sentence of helper text where a visual cue
89
+ (position, size, colour, icon, a signpost, motion-on-change) would carry it — prose used as a
90
+ crutch for missing visual steering.
86
91
  - [ ] More than ~3 sections fully visible above the fold.
87
92
  - [ ] Identical-sized cards in a uniform row (flat hierarchy — needs a dominant 2×+ cell).
88
93
  - [ ] Monospace on labels, status, timestamps, or nav.
@@ -121,6 +126,16 @@ Return a tight report (this is data, not prose):
121
126
 
122
127
  ## The core principle
123
128
 
124
- **Composition > decoration.** A page with perfect tokens and flat hierarchy is worse than one with
125
- slightly-off tokens and a clear hero, reading order, and earned elements. One hero, clear reading
126
- order, everything earns its space.
129
+ **Composition > decoration, and show > tell.** A page with perfect tokens and flat hierarchy is
130
+ worse than one with slightly-off tokens and a clear reading order where every element earns its
131
+ space. Two rules generalist agents keep getting wrong:
132
+
133
+ - **Steer with visuals, not text.** When the user needs guidance — what to do, where to look, what
134
+ state something is in — reach for a VISUAL cue first: position, size, colour, an icon, a
135
+ connecting line, a spatial signpost, motion on change. A sentence of helper text is the last
136
+ resort, not the first. Over-explaining in prose is the default AI failure mode; a well-placed
137
+ arrow, a pulse, or a bolded number beats a paragraph. Visual steering and motion beat words.
138
+ - **A hero is conditional, not mandatory.** Give the page one dominant focal point WHEN it has a
139
+ single thing that must lead. Dense tool, list, board, and settings surfaces can carry a calm, even
140
+ hierarchy — do not manufacture a hero, an oversized stat, or a promoted banner where the content
141
+ has no natural lead. Only *competing* dominance is always wrong; the absence of a hero is not.
@@ -48,15 +48,17 @@ Plus a one-line page read: `kind / audience / the one thing they must see first`
48
48
  3. **Read the target page in full** — not just the component you're editing. You need full page
49
49
  context to judge compositional hierarchy.
50
50
  4. **List all sibling components** on the page. You'll weigh each one's visual weight against the others.
51
- 5. **Check both states:** populated and empty/zero-data. Both need a hero and hierarchy.
51
+ 5. **Check both states:** populated and empty/zero-data. Both need clear hierarchy (a hero only where one is warranted — see Check 1).
52
52
 
53
53
  ## Step 2: The Seven Checks
54
54
 
55
55
  For each, give ✅ Pass / ⚠️ Weak / ❌ Fail plus a one-line reason.
56
56
 
57
- ### Check 1: Hero Element
58
- > Every page has exactly ONE dominant visual element above the fold.
59
- Name it. If you can't, or two things compete, it's ❌. Squint test: blurred, only the hero shows.
57
+ ### Check 1: Hero Element (conditional — NOT every page needs one)
58
+ > Give the page one dominant element WHEN it has one thing that must lead. A hero is not mandatory.
59
+ Does this page have a single thing that must lead? If yes: name it; missing/weak = ⚠️/❌. If no (a
60
+ dense tool / list / board / settings surface): a calm, even hierarchy is ✅ — do NOT manufacture a
61
+ hero to pass this check. Two elements *competing* for dominance is ALWAYS ❌ (the real failure).
60
62
 
61
63
  ### Check 2: Reading Order
62
64
  > hero → primary action → supporting content → tertiary.
@@ -101,7 +103,11 @@ Run through every named anti-pattern and flag matches. Common universal ones:
101
103
  - Gradient text or gradient fill used decoratively.
102
104
  - Brand accent as a glow / dark-only sheen instead of solid intentional colour.
103
105
  - A "signal" colour used as a plain decorative fill.
104
- - No single dominant above-fold hero, or two competing for it.
106
+ - Two elements competing for dominance (both fight for the eye). NOT a fail: a page with no hero
107
+ when nothing must lead (see Check 1) — only *competing* dominance fails here.
108
+ - Guidance, state, or a next action carried by a sentence of helper text where a visual cue
109
+ (position, size, colour, an icon, a signpost, motion-on-change) would do — prose as a crutch for
110
+ missing visual steering. Prefer showing over telling.
105
111
  - Identical-sized cards in a uniform row (flat hierarchy).
106
112
  - Monospace on labels / status / timestamps / nav.
107
113
  - Header, label, and value at the same size+weight.
@@ -158,5 +164,10 @@ The pre-build critique catches composition; this catches what's only visible in
158
164
 
159
165
  ## The Core Principle
160
166
 
161
- **Composition > Decoration.** A page with perfect tokens and flat hierarchy is worse than one with
162
- slightly-off tokens and clear hierarchy. One hero, clear reading order, everything earns its space.
167
+ **Composition > Decoration, and show > tell.** A page with perfect tokens and flat hierarchy is
168
+ worse than one with slightly-off tokens and clear hierarchy. Two things generalist agents keep
169
+ getting wrong: (1) **steer with visuals, not text** — reach for position, size, colour, an icon, a
170
+ signpost, or motion-on-change before a sentence of helper text; over-explaining in prose is the
171
+ default AI failure mode. (2) **A hero is conditional** — give a page one dominant element only when
172
+ it has one thing that must lead; dense list / tool / settings surfaces carry a calm, even hierarchy,
173
+ so do not manufacture a hero. Clear reading order, everything earns its space, show before you tell.
@@ -1012,6 +1012,48 @@ var init_git = __esm({
1012
1012
  }
1013
1013
  });
1014
1014
 
1015
+ // src/lib/install-id.ts
1016
+ import { randomUUID as randomUUID7 } from "crypto";
1017
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
1018
+ import { homedir } from "os";
1019
+ import { join as join2 } from "path";
1020
+ var PAPI_HOME_DIR, INSTALL_ID_FILE;
1021
+ var init_install_id = __esm({
1022
+ "src/lib/install-id.ts"() {
1023
+ "use strict";
1024
+ PAPI_HOME_DIR = join2(homedir(), ".papi");
1025
+ INSTALL_ID_FILE = join2(PAPI_HOME_DIR, "install-id.json");
1026
+ }
1027
+ });
1028
+
1029
+ // src/lib/telemetry.ts
1030
+ function reportTelemetryEmitFailure(reason, ctx) {
1031
+ consecutiveTelemetryFailures += 1;
1032
+ console.error(
1033
+ `[telemetry] emit failed (${reason}) tool=${ctx.toolName} event=${ctx.eventType} project=${ctx.projectId}`
1034
+ );
1035
+ if (consecutiveTelemetryFailures >= TELEMETRY_BLACKOUT_THRESHOLD) {
1036
+ console.error(
1037
+ `[telemetry] BLACKOUT: ${consecutiveTelemetryFailures} consecutive telemetry emit failures \u2014 activation metrics are under-counting for this pod. Check the data-proxy /telemetry path (auth/RLS/network/schema).`
1038
+ );
1039
+ }
1040
+ }
1041
+ function noteTelemetryEmitSuccess() {
1042
+ consecutiveTelemetryFailures = 0;
1043
+ }
1044
+ var HOSTED_SUPABASE_URL, DEFAULT_TELEMETRY_ENDPOINT, MD_PINGS_SUPABASE_URL, consecutiveTelemetryFailures, TELEMETRY_BLACKOUT_THRESHOLD;
1045
+ var init_telemetry = __esm({
1046
+ "src/lib/telemetry.ts"() {
1047
+ "use strict";
1048
+ init_install_id();
1049
+ HOSTED_SUPABASE_URL = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
1050
+ DEFAULT_TELEMETRY_ENDPOINT = `${HOSTED_SUPABASE_URL}/functions/v1/data-proxy`;
1051
+ MD_PINGS_SUPABASE_URL = process.env["PAPI_MD_PINGS_URL"] ?? HOSTED_SUPABASE_URL;
1052
+ consecutiveTelemetryFailures = 0;
1053
+ TELEMETRY_BLACKOUT_THRESHOLD = 3;
1054
+ }
1055
+ });
1056
+
1015
1057
  // src/proxy-adapter.ts
1016
1058
  var proxy_adapter_exports = {};
1017
1059
  __export(proxy_adapter_exports, {
@@ -1058,6 +1100,7 @@ var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, ProxyPapiAdapter;
1058
1100
  var init_proxy_adapter = __esm({
1059
1101
  "src/proxy-adapter.ts"() {
1060
1102
  "use strict";
1103
+ init_telemetry();
1061
1104
  JSONB_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set([
1062
1105
  "buildHandoff",
1063
1106
  "stateHistory",
@@ -1420,6 +1463,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1420
1463
  * telemetry failure must never surface as a tool error.
1421
1464
  */
1422
1465
  emitTelemetry(event) {
1466
+ const ctx = { projectId: event.projectId, toolName: event.toolName, eventType: event.eventType };
1423
1467
  fetch(`${this.endpoint}/telemetry`, {
1424
1468
  method: "POST",
1425
1469
  headers: {
@@ -1433,7 +1477,11 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1433
1477
  metadata: event.metadata ?? {}
1434
1478
  }),
1435
1479
  signal: AbortSignal.timeout(5e3)
1436
- }).catch(() => {
1480
+ }).then((res) => {
1481
+ if (res.ok) noteTelemetryEmitSuccess();
1482
+ else reportTelemetryEmitFailure(`HTTP ${res.status}`, ctx);
1483
+ }).catch((err) => {
1484
+ reportTelemetryEmitFailure(err instanceof Error ? err.name : "network error", ctx);
1437
1485
  });
1438
1486
  }
1439
1487
  readToolMetrics() {
@@ -4064,8 +4112,8 @@ function detectUserId() {
4064
4112
  }
4065
4113
  return void 0;
4066
4114
  }
4067
- var HOSTED_SUPABASE_URL = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
4068
- var HOSTED_PROXY_ENDPOINT = `${HOSTED_SUPABASE_URL}/functions/v1/data-proxy`;
4115
+ var HOSTED_SUPABASE_URL2 = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
4116
+ var HOSTED_PROXY_ENDPOINT = `${HOSTED_SUPABASE_URL2}/functions/v1/data-proxy`;
4069
4117
  var PLACEHOLDER_PATTERNS = [
4070
4118
  "<YOUR_DATABASE_URL>",
4071
4119
  "your-database-url",
package/dist/index.js CHANGED
@@ -1013,6 +1013,164 @@ var init_git = __esm({
1013
1013
  }
1014
1014
  });
1015
1015
 
1016
+ // src/lib/install-id.ts
1017
+ import { randomUUID as randomUUID7 } from "crypto";
1018
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
1019
+ import { homedir } from "os";
1020
+ import { join as join2 } from "path";
1021
+ function isValidUuid(s) {
1022
+ return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
1023
+ }
1024
+ function getInstallId() {
1025
+ if (cachedInstallId) return cachedInstallId;
1026
+ try {
1027
+ const raw = readFileSync(INSTALL_ID_FILE, "utf-8");
1028
+ const parsed = JSON.parse(raw);
1029
+ if (isValidUuid(parsed.install_id)) {
1030
+ cachedInstallId = parsed.install_id;
1031
+ return cachedInstallId;
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ try {
1036
+ mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
1037
+ const id = randomUUID7();
1038
+ const contents = {
1039
+ install_id: id,
1040
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1041
+ };
1042
+ writeFileSync(INSTALL_ID_FILE, JSON.stringify(contents, null, 2), { mode: 384 });
1043
+ try {
1044
+ chmodSync(INSTALL_ID_FILE, 384);
1045
+ } catch {
1046
+ }
1047
+ cachedInstallId = id;
1048
+ return cachedInstallId;
1049
+ } catch {
1050
+ return null;
1051
+ }
1052
+ }
1053
+ var PAPI_HOME_DIR, INSTALL_ID_FILE, cachedInstallId;
1054
+ var init_install_id = __esm({
1055
+ "src/lib/install-id.ts"() {
1056
+ "use strict";
1057
+ PAPI_HOME_DIR = join2(homedir(), ".papi");
1058
+ INSTALL_ID_FILE = join2(PAPI_HOME_DIR, "install-id.json");
1059
+ cachedInstallId = null;
1060
+ }
1061
+ });
1062
+
1063
+ // src/lib/telemetry.ts
1064
+ function isEnabled() {
1065
+ const val = process.env["PAPI_TELEMETRY"];
1066
+ return val !== "false" && val !== "off";
1067
+ }
1068
+ function reportTelemetryEmitFailure(reason, ctx) {
1069
+ consecutiveTelemetryFailures += 1;
1070
+ console.error(
1071
+ `[telemetry] emit failed (${reason}) tool=${ctx.toolName} event=${ctx.eventType} project=${ctx.projectId}`
1072
+ );
1073
+ if (consecutiveTelemetryFailures >= TELEMETRY_BLACKOUT_THRESHOLD) {
1074
+ console.error(
1075
+ `[telemetry] BLACKOUT: ${consecutiveTelemetryFailures} consecutive telemetry emit failures \u2014 activation metrics are under-counting for this pod. Check the data-proxy /telemetry path (auth/RLS/network/schema).`
1076
+ );
1077
+ }
1078
+ }
1079
+ function noteTelemetryEmitSuccess() {
1080
+ consecutiveTelemetryFailures = 0;
1081
+ }
1082
+ function emitTelemetryEvent(event) {
1083
+ if (!isEnabled()) return;
1084
+ const apiKey = process.env["PAPI_DATA_API_KEY"];
1085
+ if (!apiKey) return;
1086
+ const endpoint = process.env["PAPI_DATA_ENDPOINT"] ?? DEFAULT_TELEMETRY_ENDPOINT;
1087
+ const body = {
1088
+ projectId: event.project_id,
1089
+ toolName: event.tool_name,
1090
+ eventType: event.event_type,
1091
+ metadata: event.metadata ?? {}
1092
+ };
1093
+ const ctx = {
1094
+ projectId: event.project_id,
1095
+ toolName: event.tool_name,
1096
+ eventType: event.event_type
1097
+ };
1098
+ fetch(`${endpoint}/telemetry`, {
1099
+ method: "POST",
1100
+ headers: {
1101
+ "Content-Type": "application/json",
1102
+ "Authorization": `Bearer ${apiKey}`
1103
+ },
1104
+ body: JSON.stringify(body),
1105
+ signal: AbortSignal.timeout(5e3)
1106
+ }).then((res) => {
1107
+ if (res.ok) noteTelemetryEmitSuccess();
1108
+ else reportTelemetryEmitFailure(`HTTP ${res.status}`, ctx);
1109
+ }).catch((err) => {
1110
+ reportTelemetryEmitFailure(err instanceof Error ? err.name : "network error", ctx);
1111
+ });
1112
+ }
1113
+ function emitToolCall(projectId, toolName, durationMs, extra) {
1114
+ emitTelemetryEvent({
1115
+ project_id: projectId,
1116
+ tool_name: toolName,
1117
+ event_type: "tool_call",
1118
+ metadata: { duration_ms: durationMs, ...extra }
1119
+ });
1120
+ }
1121
+ function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
1122
+ if (!isEnabled()) return;
1123
+ const installId = getInstallId();
1124
+ if (!installId) return;
1125
+ const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
1126
+ const body = {
1127
+ install_id: installId,
1128
+ tool_name: toolName,
1129
+ papi_version: process.env["npm_package_version"] ?? null,
1130
+ metadata: extra ?? {}
1131
+ };
1132
+ if (resolvedUserId) body["user_id"] = resolvedUserId;
1133
+ if (projectSlug) body["project_slug"] = projectSlug;
1134
+ fetch(`${MD_PINGS_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
1135
+ method: "POST",
1136
+ headers: {
1137
+ "Content-Type": "application/json",
1138
+ "apikey": MD_PINGS_ANON_KEY,
1139
+ "Authorization": `Bearer ${MD_PINGS_ANON_KEY}`,
1140
+ "Prefer": "return=minimal"
1141
+ },
1142
+ body: JSON.stringify(body),
1143
+ signal: AbortSignal.timeout(5e3)
1144
+ }).catch(() => {
1145
+ });
1146
+ }
1147
+ function emitMilestone(projectId, milestone, extra) {
1148
+ emitTelemetryEvent({
1149
+ project_id: projectId,
1150
+ tool_name: milestone,
1151
+ event_type: "milestone",
1152
+ metadata: extra
1153
+ });
1154
+ }
1155
+ function resolveTelemetryProjectId(config2) {
1156
+ if (config2.projectId && config2.projectId.length > 0) return config2.projectId;
1157
+ const env = process.env["PAPI_PROJECT_ID"];
1158
+ return env && env.length > 0 ? env : void 0;
1159
+ }
1160
+ var HOSTED_SUPABASE_URL, DEFAULT_TELEMETRY_ENDPOINT, MD_PINGS_SUPABASE_URL, MD_PINGS_ANON_KEY, consecutiveTelemetryFailures, TELEMETRY_BLACKOUT_THRESHOLD;
1161
+ var init_telemetry = __esm({
1162
+ "src/lib/telemetry.ts"() {
1163
+ "use strict";
1164
+ init_install_id();
1165
+ HOSTED_SUPABASE_URL = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
1166
+ DEFAULT_TELEMETRY_ENDPOINT = `${HOSTED_SUPABASE_URL}/functions/v1/data-proxy`;
1167
+ MD_PINGS_SUPABASE_URL = process.env["PAPI_MD_PINGS_URL"] ?? HOSTED_SUPABASE_URL;
1168
+ MD_PINGS_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
1169
+ consecutiveTelemetryFailures = 0;
1170
+ TELEMETRY_BLACKOUT_THRESHOLD = 3;
1171
+ }
1172
+ });
1173
+
1016
1174
  // src/proxy-adapter.ts
1017
1175
  var proxy_adapter_exports = {};
1018
1176
  __export(proxy_adapter_exports, {
@@ -1059,6 +1217,7 @@ var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, ProxyPapiAdapter;
1059
1217
  var init_proxy_adapter = __esm({
1060
1218
  "src/proxy-adapter.ts"() {
1061
1219
  "use strict";
1220
+ init_telemetry();
1062
1221
  JSONB_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set([
1063
1222
  "buildHandoff",
1064
1223
  "stateHistory",
@@ -1421,6 +1580,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1421
1580
  * telemetry failure must never surface as a tool error.
1422
1581
  */
1423
1582
  emitTelemetry(event) {
1583
+ const ctx = { projectId: event.projectId, toolName: event.toolName, eventType: event.eventType };
1424
1584
  fetch(`${this.endpoint}/telemetry`, {
1425
1585
  method: "POST",
1426
1586
  headers: {
@@ -1434,7 +1594,11 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1434
1594
  metadata: event.metadata ?? {}
1435
1595
  }),
1436
1596
  signal: AbortSignal.timeout(5e3)
1437
- }).catch(() => {
1597
+ }).then((res) => {
1598
+ if (res.ok) noteTelemetryEmitSuccess();
1599
+ else reportTelemetryEmitFailure(`HTTP ${res.status}`, ctx);
1600
+ }).catch((err) => {
1601
+ reportTelemetryEmitFailure(err instanceof Error ? err.name : "network error", ctx);
1438
1602
  });
1439
1603
  }
1440
1604
  readToolMetrics() {
@@ -7440,8 +7604,8 @@ function detectUserId() {
7440
7604
  }
7441
7605
  return void 0;
7442
7606
  }
7443
- var HOSTED_SUPABASE_URL = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
7444
- var HOSTED_PROXY_ENDPOINT = `${HOSTED_SUPABASE_URL}/functions/v1/data-proxy`;
7607
+ var HOSTED_SUPABASE_URL2 = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
7608
+ var HOSTED_PROXY_ENDPOINT = `${HOSTED_SUPABASE_URL2}/functions/v1/data-proxy`;
7445
7609
  var PLACEHOLDER_PATTERNS = [
7446
7610
  "<YOUR_DATABASE_URL>",
7447
7611
  "your-database-url",
@@ -7821,7 +7985,7 @@ function errorResponse(message) {
7821
7985
  }
7822
7986
 
7823
7987
  // src/services/plan.ts
7824
- import { createHash, randomUUID as randomUUID7 } from "crypto";
7988
+ import { createHash, randomUUID as randomUUID8 } from "crypto";
7825
7989
  import { readFile as readFile2 } from "fs/promises";
7826
7990
  import path4 from "path";
7827
7991
 
@@ -11447,7 +11611,7 @@ ${cleanContent}`;
11447
11611
  const payload = {
11448
11612
  cycleNumber: newCycleNumber,
11449
11613
  cycleLog: {
11450
- uuid: randomUUID7(),
11614
+ uuid: randomUUID8(),
11451
11615
  cycleNumber: newCycleNumber,
11452
11616
  title: cleanTitle,
11453
11617
  content: logBlock,
@@ -11554,7 +11718,7 @@ ${cleanContent}`;
11554
11718
  const cycleTaskCount = legacyHandoffs.length;
11555
11719
  const cycleEffortPoints = legacyHandoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
11556
11720
  const cycleLogPromise = adapter2.writeCycleLogEntry({
11557
- uuid: randomUUID7(),
11721
+ uuid: randomUUID8(),
11558
11722
  cycleNumber: newCycleNumber,
11559
11723
  title: cleanTitle,
11560
11724
  content: logBlock,
@@ -11593,7 +11757,7 @@ ${cleanContent}`;
11593
11757
  visibility = task.visibility;
11594
11758
  }
11595
11759
  const created = await adapter2.createTask({
11596
- uuid: randomUUID7(),
11760
+ uuid: randomUUID8(),
11597
11761
  displayId: "",
11598
11762
  title: task.title,
11599
11763
  status: task.status || "Backlog",
@@ -12921,11 +13085,11 @@ ${result.userMessage}
12921
13085
  }
12922
13086
 
12923
13087
  // src/services/strategy.ts
12924
- import { randomUUID as randomUUID9, createHash as createHash2 } from "crypto";
13088
+ import { randomUUID as randomUUID10, createHash as createHash2 } from "crypto";
12925
13089
  import { execFileSync as execFileSync2 } from "child_process";
12926
13090
  import { existsSync, readdirSync, statSync } from "fs";
12927
- import { join as join2 } from "path";
12928
- import { homedir } from "os";
13091
+ import { join as join3 } from "path";
13092
+ import { homedir as homedir2 } from "os";
12929
13093
 
12930
13094
  // src/lib/hosted-mode.ts
12931
13095
  function isHostedTransport() {
@@ -12936,7 +13100,7 @@ function hasLocalWorkspace() {
12936
13100
  }
12937
13101
 
12938
13102
  // src/services/idea.ts
12939
- import { randomUUID as randomUUID8 } from "crypto";
13103
+ import { randomUUID as randomUUID9 } from "crypto";
12940
13104
  var OWNER_ACTION_PATTERNS = [
12941
13105
  // Verb match — the canonical owner-action verbs (spec C275 §2)
12942
13106
  /\b(send|reply|email|call|phone|text|message|dm)\b/i,
@@ -13274,7 +13438,7 @@ ${lines.join("\n")}
13274
13438
  }
13275
13439
  const inherited = await resolveVisibilityFromDocs(adapter2, [input.docRef]);
13276
13440
  const task = await adapter2.createTask({
13277
- uuid: randomUUID8(),
13441
+ uuid: randomUUID9(),
13278
13442
  displayId: "",
13279
13443
  title: taskTitle,
13280
13444
  status: "Backlog",
@@ -13462,6 +13626,23 @@ function generateValueReport(snapshots) {
13462
13626
 
13463
13627
  // src/services/strategy.ts
13464
13628
  var STRATEGY_DUPE_COVERAGE_THRESHOLD = 0.6;
13629
+ function taskStatusLabel(task) {
13630
+ return `[${task.status}${task.cycle != null ? ` C${task.cycle}` : ""}]`;
13631
+ }
13632
+ function annotateTaskStatuses(text, statusMap) {
13633
+ return text.replace(/\b(task-\d+)\b(?!\s*\[)/gi, (full, id) => {
13634
+ const label = statusMap.get(id.toLowerCase());
13635
+ return label ? `${id} ${label}` : full;
13636
+ });
13637
+ }
13638
+ function recActsOnTaskId(rec) {
13639
+ if (rec.target && /^task-\d+$/i.test(rec.target)) return rec.target;
13640
+ if (rec.type === "priority_change" || rec.type === "phase_transition") {
13641
+ const m = rec.content.match(/\btask-\d+\b/i);
13642
+ if (m) return m[0];
13643
+ }
13644
+ return void 0;
13645
+ }
13465
13646
  async function extractRecommendations(data, cycleNumber, adapter2) {
13466
13647
  const recs = [];
13467
13648
  if (data.activeDecisionUpdates?.length) {
@@ -13525,6 +13706,32 @@ async function extractRecommendations(data, cycleNumber, adapter2) {
13525
13706
  }
13526
13707
  }
13527
13708
  }
13709
+ if (adapter2 && recs.length > 0) {
13710
+ const targetIds = /* @__PURE__ */ new Set();
13711
+ for (const rec of recs) {
13712
+ const id = recActsOnTaskId(rec);
13713
+ if (id) targetIds.add(id.toLowerCase());
13714
+ }
13715
+ if (targetIds.size > 0) {
13716
+ try {
13717
+ const targetTasks = await adapter2.getTasks([...targetIds]);
13718
+ const shipped = new Set(
13719
+ targetTasks.filter((t) => t.status === "Done" || t.status === "Cancelled").map((t) => (t.displayId ?? t.id).toLowerCase())
13720
+ );
13721
+ if (shipped.size > 0) {
13722
+ return recs.filter((rec) => {
13723
+ const id = recActsOnTaskId(rec)?.toLowerCase();
13724
+ if (id && shipped.has(id)) {
13725
+ console.error(`[strategy_review] Dropped pending rec targeting already-shipped task ${id}: ${rec.content.slice(0, 80)}`);
13726
+ return false;
13727
+ }
13728
+ return true;
13729
+ });
13730
+ }
13731
+ } catch {
13732
+ }
13733
+ }
13734
+ }
13528
13735
  return recs;
13529
13736
  }
13530
13737
  function classifyRecommendation(text) {
@@ -13878,11 +14085,11 @@ ${lines.join("\n")}`;
13878
14085
  }
13879
14086
  let recentPlansText;
13880
14087
  try {
13881
- const plansDir = join2(homedir(), ".claude", "plans");
14088
+ const plansDir = join3(homedir2(), ".claude", "plans");
13882
14089
  if (existsSync(plansDir)) {
13883
14090
  const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
13884
14091
  const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
13885
- const fullPath = join2(plansDir, f);
14092
+ const fullPath = join3(plansDir, f);
13886
14093
  const stat4 = statSync(fullPath);
13887
14094
  return { name: f, modified: stat4.mtime, size: stat4.size };
13888
14095
  }).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
@@ -13899,7 +14106,7 @@ ${lines.join("\n")}`;
13899
14106
  }
13900
14107
  let unregisteredDocsText;
13901
14108
  try {
13902
- const docsDir = join2(projectRoot, "docs");
14109
+ const docsDir = join3(projectRoot, "docs");
13903
14110
  if (hasLocalWorkspace() && existsSync(docsDir)) {
13904
14111
  const registeredPaths = new Set(
13905
14112
  (registeredDocs ?? []).map((d) => d.path).filter(Boolean)
@@ -13907,7 +14114,7 @@ ${lines.join("\n")}`;
13907
14114
  const allDocFiles = [];
13908
14115
  const scanDir = (dir, prefix) => {
13909
14116
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
13910
- if (entry.isDirectory()) scanDir(join2(dir, entry.name), `${prefix}${entry.name}/`);
14117
+ if (entry.isDirectory()) scanDir(join3(dir, entry.name), `${prefix}${entry.name}/`);
13911
14118
  else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
13912
14119
  }
13913
14120
  };
@@ -14043,6 +14250,38 @@ ${lines.join("\n")}`;
14043
14250
  docActionStaleness: docActionStalenessText,
14044
14251
  pendingAgendaTopics: pendingAgendaText
14045
14252
  };
14253
+ try {
14254
+ const referencedIds = /* @__PURE__ */ new Set();
14255
+ const bearingFields = [
14256
+ context.sessionLog,
14257
+ context.allBuildReports,
14258
+ context.briefImplications,
14259
+ context.previousReviews,
14260
+ context.pendingRecommendations,
14261
+ context.docActionStaleness,
14262
+ context.taskComments
14263
+ ];
14264
+ for (const field of bearingFields) {
14265
+ if (typeof field === "string") {
14266
+ for (const m of field.matchAll(/\btask-\d+\b/gi)) referencedIds.add(m[0].toLowerCase());
14267
+ }
14268
+ }
14269
+ if (referencedIds.size > 0) {
14270
+ const statusTasks = await adapter2.getTasks([...referencedIds]);
14271
+ const statusMap = /* @__PURE__ */ new Map();
14272
+ for (const t of statusTasks) statusMap.set((t.displayId ?? t.id).toLowerCase(), taskStatusLabel(t));
14273
+ if (statusMap.size > 0) {
14274
+ if (context.sessionLog) context.sessionLog = annotateTaskStatuses(context.sessionLog, statusMap);
14275
+ if (context.allBuildReports) context.allBuildReports = annotateTaskStatuses(context.allBuildReports, statusMap);
14276
+ if (context.briefImplications) context.briefImplications = annotateTaskStatuses(context.briefImplications, statusMap);
14277
+ if (context.previousReviews) context.previousReviews = annotateTaskStatuses(context.previousReviews, statusMap);
14278
+ if (context.pendingRecommendations) context.pendingRecommendations = annotateTaskStatuses(context.pendingRecommendations, statusMap);
14279
+ if (context.docActionStaleness) context.docActionStaleness = annotateTaskStatuses(context.docActionStaleness, statusMap);
14280
+ if (context.taskComments) context.taskComments = annotateTaskStatuses(context.taskComments, statusMap);
14281
+ }
14282
+ }
14283
+ } catch {
14284
+ }
14046
14285
  const BUDGET_SOFT2 = 5e4;
14047
14286
  const BUDGET_HARD2 = 6e4;
14048
14287
  const compressionSteps = [];
@@ -14736,7 +14975,7 @@ async function processStrategyChangeOutput(adapter2, rawOutput, cycleNumber) {
14736
14975
 
14737
14976
  ${cleanContent}`;
14738
14977
  await adapter2.writeCycleLogEntry({
14739
- uuid: randomUUID9(),
14978
+ uuid: randomUUID10(),
14740
14979
  cycleNumber,
14741
14980
  title: cleanTitle,
14742
14981
  content: logBlock
@@ -15973,15 +16212,15 @@ ${existing}` : entry;
15973
16212
 
15974
16213
  // src/services/setup.ts
15975
16214
  import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
15976
- import { join as join7, basename, extname, dirname as dirname3 } from "path";
16215
+ import { join as join8, basename, extname, dirname as dirname3 } from "path";
15977
16216
  import { execFileSync as execFileSync3 } from "child_process";
15978
16217
 
15979
16218
  // src/lib/detect-codebase.ts
15980
16219
  import { existsSync as existsSync2 } from "fs";
15981
16220
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
15982
- import { join as join3 } from "path";
16221
+ import { join as join4 } from "path";
15983
16222
  function detectCodebaseType(projectRoot) {
15984
- if (existsSync2(join3(projectRoot, ".git"))) {
16223
+ if (existsSync2(join4(projectRoot, ".git"))) {
15985
16224
  return "existing_codebase";
15986
16225
  }
15987
16226
  const manifests = [
@@ -15995,7 +16234,7 @@ function detectCodebaseType(projectRoot) {
15995
16234
  "CMakeLists.txt"
15996
16235
  ];
15997
16236
  for (const manifest of manifests) {
15998
- if (existsSync2(join3(projectRoot, manifest))) {
16237
+ if (existsSync2(join4(projectRoot, manifest))) {
15999
16238
  return "existing_codebase";
16000
16239
  }
16001
16240
  }
@@ -16003,7 +16242,7 @@ function detectCodebaseType(projectRoot) {
16003
16242
  const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
16004
16243
  const fileCount = entries.filter((f) => {
16005
16244
  try {
16006
- return statSync2(join3(projectRoot, f)).isFile();
16245
+ return statSync2(join4(projectRoot, f)).isFile();
16007
16246
  } catch {
16008
16247
  return false;
16009
16248
  }
@@ -16015,18 +16254,18 @@ function detectCodebaseType(projectRoot) {
16015
16254
  }
16016
16255
 
16017
16256
  // src/lib/agents-bundle.ts
16018
- import { readFileSync, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
16019
- import { dirname, join as join4, resolve } from "path";
16257
+ import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
16258
+ import { dirname, join as join5, resolve } from "path";
16020
16259
  import { fileURLToPath } from "url";
16021
- var PROJECT_BUNDLE_REL = join4(".agents", "skills", "papi-cycle");
16260
+ var PROJECT_BUNDLE_REL = join5(".agents", "skills", "papi-cycle");
16022
16261
  function bundleDestRel(rel) {
16023
- return rel === "AGENTS.md" ? "AGENTS.md" : join4(PROJECT_BUNDLE_REL, rel);
16262
+ return rel === "AGENTS.md" ? "AGENTS.md" : join5(PROJECT_BUNDLE_REL, rel);
16024
16263
  }
16025
16264
  function resolveBundleDir() {
16026
16265
  let dir = dirname(fileURLToPath(import.meta.url));
16027
16266
  for (let i = 0; i < 5; i++) {
16028
- const candidate = join4(dir, "skills", "papi-cycle");
16029
- if (existsSync3(join4(candidate, "AGENTS.md"))) return candidate;
16267
+ const candidate = join5(dir, "skills", "papi-cycle");
16268
+ if (existsSync3(join5(candidate, "AGENTS.md"))) return candidate;
16030
16269
  const parent = resolve(dir, "..");
16031
16270
  if (parent === dir) break;
16032
16271
  dir = parent;
@@ -16038,10 +16277,10 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
16038
16277
  const files = [];
16039
16278
  const walk = (abs, rel) => {
16040
16279
  for (const entry of readdirSync3(abs, { withFileTypes: true })) {
16041
- const childAbs = join4(abs, entry.name);
16042
- const childRel = rel ? join4(rel, entry.name) : entry.name;
16280
+ const childAbs = join5(abs, entry.name);
16281
+ const childRel = rel ? join5(rel, entry.name) : entry.name;
16043
16282
  if (entry.isDirectory()) walk(childAbs, childRel);
16044
- else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync(childAbs, "utf8") });
16283
+ else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync2(childAbs, "utf8") });
16045
16284
  }
16046
16285
  };
16047
16286
  walk(bundleDir, "");
@@ -16050,7 +16289,7 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
16050
16289
  function planBundleInstall(projectRoot, projectName, opts = {}) {
16051
16290
  const out = {};
16052
16291
  for (const f of readBundleFiles()) {
16053
- const dest = join4(projectRoot, bundleDestRel(f.rel));
16292
+ const dest = join5(projectRoot, bundleDestRel(f.rel));
16054
16293
  if (opts.skipExisting && existsSync3(dest) && statSync3(dest).isFile()) continue;
16055
16294
  const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
16056
16295
  out[dest] = content;
@@ -16059,20 +16298,20 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
16059
16298
  }
16060
16299
 
16061
16300
  // src/lib/design-bundle.ts
16062
- import { readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync4 } from "fs";
16063
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
16301
+ import { readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync4 } from "fs";
16302
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
16064
16303
  import { fileURLToPath as fileURLToPath2 } from "url";
16065
16304
  var DESIGN_ASSETS = [
16066
- { srcRel: join5("agents", "frontend-design-engineer.md"), destRel: join5(".claude", "agents", "frontend-design-engineer.md"), executable: false },
16067
- { srcRel: join5("skills", "design-critique", "SKILL.md"), destRel: join5(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
16068
- { srcRel: join5("hooks", "frontend-design-guard.sh"), destRel: join5(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
16305
+ { srcRel: join6("agents", "frontend-design-engineer.md"), destRel: join6(".claude", "agents", "frontend-design-engineer.md"), executable: false },
16306
+ { srcRel: join6("skills", "design-critique", "SKILL.md"), destRel: join6(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
16307
+ { srcRel: join6("hooks", "frontend-design-guard.sh"), destRel: join6(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
16069
16308
  ];
16070
16309
  var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
16071
16310
  function resolveDesignAssetsDir() {
16072
16311
  let dir = dirname2(fileURLToPath2(import.meta.url));
16073
16312
  for (let i = 0; i < 5; i++) {
16074
- const candidate = join5(dir, "design-assets");
16075
- if (existsSync4(join5(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
16313
+ const candidate = join6(dir, "design-assets");
16314
+ if (existsSync4(join6(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
16076
16315
  const parent = resolve2(dir, "..");
16077
16316
  if (parent === dir) break;
16078
16317
  dir = parent;
@@ -16084,23 +16323,23 @@ function planDesignInstall(projectRoot, opts = {}) {
16084
16323
  if (!assetsDir) return [];
16085
16324
  const out = [];
16086
16325
  for (const asset of DESIGN_ASSETS) {
16087
- const srcAbs = join5(assetsDir, asset.srcRel);
16326
+ const srcAbs = join6(assetsDir, asset.srcRel);
16088
16327
  if (!existsSync4(srcAbs)) continue;
16089
- const dest = projectRoot ? join5(projectRoot, asset.destRel) : asset.destRel;
16328
+ const dest = projectRoot ? join6(projectRoot, asset.destRel) : asset.destRel;
16090
16329
  if (opts.skipExisting && projectRoot && existsSync4(dest) && statSync4(dest).isFile()) continue;
16091
- out.push({ dest, content: readFileSync2(srcAbs, "utf8"), executable: asset.executable });
16330
+ out.push({ dest, content: readFileSync3(srcAbs, "utf8"), executable: asset.executable });
16092
16331
  }
16093
16332
  return out;
16094
16333
  }
16095
16334
 
16096
16335
  // src/lib/skill-detection.ts
16097
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync3, statSync as statSync5 } from "fs";
16098
- import { join as join6 } from "path";
16336
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync5 } from "fs";
16337
+ import { join as join7 } from "path";
16099
16338
  function readPackageJson(projectRoot) {
16100
- const path7 = join6(projectRoot, "package.json");
16339
+ const path7 = join7(projectRoot, "package.json");
16101
16340
  if (!existsSync5(path7)) return null;
16102
16341
  try {
16103
- const raw = readFileSync3(path7, "utf-8");
16342
+ const raw = readFileSync4(path7, "utf-8");
16104
16343
  return JSON.parse(raw);
16105
16344
  } catch {
16106
16345
  return null;
@@ -16121,7 +16360,7 @@ function detectsFrontendStack(projectRoot) {
16121
16360
  return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
16122
16361
  }
16123
16362
  function hasGitHubWorkflows(projectRoot) {
16124
- const dir = join6(projectRoot, ".github", "workflows");
16363
+ const dir = join7(projectRoot, ".github", "workflows");
16125
16364
  if (!existsSync5(dir)) return false;
16126
16365
  try {
16127
16366
  const entries = readdirSync4(dir);
@@ -16131,18 +16370,18 @@ function hasGitHubWorkflows(projectRoot) {
16131
16370
  }
16132
16371
  }
16133
16372
  function envExampleMentionsStaging(projectRoot) {
16134
- const path7 = join6(projectRoot, ".env.example");
16373
+ const path7 = join7(projectRoot, ".env.example");
16135
16374
  if (!existsSync5(path7)) return false;
16136
16375
  try {
16137
- const raw = readFileSync3(path7, "utf-8");
16376
+ const raw = readFileSync4(path7, "utf-8");
16138
16377
  return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
16139
16378
  } catch {
16140
16379
  return false;
16141
16380
  }
16142
16381
  }
16143
16382
  function hasVercelConfig(projectRoot) {
16144
- if (existsSync5(join6(projectRoot, "vercel.json"))) return true;
16145
- const vercelDir = join6(projectRoot, ".vercel");
16383
+ if (existsSync5(join7(projectRoot, "vercel.json"))) return true;
16384
+ const vercelDir = join7(projectRoot, ".vercel");
16146
16385
  if (!existsSync5(vercelDir)) return false;
16147
16386
  try {
16148
16387
  return statSync5(vercelDir).isDirectory();
@@ -16631,7 +16870,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16631
16870
  await mkdir(config2.papiDir, { recursive: true });
16632
16871
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
16633
16872
  const content = substitute(template, vars);
16634
- await writeFile2(join7(config2.papiDir, filename), content, "utf-8");
16873
+ await writeFile2(join8(config2.papiDir, filename), content, "utf-8");
16635
16874
  }
16636
16875
  }
16637
16876
  } else {
@@ -16649,13 +16888,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16649
16888
  const useCollector = config2.adapterType === "proxy";
16650
16889
  const docsRel = "docs";
16651
16890
  const commandsRel = ".claude/commands";
16652
- const commandsDir = useCollector ? commandsRel : join7(config2.projectRoot, ".claude", "commands");
16653
- const docsDir = useCollector ? docsRel : join7(config2.projectRoot, "docs");
16891
+ const commandsDir = useCollector ? commandsRel : join8(config2.projectRoot, ".claude", "commands");
16892
+ const docsDir = useCollector ? docsRel : join8(config2.projectRoot, "docs");
16654
16893
  if (!useCollector) {
16655
16894
  await mkdir(commandsDir, { recursive: true });
16656
16895
  await mkdir(docsDir, { recursive: true });
16657
16896
  }
16658
- const claudeMdPath = useCollector ? "CLAUDE.md" : join7(config2.projectRoot, "CLAUDE.md");
16897
+ const claudeMdPath = useCollector ? "CLAUDE.md" : join8(config2.projectRoot, "CLAUDE.md");
16659
16898
  let claudeMdExists = false;
16660
16899
  if (!useCollector) {
16661
16900
  try {
@@ -16664,7 +16903,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16664
16903
  } catch {
16665
16904
  }
16666
16905
  }
16667
- const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join7(docsDir, "INDEX.md");
16906
+ const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join8(docsDir, "INDEX.md");
16668
16907
  let docsIndexExists = false;
16669
16908
  if (!useCollector) {
16670
16909
  try {
@@ -16674,9 +16913,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16674
16913
  }
16675
16914
  }
16676
16915
  const scaffoldFiles = {
16677
- [useCollector ? `${commandsRel}/papi-audit.md` : join7(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
16678
- [useCollector ? `${commandsRel}/test.md` : join7(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
16679
- [useCollector ? `${docsRel}/README.md` : join7(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
16916
+ [useCollector ? `${commandsRel}/papi-audit.md` : join8(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
16917
+ [useCollector ? `${commandsRel}/test.md` : join8(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
16918
+ [useCollector ? `${docsRel}/README.md` : join8(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
16680
16919
  };
16681
16920
  if (!docsIndexExists) {
16682
16921
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -16703,7 +16942,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16703
16942
  if (useCollector) {
16704
16943
  scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
16705
16944
  } else {
16706
- const cursorDir = join7(config2.projectRoot, ".cursor");
16945
+ const cursorDir = join8(config2.projectRoot, ".cursor");
16707
16946
  let cursorDetected = false;
16708
16947
  try {
16709
16948
  await access2(cursorDir);
@@ -16711,8 +16950,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16711
16950
  } catch {
16712
16951
  }
16713
16952
  if (cursorDetected) {
16714
- const cursorRulesDir = join7(cursorDir, "rules");
16715
- const cursorRulesPath = join7(cursorRulesDir, "papi.mdc");
16953
+ const cursorRulesDir = join8(cursorDir, "rules");
16954
+ const cursorRulesPath = join8(cursorRulesDir, "papi.mdc");
16716
16955
  await mkdir(cursorRulesDir, { recursive: true });
16717
16956
  try {
16718
16957
  await access2(cursorRulesPath);
@@ -16767,7 +17006,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16767
17006
  }
16768
17007
  var PAPI_PERMISSION = "mcp__papi__*";
16769
17008
  async function ensurePapiPermission(projectRoot) {
16770
- const settingsPath = join7(projectRoot, ".claude", "settings.json");
17009
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
16771
17010
  try {
16772
17011
  let settings = {};
16773
17012
  try {
@@ -16786,13 +17025,13 @@ async function ensurePapiPermission(projectRoot) {
16786
17025
  if (!allow.includes(PAPI_PERMISSION)) {
16787
17026
  allow.push(PAPI_PERMISSION);
16788
17027
  }
16789
- await mkdir(join7(projectRoot, ".claude"), { recursive: true });
17028
+ await mkdir(join8(projectRoot, ".claude"), { recursive: true });
16790
17029
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
16791
17030
  } catch {
16792
17031
  }
16793
17032
  }
16794
17033
  async function ensureDesignHookRegistered(projectRoot) {
16795
- const settingsPath = join7(projectRoot, ".claude", "settings.json");
17034
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
16796
17035
  try {
16797
17036
  let settings = {};
16798
17037
  try {
@@ -16822,7 +17061,7 @@ async function ensureDesignHookRegistered(projectRoot) {
16822
17061
  chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
16823
17062
  }
16824
17063
  }
16825
- await mkdir(join7(projectRoot, ".claude"), { recursive: true });
17064
+ await mkdir(join8(projectRoot, ".claude"), { recursive: true });
16826
17065
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
16827
17066
  } catch {
16828
17067
  }
@@ -16926,7 +17165,7 @@ ${conventionsText.trim()}
16926
17165
  );
16927
17166
  } else {
16928
17167
  try {
16929
- const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
17168
+ const claudeMdPath = join8(config2.projectRoot, "CLAUDE.md");
16930
17169
  const existing = await readFile4(claudeMdPath, "utf-8");
16931
17170
  if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
16932
17171
  warnings.push(
@@ -17013,13 +17252,13 @@ async function scanCodebase(projectRoot) {
17013
17252
  }
17014
17253
  let packageJson;
17015
17254
  try {
17016
- const content = await readFile4(join7(projectRoot, "package.json"), "utf-8");
17255
+ const content = await readFile4(join8(projectRoot, "package.json"), "utf-8");
17017
17256
  packageJson = JSON.parse(content);
17018
17257
  } catch {
17019
17258
  }
17020
17259
  let readme;
17021
17260
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
17022
- const content = await safeReadFile(join7(projectRoot, name), 5e3);
17261
+ const content = await safeReadFile(join8(projectRoot, name), 5e3);
17023
17262
  if (content) {
17024
17263
  readme = content;
17025
17264
  break;
@@ -17029,7 +17268,7 @@ async function scanCodebase(projectRoot) {
17029
17268
  let totalFiles = topLevelFiles.length;
17030
17269
  for (const dir of topLevelDirs) {
17031
17270
  try {
17032
- const entries = await readdir(join7(projectRoot, dir), { withFileTypes: true });
17271
+ const entries = await readdir(join8(projectRoot, dir), { withFileTypes: true });
17033
17272
  const files = entries.filter((e) => e.isFile());
17034
17273
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
17035
17274
  totalFiles += files.length;
@@ -17376,7 +17615,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17376
17615
  collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
17377
17616
  } else {
17378
17617
  try {
17379
- const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
17618
+ const claudeMdPath = join8(config2.projectRoot, "CLAUDE.md");
17380
17619
  const existing = await readFile4(claudeMdPath, "utf-8");
17381
17620
  if (!existing.includes("Dogfood Logging")) {
17382
17621
  await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
@@ -17421,7 +17660,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17421
17660
  cursorScaffolded = true;
17422
17661
  } else {
17423
17662
  try {
17424
- await access2(join7(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
17663
+ await access2(join8(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
17425
17664
  cursorScaffolded = true;
17426
17665
  } catch {
17427
17666
  }
@@ -17441,11 +17680,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17441
17680
  }
17442
17681
  async function ensureMcpJsonGitignored(projectRoot) {
17443
17682
  try {
17444
- await access2(join7(projectRoot, ".git"));
17683
+ await access2(join8(projectRoot, ".git"));
17445
17684
  } catch {
17446
17685
  return void 0;
17447
17686
  }
17448
- const gitignorePath = join7(projectRoot, ".gitignore");
17687
+ const gitignorePath = join8(projectRoot, ".gitignore");
17449
17688
  let existing = "";
17450
17689
  try {
17451
17690
  existing = await readFile4(gitignorePath, "utf-8");
@@ -17955,131 +18194,10 @@ init_git();
17955
18194
  init_git();
17956
18195
 
17957
18196
  // src/services/release.ts
18197
+ init_telemetry();
17958
18198
  import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
17959
18199
  import { join as join9 } from "path";
17960
18200
  import { execFileSync as execFileSync4 } from "child_process";
17961
-
17962
- // src/lib/install-id.ts
17963
- import { randomUUID as randomUUID10 } from "crypto";
17964
- import { mkdirSync, readFileSync as readFileSync4, writeFileSync, chmodSync } from "fs";
17965
- import { homedir as homedir2 } from "os";
17966
- import { join as join8 } from "path";
17967
- var PAPI_HOME_DIR = join8(homedir2(), ".papi");
17968
- var INSTALL_ID_FILE = join8(PAPI_HOME_DIR, "install-id.json");
17969
- var cachedInstallId = null;
17970
- function isValidUuid(s) {
17971
- return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
17972
- }
17973
- function getInstallId() {
17974
- if (cachedInstallId) return cachedInstallId;
17975
- try {
17976
- const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
17977
- const parsed = JSON.parse(raw);
17978
- if (isValidUuid(parsed.install_id)) {
17979
- cachedInstallId = parsed.install_id;
17980
- return cachedInstallId;
17981
- }
17982
- } catch {
17983
- }
17984
- try {
17985
- mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
17986
- const id = randomUUID10();
17987
- const contents = {
17988
- install_id: id,
17989
- created_at: (/* @__PURE__ */ new Date()).toISOString()
17990
- };
17991
- writeFileSync(INSTALL_ID_FILE, JSON.stringify(contents, null, 2), { mode: 384 });
17992
- try {
17993
- chmodSync(INSTALL_ID_FILE, 384);
17994
- } catch {
17995
- }
17996
- cachedInstallId = id;
17997
- return cachedInstallId;
17998
- } catch {
17999
- return null;
18000
- }
18001
- }
18002
-
18003
- // src/lib/telemetry.ts
18004
- var HOSTED_SUPABASE_URL2 = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
18005
- var DEFAULT_TELEMETRY_ENDPOINT = `${HOSTED_SUPABASE_URL2}/functions/v1/data-proxy`;
18006
- var MD_PINGS_SUPABASE_URL = process.env["PAPI_MD_PINGS_URL"] ?? HOSTED_SUPABASE_URL2;
18007
- var MD_PINGS_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
18008
- function isEnabled() {
18009
- const val = process.env["PAPI_TELEMETRY"];
18010
- return val !== "false" && val !== "off";
18011
- }
18012
- function emitTelemetryEvent(event) {
18013
- if (!isEnabled()) return;
18014
- const apiKey = process.env["PAPI_DATA_API_KEY"];
18015
- if (!apiKey) return;
18016
- const endpoint = process.env["PAPI_DATA_ENDPOINT"] ?? DEFAULT_TELEMETRY_ENDPOINT;
18017
- const body = {
18018
- projectId: event.project_id,
18019
- toolName: event.tool_name,
18020
- eventType: event.event_type,
18021
- metadata: event.metadata ?? {}
18022
- };
18023
- fetch(`${endpoint}/telemetry`, {
18024
- method: "POST",
18025
- headers: {
18026
- "Content-Type": "application/json",
18027
- "Authorization": `Bearer ${apiKey}`
18028
- },
18029
- body: JSON.stringify(body),
18030
- signal: AbortSignal.timeout(5e3)
18031
- }).catch(() => {
18032
- });
18033
- }
18034
- function emitToolCall(projectId, toolName, durationMs, extra) {
18035
- emitTelemetryEvent({
18036
- project_id: projectId,
18037
- tool_name: toolName,
18038
- event_type: "tool_call",
18039
- metadata: { duration_ms: durationMs, ...extra }
18040
- });
18041
- }
18042
- function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
18043
- if (!isEnabled()) return;
18044
- const installId = getInstallId();
18045
- if (!installId) return;
18046
- const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
18047
- const body = {
18048
- install_id: installId,
18049
- tool_name: toolName,
18050
- papi_version: process.env["npm_package_version"] ?? null,
18051
- metadata: extra ?? {}
18052
- };
18053
- if (resolvedUserId) body["user_id"] = resolvedUserId;
18054
- if (projectSlug) body["project_slug"] = projectSlug;
18055
- fetch(`${MD_PINGS_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
18056
- method: "POST",
18057
- headers: {
18058
- "Content-Type": "application/json",
18059
- "apikey": MD_PINGS_ANON_KEY,
18060
- "Authorization": `Bearer ${MD_PINGS_ANON_KEY}`,
18061
- "Prefer": "return=minimal"
18062
- },
18063
- body: JSON.stringify(body),
18064
- signal: AbortSignal.timeout(5e3)
18065
- }).catch(() => {
18066
- });
18067
- }
18068
- function emitMilestone(projectId, milestone, extra) {
18069
- emitTelemetryEvent({
18070
- project_id: projectId,
18071
- tool_name: milestone,
18072
- event_type: "milestone",
18073
- metadata: extra
18074
- });
18075
- }
18076
- function resolveTelemetryProjectId(config2) {
18077
- if (config2.projectId && config2.projectId.length > 0) return config2.projectId;
18078
- const env = process.env["PAPI_PROJECT_ID"];
18079
- return env && env.length > 0 ? env : void 0;
18080
- }
18081
-
18082
- // src/services/release.ts
18083
18201
  init_git();
18084
18202
  var INITIAL_RELEASE_NOTES = `# Changelog
18085
18203
 
@@ -27099,6 +27217,9 @@ ${serializeBuildHandoff(task.buildHandoff)}`);
27099
27217
  }
27100
27218
  }
27101
27219
 
27220
+ // src/server.ts
27221
+ init_telemetry();
27222
+
27102
27223
  // src/services/metering.ts
27103
27224
  var TIER_ALLOWANCES = {
27104
27225
  free: 1e3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.41",
3
+ "version": "0.7.42",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "mcpName": "io.github.cathalos92/papi",