@papi-ai/server 0.7.40 → 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.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ __export(git_exports, {
22
22
  cycleBranchName: () => cycleBranchName,
23
23
  deleteLocalBranch: () => deleteLocalBranch,
24
24
  detectBoardMismatches: () => detectBoardMismatches,
25
+ detectMergedInProgress: () => detectMergedInProgress,
25
26
  detectUnrecordedCommits: () => detectUnrecordedCommits,
26
27
  ensureLatestDevelop: () => ensureLatestDevelop,
27
28
  ensureTagAtHead: () => ensureTagAtHead,
@@ -63,6 +64,7 @@ __export(git_exports, {
63
64
  normalizeGitUrl: () => normalizeGitUrl,
64
65
  pickModuleCycleBranch: () => pickModuleCycleBranch,
65
66
  resolveBaseBranch: () => resolveBaseBranch,
67
+ resolveDefaultBranch: () => resolveDefaultBranch,
66
68
  runAutoCommit: () => runAutoCommit,
67
69
  squashMergePullRequest: () => squashMergePullRequest,
68
70
  stageAllAndCommit: () => stageAllAndCommit,
@@ -660,6 +662,22 @@ function resolveBaseBranch(cwd, preferred) {
660
662
  if (preferred !== "master" && branchExists(cwd, "master")) return "master";
661
663
  return preferred;
662
664
  }
665
+ function resolveDefaultBranch(cwd, preferred) {
666
+ try {
667
+ const ref = execFileSync(
668
+ "git",
669
+ ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
670
+ { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
671
+ ).trim();
672
+ const name = ref.replace(/^origin\//, "");
673
+ if (name && branchExists(cwd, name)) return name;
674
+ } catch {
675
+ }
676
+ for (const candidate of [preferred, "main", "master", "trunk"]) {
677
+ if (candidate && branchExists(cwd, candidate)) return candidate;
678
+ }
679
+ return preferred;
680
+ }
663
681
  function detectBoardMismatches(cwd, tasks) {
664
682
  const empty = { codeAhead: [], staleInProgress: [] };
665
683
  if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
@@ -693,6 +711,46 @@ function detectBoardMismatches(cwd, tasks) {
693
711
  return empty;
694
712
  }
695
713
  }
714
+ function escapeRegexLiteral(s) {
715
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
716
+ }
717
+ function detectMergedInProgress(cwd, preferredBase, tasks) {
718
+ if (!isGitAvailable() || !isGitRepo(cwd)) return [];
719
+ const inProgress = tasks.filter((t) => t.status === "In Progress");
720
+ if (inProgress.length === 0) return [];
721
+ const base = resolveDefaultBranch(cwd, preferredBase);
722
+ if (!branchExists(cwd, base)) return [];
723
+ let raw;
724
+ try {
725
+ raw = execFileSync(
726
+ "git",
727
+ ["log", base, "--format=%h%x01%s", "-n", "1000"],
728
+ { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
729
+ );
730
+ } catch {
731
+ return [];
732
+ }
733
+ const commits = raw.split("\n").map((line) => {
734
+ const idx = line.indexOf("");
735
+ if (idx === -1) return null;
736
+ return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
737
+ }).filter((c) => c !== null && c.hash !== "");
738
+ const hits = [];
739
+ for (const task of inProgress) {
740
+ const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
741
+ const hit = commits.find((c) => re.test(c.subject));
742
+ if (!hit) continue;
743
+ const prMatch = hit.subject.match(/#(\d+)/);
744
+ hits.push({
745
+ displayId: task.displayId,
746
+ title: task.title,
747
+ commit: hit.hash,
748
+ subject: hit.subject,
749
+ pr: prMatch ? `#${prMatch[1]}` : null
750
+ });
751
+ }
752
+ return hits;
753
+ }
696
754
  function detectUnrecordedCommits(cwd, baseBranch) {
697
755
  if (!isGitAvailable() || !isGitRepo(cwd)) return [];
698
756
  const latestTag = getLatestTag(cwd);
@@ -955,6 +1013,164 @@ var init_git = __esm({
955
1013
  }
956
1014
  });
957
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
+
958
1174
  // src/proxy-adapter.ts
959
1175
  var proxy_adapter_exports = {};
960
1176
  __export(proxy_adapter_exports, {
@@ -1001,6 +1217,7 @@ var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, ProxyPapiAdapter;
1001
1217
  var init_proxy_adapter = __esm({
1002
1218
  "src/proxy-adapter.ts"() {
1003
1219
  "use strict";
1220
+ init_telemetry();
1004
1221
  JSONB_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set([
1005
1222
  "buildHandoff",
1006
1223
  "stateHistory",
@@ -1256,6 +1473,13 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1256
1473
  updateTaskStatus(id, status) {
1257
1474
  return this.invoke("updateTaskStatus", [id, status]);
1258
1475
  }
1476
+ // task-2292: cross-project move. The bound projectId (set on this adapter) is
1477
+ // the SOURCE; the target is passed in args. Ownership of BOTH projects is
1478
+ // verified server-side in the edge function from the bearer — callerUserId is
1479
+ // ignored here (cannot be spoofed). The edge resolves a target slug → UUID.
1480
+ moveTask(taskId, targetProject, _callerUserId) {
1481
+ return this.invoke("moveTask", [taskId, targetProject]);
1482
+ }
1259
1483
  // task-2155 (MU-3 task B): hosted/proxy claim write-path. The data-proxy gates
1260
1484
  // these via WRITE_METHODS (active editor may write, viewer cannot) and binds the
1261
1485
  // assignee to the bearer-derived caller server-side — the assigneeId passed here
@@ -1356,6 +1580,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1356
1580
  * telemetry failure must never surface as a tool error.
1357
1581
  */
1358
1582
  emitTelemetry(event) {
1583
+ const ctx = { projectId: event.projectId, toolName: event.toolName, eventType: event.eventType };
1359
1584
  fetch(`${this.endpoint}/telemetry`, {
1360
1585
  method: "POST",
1361
1586
  headers: {
@@ -1369,7 +1594,11 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1369
1594
  metadata: event.metadata ?? {}
1370
1595
  }),
1371
1596
  signal: AbortSignal.timeout(5e3)
1372
- }).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);
1373
1602
  });
1374
1603
  }
1375
1604
  readToolMetrics() {
@@ -1712,9 +1941,9 @@ var init_query = __esm({
1712
1941
  CLOSE = {};
1713
1942
  Query = class extends Promise {
1714
1943
  constructor(strings, args, handler, canceller, options = {}) {
1715
- let resolve2, reject;
1944
+ let resolve3, reject;
1716
1945
  super((a, b2) => {
1717
- resolve2 = a;
1946
+ resolve3 = a;
1718
1947
  reject = b2;
1719
1948
  });
1720
1949
  this.tagged = Array.isArray(strings.raw);
@@ -1725,7 +1954,7 @@ var init_query = __esm({
1725
1954
  this.options = options;
1726
1955
  this.state = null;
1727
1956
  this.statement = null;
1728
- this.resolve = (x) => (this.active = false, resolve2(x));
1957
+ this.resolve = (x) => (this.active = false, resolve3(x));
1729
1958
  this.reject = (x) => (this.active = false, reject(x));
1730
1959
  this.active = false;
1731
1960
  this.cancelled = null;
@@ -1773,12 +2002,12 @@ var init_query = __esm({
1773
2002
  if (this.executed && !this.active)
1774
2003
  return { done: true };
1775
2004
  prev && prev();
1776
- const promise = new Promise((resolve2, reject) => {
2005
+ const promise = new Promise((resolve3, reject) => {
1777
2006
  this.cursorFn = (value) => {
1778
- resolve2({ value, done: false });
2007
+ resolve3({ value, done: false });
1779
2008
  return new Promise((r) => prev = r);
1780
2009
  };
1781
- this.resolve = () => (this.active = false, resolve2({ done: true }));
2010
+ this.resolve = () => (this.active = false, resolve3({ done: true }));
1782
2011
  this.reject = (x) => (this.active = false, reject(x));
1783
2012
  });
1784
2013
  this.execute();
@@ -2376,12 +2605,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
2376
2605
  x.on("drain", drain);
2377
2606
  return x;
2378
2607
  }
2379
- async function cancel({ pid, secret }, resolve2, reject) {
2608
+ async function cancel({ pid, secret }, resolve3, reject) {
2380
2609
  try {
2381
2610
  cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
2382
2611
  await connect();
2383
2612
  socket.once("error", reject);
2384
- socket.once("close", resolve2);
2613
+ socket.once("close", resolve3);
2385
2614
  } catch (error2) {
2386
2615
  reject(error2);
2387
2616
  }
@@ -3398,7 +3627,7 @@ var init_subscribe = __esm({
3398
3627
  // ../../node_modules/postgres/src/large.js
3399
3628
  import Stream2 from "stream";
3400
3629
  function largeObject(sql, oid, mode = 131072 | 262144) {
3401
- return new Promise(async (resolve2, reject) => {
3630
+ return new Promise(async (resolve3, reject) => {
3402
3631
  await sql.begin(async (sql2) => {
3403
3632
  let finish;
3404
3633
  !oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
@@ -3424,7 +3653,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
3424
3653
  ) seek
3425
3654
  `
3426
3655
  };
3427
- resolve2(lo);
3656
+ resolve3(lo);
3428
3657
  return new Promise(async (r) => finish = r);
3429
3658
  async function readable({
3430
3659
  highWaterMark = 2048 * 8,
@@ -3589,8 +3818,8 @@ function Postgres(a, b2) {
3589
3818
  }
3590
3819
  async function reserve() {
3591
3820
  const queue = queue_default();
3592
- const c = open.length ? open.shift() : await new Promise((resolve2, reject) => {
3593
- const query = { reserve: resolve2, reject };
3821
+ const c = open.length ? open.shift() : await new Promise((resolve3, reject) => {
3822
+ const query = { reserve: resolve3, reject };
3594
3823
  queries.push(query);
3595
3824
  closed.length && connect(closed.shift(), query);
3596
3825
  });
@@ -3627,9 +3856,9 @@ function Postgres(a, b2) {
3627
3856
  let uncaughtError, result;
3628
3857
  name && await sql2`savepoint ${sql2(name)}`;
3629
3858
  try {
3630
- result = await new Promise((resolve2, reject) => {
3859
+ result = await new Promise((resolve3, reject) => {
3631
3860
  const x = fn2(sql2);
3632
- Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve2, reject);
3861
+ Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve3, reject);
3633
3862
  });
3634
3863
  if (uncaughtError)
3635
3864
  throw uncaughtError;
@@ -3686,8 +3915,8 @@ function Postgres(a, b2) {
3686
3915
  return c.execute(query) ? move(c, busy) : move(c, full);
3687
3916
  }
3688
3917
  function cancel(query) {
3689
- return new Promise((resolve2, reject) => {
3690
- query.state ? query.active ? connection_default(options).cancel(query.state, resolve2, reject) : query.cancelled = { resolve: resolve2, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve2());
3918
+ return new Promise((resolve3, reject) => {
3919
+ query.state ? query.active ? connection_default(options).cancel(query.state, resolve3, reject) : query.cancelled = { resolve: resolve3, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve3());
3691
3920
  });
3692
3921
  }
3693
3922
  async function end({ timeout = null } = {}) {
@@ -3706,11 +3935,11 @@ function Postgres(a, b2) {
3706
3935
  async function close() {
3707
3936
  await Promise.all(connections.map((c) => c.end()));
3708
3937
  }
3709
- async function destroy(resolve2) {
3938
+ async function destroy(resolve3) {
3710
3939
  await Promise.all(connections.map((c) => c.terminate()));
3711
3940
  while (queries.length)
3712
3941
  queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
3713
- resolve2();
3942
+ resolve3();
3714
3943
  }
3715
3944
  function connect(c, query) {
3716
3945
  move(c, connecting);
@@ -3894,9 +4123,9 @@ __export(doctor_exports, {
3894
4123
  __testing: () => __testing,
3895
4124
  runDoctor: () => runDoctor
3896
4125
  });
3897
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
4126
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
3898
4127
  import { homedir as homedir4 } from "os";
3899
- import { join as join18 } from "path";
4128
+ import { join as join19 } from "path";
3900
4129
  function redact(name, value) {
3901
4130
  if (!value) return "(empty)";
3902
4131
  if (SECRET_VARS.has(name)) {
@@ -3907,14 +4136,14 @@ function redact(name, value) {
3907
4136
  }
3908
4137
  function findMcpJson() {
3909
4138
  const candidates = [
3910
- join18(process.cwd(), ".mcp.json"),
3911
- join18(homedir4(), ".claude", ".mcp.json"),
3912
- join18(homedir4(), ".mcp.json")
4139
+ join19(process.cwd(), ".mcp.json"),
4140
+ join19(homedir4(), ".claude", ".mcp.json"),
4141
+ join19(homedir4(), ".mcp.json")
3913
4142
  ];
3914
4143
  for (const path7 of candidates) {
3915
- if (!existsSync9(path7)) continue;
4144
+ if (!existsSync10(path7)) continue;
3916
4145
  try {
3917
- const raw = readFileSync10(path7, "utf-8");
4146
+ const raw = readFileSync11(path7, "utf-8");
3918
4147
  const parsed = JSON.parse(raw);
3919
4148
  const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
3920
4149
  if (!papiEntry) continue;
@@ -4188,17 +4417,17 @@ __export(reset_exports, {
4188
4417
  removePapiEntry: () => removePapiEntry,
4189
4418
  runReset: () => runReset
4190
4419
  });
4191
- import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
4420
+ import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
4192
4421
  import { homedir as homedir5 } from "os";
4193
- import { join as join19 } from "path";
4422
+ import { join as join20 } from "path";
4194
4423
  import { createInterface } from "readline/promises";
4195
4424
  function findResetTarget() {
4196
4425
  for (const path7 of CANDIDATE_PATHS()) {
4197
- if (!existsSync10(path7)) continue;
4426
+ if (!existsSync11(path7)) continue;
4198
4427
  let raw;
4199
4428
  let parsed;
4200
4429
  try {
4201
- raw = readFileSync11(path7, "utf-8");
4430
+ raw = readFileSync12(path7, "utf-8");
4202
4431
  parsed = JSON.parse(raw);
4203
4432
  } catch {
4204
4433
  continue;
@@ -4286,9 +4515,9 @@ var init_reset = __esm({
4286
4515
  "src/cli/reset.ts"() {
4287
4516
  "use strict";
4288
4517
  CANDIDATE_PATHS = () => [
4289
- join19(process.cwd(), ".mcp.json"),
4290
- join19(homedir5(), ".claude", ".mcp.json"),
4291
- join19(homedir5(), ".mcp.json")
4518
+ join20(process.cwd(), ".mcp.json"),
4519
+ join20(homedir5(), ".claude", ".mcp.json"),
4520
+ join20(homedir5(), ".mcp.json")
4292
4521
  ];
4293
4522
  }
4294
4523
  });
@@ -4299,9 +4528,9 @@ __export(audit_exports, {
4299
4528
  __testing: () => __testing2,
4300
4529
  runAudit: () => runAudit
4301
4530
  });
4302
- import { existsSync as existsSync11, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
4531
+ import { existsSync as existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
4303
4532
  import { homedir as homedir6 } from "os";
4304
- import { join as join20 } from "path";
4533
+ import { join as join21 } from "path";
4305
4534
  function safeListDirs(dir) {
4306
4535
  try {
4307
4536
  return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
@@ -4317,10 +4546,10 @@ function safeListFiles(dir, ext) {
4317
4546
  }
4318
4547
  }
4319
4548
  function readMcp(projectPath) {
4320
- const path7 = join20(projectPath, ".mcp.json");
4321
- if (!existsSync11(path7)) return { servers: [] };
4549
+ const path7 = join21(projectPath, ".mcp.json");
4550
+ if (!existsSync12(path7)) return { servers: [] };
4322
4551
  try {
4323
- const parsed = JSON.parse(readFileSync12(path7, "utf-8"));
4552
+ const parsed = JSON.parse(readFileSync13(path7, "utf-8"));
4324
4553
  const mcpServers = parsed.mcpServers ?? {};
4325
4554
  const servers = Object.keys(mcpServers);
4326
4555
  if (parsed.papi && !servers.includes("papi")) servers.push("papi");
@@ -4352,18 +4581,18 @@ function auditProjectSync(projectPath, name) {
4352
4581
  path: projectPath,
4353
4582
  papiProjectId,
4354
4583
  mcpServers: servers,
4355
- skills: safeListDirs(join20(projectPath, ".claude", "skills")),
4356
- agentSkills: safeListDirs(join20(projectPath, ".agents", "skills")),
4357
- agents: safeListFiles(join20(projectPath, ".claude", "agents"), ".md"),
4358
- hooks: safeListFiles(join20(projectPath, ".claude", "hooks"), ".sh")
4584
+ skills: safeListDirs(join21(projectPath, ".claude", "skills")),
4585
+ agentSkills: safeListDirs(join21(projectPath, ".agents", "skills")),
4586
+ agents: safeListFiles(join21(projectPath, ".claude", "agents"), ".md"),
4587
+ hooks: safeListFiles(join21(projectPath, ".claude", "hooks"), ".sh")
4359
4588
  };
4360
4589
  }
4361
4590
  function discoverProjects() {
4362
4591
  const out = [];
4363
4592
  for (const root of PROJECT_ROOTS) {
4364
4593
  for (const name of safeListDirs(root)) {
4365
- const path7 = join20(root, name);
4366
- if (existsSync11(join20(path7, ".mcp.json")) || existsSync11(join20(path7, ".claude"))) {
4594
+ const path7 = join21(root, name);
4595
+ if (existsSync12(join21(path7, ".mcp.json")) || existsSync12(join21(path7, ".claude"))) {
4367
4596
  out.push({ name, path: path7 });
4368
4597
  }
4369
4598
  }
@@ -4374,9 +4603,9 @@ function readGlobalSkills() {
4374
4603
  return safeListDirs(GLOBAL_SKILLS_DIR);
4375
4604
  }
4376
4605
  function readGlobalMcpServers() {
4377
- if (!existsSync11(GLOBAL_CLAUDE_JSON)) return [];
4606
+ if (!existsSync12(GLOBAL_CLAUDE_JSON)) return [];
4378
4607
  try {
4379
- const parsed = JSON.parse(readFileSync12(GLOBAL_CLAUDE_JSON, "utf-8"));
4608
+ const parsed = JSON.parse(readFileSync13(GLOBAL_CLAUDE_JSON, "utf-8"));
4380
4609
  const servers = parsed.mcpServers ?? {};
4381
4610
  return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
4382
4611
  } catch {
@@ -4528,9 +4757,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
4528
4757
  var init_audit = __esm({
4529
4758
  "src/cli/audit.ts"() {
4530
4759
  "use strict";
4531
- PROJECT_ROOTS = [join20(homedir6(), "Ai-App-Projects"), join20(homedir6(), "android-projects")];
4532
- GLOBAL_SKILLS_DIR = join20(homedir6(), ".claude", "skills");
4533
- GLOBAL_CLAUDE_JSON = join20(homedir6(), ".claude.json");
4760
+ PROJECT_ROOTS = [join21(homedir6(), "Ai-App-Projects"), join21(homedir6(), "android-projects")];
4761
+ GLOBAL_SKILLS_DIR = join21(homedir6(), ".claude", "skills");
4762
+ GLOBAL_CLAUDE_JSON = join21(homedir6(), ".claude.json");
4534
4763
  IDLE_WINDOW_DAYS = 30;
4535
4764
  GLOBALIZE_THRESHOLD = 3;
4536
4765
  __testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
@@ -4542,8 +4771,8 @@ var setup_exports = {};
4542
4771
  __export(setup_exports, {
4543
4772
  runSetup: () => runSetup
4544
4773
  });
4545
- import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync6, chmodSync as chmodSync2, statSync as statSync5 } from "fs";
4546
- import { join as join21 } from "path";
4774
+ import { existsSync as existsSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync6, chmodSync as chmodSync2, statSync as statSync6 } from "fs";
4775
+ import { join as join22 } from "path";
4547
4776
  function baseUrl() {
4548
4777
  const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
4549
4778
  if (fromEnv) return fromEnv.replace(/\/$/, "");
@@ -4572,14 +4801,14 @@ async function postJson(url, body) {
4572
4801
  return { status: res.status, data };
4573
4802
  }
4574
4803
  function sleep(ms) {
4575
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4804
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4576
4805
  }
4577
4806
  function writeMcpJson(opts) {
4578
- const path7 = join21(process.cwd(), ".mcp.json");
4807
+ const path7 = join22(process.cwd(), ".mcp.json");
4579
4808
  let parsed = {};
4580
- if (existsSync12(path7)) {
4809
+ if (existsSync13(path7)) {
4581
4810
  try {
4582
- parsed = JSON.parse(readFileSync13(path7, "utf-8"));
4811
+ parsed = JSON.parse(readFileSync14(path7, "utf-8"));
4583
4812
  } catch {
4584
4813
  throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
4585
4814
  }
@@ -4602,7 +4831,7 @@ function writeMcpJson(opts) {
4602
4831
  parsed.mcpServers = mcpServers;
4603
4832
  writeFileSync6(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
4604
4833
  try {
4605
- const mode = statSync5(path7).mode & 511;
4834
+ const mode = statSync6(path7).mode & 511;
4606
4835
  if (mode !== 384) chmodSync2(path7, 384);
4607
4836
  } catch {
4608
4837
  }
@@ -4710,9 +4939,9 @@ var init_setup = __esm({
4710
4939
  });
4711
4940
 
4712
4941
  // src/index.ts
4713
- import { readFileSync as readFileSync14 } from "fs";
4714
- import { dirname as dirname5, join as join22 } from "path";
4715
- import { fileURLToPath as fileURLToPath3 } from "url";
4942
+ import { readFileSync as readFileSync15 } from "fs";
4943
+ import { dirname as dirname6, join as join23 } from "path";
4944
+ import { fileURLToPath as fileURLToPath4 } from "url";
4716
4945
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4717
4946
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
4718
4947
  import {
@@ -4744,6 +4973,8 @@ var HELP_FOOTER_MD = `
4744
4973
  `;
4745
4974
 
4746
4975
  // src/config.ts
4976
+ var STRATEGY_REVIEW_OFFER_GAP = 5;
4977
+ var STRATEGY_REVIEW_BLOCK_GAP = 7;
4747
4978
  function loadConfig() {
4748
4979
  const projectArgIdx = process.argv.indexOf("--project");
4749
4980
  const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
@@ -7373,8 +7604,8 @@ function detectUserId() {
7373
7604
  }
7374
7605
  return void 0;
7375
7606
  }
7376
- var HOSTED_SUPABASE_URL = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
7377
- 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`;
7378
7609
  var PLACEHOLDER_PATTERNS = [
7379
7610
  "<YOUR_DATABASE_URL>",
7380
7611
  "your-database-url",
@@ -7709,18 +7940,36 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
7709
7940
  }
7710
7941
 
7711
7942
  // src/server.ts
7712
- import { readFileSync as readFileSync9 } from "fs";
7943
+ import { readFileSync as readFileSync10 } from "fs";
7713
7944
  import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
7714
- import { join as join17, dirname as dirname4 } from "path";
7715
- import { fileURLToPath as fileURLToPath2 } from "url";
7945
+ import { join as join18, dirname as dirname5 } from "path";
7946
+ import { fileURLToPath as fileURLToPath3 } from "url";
7716
7947
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7717
7948
  import {
7718
7949
  CallToolRequestSchema,
7719
7950
  ListToolsRequestSchema,
7720
7951
  ListPromptsRequestSchema,
7721
- GetPromptRequestSchema
7952
+ GetPromptRequestSchema,
7953
+ ListResourcesRequestSchema,
7954
+ ListResourceTemplatesRequestSchema,
7955
+ ReadResourceRequestSchema
7722
7956
  } from "@modelcontextprotocol/sdk/types.js";
7723
7957
 
7958
+ // src/universal-frame.ts
7959
+ var UNIVERSAL_FRAME = `PAPI gives this project a structured plan \u2192 build \u2192 review cycle, persisted across sessions. Follow it:
7960
+
7961
+ 1. ORIENT FIRST. At the start of every session, call \`orient\` (or \`papi\`) before anything else \u2014 it returns the current cycle, what's in flight, and the recommended next action. Re-run it after any context compression.
7962
+
7963
+ 2. THE CYCLE, IN ORDER: \`plan\` (once per cycle) \u2192 \`build_list\` (pick a task) \u2192 \`build_execute <task>\` to start \u2192 implement the task from its BUILD HANDOFF \u2192 \`build_execute\` again to complete with a build report \u2192 \`review_submit\` \u2192 \`release\` when every cycle task is done.
7964
+
7965
+ 3. STATUS DISCIPLINE. Check a task's status before acting. "In Review" = already built \u2014 never re-build it; submit a review instead. "In Progress" = a build started \u2014 check the existing branch before writing new code. "Backlog" = not started, but check for an existing feature branch first.
7966
+
7967
+ 4. STAY IN SCOPE. Build exactly what the BUILD HANDOFF specifies. Mechanical steps (branch creation, commits, status updates) are automatic \u2014 only "what to build" needs the user's confirmation.
7968
+
7969
+ 5. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
7970
+
7971
+ PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.`;
7972
+
7724
7973
  // src/lib/response.ts
7725
7974
  function textResponse(text, usage) {
7726
7975
  const result = {
@@ -7736,7 +7985,7 @@ function errorResponse(message) {
7736
7985
  }
7737
7986
 
7738
7987
  // src/services/plan.ts
7739
- import { createHash, randomUUID as randomUUID7 } from "crypto";
7988
+ import { createHash, randomUUID as randomUUID8 } from "crypto";
7740
7989
  import { readFile as readFile2 } from "fs/promises";
7741
7990
  import path4 from "path";
7742
7991
 
@@ -11041,7 +11290,7 @@ ${lines.join("\n")}`;
11041
11290
  console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
11042
11291
  const gap = health.cyclesSinceLastStrategyReview;
11043
11292
  const lastReviewCycle = health.totalCycles - gap;
11044
- const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Due now.`;
11293
+ const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < STRATEGY_REVIEW_OFFER_GAP ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + STRATEGY_REVIEW_OFFER_GAP}.` : gap < STRATEGY_REVIEW_BLOCK_GAP ? `\u25CB Strategy review available \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Offered now (optional); planning hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Planning is blocked until it runs (or \`force: true\`).`;
11045
11294
  let ctx2 = {
11046
11295
  mode,
11047
11296
  cycleNumber: health.totalCycles,
@@ -11231,7 +11480,7 @@ ${logLines}`);
11231
11480
  const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
11232
11481
  const gapFull = health.cyclesSinceLastStrategyReview;
11233
11482
  const lastReviewCycleFull = health.totalCycles - gapFull;
11234
- const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Due now.`;
11483
+ const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < STRATEGY_REVIEW_OFFER_GAP ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + STRATEGY_REVIEW_OFFER_GAP}.` : gapFull < STRATEGY_REVIEW_BLOCK_GAP ? `\u25CB Strategy review available \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Offered now (optional); planning hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Planning is blocked until it runs (or \`force: true\`).`;
11235
11484
  let ctx = {
11236
11485
  mode,
11237
11486
  cycleNumber: health.totalCycles,
@@ -11362,7 +11611,7 @@ ${cleanContent}`;
11362
11611
  const payload = {
11363
11612
  cycleNumber: newCycleNumber,
11364
11613
  cycleLog: {
11365
- uuid: randomUUID7(),
11614
+ uuid: randomUUID8(),
11366
11615
  cycleNumber: newCycleNumber,
11367
11616
  title: cleanTitle,
11368
11617
  content: logBlock,
@@ -11469,7 +11718,7 @@ ${cleanContent}`;
11469
11718
  const cycleTaskCount = legacyHandoffs.length;
11470
11719
  const cycleEffortPoints = legacyHandoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
11471
11720
  const cycleLogPromise = adapter2.writeCycleLogEntry({
11472
- uuid: randomUUID7(),
11721
+ uuid: randomUUID8(),
11473
11722
  cycleNumber: newCycleNumber,
11474
11723
  title: cleanTitle,
11475
11724
  content: logBlock,
@@ -11508,7 +11757,7 @@ ${cleanContent}`;
11508
11757
  visibility = task.visibility;
11509
11758
  }
11510
11759
  const created = await adapter2.createTask({
11511
- uuid: randomUUID7(),
11760
+ uuid: randomUUID8(),
11512
11761
  displayId: "",
11513
11762
  title: task.title,
11514
11763
  status: task.status || "Backlog",
@@ -11835,15 +12084,15 @@ Run \`review_submit\` to clear them, or pass \`force: true\` to bypass this bloc
11835
12084
  console.error(`[plan] ${note}`);
11836
12085
  }
11837
12086
  const gap = health.cyclesSinceLastStrategyReview;
11838
- if (gap >= 5 && !force) {
12087
+ if (gap >= STRATEGY_REVIEW_BLOCK_GAP && !force) {
11839
12088
  const lastReviewCycle = cycleNumber - gap;
11840
12089
  throw new Error(
11841
- `Strategy Review gate \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). A strategy review is required every 5 cycles before planning can continue.
12090
+ `Strategy Review gate \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). A strategy review is required at least every ${STRATEGY_REVIEW_BLOCK_GAP} cycles before planning can continue.
11842
12091
 
11843
12092
  Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
11844
12093
  );
11845
12094
  }
11846
- if (gap >= 5 && force) {
12095
+ if (gap >= STRATEGY_REVIEW_BLOCK_GAP && force) {
11847
12096
  const lastReviewCycle = cycleNumber - gap;
11848
12097
  strategyReviewWarning = `> \u26A0\uFE0F **Strategy Review gate bypassed** (force: true) \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). Run \`strategy_review\` after this cycle.
11849
12098
 
@@ -12416,8 +12665,6 @@ function getState(callerKey) {
12416
12665
  function callerKeyFromConfig(config2) {
12417
12666
  return config2.projectId ?? config2.userId ?? void 0;
12418
12667
  }
12419
- var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
12420
- var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
12421
12668
  var REVIEW_LIST_GUARD_WINDOW_MS = 15 * 60 * 1e3;
12422
12669
  var FAILURE_WINDOW_MS = 30 * 60 * 1e3;
12423
12670
  var FAILURE_RATE_THRESHOLD = 8;
@@ -12457,37 +12704,20 @@ function getProjectConnectionBanner(projectName, projectSlug) {
12457
12704
  function detectContextDegradation(now = Date.now(), callerKey) {
12458
12705
  const state = getState(callerKey);
12459
12706
  if (state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD) {
12460
- return `Context degradation (clash): ${state.consecutiveFailures} tool calls failed in a row \u2014 the session may be stuck on a contradiction. Consider a fresh window after this task.`;
12707
+ return `${state.consecutiveFailures} tool calls failed in a row \u2014 the session may be stuck on a contradiction. Re-read the last error and re-check your assumptions before continuing.`;
12461
12708
  }
12462
12709
  const cutoff = now - FAILURE_WINDOW_MS;
12463
12710
  const recentFailures = state.failureTimestamps.filter((t) => t >= cutoff).length;
12464
12711
  if (recentFailures >= FAILURE_RATE_THRESHOLD) {
12465
12712
  const mins = Math.round(FAILURE_WINDOW_MS / 6e4);
12466
- return `Context degradation (distraction/confusion): ${recentFailures} tool failures in the last ${mins}min. A fresh window often clears this faster than pushing on.`;
12713
+ return `${recentFailures} tool failures in the last ${mins}min \u2014 something's off. Re-read the last error and re-check your assumptions before continuing.`;
12467
12714
  }
12468
12715
  return null;
12469
12716
  }
12470
12717
  async function buildSessionGuidance(callerKey) {
12471
- const state = getState(callerKey);
12472
12718
  const signals = [];
12473
12719
  const degradation = detectContextDegradation(Date.now(), callerKey);
12474
12720
  if (degradation) signals.push(degradation);
12475
- if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
12476
- signals.push(
12477
- `${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
12478
- );
12479
- }
12480
- if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
12481
- const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
12482
- signals.push(
12483
- `${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
12484
- );
12485
- }
12486
- if (state.releaseSinceLastOrient) {
12487
- signals.push(
12488
- "Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
12489
- );
12490
- }
12491
12721
  return signals.slice(0, 3);
12492
12722
  }
12493
12723
 
@@ -12855,11 +13085,11 @@ ${result.userMessage}
12855
13085
  }
12856
13086
 
12857
13087
  // src/services/strategy.ts
12858
- import { randomUUID as randomUUID9, createHash as createHash2 } from "crypto";
13088
+ import { randomUUID as randomUUID10, createHash as createHash2 } from "crypto";
12859
13089
  import { execFileSync as execFileSync2 } from "child_process";
12860
13090
  import { existsSync, readdirSync, statSync } from "fs";
12861
- import { join as join2 } from "path";
12862
- import { homedir } from "os";
13091
+ import { join as join3 } from "path";
13092
+ import { homedir as homedir2 } from "os";
12863
13093
 
12864
13094
  // src/lib/hosted-mode.ts
12865
13095
  function isHostedTransport() {
@@ -12870,7 +13100,7 @@ function hasLocalWorkspace() {
12870
13100
  }
12871
13101
 
12872
13102
  // src/services/idea.ts
12873
- import { randomUUID as randomUUID8 } from "crypto";
13103
+ import { randomUUID as randomUUID9 } from "crypto";
12874
13104
  var OWNER_ACTION_PATTERNS = [
12875
13105
  // Verb match — the canonical owner-action verbs (spec C275 §2)
12876
13106
  /\b(send|reply|email|call|phone|text|message|dm)\b/i,
@@ -13208,7 +13438,7 @@ ${lines.join("\n")}
13208
13438
  }
13209
13439
  const inherited = await resolveVisibilityFromDocs(adapter2, [input.docRef]);
13210
13440
  const task = await adapter2.createTask({
13211
- uuid: randomUUID8(),
13441
+ uuid: randomUUID9(),
13212
13442
  displayId: "",
13213
13443
  title: taskTitle,
13214
13444
  status: "Backlog",
@@ -13396,6 +13626,23 @@ function generateValueReport(snapshots) {
13396
13626
 
13397
13627
  // src/services/strategy.ts
13398
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
+ }
13399
13646
  async function extractRecommendations(data, cycleNumber, adapter2) {
13400
13647
  const recs = [];
13401
13648
  if (data.activeDecisionUpdates?.length) {
@@ -13459,6 +13706,32 @@ async function extractRecommendations(data, cycleNumber, adapter2) {
13459
13706
  }
13460
13707
  }
13461
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
+ }
13462
13735
  return recs;
13463
13736
  }
13464
13737
  function classifyRecommendation(text) {
@@ -13812,11 +14085,11 @@ ${lines.join("\n")}`;
13812
14085
  }
13813
14086
  let recentPlansText;
13814
14087
  try {
13815
- const plansDir = join2(homedir(), ".claude", "plans");
14088
+ const plansDir = join3(homedir2(), ".claude", "plans");
13816
14089
  if (existsSync(plansDir)) {
13817
14090
  const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
13818
14091
  const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
13819
- const fullPath = join2(plansDir, f);
14092
+ const fullPath = join3(plansDir, f);
13820
14093
  const stat4 = statSync(fullPath);
13821
14094
  return { name: f, modified: stat4.mtime, size: stat4.size };
13822
14095
  }).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
@@ -13833,7 +14106,7 @@ ${lines.join("\n")}`;
13833
14106
  }
13834
14107
  let unregisteredDocsText;
13835
14108
  try {
13836
- const docsDir = join2(projectRoot, "docs");
14109
+ const docsDir = join3(projectRoot, "docs");
13837
14110
  if (hasLocalWorkspace() && existsSync(docsDir)) {
13838
14111
  const registeredPaths = new Set(
13839
14112
  (registeredDocs ?? []).map((d) => d.path).filter(Boolean)
@@ -13841,7 +14114,7 @@ ${lines.join("\n")}`;
13841
14114
  const allDocFiles = [];
13842
14115
  const scanDir = (dir, prefix) => {
13843
14116
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
13844
- if (entry.isDirectory()) scanDir(join2(dir, entry.name), `${prefix}${entry.name}/`);
14117
+ if (entry.isDirectory()) scanDir(join3(dir, entry.name), `${prefix}${entry.name}/`);
13845
14118
  else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
13846
14119
  }
13847
14120
  };
@@ -13977,6 +14250,38 @@ ${lines.join("\n")}`;
13977
14250
  docActionStaleness: docActionStalenessText,
13978
14251
  pendingAgendaTopics: pendingAgendaText
13979
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
+ }
13980
14285
  const BUDGET_SOFT2 = 5e4;
13981
14286
  const BUDGET_HARD2 = 6e4;
13982
14287
  const compressionSteps = [];
@@ -14670,7 +14975,7 @@ async function processStrategyChangeOutput(adapter2, rawOutput, cycleNumber) {
14670
14975
 
14671
14976
  ${cleanContent}`;
14672
14977
  await adapter2.writeCycleLogEntry({
14673
- uuid: randomUUID9(),
14978
+ uuid: randomUUID10(),
14674
14979
  cycleNumber,
14675
14980
  title: cleanTitle,
14676
14981
  content: logBlock
@@ -15906,16 +16211,16 @@ ${existing}` : entry;
15906
16211
  }
15907
16212
 
15908
16213
  // src/services/setup.ts
15909
- import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2 } from "fs/promises";
15910
- import { join as join5, basename, extname, dirname as dirname2 } from "path";
16214
+ import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
16215
+ import { join as join8, basename, extname, dirname as dirname3 } from "path";
15911
16216
  import { execFileSync as execFileSync3 } from "child_process";
15912
16217
 
15913
16218
  // src/lib/detect-codebase.ts
15914
16219
  import { existsSync as existsSync2 } from "fs";
15915
16220
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
15916
- import { join as join3 } from "path";
16221
+ import { join as join4 } from "path";
15917
16222
  function detectCodebaseType(projectRoot) {
15918
- if (existsSync2(join3(projectRoot, ".git"))) {
16223
+ if (existsSync2(join4(projectRoot, ".git"))) {
15919
16224
  return "existing_codebase";
15920
16225
  }
15921
16226
  const manifests = [
@@ -15929,7 +16234,7 @@ function detectCodebaseType(projectRoot) {
15929
16234
  "CMakeLists.txt"
15930
16235
  ];
15931
16236
  for (const manifest of manifests) {
15932
- if (existsSync2(join3(projectRoot, manifest))) {
16237
+ if (existsSync2(join4(projectRoot, manifest))) {
15933
16238
  return "existing_codebase";
15934
16239
  }
15935
16240
  }
@@ -15937,7 +16242,7 @@ function detectCodebaseType(projectRoot) {
15937
16242
  const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
15938
16243
  const fileCount = entries.filter((f) => {
15939
16244
  try {
15940
- return statSync2(join3(projectRoot, f)).isFile();
16245
+ return statSync2(join4(projectRoot, f)).isFile();
15941
16246
  } catch {
15942
16247
  return false;
15943
16248
  }
@@ -15949,18 +16254,18 @@ function detectCodebaseType(projectRoot) {
15949
16254
  }
15950
16255
 
15951
16256
  // src/lib/agents-bundle.ts
15952
- import { readFileSync, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
15953
- 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";
15954
16259
  import { fileURLToPath } from "url";
15955
- var PROJECT_BUNDLE_REL = join4(".agents", "skills", "papi-cycle");
16260
+ var PROJECT_BUNDLE_REL = join5(".agents", "skills", "papi-cycle");
15956
16261
  function bundleDestRel(rel) {
15957
- return rel === "AGENTS.md" ? "AGENTS.md" : join4(PROJECT_BUNDLE_REL, rel);
16262
+ return rel === "AGENTS.md" ? "AGENTS.md" : join5(PROJECT_BUNDLE_REL, rel);
15958
16263
  }
15959
16264
  function resolveBundleDir() {
15960
16265
  let dir = dirname(fileURLToPath(import.meta.url));
15961
16266
  for (let i = 0; i < 5; i++) {
15962
- const candidate = join4(dir, "skills", "papi-cycle");
15963
- 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;
15964
16269
  const parent = resolve(dir, "..");
15965
16270
  if (parent === dir) break;
15966
16271
  dir = parent;
@@ -15972,10 +16277,10 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
15972
16277
  const files = [];
15973
16278
  const walk = (abs, rel) => {
15974
16279
  for (const entry of readdirSync3(abs, { withFileTypes: true })) {
15975
- const childAbs = join4(abs, entry.name);
15976
- 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;
15977
16282
  if (entry.isDirectory()) walk(childAbs, childRel);
15978
- 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") });
15979
16284
  }
15980
16285
  };
15981
16286
  walk(bundleDir, "");
@@ -15984,7 +16289,7 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
15984
16289
  function planBundleInstall(projectRoot, projectName, opts = {}) {
15985
16290
  const out = {};
15986
16291
  for (const f of readBundleFiles()) {
15987
- const dest = join4(projectRoot, bundleDestRel(f.rel));
16292
+ const dest = join5(projectRoot, bundleDestRel(f.rel));
15988
16293
  if (opts.skipExisting && existsSync3(dest) && statSync3(dest).isFile()) continue;
15989
16294
  const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
15990
16295
  out[dest] = content;
@@ -15992,44 +16297,197 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
15992
16297
  return out;
15993
16298
  }
15994
16299
 
15995
- // src/templates.ts
15996
- var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
15997
-
15998
- > The persistent record of WHY decisions were made. Read by the Cycle Planner at the start of every cycle.
15999
-
16000
- ---
16001
-
16002
- ## Cycle Health
16003
-
16004
- | Metric | Value |
16005
- |--------|-------|
16006
- | Total cycles | 0 |
16007
- | Cycles since last Strategy Review | 0 |
16008
- | Strategy Review due | Cycle 5 |
16009
- | Board health | 0 tasks |
16010
- | Strategic direction | Not yet established |
16011
- | Last Full Mode | 0 |
16012
-
16013
- ---
16014
-
16015
- ## North Star
16016
-
16017
- **{{project_name}}** \u2014 {{description}}
16018
-
16019
- **Success =** *Define after first planning cycle.*
16020
-
16021
- **Key metric:** *Define after first planning cycle.*
16022
-
16023
- ---
16024
-
16025
- ## Deferred / Parking Lot
16026
-
16027
- *Nothing deferred yet.*
16028
- `;
16029
- var ACTIVE_DECISIONS_TEMPLATE = `# Active Decisions
16300
+ // src/lib/design-bundle.ts
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";
16303
+ import { fileURLToPath as fileURLToPath2 } from "url";
16304
+ var DESIGN_ASSETS = [
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 }
16308
+ ];
16309
+ var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
16310
+ function resolveDesignAssetsDir() {
16311
+ let dir = dirname2(fileURLToPath2(import.meta.url));
16312
+ for (let i = 0; i < 5; i++) {
16313
+ const candidate = join6(dir, "design-assets");
16314
+ if (existsSync4(join6(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
16315
+ const parent = resolve2(dir, "..");
16316
+ if (parent === dir) break;
16317
+ dir = parent;
16318
+ }
16319
+ return void 0;
16320
+ }
16321
+ function planDesignInstall(projectRoot, opts = {}) {
16322
+ const assetsDir = resolveDesignAssetsDir();
16323
+ if (!assetsDir) return [];
16324
+ const out = [];
16325
+ for (const asset of DESIGN_ASSETS) {
16326
+ const srcAbs = join6(assetsDir, asset.srcRel);
16327
+ if (!existsSync4(srcAbs)) continue;
16328
+ const dest = projectRoot ? join6(projectRoot, asset.destRel) : asset.destRel;
16329
+ if (opts.skipExisting && projectRoot && existsSync4(dest) && statSync4(dest).isFile()) continue;
16330
+ out.push({ dest, content: readFileSync3(srcAbs, "utf8"), executable: asset.executable });
16331
+ }
16332
+ return out;
16333
+ }
16030
16334
 
16031
- *No decisions yet. Active Decisions are created during planning cycles.*
16032
- `;
16335
+ // src/lib/skill-detection.ts
16336
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync5 } from "fs";
16337
+ import { join as join7 } from "path";
16338
+ function readPackageJson(projectRoot) {
16339
+ const path7 = join7(projectRoot, "package.json");
16340
+ if (!existsSync5(path7)) return null;
16341
+ try {
16342
+ const raw = readFileSync4(path7, "utf-8");
16343
+ return JSON.parse(raw);
16344
+ } catch {
16345
+ return null;
16346
+ }
16347
+ }
16348
+ function allDeps(pkg) {
16349
+ if (!pkg) return {};
16350
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
16351
+ }
16352
+ function hasDependencyMatching(deps, pattern) {
16353
+ for (const name of Object.keys(deps)) {
16354
+ if (pattern.test(name)) return true;
16355
+ }
16356
+ return false;
16357
+ }
16358
+ var FRONTEND_DEP_PATTERN = /^(react|react-dom|next|vue|svelte|preact|solid-js|astro|nuxt|tailwindcss)$|^@(sveltejs|angular|remix-run)\//;
16359
+ function detectsFrontendStack(projectRoot) {
16360
+ return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
16361
+ }
16362
+ function hasGitHubWorkflows(projectRoot) {
16363
+ const dir = join7(projectRoot, ".github", "workflows");
16364
+ if (!existsSync5(dir)) return false;
16365
+ try {
16366
+ const entries = readdirSync4(dir);
16367
+ return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
16368
+ } catch {
16369
+ return false;
16370
+ }
16371
+ }
16372
+ function envExampleMentionsStaging(projectRoot) {
16373
+ const path7 = join7(projectRoot, ".env.example");
16374
+ if (!existsSync5(path7)) return false;
16375
+ try {
16376
+ const raw = readFileSync4(path7, "utf-8");
16377
+ return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
16378
+ } catch {
16379
+ return false;
16380
+ }
16381
+ }
16382
+ function hasVercelConfig(projectRoot) {
16383
+ if (existsSync5(join7(projectRoot, "vercel.json"))) return true;
16384
+ const vercelDir = join7(projectRoot, ".vercel");
16385
+ if (!existsSync5(vercelDir)) return false;
16386
+ try {
16387
+ return statSync5(vercelDir).isDirectory();
16388
+ } catch {
16389
+ return false;
16390
+ }
16391
+ }
16392
+ function scanForSkillSignals(projectRoot) {
16393
+ const proposals = [];
16394
+ const pkg = readPackageJson(projectRoot);
16395
+ const deps = allDeps(pkg);
16396
+ if (hasGitHubWorkflows(projectRoot)) {
16397
+ proposals.push({
16398
+ id: "gh-actions-debug",
16399
+ name: "GitHub Actions debugger",
16400
+ rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
16401
+ hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
16402
+ });
16403
+ }
16404
+ if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
16405
+ proposals.push({
16406
+ id: "error-tracking",
16407
+ name: "Error tracking helper",
16408
+ rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
16409
+ hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
16410
+ });
16411
+ }
16412
+ if (hasVercelConfig(projectRoot)) {
16413
+ proposals.push({
16414
+ id: "read-vercel-logs",
16415
+ name: "Vercel deploy logs",
16416
+ rationale: "Detected vercel.json or .vercel/ \u2014 a skill for pulling deploy + runtime logs from Vercel directly into Claude Code helps when builds fail or runtime errors land.",
16417
+ hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
16418
+ });
16419
+ }
16420
+ if (envExampleMentionsStaging(projectRoot)) {
16421
+ proposals.push({
16422
+ id: "staging-environment",
16423
+ name: "Staging-environment helper",
16424
+ rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
16425
+ hint: 'Search Claude Code skill registry for "staging" or define your own.'
16426
+ });
16427
+ }
16428
+ if (detectsFrontendStack(projectRoot)) {
16429
+ proposals.push({
16430
+ id: "frontend-design-engineer",
16431
+ name: "Frontend design engineer (agent + critique)",
16432
+ rationale: "Detected a frontend stack (React/Next/Vue/Svelte/Tailwind) \u2014 PAPI ships a frontend-design-engineer sub-agent + a design-critique skill that isolate the visual layer, run a falsifiable anti-slop critique before and after building, and verify in-browser. They read your own DESIGN.md/PRODUCT.md for brand, so the output is yours, not generic.",
16433
+ hint: "Re-run `setup` to install them into .claude/, or copy from the PAPI server package under design-assets/. A build-time guard hook is included (opt-in)."
16434
+ });
16435
+ }
16436
+ return proposals;
16437
+ }
16438
+ function formatSkillProposals(proposals) {
16439
+ if (proposals.length === 0) return "";
16440
+ const lines = ["", "## Skill proposals", "_PAPI scanned your project once and noticed tooling that often pairs with a Claude Code skill. Each proposal is one-shot \u2014 they won't resurface._"];
16441
+ for (const p of proposals) {
16442
+ lines.push("");
16443
+ lines.push(`### ${p.name}`);
16444
+ lines.push(p.rationale);
16445
+ if (p.hint) {
16446
+ lines.push(`_${p.hint}_`);
16447
+ }
16448
+ lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
16449
+ }
16450
+ return "\n" + lines.join("\n");
16451
+ }
16452
+
16453
+ // src/templates.ts
16454
+ var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
16455
+
16456
+ > The persistent record of WHY decisions were made. Read by the Cycle Planner at the start of every cycle.
16457
+
16458
+ ---
16459
+
16460
+ ## Cycle Health
16461
+
16462
+ | Metric | Value |
16463
+ |--------|-------|
16464
+ | Total cycles | 0 |
16465
+ | Cycles since last Strategy Review | 0 |
16466
+ | Strategy Review due | Cycle 5 |
16467
+ | Board health | 0 tasks |
16468
+ | Strategic direction | Not yet established |
16469
+ | Last Full Mode | 0 |
16470
+
16471
+ ---
16472
+
16473
+ ## North Star
16474
+
16475
+ **{{project_name}}** \u2014 {{description}}
16476
+
16477
+ **Success =** *Define after first planning cycle.*
16478
+
16479
+ **Key metric:** *Define after first planning cycle.*
16480
+
16481
+ ---
16482
+
16483
+ ## Deferred / Parking Lot
16484
+
16485
+ *Nothing deferred yet.*
16486
+ `;
16487
+ var ACTIVE_DECISIONS_TEMPLATE = `# Active Decisions
16488
+
16489
+ *No decisions yet. Active Decisions are created during planning cycles.*
16490
+ `;
16033
16491
  var SPRINT_LOG_TEMPLATE = `## Cycle Log
16034
16492
 
16035
16493
  *No cycles yet. Run \`plan\` to start your first planning cycle.*
@@ -16412,7 +16870,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16412
16870
  await mkdir(config2.papiDir, { recursive: true });
16413
16871
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
16414
16872
  const content = substitute(template, vars);
16415
- await writeFile2(join5(config2.papiDir, filename), content, "utf-8");
16873
+ await writeFile2(join8(config2.papiDir, filename), content, "utf-8");
16416
16874
  }
16417
16875
  }
16418
16876
  } else {
@@ -16430,13 +16888,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16430
16888
  const useCollector = config2.adapterType === "proxy";
16431
16889
  const docsRel = "docs";
16432
16890
  const commandsRel = ".claude/commands";
16433
- const commandsDir = useCollector ? commandsRel : join5(config2.projectRoot, ".claude", "commands");
16434
- const docsDir = useCollector ? docsRel : join5(config2.projectRoot, "docs");
16891
+ const commandsDir = useCollector ? commandsRel : join8(config2.projectRoot, ".claude", "commands");
16892
+ const docsDir = useCollector ? docsRel : join8(config2.projectRoot, "docs");
16435
16893
  if (!useCollector) {
16436
16894
  await mkdir(commandsDir, { recursive: true });
16437
16895
  await mkdir(docsDir, { recursive: true });
16438
16896
  }
16439
- const claudeMdPath = useCollector ? "CLAUDE.md" : join5(config2.projectRoot, "CLAUDE.md");
16897
+ const claudeMdPath = useCollector ? "CLAUDE.md" : join8(config2.projectRoot, "CLAUDE.md");
16440
16898
  let claudeMdExists = false;
16441
16899
  if (!useCollector) {
16442
16900
  try {
@@ -16445,7 +16903,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16445
16903
  } catch {
16446
16904
  }
16447
16905
  }
16448
- const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join5(docsDir, "INDEX.md");
16906
+ const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join8(docsDir, "INDEX.md");
16449
16907
  let docsIndexExists = false;
16450
16908
  if (!useCollector) {
16451
16909
  try {
@@ -16455,9 +16913,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16455
16913
  }
16456
16914
  }
16457
16915
  const scaffoldFiles = {
16458
- [useCollector ? `${commandsRel}/papi-audit.md` : join5(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
16459
- [useCollector ? `${commandsRel}/test.md` : join5(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
16460
- [useCollector ? `${docsRel}/README.md` : join5(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)
16461
16919
  };
16462
16920
  if (!docsIndexExists) {
16463
16921
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -16477,14 +16935,14 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16477
16935
  const bundleRoot = useCollector ? "" : config2.projectRoot;
16478
16936
  for (const [dest, content] of Object.entries(planBundleInstall(bundleRoot, input.projectName, { skipExisting: true }))) {
16479
16937
  if (!useCollector) {
16480
- await mkdir(dirname2(dest), { recursive: true });
16938
+ await mkdir(dirname3(dest), { recursive: true });
16481
16939
  }
16482
16940
  scaffoldFiles[dest] = content;
16483
16941
  }
16484
16942
  if (useCollector) {
16485
16943
  scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
16486
16944
  } else {
16487
- const cursorDir = join5(config2.projectRoot, ".cursor");
16945
+ const cursorDir = join8(config2.projectRoot, ".cursor");
16488
16946
  let cursorDetected = false;
16489
16947
  try {
16490
16948
  await access2(cursorDir);
@@ -16492,8 +16950,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16492
16950
  } catch {
16493
16951
  }
16494
16952
  if (cursorDetected) {
16495
- const cursorRulesDir = join5(cursorDir, "rules");
16496
- const cursorRulesPath = join5(cursorRulesDir, "papi.mdc");
16953
+ const cursorRulesDir = join8(cursorDir, "rules");
16954
+ const cursorRulesPath = join8(cursorRulesDir, "papi.mdc");
16497
16955
  await mkdir(cursorRulesDir, { recursive: true });
16498
16956
  try {
16499
16957
  await access2(cursorRulesPath);
@@ -16511,6 +16969,19 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16511
16969
  await writeFile2(filepath, content, "utf-8");
16512
16970
  }
16513
16971
  }
16972
+ if (!useCollector && detectsFrontendStack(config2.projectRoot)) {
16973
+ for (const entry of planDesignInstall(config2.projectRoot, { skipExisting: true })) {
16974
+ await mkdir(dirname3(entry.dest), { recursive: true });
16975
+ await writeFile2(entry.dest, entry.content, "utf-8");
16976
+ if (entry.executable) {
16977
+ try {
16978
+ await chmod(entry.dest, 493);
16979
+ } catch {
16980
+ }
16981
+ }
16982
+ }
16983
+ await ensureDesignHookRegistered(config2.projectRoot);
16984
+ }
16514
16985
  if (!isPg) {
16515
16986
  await adapter2.writePhases([{
16516
16987
  id: "phase-0",
@@ -16535,7 +17006,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16535
17006
  }
16536
17007
  var PAPI_PERMISSION = "mcp__papi__*";
16537
17008
  async function ensurePapiPermission(projectRoot) {
16538
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
17009
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
16539
17010
  try {
16540
17011
  let settings = {};
16541
17012
  try {
@@ -16554,7 +17025,43 @@ async function ensurePapiPermission(projectRoot) {
16554
17025
  if (!allow.includes(PAPI_PERMISSION)) {
16555
17026
  allow.push(PAPI_PERMISSION);
16556
17027
  }
16557
- await mkdir(join5(projectRoot, ".claude"), { recursive: true });
17028
+ await mkdir(join8(projectRoot, ".claude"), { recursive: true });
17029
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
17030
+ } catch {
17031
+ }
17032
+ }
17033
+ async function ensureDesignHookRegistered(projectRoot) {
17034
+ const settingsPath = join8(projectRoot, ".claude", "settings.json");
17035
+ try {
17036
+ let settings = {};
17037
+ try {
17038
+ settings = JSON.parse(await readFile4(settingsPath, "utf-8"));
17039
+ } catch {
17040
+ }
17041
+ if (!settings.hooks || typeof settings.hooks !== "object") {
17042
+ settings.hooks = {};
17043
+ }
17044
+ const hooks = settings.hooks;
17045
+ if (!Array.isArray(hooks.PreToolUse)) {
17046
+ hooks.PreToolUse = [];
17047
+ }
17048
+ const pre = hooks.PreToolUse;
17049
+ for (const matcher of ["Edit", "Write"]) {
17050
+ let entry = pre.find((e) => e && e["matcher"] === matcher);
17051
+ if (!entry) {
17052
+ entry = { matcher, hooks: [] };
17053
+ pre.push(entry);
17054
+ }
17055
+ if (!Array.isArray(entry["hooks"])) {
17056
+ entry["hooks"] = [];
17057
+ }
17058
+ const chain = entry["hooks"];
17059
+ const already = chain.some((h) => h && h["command"] === DESIGN_HOOK_COMMAND);
17060
+ if (!already) {
17061
+ chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
17062
+ }
17063
+ }
17064
+ await mkdir(join8(projectRoot, ".claude"), { recursive: true });
16558
17065
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
16559
17066
  } catch {
16560
17067
  }
@@ -16658,7 +17165,7 @@ ${conventionsText.trim()}
16658
17165
  );
16659
17166
  } else {
16660
17167
  try {
16661
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
17168
+ const claudeMdPath = join8(config2.projectRoot, "CLAUDE.md");
16662
17169
  const existing = await readFile4(claudeMdPath, "utf-8");
16663
17170
  if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
16664
17171
  warnings.push(
@@ -16745,13 +17252,13 @@ async function scanCodebase(projectRoot) {
16745
17252
  }
16746
17253
  let packageJson;
16747
17254
  try {
16748
- const content = await readFile4(join5(projectRoot, "package.json"), "utf-8");
17255
+ const content = await readFile4(join8(projectRoot, "package.json"), "utf-8");
16749
17256
  packageJson = JSON.parse(content);
16750
17257
  } catch {
16751
17258
  }
16752
17259
  let readme;
16753
17260
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
16754
- const content = await safeReadFile(join5(projectRoot, name), 5e3);
17261
+ const content = await safeReadFile(join8(projectRoot, name), 5e3);
16755
17262
  if (content) {
16756
17263
  readme = content;
16757
17264
  break;
@@ -16761,7 +17268,7 @@ async function scanCodebase(projectRoot) {
16761
17268
  let totalFiles = topLevelFiles.length;
16762
17269
  for (const dir of topLevelDirs) {
16763
17270
  try {
16764
- const entries = await readdir(join5(projectRoot, dir), { withFileTypes: true });
17271
+ const entries = await readdir(join8(projectRoot, dir), { withFileTypes: true });
16765
17272
  const files = entries.filter((e) => e.isFile());
16766
17273
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
16767
17274
  totalFiles += files.length;
@@ -17108,7 +17615,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17108
17615
  collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
17109
17616
  } else {
17110
17617
  try {
17111
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
17618
+ const claudeMdPath = join8(config2.projectRoot, "CLAUDE.md");
17112
17619
  const existing = await readFile4(claudeMdPath, "utf-8");
17113
17620
  if (!existing.includes("Dogfood Logging")) {
17114
17621
  await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
@@ -17153,7 +17660,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17153
17660
  cursorScaffolded = true;
17154
17661
  } else {
17155
17662
  try {
17156
- await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
17663
+ await access2(join8(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
17157
17664
  cursorScaffolded = true;
17158
17665
  } catch {
17159
17666
  }
@@ -17173,11 +17680,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17173
17680
  }
17174
17681
  async function ensureMcpJsonGitignored(projectRoot) {
17175
17682
  try {
17176
- await access2(join5(projectRoot, ".git"));
17683
+ await access2(join8(projectRoot, ".git"));
17177
17684
  } catch {
17178
17685
  return void 0;
17179
17686
  }
17180
- const gitignorePath = join5(projectRoot, ".gitignore");
17687
+ const gitignorePath = join8(projectRoot, ".gitignore");
17181
17688
  let existing = "";
17182
17689
  try {
17183
17690
  existing = await readFile4(gitignorePath, "utf-8");
@@ -17560,141 +18067,137 @@ ${result.initialTasksPrompt.user}
17560
18067
  }
17561
18068
  }
17562
18069
 
17563
- // src/services/build.ts
17564
- import { randomUUID as randomUUID11 } from "crypto";
17565
- import { readdirSync as readdirSync4, existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
17566
- import { join as join8 } from "path";
17567
- init_git();
17568
-
17569
- // src/lib/owns-local-workspace.ts
17570
- init_git();
18070
+ // src/lib/model-routing.ts
18071
+ var TIER_SPECS = {
18072
+ cheap: {
18073
+ label: "Cheap / fast",
18074
+ rationale: "XS\u2013S work \u2014 single-file fixes, UI polish, mechanical edits. A small model is faster and cheaper with no quality loss here.",
18075
+ examples: "Anthropic Claude Haiku \xB7 OpenAI GPT-5 mini \xB7 Google Gemini Flash \xB7 Mistral/Groq small"
18076
+ },
18077
+ capable: {
18078
+ label: "Capable",
18079
+ rationale: "M\u2013L work \u2014 multi-file features, data/plumbing, ambiguous handoffs. Needs a strong general-purpose model.",
18080
+ examples: "Anthropic Claude Sonnet \xB7 OpenAI GPT-5 \xB7 Google Gemini Pro \xB7 Mistral Large"
18081
+ },
18082
+ frontier: {
18083
+ label: "Frontier",
18084
+ rationale: "XL work \u2014 architecture, cross-cutting refactors, the hardest reasoning. Reach for your most capable model.",
18085
+ examples: "Anthropic Claude Opus \xB7 OpenAI GPT-5 Pro / o-series \xB7 Google Gemini Ultra"
18086
+ }
18087
+ };
18088
+ function tierForComplexity(complexity) {
18089
+ switch ((complexity ?? "").trim()) {
18090
+ case "XS":
18091
+ case "S":
18092
+ case "Small":
18093
+ return "cheap";
18094
+ case "XL":
18095
+ return "frontier";
18096
+ case "M":
18097
+ case "Medium":
18098
+ case "L":
18099
+ case "Large":
18100
+ return "capable";
18101
+ default:
18102
+ return "capable";
18103
+ }
18104
+ }
18105
+ function modelTierTag(complexity) {
18106
+ return `\u{1F916} ${TIER_SPECS[tierForComplexity(complexity)].label}`;
18107
+ }
18108
+ function formatModelRecommendation(complexity) {
18109
+ const spec = TIER_SPECS[tierForComplexity(complexity)];
18110
+ return `
17571
18111
 
17572
- // src/services/release.ts
17573
- import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
17574
- import { join as join7 } from "path";
17575
- import { execFileSync as execFileSync4 } from "child_process";
18112
+ ---
17576
18113
 
17577
- // src/lib/install-id.ts
17578
- import { randomUUID as randomUUID10 } from "crypto";
17579
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync, chmodSync } from "fs";
17580
- import { homedir as homedir2 } from "os";
17581
- import { join as join6 } from "path";
17582
- var PAPI_HOME_DIR = join6(homedir2(), ".papi");
17583
- var INSTALL_ID_FILE = join6(PAPI_HOME_DIR, "install-id.json");
17584
- var cachedInstallId = null;
17585
- function isValidUuid(s) {
17586
- 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);
18114
+ **\u{1F916} Suggested model tier: ${spec.label}**
18115
+ ${spec.rationale}
18116
+ Examples (use whichever provider you have): ${spec.examples}.
18117
+ _Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
17587
18118
  }
17588
- function getInstallId() {
17589
- if (cachedInstallId) return cachedInstallId;
17590
- try {
17591
- const raw = readFileSync2(INSTALL_ID_FILE, "utf-8");
17592
- const parsed = JSON.parse(raw);
17593
- if (isValidUuid(parsed.install_id)) {
17594
- cachedInstallId = parsed.install_id;
17595
- return cachedInstallId;
17596
- }
17597
- } catch {
17598
- }
17599
- try {
17600
- mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
17601
- const id = randomUUID10();
17602
- const contents = {
17603
- install_id: id,
17604
- created_at: (/* @__PURE__ */ new Date()).toISOString()
17605
- };
17606
- writeFileSync(INSTALL_ID_FILE, JSON.stringify(contents, null, 2), { mode: 384 });
17607
- try {
17608
- chmodSync(INSTALL_ID_FILE, 384);
17609
- } catch {
17610
- }
17611
- cachedInstallId = id;
17612
- return cachedInstallId;
17613
- } catch {
17614
- return null;
18119
+
18120
+ // src/services/build.ts
18121
+ import { randomUUID as randomUUID11 } from "crypto";
18122
+ import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
18123
+ import { join as join10 } from "path";
18124
+
18125
+ // src/lib/harness-capability.ts
18126
+ var HARNESS_REGISTRY = {
18127
+ // Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
18128
+ "claude-code": { build: true, label: "Claude Code" },
18129
+ "opencode": { build: true, label: "opencode" },
18130
+ "zcode": { build: true, label: "zcode" },
18131
+ "codex": { build: true, label: "Codex" },
18132
+ "cursor": { build: true, label: "Cursor" },
18133
+ // Cloud harnesses with code sandboxes — their agent builds in-sandbox over HTTP+OAuth.
18134
+ "lovable": { build: true, label: "Lovable" },
18135
+ "bolt": { build: true, label: "Bolt" },
18136
+ "replit": { build: true, label: "Replit" },
18137
+ // Chat-only surfaces, no sandbox planning only.
18138
+ "chatgpt": { build: false, label: "ChatGPT" },
18139
+ "claude.ai": { build: false, label: "Claude.ai" },
18140
+ "claude-desktop": { build: false, label: "Claude Desktop" }
18141
+ };
18142
+ var UNKNOWN_DEFAULT = { build: false, label: "your tool" };
18143
+ function detectHarness(clientName) {
18144
+ const raw = clientName?.trim() || null;
18145
+ if (!raw) {
18146
+ return { ...UNKNOWN_DEFAULT, raw: null, key: null, known: false };
18147
+ }
18148
+ const norm = raw.toLowerCase();
18149
+ const key = HARNESS_REGISTRY[norm] ? norm : Object.keys(HARNESS_REGISTRY).find((k) => norm.includes(k)) ?? null;
18150
+ if (!key) {
18151
+ return { ...UNKNOWN_DEFAULT, label: raw, raw, key: null, known: false };
17615
18152
  }
18153
+ return { ...HARNESS_REGISTRY[key], raw, key, known: true };
17616
18154
  }
17617
18155
 
17618
- // src/lib/telemetry.ts
17619
- var HOSTED_SUPABASE_URL2 = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
17620
- var DEFAULT_TELEMETRY_ENDPOINT = `${HOSTED_SUPABASE_URL2}/functions/v1/data-proxy`;
17621
- var MD_PINGS_SUPABASE_URL = process.env["PAPI_MD_PINGS_URL"] ?? HOSTED_SUPABASE_URL2;
17622
- var MD_PINGS_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
17623
- function isEnabled() {
17624
- const val = process.env["PAPI_TELEMETRY"];
17625
- return val !== "false" && val !== "off";
17626
- }
17627
- function emitTelemetryEvent(event) {
17628
- if (!isEnabled()) return;
17629
- const apiKey = process.env["PAPI_DATA_API_KEY"];
17630
- if (!apiKey) return;
17631
- const endpoint = process.env["PAPI_DATA_ENDPOINT"] ?? DEFAULT_TELEMETRY_ENDPOINT;
17632
- const body = {
17633
- projectId: event.project_id,
17634
- toolName: event.tool_name,
17635
- eventType: event.event_type,
17636
- metadata: event.metadata ?? {}
17637
- };
17638
- fetch(`${endpoint}/telemetry`, {
17639
- method: "POST",
17640
- headers: {
17641
- "Content-Type": "application/json",
17642
- "Authorization": `Bearer ${apiKey}`
17643
- },
17644
- body: JSON.stringify(body),
17645
- signal: AbortSignal.timeout(5e3)
17646
- }).catch(() => {
17647
- });
18156
+ // src/lib/harness-build-steps.ts
18157
+ init_git();
18158
+ function startBuildSteps(branch, base, taskId) {
18159
+ return [
18160
+ `PAPI is connected over HTTP and can't reach your files \u2014 your harness runs git in its own sandbox. To start ${taskId}:`,
18161
+ ` git fetch origin`,
18162
+ ` git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${base} 2>/dev/null || git checkout -b ${branch}`,
18163
+ `Then implement the task on branch \`${branch}\`.`
18164
+ ];
17648
18165
  }
17649
- function emitToolCall(projectId, toolName, durationMs, extra) {
17650
- emitTelemetryEvent({
17651
- project_id: projectId,
17652
- tool_name: toolName,
17653
- event_type: "tool_call",
17654
- metadata: { duration_ms: durationMs, ...extra }
17655
- });
18166
+ function completeBuildSteps(branch, base, taskId, title) {
18167
+ const message = `${taskId}: ${title}`.replace(/["`\\]/g, "'");
18168
+ return [
18169
+ `To ship ${taskId} from your sandbox:`,
18170
+ ` git add -A`,
18171
+ ` git commit -m "${message}"`,
18172
+ ` git push -u origin ${branch}`,
18173
+ `Then open a PR from \`${branch}\` into \`${base}\`.`
18174
+ ];
17656
18175
  }
17657
- function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
17658
- if (!isEnabled()) return;
17659
- const installId = getInstallId();
17660
- if (!installId) return;
17661
- const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
17662
- const body = {
17663
- install_id: installId,
17664
- tool_name: toolName,
17665
- papi_version: process.env["npm_package_version"] ?? null,
17666
- metadata: extra ?? {}
17667
- };
17668
- if (resolvedUserId) body["user_id"] = resolvedUserId;
17669
- if (projectSlug) body["project_slug"] = projectSlug;
17670
- fetch(`${MD_PINGS_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
17671
- method: "POST",
17672
- headers: {
17673
- "Content-Type": "application/json",
17674
- "apikey": MD_PINGS_ANON_KEY,
17675
- "Authorization": `Bearer ${MD_PINGS_ANON_KEY}`,
17676
- "Prefer": "return=minimal"
17677
- },
17678
- body: JSON.stringify(body),
17679
- signal: AbortSignal.timeout(5e3)
17680
- }).catch(() => {
17681
- });
18176
+ function hostedBranchName(taskId, module, cycleNumber) {
18177
+ return module && cycleNumber > 0 ? cycleBranchName(cycleNumber, module) : taskBranchName(taskId);
17682
18178
  }
17683
- function emitMilestone(projectId, milestone, extra) {
17684
- emitTelemetryEvent({
17685
- project_id: projectId,
17686
- tool_name: milestone,
17687
- event_type: "milestone",
17688
- metadata: extra
17689
- });
18179
+ function hostedStartSteps(clientName, taskId, module, cycleNumber, base) {
18180
+ if (!detectHarness(clientName).build) return null;
18181
+ const branch = hostedBranchName(taskId, module, cycleNumber);
18182
+ return { branch, steps: startBuildSteps(branch, base, taskId) };
17690
18183
  }
17691
- function resolveTelemetryProjectId(config2) {
17692
- if (config2.projectId && config2.projectId.length > 0) return config2.projectId;
17693
- const env = process.env["PAPI_PROJECT_ID"];
17694
- return env && env.length > 0 ? env : void 0;
18184
+ function hostedCompleteSteps(clientName, taskId, module, cycleNumber, base, title) {
18185
+ if (!detectHarness(clientName).build) return null;
18186
+ const branch = hostedBranchName(taskId, module, cycleNumber);
18187
+ return completeBuildSteps(branch, base, taskId, title);
17695
18188
  }
17696
18189
 
18190
+ // src/services/build.ts
18191
+ init_git();
18192
+
18193
+ // src/lib/owns-local-workspace.ts
18194
+ init_git();
18195
+
17697
18196
  // src/services/release.ts
18197
+ init_telemetry();
18198
+ import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
18199
+ import { join as join9 } from "path";
18200
+ import { execFileSync as execFileSync4 } from "child_process";
17698
18201
  init_git();
17699
18202
  var INITIAL_RELEASE_NOTES = `# Changelog
17700
18203
 
@@ -18132,7 +18635,7 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
18132
18635
  }
18133
18636
  }
18134
18637
  const latestTag = getLatestTag(config2.projectRoot);
18135
- const changelogPath = join7(config2.projectRoot, "CHANGELOG.md");
18638
+ const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
18136
18639
  if (!latestTag) {
18137
18640
  const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
18138
18641
  if (config2.adapterType === "proxy") {
@@ -18827,7 +19330,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
18827
19330
  if (modified.length === 0) {
18828
19331
  return "Auto-commit: skipped (no working-tree changes).";
18829
19332
  }
18830
- const dirname6 = (p) => {
19333
+ const dirname7 = (p) => {
18831
19334
  const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
18832
19335
  return idx > 0 ? p.slice(0, idx) : "";
18833
19336
  };
@@ -18839,7 +19342,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
18839
19342
  return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
18840
19343
  }
18841
19344
  const untracked = getUntrackedFiles(cwd);
18842
- const scopedDirs = [...new Set(scoped.map(dirname6).filter((d) => d.length > 0))];
19345
+ const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
18843
19346
  const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
18844
19347
  const scopedSet = new Set(scoped);
18845
19348
  const adjacentUntracked = untracked.filter(
@@ -18864,8 +19367,13 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
18864
19367
  }
18865
19368
  return safeRun(() => stageAllAndCommit(cwd, message));
18866
19369
  }
18867
- function pushAndCreatePR(config2, taskId, taskTitle) {
19370
+ function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
18868
19371
  const lines = [];
19372
+ if (!hasLocalWorkspace()) {
19373
+ const steps = hostedCompleteSteps(clientName, taskId, module, cycleNumber ?? 0, config2.baseBranch, taskTitle);
19374
+ if (steps) lines.push(...steps);
19375
+ return lines;
19376
+ }
18869
19377
  if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
18870
19378
  return lines;
18871
19379
  }
@@ -19050,7 +19558,7 @@ async function describeTask(adapter2, taskId) {
19050
19558
  }
19051
19559
  return { task };
19052
19560
  }
19053
- async function startBuild(adapter2, config2, taskId, options = {}) {
19561
+ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
19054
19562
  const task = await adapter2.getTask(taskId);
19055
19563
  if (!task) {
19056
19564
  throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
@@ -19092,6 +19600,12 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
19092
19600
  const branchLines = [];
19093
19601
  if (options.light) {
19094
19602
  branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
19603
+ } else if (!hasLocalWorkspace()) {
19604
+ const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
19605
+ const hosted = hostedStartSteps(clientName, taskId, task.module, cycleHealth?.totalCycles ?? 0, config2.baseBranch);
19606
+ if (hosted) {
19607
+ branchLines.push(...hosted.steps);
19608
+ }
19095
19609
  } else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
19096
19610
  const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
19097
19611
  const cycleNumber = cycleHealth?.totalCycles ?? 0;
@@ -19295,16 +19809,16 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
19295
19809
  collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
19296
19810
  return;
19297
19811
  }
19298
- const papiDir = join8(projectRoot, ".papi");
19299
- if (!existsSync4(papiDir)) {
19812
+ const papiDir = join10(projectRoot, ".papi");
19813
+ if (!existsSync6(papiDir)) {
19300
19814
  mkdirSync2(papiDir, { recursive: true });
19301
19815
  }
19302
- const scopePath = join8(papiDir, "active-task-scope.txt");
19816
+ const scopePath = join10(papiDir, "active-task-scope.txt");
19303
19817
  writeFileSync2(scopePath, content, "utf-8");
19304
19818
  }
19305
19819
  function clearActiveTaskScope(projectRoot) {
19306
- const scopePath = join8(projectRoot, ".papi", "active-task-scope.txt");
19307
- if (existsSync4(scopePath)) {
19820
+ const scopePath = join10(projectRoot, ".papi", "active-task-scope.txt");
19821
+ if (existsSync6(scopePath)) {
19308
19822
  unlinkSync(scopePath);
19309
19823
  }
19310
19824
  }
@@ -19324,7 +19838,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
19324
19838
  else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
19325
19839
  else if (relativePath.startsWith("docs/audits/")) type = "audit";
19326
19840
  try {
19327
- const content = readFileSync3(absolutePath, "utf-8").slice(0, 2e3);
19841
+ const content = readFileSync5(absolutePath, "utf-8").slice(0, 2e3);
19328
19842
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
19329
19843
  if (fmMatch) {
19330
19844
  const fm = fmMatch[1];
@@ -19371,7 +19885,7 @@ Fix the deploy first, then re-run build_execute complete with a 2xx verification
19371
19885
  );
19372
19886
  }
19373
19887
  }
19374
- async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19888
+ async function completeBuild(adapter2, config2, taskId, input, options = {}, clientName) {
19375
19889
  const task = await adapter2.getTask(taskId);
19376
19890
  if (!task) {
19377
19891
  throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
@@ -19649,7 +20163,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19649
20163
  if (options.light) {
19650
20164
  prLines.push("Light mode: skipping push and PR creation.");
19651
20165
  } else if (config2.autoCommit && input.completed === "yes") {
19652
- prLines = pushAndCreatePR(config2, taskId, task.title);
20166
+ prLines = pushAndCreatePR(config2, taskId, task.title, clientName, task.module, cycleNumber);
19653
20167
  }
19654
20168
  const allTasks = await adapter2.queryBoard();
19655
20169
  warnIfEmpty("queryBoard (cycle progress)", allTasks);
@@ -19669,14 +20183,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19669
20183
  let docWarning;
19670
20184
  try {
19671
20185
  if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
19672
- const docsDir = join8(config2.projectRoot, "docs");
19673
- if (existsSync4(docsDir)) {
20186
+ const docsDir = join10(config2.projectRoot, "docs");
20187
+ if (existsSync6(docsDir)) {
19674
20188
  const scanDir = (dir, depth = 0) => {
19675
20189
  if (depth > 8) return [];
19676
- const entries = readdirSync4(dir, { withFileTypes: true });
20190
+ const entries = readdirSync5(dir, { withFileTypes: true });
19677
20191
  const files = [];
19678
20192
  for (const e of entries) {
19679
- const full = join8(dir, e.name);
20193
+ const full = join10(dir, e.name);
19680
20194
  if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
19681
20195
  else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
19682
20196
  }
@@ -19691,7 +20205,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19691
20205
  const failed = [];
19692
20206
  for (const docPath of unregistered) {
19693
20207
  try {
19694
- const meta = extractDocMeta(join8(config2.projectRoot, docPath), docPath, cycleNumber);
20208
+ const meta = extractDocMeta(join10(config2.projectRoot, docPath), docPath, cycleNumber);
19695
20209
  await adapter2.registerDoc({
19696
20210
  title: meta.title,
19697
20211
  type: meta.type,
@@ -19768,6 +20282,9 @@ var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
19768
20282
  var MODULE_INSTRUCTIONS = {
19769
20283
  Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
19770
20284
 
20285
+ **STEP 0 \u2014 Dispatch the design agent for the visual layer (do NOT hand-write UI in the main build context).**
20286
+ Any \`.tsx\` that renders visible UI must be built by the \`frontend-design-engineer\` subagent (dispatch via the Task tool), not inline here. It works in an isolated window loaded only with brand canon (DESIGN.md / PRODUCT.md / brand-book), runs design-critique before and after, builds via the \`frontend-design\` / \`impeccable\` skills, self-checks the falsifiable anti-slop blocklist, and verifies in-browser at populated / empty / mobile states. You remain responsible for data, routes, types, and tests \u2014 hand the visual layer to the agent and integrate the report it returns. Components hand-written in this context produce generic output that fails review. (If \`.claude/agents/frontend-design-engineer.md\` is absent \u2014 e.g. a fresh external install \u2014 run STEPs 1-4 below yourself.)
20287
+
19771
20288
  **STEP 1 \u2014 BEFORE writing any code:**
19772
20289
  If you use the \`impeccable\` skill (recommended for dashboard work), it reads two root files for design context: **PRODUCT.md** (strategic \u2014 brand, users, product purpose, design principles) and **DESIGN.md** (visual tokens \u2014 palette, typography, elevation, components). Run \`impeccable init\` to create them if they don't exist yet. Every visual decision must align with these files; if your output contradicts them, it is wrong. (The legacy \`.impeccable.md\` is no longer read by the skill.)
19773
20290
 
@@ -20007,7 +20524,7 @@ function isResearchOrSpike(task) {
20007
20524
  }
20008
20525
  function formatListItem(task) {
20009
20526
  const base = `- **${task.id}:** ${task.title}
20010
- Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity}`;
20527
+ Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity} | ${modelTierTag(task.complexity)}`;
20011
20528
  if (isResearchOrSpike(task)) {
20012
20529
  return base + "\n _Research/spike \u2014 still needed?_ **(a)** build_execute | **(b)** fast-close (answered) | **(c)** deprioritise | **(d)** discuss";
20013
20530
  }
@@ -20129,7 +20646,7 @@ async function handleBuildDescribe(adapter2, args) {
20129
20646
  return errorResponse(err instanceof Error ? err.message : String(err));
20130
20647
  }
20131
20648
  }
20132
- async function handleBuildExecute(adapter2, config2, args) {
20649
+ async function handleBuildExecute(adapter2, config2, args, clientName) {
20133
20650
  const taskId = args.task_id;
20134
20651
  if (!taskId) {
20135
20652
  return errorResponse("task_id is required.");
@@ -20153,11 +20670,11 @@ async function handleBuildExecute(adapter2, config2, args) {
20153
20670
  }
20154
20671
  }
20155
20672
  if (hasReportFields(args)) {
20156
- return handleExecuteComplete(adapter2, config2, taskId, args, light);
20673
+ return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
20157
20674
  }
20158
20675
  const tracker = new ProgressTracker("start_build");
20159
20676
  try {
20160
- const result = await startBuild(adapter2, config2, taskId, { light });
20677
+ const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
20161
20678
  tracker.mark("start_decorate_handoff");
20162
20679
  const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
20163
20680
  const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
@@ -20209,7 +20726,8 @@ ${entries}`;
20209
20726
  const moduleInstructions = getModuleInstructions(result.task.module);
20210
20727
  const moduleContext = await getModuleContext(adapter2, result.task);
20211
20728
  const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
20212
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
20729
+ const modelNote = formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity);
20730
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
20213
20731
  } catch (err) {
20214
20732
  if (isNoHandoffError(err)) {
20215
20733
  const lines = [
@@ -20245,7 +20763,7 @@ ${entries}`;
20245
20763
  }));
20246
20764
  }
20247
20765
  }
20248
- async function handleExecuteComplete(adapter2, config2, taskId, args, light = false) {
20766
+ async function handleExecuteComplete(adapter2, config2, taskId, args, light = false, clientName) {
20249
20767
  const completed = args.completed;
20250
20768
  const effort = args.effort;
20251
20769
  const estimatedEffort = args.estimated_effort;
@@ -20323,7 +20841,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
20323
20841
  })),
20324
20842
  productionVerification,
20325
20843
  preview
20326
- }, { light });
20844
+ }, { light }, clientName);
20327
20845
  tracker.mark("complete_format");
20328
20846
  if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
20329
20847
  for (const learningId of resolvesLearnings) {
@@ -20454,14 +20972,12 @@ function formatCompleteResult(result) {
20454
20972
  } else {
20455
20973
  lines.push("", `Next: run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, then \`build_execute <task_id>\` to start the next one.`);
20456
20974
  }
20457
- lines.push("", "*Tip: Start a new chat for your next build to keep context fresh and avoid hitting context window limits.*");
20458
20975
  } else {
20459
20976
  if (hasDiscoveredIssues) {
20460
20977
  lines.push("", "All cycle tasks complete. Discovered issues logged \u2014 run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
20461
20978
  } else {
20462
20979
  lines.push("", "All cycle tasks complete. Run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
20463
20980
  }
20464
- lines.push("", "*Tip: Start a new chat for your next session to keep context fresh.*");
20465
20981
  }
20466
20982
  return lines.join("\n");
20467
20983
  }
@@ -21050,13 +21566,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
21050
21566
  init_git();
21051
21567
 
21052
21568
  // src/services/reconcile.ts
21053
- import { readFileSync as readFileSync4 } from "fs";
21054
- import { join as join9 } from "path";
21569
+ import { readFileSync as readFileSync6 } from "fs";
21570
+ import { join as join11 } from "path";
21055
21571
  function loadDocsIndex(projectRoot) {
21056
21572
  if (!hasLocalWorkspace()) return "";
21057
21573
  try {
21058
- const indexPath = join9(projectRoot, "docs", "INDEX.md");
21059
- const raw = readFileSync4(indexPath, "utf8");
21574
+ const indexPath = join11(projectRoot, "docs", "INDEX.md");
21575
+ const raw = readFileSync6(indexPath, "utf8");
21060
21576
  const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
21061
21577
  if (rows.length === 0) return "";
21062
21578
  const entries = rows.map((row) => {
@@ -21631,8 +22147,8 @@ Produce your analysis and structured output above. Present Part 1 to the user an
21631
22147
  }
21632
22148
 
21633
22149
  // src/tools/review.ts
21634
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
21635
- import { join as join10 } from "path";
22150
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
22151
+ import { join as join12 } from "path";
21636
22152
  init_git();
21637
22153
 
21638
22154
  // src/services/review.ts
@@ -21844,11 +22360,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
21844
22360
  ${diff}
21845
22361
  \`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
21846
22362
  let projectContext = "";
21847
- const ctxPath = join10(config2.projectRoot, ".agents", "papi-context.md");
21848
- if (existsSync5(ctxPath)) {
22363
+ const ctxPath = join12(config2.projectRoot, ".agents", "papi-context.md");
22364
+ if (existsSync7(ctxPath)) {
21849
22365
  try {
21850
22366
  projectContext = `### Project context (.agents/papi-context.md)
21851
- ${readFileSync5(ctxPath, "utf-8")}
22367
+ ${readFileSync7(ctxPath, "utf-8")}
21852
22368
 
21853
22369
  `;
21854
22370
  } catch {
@@ -22003,8 +22519,8 @@ function mergeAfterAccept(config2, taskId) {
22003
22519
  };
22004
22520
  }
22005
22521
  const details = [];
22006
- const papiDir = join10(config2.projectRoot, ".papi");
22007
- if (existsSync5(papiDir)) {
22522
+ const papiDir = join12(config2.projectRoot, ".papi");
22523
+ if (existsSync7(papiDir)) {
22008
22524
  try {
22009
22525
  const commitResult = stageDirAndCommit(
22010
22526
  config2.projectRoot,
@@ -23034,8 +23550,9 @@ async function getHealthSummary(adapter2) {
23034
23550
  const cycleNumber = health.totalCycles;
23035
23551
  const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
23036
23552
  const reviewDue = health.strategyReviewDue;
23037
- const reviewGateBlocking = cyclesSinceReview >= 5;
23038
- const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
23553
+ const reviewGateBlocking = cyclesSinceReview >= STRATEGY_REVIEW_BLOCK_GAP;
23554
+ const reviewAvailable = cyclesSinceReview >= STRATEGY_REVIEW_OFFER_GAP && !reviewGateBlocking;
23555
+ const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : reviewAvailable ? `\u25CB Strategy review available \u2014 ${cyclesSinceReview} cycles since last review. Offered now (optional); \`plan\` hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
23039
23556
  const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
23040
23557
  const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
23041
23558
  const statusCounts = countByStatus(nonDeferredTasks);
@@ -23314,115 +23831,9 @@ function formatUnblockSection(candidates) {
23314
23831
  return lines.join("\n");
23315
23832
  }
23316
23833
 
23317
- // src/lib/skill-detection.ts
23318
- import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
23319
- import { join as join11 } from "path";
23320
- function readPackageJson(projectRoot) {
23321
- const path7 = join11(projectRoot, "package.json");
23322
- if (!existsSync6(path7)) return null;
23323
- try {
23324
- const raw = readFileSync6(path7, "utf-8");
23325
- return JSON.parse(raw);
23326
- } catch {
23327
- return null;
23328
- }
23329
- }
23330
- function allDeps(pkg) {
23331
- if (!pkg) return {};
23332
- return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
23333
- }
23334
- function hasDependencyMatching(deps, pattern) {
23335
- for (const name of Object.keys(deps)) {
23336
- if (pattern.test(name)) return true;
23337
- }
23338
- return false;
23339
- }
23340
- function hasGitHubWorkflows(projectRoot) {
23341
- const dir = join11(projectRoot, ".github", "workflows");
23342
- if (!existsSync6(dir)) return false;
23343
- try {
23344
- const entries = readdirSync5(dir);
23345
- return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
23346
- } catch {
23347
- return false;
23348
- }
23349
- }
23350
- function envExampleMentionsStaging(projectRoot) {
23351
- const path7 = join11(projectRoot, ".env.example");
23352
- if (!existsSync6(path7)) return false;
23353
- try {
23354
- const raw = readFileSync6(path7, "utf-8");
23355
- return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
23356
- } catch {
23357
- return false;
23358
- }
23359
- }
23360
- function hasVercelConfig(projectRoot) {
23361
- if (existsSync6(join11(projectRoot, "vercel.json"))) return true;
23362
- const vercelDir = join11(projectRoot, ".vercel");
23363
- if (!existsSync6(vercelDir)) return false;
23364
- try {
23365
- return statSync4(vercelDir).isDirectory();
23366
- } catch {
23367
- return false;
23368
- }
23369
- }
23370
- function scanForSkillSignals(projectRoot) {
23371
- const proposals = [];
23372
- const pkg = readPackageJson(projectRoot);
23373
- const deps = allDeps(pkg);
23374
- if (hasGitHubWorkflows(projectRoot)) {
23375
- proposals.push({
23376
- id: "gh-actions-debug",
23377
- name: "GitHub Actions debugger",
23378
- rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
23379
- hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
23380
- });
23381
- }
23382
- if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
23383
- proposals.push({
23384
- id: "error-tracking",
23385
- name: "Error tracking helper",
23386
- rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
23387
- hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
23388
- });
23389
- }
23390
- if (hasVercelConfig(projectRoot)) {
23391
- proposals.push({
23392
- id: "read-vercel-logs",
23393
- name: "Vercel deploy logs",
23394
- rationale: "Detected vercel.json or .vercel/ \u2014 a skill for pulling deploy + runtime logs from Vercel directly into Claude Code helps when builds fail or runtime errors land.",
23395
- hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
23396
- });
23397
- }
23398
- if (envExampleMentionsStaging(projectRoot)) {
23399
- proposals.push({
23400
- id: "staging-environment",
23401
- name: "Staging-environment helper",
23402
- rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
23403
- hint: 'Search Claude Code skill registry for "staging" or define your own.'
23404
- });
23405
- }
23406
- return proposals;
23407
- }
23408
- function formatSkillProposals(proposals) {
23409
- if (proposals.length === 0) return "";
23410
- const lines = ["", "## Skill proposals", "_PAPI scanned your project once and noticed tooling that often pairs with a Claude Code skill. Each proposal is one-shot \u2014 they won't resurface._"];
23411
- for (const p of proposals) {
23412
- lines.push("");
23413
- lines.push(`### ${p.name}`);
23414
- lines.push(p.rationale);
23415
- if (p.hint) {
23416
- lines.push(`_${p.hint}_`);
23417
- }
23418
- lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
23419
- }
23420
- return "\n" + lines.join("\n");
23421
- }
23422
-
23423
23834
  // src/tools/agent-list.ts
23424
23835
  import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
23425
- import { join as join12 } from "path";
23836
+ import { join as join13 } from "path";
23426
23837
  var NO_AGENTS_HINT = "No project sub-agents found in `.claude/agents/`. Add a `*.md` file with `name` + `description` frontmatter \u2014 see the 1926-Census marketing sub-agent for a reference implementation.";
23427
23838
  function parseAgentFrontmatter(content) {
23428
23839
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -23434,7 +23845,7 @@ function parseAgentFrontmatter(content) {
23434
23845
  return { name: nameMatch?.[1].trim(), description };
23435
23846
  }
23436
23847
  async function listAgents(projectRoot) {
23437
- const agentsDir = join12(projectRoot, ".claude", "agents");
23848
+ const agentsDir = join13(projectRoot, ".claude", "agents");
23438
23849
  let files;
23439
23850
  try {
23440
23851
  files = await readdir2(agentsDir);
@@ -23445,7 +23856,7 @@ async function listAgents(projectRoot) {
23445
23856
  for (const file of files.filter((f) => f.endsWith(".md"))) {
23446
23857
  let content;
23447
23858
  try {
23448
- content = await readFile7(join12(agentsDir, file), "utf-8");
23859
+ content = await readFile7(join13(agentsDir, file), "utf-8");
23449
23860
  } catch {
23450
23861
  continue;
23451
23862
  }
@@ -23453,7 +23864,7 @@ async function listAgents(projectRoot) {
23453
23864
  agents.push({
23454
23865
  name: meta?.name ?? file.replace(/\.md$/, ""),
23455
23866
  description: meta?.description ?? "",
23456
- path: join12(".claude", "agents", file)
23867
+ path: join13(".claude", "agents", file)
23457
23868
  });
23458
23869
  }
23459
23870
  agents.sort((a, b2) => a.name.localeCompare(b2.name));
@@ -23494,8 +23905,8 @@ ${formatted}`);
23494
23905
  }
23495
23906
 
23496
23907
  // src/tools/doc-registry.ts
23497
- import { readdirSync as readdirSync6, existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
23498
- import { join as join13, relative } from "path";
23908
+ import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
23909
+ import { join as join14, relative } from "path";
23499
23910
  import { homedir as homedir3 } from "os";
23500
23911
  import { randomUUID as randomUUID16 } from "crypto";
23501
23912
  var docRegisterTool = {
@@ -23692,7 +24103,7 @@ async function handleDocSearch(adapter2, args, config2) {
23692
24103
  const lines = docs.map((d) => {
23693
24104
  const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
23694
24105
  const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
23695
- const missingNote = root && d.path && !existsSync7(join13(root, d.path)) ? `
24106
+ const missingNote = root && d.path && !existsSync8(join14(root, d.path)) ? `
23696
24107
  > \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there. Check \`git stash list\` for a papi-autostash entry, or re-create/deregister the doc.` : "";
23697
24108
  return `### ${d.title}
23698
24109
  **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
@@ -23706,12 +24117,12 @@ ${d.summary}
23706
24117
  ${lines.join("\n---\n\n")}`);
23707
24118
  }
23708
24119
  function scanMdFiles(dir, rootDir) {
23709
- if (!existsSync7(dir)) return [];
24120
+ if (!existsSync8(dir)) return [];
23710
24121
  const files = [];
23711
24122
  try {
23712
24123
  const entries = readdirSync6(dir, { withFileTypes: true });
23713
24124
  for (const entry of entries) {
23714
- const full = join13(dir, entry.name);
24125
+ const full = join14(dir, entry.name);
23715
24126
  if (entry.isDirectory()) {
23716
24127
  files.push(...scanMdFiles(full, rootDir));
23717
24128
  } else if (entry.name.endsWith(".md")) {
@@ -23724,7 +24135,7 @@ function scanMdFiles(dir, rootDir) {
23724
24135
  }
23725
24136
  function extractTitle(filePath) {
23726
24137
  try {
23727
- const content = readFileSync7(filePath, "utf-8").slice(0, 1e3);
24138
+ const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
23728
24139
  const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
23729
24140
  if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
23730
24141
  const headingMatch = content.match(/^#+\s+(.+)$/m);
@@ -23745,17 +24156,17 @@ async function handleDocScan(adapter2, config2, args) {
23745
24156
  const includePlans = args.include_plans ?? false;
23746
24157
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
23747
24158
  const registeredPaths = new Set(registered.map((d) => d.path));
23748
- const docsDir = join13(config2.projectRoot, "docs");
24159
+ const docsDir = join14(config2.projectRoot, "docs");
23749
24160
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
23750
24161
  const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
23751
24162
  let unregisteredPlans = [];
23752
24163
  if (includePlans) {
23753
- const plansDir = join13(homedir3(), ".claude", "plans");
23754
- if (existsSync7(plansDir)) {
24164
+ const plansDir = join14(homedir3(), ".claude", "plans");
24165
+ if (existsSync8(plansDir)) {
23755
24166
  const planFiles = scanMdFiles(plansDir, plansDir);
23756
24167
  unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
23757
24168
  path: f,
23758
- title: extractTitle(join13(plansDir, f.replace("plans/", "")))
24169
+ title: extractTitle(join14(plansDir, f.replace("plans/", "")))
23759
24170
  }));
23760
24171
  }
23761
24172
  }
@@ -23766,7 +24177,7 @@ async function handleDocScan(adapter2, config2, args) {
23766
24177
  if (unregisteredDocs.length > 0) {
23767
24178
  lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
23768
24179
  for (const f of unregisteredDocs) {
23769
- const title = extractTitle(join13(config2.projectRoot, f));
24180
+ const title = extractTitle(join14(config2.projectRoot, f));
23770
24181
  lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
23771
24182
  }
23772
24183
  }
@@ -23876,6 +24287,22 @@ ${userNotes}` : referenceLine;
23876
24287
  );
23877
24288
  }
23878
24289
 
24290
+ // src/lib/harness-guidance.ts
24291
+ function buildLoopRunsHere(i) {
24292
+ if (i.forceLocal) return true;
24293
+ if (i.harness.known) return i.harness.build;
24294
+ if (i.envHintsNoGit) return false;
24295
+ return i.localWorkspace;
24296
+ }
24297
+ function nextActionQualifier(harness) {
24298
+ const where = harness.known ? harness.label : "your tool";
24299
+ return `_Note: \`build_execute\`, \`review_submit\`, and \`release\` run where your project files live. From ${where} you can plan, log ideas, and steer the board \u2014 the build / review / release loop runs in the environment that holds your code._`;
24300
+ }
24301
+ function connectOnlyRoleNote(harness) {
24302
+ if (harness.build || !harness.known) return null;
24303
+ return `**You're connected as a planning surface.** ${harness.label} is a first-class place to plan, capture decisions, and steer the board. The code gets built wherever your build harness lives (a CLI like Codex, or a sandbox like Lovable or Bolt) and your plan + decisions travel with it. This is a full half of the loop, not a limited mode.`;
24304
+ }
24305
+
23879
24306
  // src/lib/enrichment.ts
23880
24307
  function headingSlug(line) {
23881
24308
  const match = line.match(/^##\s+(.+?)(?:\s*\([^)]*\))?\s*$/);
@@ -23961,8 +24388,8 @@ async function verifyProject(adapter2) {
23961
24388
  // src/tools/orient.ts
23962
24389
  import { execFile as execFile2 } from "child_process";
23963
24390
  import { promisify as promisify2 } from "util";
23964
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, existsSync as existsSync8 } from "fs";
23965
- import { join as join14 } from "path";
24391
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync9 } from "fs";
24392
+ import { join as join15 } from "path";
23966
24393
  var execFileAsync2 = promisify2(execFile2);
23967
24394
  var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
23968
24395
  var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
@@ -24007,7 +24434,7 @@ async function runWithLimit(concurrency, tasks) {
24007
24434
  }
24008
24435
  var orientTool = {
24009
24436
  name: "orient",
24010
- description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. Pass `environment` to qualify git-dependent recommendations (build_execute, release, review_submit) for non-Claude-Code callers.",
24437
+ description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. PAPI detects build capability from the connecting harness (clientInfo); pass `environment` only to override that detection for git-dependent recommendations (build_execute, release, review_submit).",
24011
24438
  annotations: { readOnlyHint: true, destructiveHint: false },
24012
24439
  inputSchema: {
24013
24440
  type: "object",
@@ -24015,7 +24442,7 @@ var orientTool = {
24015
24442
  environment: {
24016
24443
  type: "string",
24017
24444
  enum: ["local-cli", "hosted", "api", "unknown"],
24018
- description: 'Caller environment. "local-cli" (local CLI with git \u2014 Claude Code, Codex, or any local MCP client) sees all recommendations unchanged. "hosted" or "api" (no local git) suppresses/qualifies build_execute, release, and review_submit recommendations because those require a local session with git access. Default "unknown" = show everything (safe default). Legacy values "claude-code" and "cowork" are accepted as aliases (deprecated).'
24445
+ description: `Caller environment OVERRIDE. By default PAPI branches git-dependent recommendations on the connecting harness's detected capability \u2014 a build-capable harness (local CLI like Codex/opencode, or a sandboxed cloud harness like Lovable/Bolt/Replit) sees the full loop; a connect-only chat surface sees planning-only framing. Pass this only to override: "local-cli" forces the full loop, "hosted"/"api" hints no local git for an UNRECOGNISED harness. Default "unknown" = trust detection. Legacy "claude-code"/"cowork" accepted as aliases (deprecated).`
24019
24446
  },
24020
24447
  deep_housekeeping: {
24021
24448
  type: "boolean",
@@ -24059,7 +24486,7 @@ function formatSynthesisParagraph(opts) {
24059
24486
  parts.push(`Next: ${cleanMode}`);
24060
24487
  return parts.join(" ");
24061
24488
  }
24062
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = [], projectName, teamSummary) {
24489
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = [], projectName, teamSummary, clientName) {
24063
24490
  const lines = [];
24064
24491
  const cycleIsComplete = health.latestCycleStatus === "complete";
24065
24492
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -24081,6 +24508,12 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
24081
24508
  lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
24082
24509
  lines.push("");
24083
24510
  }
24511
+ const harness = detectHarness(clientName);
24512
+ const roleNote = connectOnlyRoleNote(harness);
24513
+ if (roleNote) {
24514
+ lines.push(`> ${roleNote}`);
24515
+ lines.push("");
24516
+ }
24084
24517
  if (buildInfo.warnings.length > 0) {
24085
24518
  for (const w of buildInfo.warnings) {
24086
24519
  lines.push(`> ${w}`);
@@ -24092,11 +24525,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
24092
24525
  lines.push("");
24093
24526
  }
24094
24527
  const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
24095
- if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
24096
- lines.push(`> **Next action:** ${health.recommendedMode}`);
24097
- lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local session in your AI coding tool with git access. From \`${environment}\` you can view state, log ideas, and edit the board \u2014 but the build/review/release loop must run through your local tool._`);
24098
- } else {
24099
- lines.push(`> **Next action:** ${health.recommendedMode}`);
24528
+ const loopRunsHere = buildLoopRunsHere({
24529
+ harness,
24530
+ localWorkspace: hasLocalWorkspace(),
24531
+ forceLocal: environment === "local-cli",
24532
+ envHintsNoGit: GIT_DEPENDENT_ENVS.has(environment)
24533
+ });
24534
+ lines.push(`> **Next action:** ${health.recommendedMode}`);
24535
+ if (isGitDependentRec && !loopRunsHere) {
24536
+ lines.push(`> ${nextActionQualifier(harness)}`);
24100
24537
  }
24101
24538
  lines.push("");
24102
24539
  lines.push(`**Strategy Review:** ${health.reviewWarning}`);
@@ -24262,8 +24699,8 @@ async function getLatestGitTag(projectRoot) {
24262
24699
  }
24263
24700
  async function checkNpmVersionDrift() {
24264
24701
  try {
24265
- const pkgPath = join14(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
24266
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
24702
+ const pkgPath = join15(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
24703
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
24267
24704
  const localVersion = pkg.version;
24268
24705
  const packageName = pkg.name;
24269
24706
  const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
@@ -24396,7 +24833,7 @@ function formatResolvedFeedback(items) {
24396
24833
  lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
24397
24834
  return lines.join("\n");
24398
24835
  }
24399
- async function handleOrient(adapter2, config2, args = {}) {
24836
+ async function handleOrient(adapter2, config2, args = {}, clientName) {
24400
24837
  const environment = normaliseEnvironment(args.environment);
24401
24838
  const deepHousekeeping = args.deep_housekeeping === true;
24402
24839
  const fullEnrichment = args.full === true || deepHousekeeping;
@@ -24720,7 +25157,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24720
25157
  // task-1652: deep housekeeping — opt-in sweep (board, unrecorded commits, unregistered docs)
24721
25158
  // task-1865: also detects stale skill forks vs the @papi-ai/skills registry.
24722
25159
  tracked("deep-housekeeping", async () => {
24723
- if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
25160
+ if (!deepHousekeeping) return { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
24724
25161
  let reconciliationNote2 = "";
24725
25162
  try {
24726
25163
  const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
@@ -24732,6 +25169,22 @@ async function handleOrient(adapter2, config2, args = {}) {
24732
25169
  }
24733
25170
  } catch {
24734
25171
  }
25172
+ let mergedInProgressNote2 = "";
25173
+ try {
25174
+ if (hasLocalWorkspace()) {
25175
+ const merged = detectMergedInProgress(config2.projectRoot, config2.baseBranch, allTasks);
25176
+ if (merged.length > 0) {
25177
+ const lines = ["\n\n## Merged but In Progress"];
25178
+ lines.push(`${merged.length} In-Progress task(s) already merged to the default branch \u2014 close them out with \`build_execute\` complete (or \`board_edit\` to mark Done):`);
25179
+ for (const m of merged) {
25180
+ const ref = m.pr ? `${m.pr} (\`${m.commit}\`)` : `\`${m.commit}\``;
25181
+ lines.push(`- \u26A0\uFE0F **${m.displayId}** \u2014 merged via ${ref}: ${m.subject}`);
25182
+ }
25183
+ mergedInProgressNote2 = lines.join("\n");
25184
+ }
25185
+ }
25186
+ } catch {
25187
+ }
24735
25188
  let unrecordedNote2 = "";
24736
25189
  try {
24737
25190
  const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
@@ -24756,7 +25209,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24756
25209
  let unregisteredDocsNote2 = "";
24757
25210
  try {
24758
25211
  if (adapter2.searchDocs && hasLocalWorkspace()) {
24759
- const docsDir = join14(config2.projectRoot, "docs");
25212
+ const docsDir = join15(config2.projectRoot, "docs");
24760
25213
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
24761
25214
  if (docsFiles.length > 0) {
24762
25215
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -24783,7 +25236,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24783
25236
  }
24784
25237
  } catch {
24785
25238
  }
24786
- return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
25239
+ return { reconciliationNote: reconciliationNote2, mergedInProgressNote: mergedInProgressNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
24787
25240
  })
24788
25241
  ]);
24789
25242
  const proxyWarning = proxyVersionOutcome.status === "fulfilled" ? proxyVersionOutcome.value : void 0;
@@ -24825,7 +25278,7 @@ ${versionDrift}` : "";
24825
25278
  void adapter2.markFeedbackNotified(resolvedFeedback.map((f) => f.id), config2.userId).catch(() => {
24826
25279
  });
24827
25280
  }
24828
- const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
25281
+ const { reconciliationNote, mergedInProgressNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
24829
25282
  let enrichmentNote = "";
24830
25283
  const enrichmentCollector = new FileWriteCollector();
24831
25284
  try {
@@ -24872,8 +25325,8 @@ ${section}`;
24872
25325
  tracked("release-history", () => computeReleaseHistory(adapter2))().catch(() => void 0)
24873
25326
  ]);
24874
25327
  const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
24875
- const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
24876
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
25328
+ const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
25329
+ return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
24877
25330
  } catch (err) {
24878
25331
  const message = err instanceof Error ? err.message : String(err);
24879
25332
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -24904,9 +25357,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector) {
24904
25357
 
24905
25358
  \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
24906
25359
  }
24907
- const claudeMdPath = join14(projectRoot, "CLAUDE.md");
24908
- if (!existsSync8(claudeMdPath)) return "";
24909
- const content = readFileSync8(claudeMdPath, "utf-8");
25360
+ const claudeMdPath = join15(projectRoot, "CLAUDE.md");
25361
+ if (!existsSync9(claudeMdPath)) return "";
25362
+ const content = readFileSync9(claudeMdPath, "utf-8");
24910
25363
  const additions = [];
24911
25364
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
24912
25365
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
@@ -25612,7 +26065,7 @@ ${result.userMessage}
25612
26065
 
25613
26066
  // src/services/scope-brief.ts
25614
26067
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
25615
- import { join as join15, dirname as dirname3 } from "path";
26068
+ import { join as join16, dirname as dirname4 } from "path";
25616
26069
  import Anthropic from "@anthropic-ai/sdk";
25617
26070
  var SCOPE_BRIEF_SYSTEM = `You are a technical scoping tool. You receive a brief-class task (too large to build directly) and decompose it into a structured scope document.
25618
26071
 
@@ -25667,13 +26120,13 @@ async function runScopeBrief(adapter2, input) {
25667
26120
  }
25668
26121
  const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
25669
26122
  const relPath = `docs/scopes/${slug}.md`;
25670
- const absPath = join15(input.projectRoot, relPath);
26123
+ const absPath = join16(input.projectRoot, relPath);
25671
26124
  const docBody = addFrontmatter(docContent, task, input.cycleNumber);
25672
26125
  const collector = new FileWriteCollector();
25673
26126
  if (input.adapterType === "proxy") {
25674
26127
  collector.add({ path: relPath, content: docBody, mode: "overwrite" });
25675
26128
  } else {
25676
- mkdirSync3(dirname3(absPath), { recursive: true });
26129
+ mkdirSync3(dirname4(absPath), { recursive: true });
25677
26130
  writeFileSync4(absPath, docBody, "utf-8");
25678
26131
  }
25679
26132
  const taskCount = countSubTasks(docContent);
@@ -26411,21 +26864,90 @@ async function handleTaskUnclaim(adapter2, config2, args) {
26411
26864
  return textResponse(`\u2705 Released **${result.id}** (${result.title}) back to the shared Pool.`);
26412
26865
  }
26413
26866
 
26867
+ // src/tools/task-move.ts
26868
+ var taskMoveTool = {
26869
+ name: "task_move",
26870
+ description: "Move a task from the current project to another project you own. Reassigns the task a fresh id in the target (collision-free) and carries its build reports, comments, and history with it; the cycle assignment is cleared so it lands in the target project's backlog. You must own (or have write access to) BOTH projects. Destructive-ish and cross-project, so it requires confirm=true \u2014 without it you get a preview only. Does not call the Anthropic API.",
26871
+ annotations: { readOnlyHint: false, destructiveHint: true },
26872
+ inputSchema: {
26873
+ type: "object",
26874
+ properties: {
26875
+ task_id: { type: "string", description: 'The task to move, e.g. "task-2292".' },
26876
+ target_project: {
26877
+ type: "string",
26878
+ description: 'The destination project \u2014 its slug (e.g. "papi-ui") or UUID. Must be a project you own.'
26879
+ },
26880
+ confirm: {
26881
+ type: "boolean",
26882
+ description: "Set true to perform the move. Omit (or false) to get a preview of what would happen."
26883
+ }
26884
+ },
26885
+ required: ["task_id", "target_project"]
26886
+ }
26887
+ };
26888
+ function requireStr(value) {
26889
+ const s = typeof value === "string" ? value.trim() : "";
26890
+ return s.length > 0 ? s : null;
26891
+ }
26892
+ async function handleTaskMove(adapter2, config2, args) {
26893
+ const taskId = requireStr(args.task_id);
26894
+ if (!taskId) return errorResponse('A task_id is required. Example: task_move task_id="task-42" target_project="other-project" confirm=true');
26895
+ const targetProject = requireStr(args.target_project);
26896
+ if (!targetProject) return errorResponse("A target_project (slug or UUID) is required \u2014 the project you want to move the task into.");
26897
+ if (typeof adapter2.moveTask !== "function") {
26898
+ return errorResponse("Cross-project move is not available on this adapter.");
26899
+ }
26900
+ const gate = await resolveOwnerGate(adapter2, config2);
26901
+ const callerUserId = gate.callerUserId;
26902
+ if (!callerUserId) {
26903
+ const note = gate.resolutionError ? ` (${gate.resolutionError})` : "";
26904
+ return errorResponse(
26905
+ "Moving a task requires a resolvable user identity, but none was found" + note + ". Set PAPI_USER_ID to your account UUID (local) \u2014 hosted sessions derive it from your bearer token."
26906
+ );
26907
+ }
26908
+ const task = await adapter2.getTask(taskId);
26909
+ if (!task) return errorResponse(`Task "${taskId}" was not found on this project's board. task_move moves a task FROM the project you're connected to \u2014 switch to the source project first if needed.`);
26910
+ if (args.confirm !== true) {
26911
+ return textResponse(
26912
+ `\u26A0\uFE0F **Preview \u2014 no changes made.**
26913
+
26914
+ This will move **${taskId}** (${task.title}) into project \`${targetProject}\`:
26915
+ - it gets a NEW task id in the target (the current "${taskId}" is freed in this project)
26916
+ - its build reports, comments, and history move with it
26917
+ - its cycle assignment is cleared (it lands in the target's backlog)
26918
+ - you must own both this project and \`${targetProject}\`
26919
+
26920
+ This is a cross-project write and cannot be auto-undone. To proceed:
26921
+ \`task_move task_id="${taskId}" target_project="${targetProject}" confirm=true\``
26922
+ );
26923
+ }
26924
+ try {
26925
+ const res = await adapter2.moveTask(taskId, targetProject, callerUserId);
26926
+ return textResponse(
26927
+ `\u2705 Moved **${taskId}** from ${res.sourceName} to ${res.targetName} as **${res.newDisplayId}**.
26928
+
26929
+ Its build reports, comments, and history moved with it; the cycle assignment was cleared, so it sits in ${res.targetName}'s backlog. Switch to ${res.targetName} to plan or build it.`
26930
+ );
26931
+ } catch (err) {
26932
+ return errorResponse(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
26933
+ }
26934
+ }
26935
+
26414
26936
  // src/services/harness-inventory.ts
26415
26937
  import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
26416
- import { join as join16 } from "path";
26938
+ import { join as join17 } from "path";
26417
26939
  import { createHash as createHash3 } from "crypto";
26418
26940
  var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
26419
26941
  async function computeFingerprint(root) {
26420
26942
  const parts = [];
26421
26943
  for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
26422
- const dir = join16(root, sub);
26944
+ const dir = join17(root, sub);
26423
26945
  try {
26424
26946
  const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
26425
26947
  for (const name of names) {
26426
26948
  let mtime = "";
26427
26949
  try {
26428
- mtime = String(Math.floor((await stat3(join16(dir, name))).mtimeMs));
26950
+ mtime = String(Math.floor((await stat3(join17(dir, name))).mtimeMs));
26429
26951
  } catch {
26430
26952
  }
26431
26953
  parts.push(`${sub}/${name}:${mtime}`);
@@ -26444,7 +26966,7 @@ async function computeFingerprint(root) {
26444
26966
  }
26445
26967
  async function readSkillDescription(skillDir) {
26446
26968
  try {
26447
- const content = await readFile8(join16(skillDir, "SKILL.md"), "utf-8");
26969
+ const content = await readFile8(join17(skillDir, "SKILL.md"), "utf-8");
26448
26970
  const fm = content.match(/^---\n([\s\S]*?)\n---/);
26449
26971
  if (!fm) return void 0;
26450
26972
  const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
@@ -26464,17 +26986,17 @@ async function scanInventory(root, toolDefs) {
26464
26986
  version = loadManifest().packageVersion;
26465
26987
  } catch {
26466
26988
  }
26467
- const skillsDir = join16(root, ".claude", "skills");
26989
+ const skillsDir = join17(root, ".claude", "skills");
26468
26990
  try {
26469
26991
  const dirents = await readdir3(skillsDir, { withFileTypes: true });
26470
26992
  for (const d of dirents.filter((e) => e.isDirectory())) {
26471
26993
  entries.push({
26472
26994
  kind: "skill",
26473
26995
  name: d.name,
26474
- description: await readSkillDescription(join16(skillsDir, d.name)),
26996
+ description: await readSkillDescription(join17(skillsDir, d.name)),
26475
26997
  version,
26476
26998
  status: stale.has(d.name) ? "stale_fork" : "ok",
26477
- path: join16(".claude", "skills", d.name)
26999
+ path: join17(".claude", "skills", d.name)
26478
27000
  });
26479
27001
  }
26480
27002
  } catch {
@@ -26490,9 +27012,9 @@ async function scanInventory(root, toolDefs) {
26490
27012
  }
26491
27013
  const present = /* @__PURE__ */ new Set();
26492
27014
  try {
26493
- for (const f of (await readdir3(join16(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
27015
+ for (const f of (await readdir3(join17(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
26494
27016
  present.add(f);
26495
- entries.push({ kind: "hook", name: f, status: "ok", path: join16(".claude", "hooks", f) });
27017
+ entries.push({ kind: "hook", name: f, status: "ok", path: join17(".claude", "hooks", f) });
26496
27018
  }
26497
27019
  } catch {
26498
27020
  }
@@ -26571,6 +27093,133 @@ async function handleInventorySync(adapter2, config2, args, toolDefs) {
26571
27093
  return textResponse(lines.join("\n"));
26572
27094
  }
26573
27095
 
27096
+ // src/resources.ts
27097
+ var SCHEME = "mcp://papi";
27098
+ var RESOURCE_TEMPLATES = [
27099
+ {
27100
+ uriTemplate: `${SCHEME}/decisions/{adId}`,
27101
+ name: "Active Decision",
27102
+ description: "A single Active Decision (e.g. AD-12) \u2014 title, confidence, body, and supersession status.",
27103
+ mimeType: "text/markdown"
27104
+ },
27105
+ {
27106
+ uriTemplate: `${SCHEME}/build-reports/cycle-{cycle}`,
27107
+ name: "Cycle build reports",
27108
+ description: "Every build report recorded for a given cycle number (e.g. cycle-310).",
27109
+ mimeType: "text/markdown"
27110
+ },
27111
+ {
27112
+ uriTemplate: `${SCHEME}/cycles/{cycle}`,
27113
+ name: "Cycle summary",
27114
+ description: "Summary, task count, effort, and carry-forward for a given cycle number.",
27115
+ mimeType: "text/markdown"
27116
+ },
27117
+ {
27118
+ uriTemplate: `${SCHEME}/handoffs/{taskId}`,
27119
+ name: "Build handoff",
27120
+ description: "The read-only BUILD HANDOFF for a task (e.g. task-1801).",
27121
+ mimeType: "text/markdown"
27122
+ }
27123
+ ];
27124
+ async function listPapiResources(adapter2) {
27125
+ const out = [];
27126
+ try {
27127
+ const ads = await adapter2.getActiveDecisions();
27128
+ for (const d of ads.filter((d2) => !d2.superseded).slice(0, 50)) {
27129
+ out.push({
27130
+ uri: `${SCHEME}/decisions/${d.id}`,
27131
+ name: `${d.id}: ${d.title}`,
27132
+ description: `Active Decision [${d.confidence}]`,
27133
+ mimeType: "text/markdown"
27134
+ });
27135
+ }
27136
+ } catch {
27137
+ }
27138
+ try {
27139
+ const log2 = await adapter2.getCycleLog(10);
27140
+ for (const e of log2) {
27141
+ out.push({
27142
+ uri: `${SCHEME}/cycles/${e.cycleNumber}`,
27143
+ name: `Cycle ${e.cycleNumber} summary`,
27144
+ description: e.title,
27145
+ mimeType: "text/markdown"
27146
+ });
27147
+ out.push({
27148
+ uri: `${SCHEME}/build-reports/cycle-${e.cycleNumber}`,
27149
+ name: `Cycle ${e.cycleNumber} build reports`,
27150
+ mimeType: "text/markdown"
27151
+ });
27152
+ }
27153
+ } catch {
27154
+ }
27155
+ return out;
27156
+ }
27157
+ function parseCycleNumber(raw) {
27158
+ const m = raw.match(/(\d+)\s*$/);
27159
+ return m ? parseInt(m[1], 10) : NaN;
27160
+ }
27161
+ async function readPapiResource(adapter2, uri) {
27162
+ if (!uri.startsWith(`${SCHEME}/`)) {
27163
+ throw new Error(`Unknown resource URI: ${uri}. Expected ${SCHEME}/...`);
27164
+ }
27165
+ const path7 = uri.slice(`${SCHEME}/`.length);
27166
+ const slash = path7.indexOf("/");
27167
+ const kind = slash === -1 ? path7 : path7.slice(0, slash);
27168
+ const rest = slash === -1 ? "" : path7.slice(slash + 1);
27169
+ const md = (text) => ({ uri, mimeType: "text/markdown", text });
27170
+ switch (kind) {
27171
+ case "decisions": {
27172
+ const adId = rest.trim();
27173
+ if (!adId) throw new Error(`Resource URI missing an AD id: ${uri}`);
27174
+ const ads = await adapter2.getActiveDecisions({ includeRetired: true });
27175
+ const target = ads.find((d) => d.id === adId);
27176
+ if (!target) throw new Error(`Active Decision not found in this project: ${adId}`);
27177
+ const supersededNote = target.superseded ? ` [SUPERSEDED by ${target.supersededBy ?? "unknown"}]` : "";
27178
+ const supersedes = ads.filter((d) => d.supersededBy === target.id).map((d) => d.id);
27179
+ const chain = supersedes.length > 0 ? `
27180
+
27181
+ _Supersedes: ${supersedes.join(", ")}_` : "";
27182
+ return md(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
27183
+
27184
+ ${target.body}${chain}`);
27185
+ }
27186
+ case "build-reports": {
27187
+ const cycle = parseCycleNumber(rest);
27188
+ if (Number.isNaN(cycle)) throw new Error(`Resource URI missing a cycle number: ${uri}`);
27189
+ const reports = (await adapter2.getBuildReportsSince(cycle)).filter((r) => r.cycle === cycle);
27190
+ return md(`# Build Reports \u2014 Cycle ${cycle} (${reports.length})
27191
+
27192
+ ${formatBuildReports(reports)}`);
27193
+ }
27194
+ case "cycles": {
27195
+ const cycle = parseCycleNumber(rest);
27196
+ if (Number.isNaN(cycle)) throw new Error(`Resource URI missing a cycle number: ${uri}`);
27197
+ const entry = (await adapter2.getCycleLogSince(cycle)).find((e) => e.cycleNumber === cycle);
27198
+ if (!entry) throw new Error(`No cycle log entry found in this project for cycle ${cycle}`);
27199
+ return md(formatCycleLog([entry]));
27200
+ }
27201
+ case "handoffs": {
27202
+ const taskId = rest.trim();
27203
+ if (!taskId) throw new Error(`Resource URI missing a task id: ${uri}`);
27204
+ const task = await adapter2.getTask(taskId);
27205
+ if (!task) throw new Error(`Task not found in this project: ${taskId}`);
27206
+ if (!task.buildHandoff) {
27207
+ return md(`# ${taskId}: ${task.title}
27208
+
27209
+ _No BUILD HANDOFF recorded for this task._`);
27210
+ }
27211
+ return md(`# BUILD HANDOFF \u2014 ${taskId}: ${task.title}
27212
+
27213
+ ${serializeBuildHandoff(task.buildHandoff)}`);
27214
+ }
27215
+ default:
27216
+ throw new Error(`Unknown resource family "${kind}" in ${uri}. Known: decisions, build-reports, cycles, handoffs.`);
27217
+ }
27218
+ }
27219
+
27220
+ // src/server.ts
27221
+ init_telemetry();
27222
+
26574
27223
  // src/services/metering.ts
26575
27224
  var TIER_ALLOWANCES = {
26576
27225
  free: 1e3,
@@ -26791,32 +27440,36 @@ var PAPI_TOOLS = [
26791
27440
  contributorListTool,
26792
27441
  taskClaimTool,
26793
27442
  taskUnclaimTool,
27443
+ taskMoveTool,
26794
27444
  inventorySyncTool
26795
27445
  ];
26796
27446
  function getToolMetadata() {
26797
27447
  return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
26798
27448
  }
26799
27449
  function createServer(adapter2, config2) {
26800
- const __pkgFilename = fileURLToPath2(import.meta.url);
26801
- const __pkgDir = dirname4(__pkgFilename);
27450
+ const __pkgFilename = fileURLToPath3(import.meta.url);
27451
+ const __pkgDir = dirname5(__pkgFilename);
26802
27452
  let serverVersion = "unknown";
26803
27453
  try {
26804
- const pkg = JSON.parse(readFileSync9(join17(__pkgDir, "..", "package.json"), "utf-8"));
27454
+ const pkg = JSON.parse(readFileSync10(join18(__pkgDir, "..", "package.json"), "utf-8"));
26805
27455
  serverVersion = pkg.version ?? "unknown";
26806
27456
  } catch {
26807
27457
  }
26808
27458
  const server2 = new Server(
26809
27459
  { name: "papi", version: serverVersion },
26810
- { capabilities: { tools: {}, prompts: {} } }
27460
+ // task-2305: `instructions` is surfaced in the MCP initialize response, so every
27461
+ // connecting host LLM gets PAPI's cycle frame on connect — zero config, any harness.
27462
+ // task-1801: `resources` capability for the PAPI read surface exposed as MCP resources.
27463
+ { capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: UNIVERSAL_FRAME }
26811
27464
  );
26812
27465
  if (config2.adapterType === "md") {
26813
27466
  process.stderr.write(
26814
27467
  "\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
26815
27468
  );
26816
27469
  }
26817
- const __filename = fileURLToPath2(import.meta.url);
26818
- const __dirname2 = dirname4(__filename);
26819
- const skillsDir = join17(__dirname2, "..", "skills");
27470
+ const __filename = fileURLToPath3(import.meta.url);
27471
+ const __dirname2 = dirname5(__filename);
27472
+ const skillsDir = join18(__dirname2, "..", "skills");
26820
27473
  function parseSkillFrontmatter(content) {
26821
27474
  const match = content.match(/^---\n([\s\S]*?)\n---/);
26822
27475
  if (!match) return null;
@@ -26834,7 +27487,7 @@ function createServer(adapter2, config2) {
26834
27487
  const mdFiles = files.filter((f) => f.endsWith(".md"));
26835
27488
  const prompts = [];
26836
27489
  for (const file of mdFiles) {
26837
- const content = await readFile9(join17(skillsDir, file), "utf-8");
27490
+ const content = await readFile9(join18(skillsDir, file), "utf-8");
26838
27491
  const meta = parseSkillFrontmatter(content);
26839
27492
  if (meta) {
26840
27493
  prompts.push({ name: meta.name, description: meta.description });
@@ -26850,7 +27503,7 @@ function createServer(adapter2, config2) {
26850
27503
  try {
26851
27504
  const files = await readdir4(skillsDir);
26852
27505
  for (const file of files.filter((f) => f.endsWith(".md"))) {
26853
- const content = await readFile9(join17(skillsDir, file), "utf-8");
27506
+ const content = await readFile9(join18(skillsDir, file), "utf-8");
26854
27507
  const meta = parseSkillFrontmatter(content);
26855
27508
  if (meta?.name === name) {
26856
27509
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -26864,6 +27517,15 @@ function createServer(adapter2, config2) {
26864
27517
  }
26865
27518
  throw new Error(`Prompt not found: ${name}`);
26866
27519
  });
27520
+ server2.setRequestHandler(ListResourcesRequestSchema, async () => ({
27521
+ resources: await listPapiResources(adapter2)
27522
+ }));
27523
+ server2.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
27524
+ resourceTemplates: RESOURCE_TEMPLATES
27525
+ }));
27526
+ server2.setRequestHandler(ReadResourceRequestSchema, async (request) => ({
27527
+ contents: [await readPapiResource(adapter2, request.params.uri)]
27528
+ }));
26867
27529
  server2.setRequestHandler(ListToolsRequestSchema, async () => ({
26868
27530
  tools: [...PAPI_TOOLS]
26869
27531
  }));
@@ -26921,7 +27583,7 @@ function createServer(adapter2, config2) {
26921
27583
  case "build_describe":
26922
27584
  return handleBuildDescribe(adapter2, safeArgs);
26923
27585
  case "build_execute":
26924
- return handleBuildExecute(adapter2, config2, safeArgs);
27586
+ return handleBuildExecute(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26925
27587
  case "build_cancel":
26926
27588
  return handleBuildCancel(adapter2, safeArgs);
26927
27589
  case "idea":
@@ -26943,9 +27605,9 @@ function createServer(adapter2, config2) {
26943
27605
  case "init":
26944
27606
  return handleInit(config2, safeArgs);
26945
27607
  case "orient":
26946
- return handleOrient(adapter2, config2, safeArgs);
27608
+ return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26947
27609
  case "papi":
26948
- return handleOrient(adapter2, config2, safeArgs);
27610
+ return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26949
27611
  // alias (task-2223)
26950
27612
  case "hierarchy_update":
26951
27613
  return handleHierarchyUpdate(adapter2, safeArgs);
@@ -26987,6 +27649,8 @@ function createServer(adapter2, config2) {
26987
27649
  return handleContributorList(adapter2, config2, safeArgs);
26988
27650
  case "task_claim":
26989
27651
  return handleTaskClaim(adapter2, config2, safeArgs);
27652
+ case "task_move":
27653
+ return handleTaskMove(adapter2, config2, safeArgs);
26990
27654
  case "task_unclaim":
26991
27655
  return handleTaskUnclaim(adapter2, config2, safeArgs);
26992
27656
  case "inventory_sync":
@@ -27506,10 +28170,10 @@ async function dispatchRequest(args) {
27506
28170
  }
27507
28171
 
27508
28172
  // src/index.ts
27509
- var __dirname = dirname5(fileURLToPath3(import.meta.url));
28173
+ var __dirname = dirname6(fileURLToPath4(import.meta.url));
27510
28174
  var pkgVersion = "unknown";
27511
28175
  try {
27512
- const pkg = JSON.parse(readFileSync14(join22(__dirname, "..", "package.json"), "utf-8"));
28176
+ const pkg = JSON.parse(readFileSync15(join23(__dirname, "..", "package.json"), "utf-8"));
27513
28177
  pkgVersion = pkg.version;
27514
28178
  } catch {
27515
28179
  }