@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/design-assets/agents/frontend-design-engineer.md +141 -0
- package/design-assets/hooks/frontend-design-guard.sh +52 -0
- package/design-assets/skills/design-critique/SKILL.md +173 -0
- package/dist/backfill-cycle-metrics.js +116 -3
- package/dist/index.js +1175 -511
- package/package.json +3 -2
- package/skills/papi-cycle/papi-advanced/SKILL.md +5 -10
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
|
-
}).
|
|
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
|
|
1944
|
+
let resolve3, reject;
|
|
1716
1945
|
super((a, b2) => {
|
|
1717
|
-
|
|
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,
|
|
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((
|
|
2005
|
+
const promise = new Promise((resolve3, reject) => {
|
|
1777
2006
|
this.cursorFn = (value) => {
|
|
1778
|
-
|
|
2007
|
+
resolve3({ value, done: false });
|
|
1779
2008
|
return new Promise((r) => prev = r);
|
|
1780
2009
|
};
|
|
1781
|
-
this.resolve = () => (this.active = false,
|
|
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 },
|
|
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",
|
|
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 (
|
|
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
|
-
|
|
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((
|
|
3593
|
-
const query = { reserve:
|
|
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((
|
|
3859
|
+
result = await new Promise((resolve3, reject) => {
|
|
3631
3860
|
const x = fn2(sql2);
|
|
3632
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
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((
|
|
3690
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
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(
|
|
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
|
-
|
|
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
|
|
4126
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
|
|
3898
4127
|
import { homedir as homedir4 } from "os";
|
|
3899
|
-
import { join as
|
|
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
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
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 (!
|
|
4144
|
+
if (!existsSync10(path7)) continue;
|
|
3916
4145
|
try {
|
|
3917
|
-
const raw =
|
|
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
|
|
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
|
|
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 (!
|
|
4426
|
+
if (!existsSync11(path7)) continue;
|
|
4198
4427
|
let raw;
|
|
4199
4428
|
let parsed;
|
|
4200
4429
|
try {
|
|
4201
|
-
raw =
|
|
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
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
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
|
|
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
|
|
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 =
|
|
4321
|
-
if (!
|
|
4549
|
+
const path7 = join21(projectPath, ".mcp.json");
|
|
4550
|
+
if (!existsSync12(path7)) return { servers: [] };
|
|
4322
4551
|
try {
|
|
4323
|
-
const parsed = JSON.parse(
|
|
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(
|
|
4356
|
-
agentSkills: safeListDirs(
|
|
4357
|
-
agents: safeListFiles(
|
|
4358
|
-
hooks: safeListFiles(
|
|
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 =
|
|
4366
|
-
if (
|
|
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 (!
|
|
4606
|
+
if (!existsSync12(GLOBAL_CLAUDE_JSON)) return [];
|
|
4378
4607
|
try {
|
|
4379
|
-
const parsed = JSON.parse(
|
|
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 = [
|
|
4532
|
-
GLOBAL_SKILLS_DIR =
|
|
4533
|
-
GLOBAL_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
|
|
4546
|
-
import { join as
|
|
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((
|
|
4804
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4576
4805
|
}
|
|
4577
4806
|
function writeMcpJson(opts) {
|
|
4578
|
-
const path7 =
|
|
4807
|
+
const path7 = join22(process.cwd(), ".mcp.json");
|
|
4579
4808
|
let parsed = {};
|
|
4580
|
-
if (
|
|
4809
|
+
if (existsSync13(path7)) {
|
|
4581
4810
|
try {
|
|
4582
|
-
parsed = JSON.parse(
|
|
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 =
|
|
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
|
|
4714
|
-
import { dirname as
|
|
4715
|
-
import { fileURLToPath as
|
|
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
|
|
7377
|
-
var HOSTED_PROXY_ENDPOINT = `${
|
|
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
|
|
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
|
|
7715
|
-
import { fileURLToPath as
|
|
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
|
|
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 <
|
|
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 <
|
|
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:
|
|
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:
|
|
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:
|
|
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 >=
|
|
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
|
|
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 >=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
16221
|
+
import { join as join4 } from "path";
|
|
15917
16222
|
function detectCodebaseType(projectRoot) {
|
|
15918
|
-
if (existsSync2(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
16260
|
+
var PROJECT_BUNDLE_REL = join5(".agents", "skills", "papi-cycle");
|
|
15956
16261
|
function bundleDestRel(rel) {
|
|
15957
|
-
return rel === "AGENTS.md" ? "AGENTS.md" :
|
|
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 =
|
|
15963
|
-
if (existsSync3(
|
|
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 =
|
|
15976
|
-
const childRel = rel ?
|
|
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:
|
|
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 =
|
|
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/
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
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
|
-
|
|
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(
|
|
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 :
|
|
16434
|
-
const docsDir = useCollector ? docsRel :
|
|
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" :
|
|
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` :
|
|
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` :
|
|
16459
|
-
[useCollector ? `${commandsRel}/test.md` :
|
|
16460
|
-
[useCollector ? `${docsRel}/README.md` :
|
|
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(
|
|
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 =
|
|
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 =
|
|
16496
|
-
const cursorRulesPath =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
17683
|
+
await access2(join8(projectRoot, ".git"));
|
|
17177
17684
|
} catch {
|
|
17178
17685
|
return void 0;
|
|
17179
17686
|
}
|
|
17180
|
-
const gitignorePath =
|
|
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/
|
|
17564
|
-
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17578
|
-
|
|
17579
|
-
|
|
17580
|
-
|
|
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
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
}
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
|
|
17605
|
-
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
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/
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17625
|
-
|
|
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
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
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
|
|
17658
|
-
|
|
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
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
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
|
|
17692
|
-
if (
|
|
17693
|
-
const
|
|
17694
|
-
return
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
19299
|
-
if (!
|
|
19812
|
+
const papiDir = join10(projectRoot, ".papi");
|
|
19813
|
+
if (!existsSync6(papiDir)) {
|
|
19300
19814
|
mkdirSync2(papiDir, { recursive: true });
|
|
19301
19815
|
}
|
|
19302
|
-
const scopePath =
|
|
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 =
|
|
19307
|
-
if (
|
|
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 =
|
|
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 =
|
|
19673
|
-
if (
|
|
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 =
|
|
20190
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
19677
20191
|
const files = [];
|
|
19678
20192
|
for (const e of entries) {
|
|
19679
|
-
const full =
|
|
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(
|
|
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
|
-
|
|
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
|
|
21054
|
-
import { join as
|
|
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 =
|
|
21059
|
-
const raw =
|
|
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
|
|
21635
|
-
import { join as
|
|
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 =
|
|
21848
|
-
if (
|
|
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
|
-
${
|
|
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 =
|
|
22007
|
-
if (
|
|
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 >=
|
|
23038
|
-
const
|
|
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
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
23498
|
-
import { join as
|
|
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 && !
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
23754
|
-
if (
|
|
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(
|
|
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(
|
|
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
|
|
23965
|
-
import { join as
|
|
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.
|
|
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:
|
|
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
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
|
|
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 =
|
|
24266
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
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 =
|
|
24908
|
-
if (!
|
|
24909
|
-
const content =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
26996
|
+
description: await readSkillDescription(join17(skillsDir, d.name)),
|
|
26475
26997
|
version,
|
|
26476
26998
|
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
26477
|
-
path:
|
|
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(
|
|
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:
|
|
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 =
|
|
26801
|
-
const __pkgDir =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
26818
|
-
const __dirname2 =
|
|
26819
|
-
const skillsDir =
|
|
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(
|
|
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(
|
|
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 =
|
|
28173
|
+
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
27510
28174
|
var pkgVersion = "unknown";
|
|
27511
28175
|
try {
|
|
27512
|
-
const pkg = JSON.parse(
|
|
28176
|
+
const pkg = JSON.parse(readFileSync15(join23(__dirname, "..", "package.json"), "utf-8"));
|
|
27513
28177
|
pkgVersion = pkg.version;
|
|
27514
28178
|
} catch {
|
|
27515
28179
|
}
|