@papi-ai/server 0.7.38 → 0.7.41
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 +126 -0
- package/design-assets/hooks/frontend-design-guard.sh +52 -0
- package/design-assets/skills/design-critique/SKILL.md +162 -0
- package/dist/backfill-cycle-metrics.js +121 -0
- package/dist/index.js +1110 -358
- 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,
|
|
@@ -44,6 +45,7 @@ __export(git_exports, {
|
|
|
44
45
|
getStagedFiles: () => getStagedFiles,
|
|
45
46
|
getTagTarget: () => getTagTarget,
|
|
46
47
|
getTaskIdsOnBranch: () => getTaskIdsOnBranch,
|
|
48
|
+
getTrackedModifiedFiles: () => getTrackedModifiedFiles,
|
|
47
49
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
48
50
|
getUntrackedFiles: () => getUntrackedFiles,
|
|
49
51
|
gitPull: () => gitPull,
|
|
@@ -62,6 +64,7 @@ __export(git_exports, {
|
|
|
62
64
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
63
65
|
pickModuleCycleBranch: () => pickModuleCycleBranch,
|
|
64
66
|
resolveBaseBranch: () => resolveBaseBranch,
|
|
67
|
+
resolveDefaultBranch: () => resolveDefaultBranch,
|
|
65
68
|
runAutoCommit: () => runAutoCommit,
|
|
66
69
|
squashMergePullRequest: () => squashMergePullRequest,
|
|
67
70
|
stageAllAndCommit: () => stageAllAndCommit,
|
|
@@ -176,6 +179,25 @@ function getModifiedFiles(cwd) {
|
|
|
176
179
|
return [];
|
|
177
180
|
}
|
|
178
181
|
}
|
|
182
|
+
function getTrackedModifiedFiles(cwd) {
|
|
183
|
+
try {
|
|
184
|
+
const out = execFileSync("git", ["status", "--porcelain"], {
|
|
185
|
+
cwd,
|
|
186
|
+
encoding: "utf-8"
|
|
187
|
+
}).replace(/\n+$/, "");
|
|
188
|
+
if (!out) return [];
|
|
189
|
+
const paths = /* @__PURE__ */ new Set();
|
|
190
|
+
for (const line of out.split("\n")) {
|
|
191
|
+
if (line.length < 4) continue;
|
|
192
|
+
if (line.startsWith("??")) continue;
|
|
193
|
+
const path7 = line.slice(3).trim();
|
|
194
|
+
if (path7) paths.add(path7);
|
|
195
|
+
}
|
|
196
|
+
return Array.from(paths);
|
|
197
|
+
} catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
179
201
|
function getUntrackedFiles(cwd) {
|
|
180
202
|
try {
|
|
181
203
|
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
@@ -640,6 +662,22 @@ function resolveBaseBranch(cwd, preferred) {
|
|
|
640
662
|
if (preferred !== "master" && branchExists(cwd, "master")) return "master";
|
|
641
663
|
return preferred;
|
|
642
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
|
+
}
|
|
643
681
|
function detectBoardMismatches(cwd, tasks) {
|
|
644
682
|
const empty = { codeAhead: [], staleInProgress: [] };
|
|
645
683
|
if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
|
|
@@ -673,6 +711,46 @@ function detectBoardMismatches(cwd, tasks) {
|
|
|
673
711
|
return empty;
|
|
674
712
|
}
|
|
675
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
|
+
}
|
|
676
754
|
function detectUnrecordedCommits(cwd, baseBranch) {
|
|
677
755
|
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
678
756
|
const latestTag = getLatestTag(cwd);
|
|
@@ -1236,6 +1314,13 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1236
1314
|
updateTaskStatus(id, status) {
|
|
1237
1315
|
return this.invoke("updateTaskStatus", [id, status]);
|
|
1238
1316
|
}
|
|
1317
|
+
// task-2292: cross-project move. The bound projectId (set on this adapter) is
|
|
1318
|
+
// the SOURCE; the target is passed in args. Ownership of BOTH projects is
|
|
1319
|
+
// verified server-side in the edge function from the bearer — callerUserId is
|
|
1320
|
+
// ignored here (cannot be spoofed). The edge resolves a target slug → UUID.
|
|
1321
|
+
moveTask(taskId, targetProject, _callerUserId) {
|
|
1322
|
+
return this.invoke("moveTask", [taskId, targetProject]);
|
|
1323
|
+
}
|
|
1239
1324
|
// task-2155 (MU-3 task B): hosted/proxy claim write-path. The data-proxy gates
|
|
1240
1325
|
// these via WRITE_METHODS (active editor may write, viewer cannot) and binds the
|
|
1241
1326
|
// assignee to the bearer-derived caller server-side — the assigneeId passed here
|
|
@@ -1325,6 +1410,33 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1325
1410
|
appendToolMetric(metric) {
|
|
1326
1411
|
return this.invoke("appendToolMetric", [metric]);
|
|
1327
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* task-2288 (C308): emit a telemetry event via the proxy's /telemetry endpoint
|
|
1415
|
+
* using THIS adapter's per-request bearer (this.apiKey) — the same credential
|
|
1416
|
+
* invoke() uses. This is the fix for the hosted-transport blackout: on the
|
|
1417
|
+
* multi-tenant Railway pod there is no PAPI_DATA_API_KEY env var, so the
|
|
1418
|
+
* env-var emitTelemetryEvent path (lib/telemetry.ts) dropped every event since
|
|
1419
|
+
* 2026-06-16. Fire-and-forget — never throws, never blocks the tool response.
|
|
1420
|
+
* Not routed through invoke() because /telemetry is a distinct endpoint and a
|
|
1421
|
+
* telemetry failure must never surface as a tool error.
|
|
1422
|
+
*/
|
|
1423
|
+
emitTelemetry(event) {
|
|
1424
|
+
fetch(`${this.endpoint}/telemetry`, {
|
|
1425
|
+
method: "POST",
|
|
1426
|
+
headers: {
|
|
1427
|
+
"Content-Type": "application/json",
|
|
1428
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1429
|
+
},
|
|
1430
|
+
body: JSON.stringify({
|
|
1431
|
+
projectId: event.projectId,
|
|
1432
|
+
toolName: event.toolName,
|
|
1433
|
+
eventType: event.eventType,
|
|
1434
|
+
metadata: event.metadata ?? {}
|
|
1435
|
+
}),
|
|
1436
|
+
signal: AbortSignal.timeout(5e3)
|
|
1437
|
+
}).catch(() => {
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1328
1440
|
readToolMetrics() {
|
|
1329
1441
|
return this.invoke("readToolMetrics");
|
|
1330
1442
|
}
|
|
@@ -1476,6 +1588,15 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1476
1588
|
submitBugReport(report) {
|
|
1477
1589
|
return this.invoke("submitBugReport", [report]);
|
|
1478
1590
|
}
|
|
1591
|
+
// task-2270: notify-back. The data-proxy ignores the client-supplied userId
|
|
1592
|
+
// and scopes to the bearer-validated caller (same pattern as the owner-action
|
|
1593
|
+
// counts), so a user can only ever read/mark their OWN resolved submissions.
|
|
1594
|
+
getUnnotifiedResolvedFeedback(userId) {
|
|
1595
|
+
return this.invoke("getUnnotifiedResolvedFeedback", [userId]);
|
|
1596
|
+
}
|
|
1597
|
+
markFeedbackNotified(ids, userId) {
|
|
1598
|
+
return this.invoke("markFeedbackNotified", [ids, userId]);
|
|
1599
|
+
}
|
|
1479
1600
|
// --- Doc Registry ---
|
|
1480
1601
|
registerDoc(entry) {
|
|
1481
1602
|
return this.invoke("registerDoc", [entry]);
|
|
@@ -1656,9 +1777,9 @@ var init_query = __esm({
|
|
|
1656
1777
|
CLOSE = {};
|
|
1657
1778
|
Query = class extends Promise {
|
|
1658
1779
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
1659
|
-
let
|
|
1780
|
+
let resolve3, reject;
|
|
1660
1781
|
super((a, b2) => {
|
|
1661
|
-
|
|
1782
|
+
resolve3 = a;
|
|
1662
1783
|
reject = b2;
|
|
1663
1784
|
});
|
|
1664
1785
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -1669,7 +1790,7 @@ var init_query = __esm({
|
|
|
1669
1790
|
this.options = options;
|
|
1670
1791
|
this.state = null;
|
|
1671
1792
|
this.statement = null;
|
|
1672
|
-
this.resolve = (x) => (this.active = false,
|
|
1793
|
+
this.resolve = (x) => (this.active = false, resolve3(x));
|
|
1673
1794
|
this.reject = (x) => (this.active = false, reject(x));
|
|
1674
1795
|
this.active = false;
|
|
1675
1796
|
this.cancelled = null;
|
|
@@ -1717,12 +1838,12 @@ var init_query = __esm({
|
|
|
1717
1838
|
if (this.executed && !this.active)
|
|
1718
1839
|
return { done: true };
|
|
1719
1840
|
prev && prev();
|
|
1720
|
-
const promise = new Promise((
|
|
1841
|
+
const promise = new Promise((resolve3, reject) => {
|
|
1721
1842
|
this.cursorFn = (value) => {
|
|
1722
|
-
|
|
1843
|
+
resolve3({ value, done: false });
|
|
1723
1844
|
return new Promise((r) => prev = r);
|
|
1724
1845
|
};
|
|
1725
|
-
this.resolve = () => (this.active = false,
|
|
1846
|
+
this.resolve = () => (this.active = false, resolve3({ done: true }));
|
|
1726
1847
|
this.reject = (x) => (this.active = false, reject(x));
|
|
1727
1848
|
});
|
|
1728
1849
|
this.execute();
|
|
@@ -2320,12 +2441,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
2320
2441
|
x.on("drain", drain);
|
|
2321
2442
|
return x;
|
|
2322
2443
|
}
|
|
2323
|
-
async function cancel({ pid, secret },
|
|
2444
|
+
async function cancel({ pid, secret }, resolve3, reject) {
|
|
2324
2445
|
try {
|
|
2325
2446
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
2326
2447
|
await connect();
|
|
2327
2448
|
socket.once("error", reject);
|
|
2328
|
-
socket.once("close",
|
|
2449
|
+
socket.once("close", resolve3);
|
|
2329
2450
|
} catch (error2) {
|
|
2330
2451
|
reject(error2);
|
|
2331
2452
|
}
|
|
@@ -3342,7 +3463,7 @@ var init_subscribe = __esm({
|
|
|
3342
3463
|
// ../../node_modules/postgres/src/large.js
|
|
3343
3464
|
import Stream2 from "stream";
|
|
3344
3465
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3345
|
-
return new Promise(async (
|
|
3466
|
+
return new Promise(async (resolve3, reject) => {
|
|
3346
3467
|
await sql.begin(async (sql2) => {
|
|
3347
3468
|
let finish;
|
|
3348
3469
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -3368,7 +3489,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3368
3489
|
) seek
|
|
3369
3490
|
`
|
|
3370
3491
|
};
|
|
3371
|
-
|
|
3492
|
+
resolve3(lo);
|
|
3372
3493
|
return new Promise(async (r) => finish = r);
|
|
3373
3494
|
async function readable({
|
|
3374
3495
|
highWaterMark = 2048 * 8,
|
|
@@ -3533,8 +3654,8 @@ function Postgres(a, b2) {
|
|
|
3533
3654
|
}
|
|
3534
3655
|
async function reserve() {
|
|
3535
3656
|
const queue = queue_default();
|
|
3536
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
3537
|
-
const query = { reserve:
|
|
3657
|
+
const c = open.length ? open.shift() : await new Promise((resolve3, reject) => {
|
|
3658
|
+
const query = { reserve: resolve3, reject };
|
|
3538
3659
|
queries.push(query);
|
|
3539
3660
|
closed.length && connect(closed.shift(), query);
|
|
3540
3661
|
});
|
|
@@ -3571,9 +3692,9 @@ function Postgres(a, b2) {
|
|
|
3571
3692
|
let uncaughtError, result;
|
|
3572
3693
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
3573
3694
|
try {
|
|
3574
|
-
result = await new Promise((
|
|
3695
|
+
result = await new Promise((resolve3, reject) => {
|
|
3575
3696
|
const x = fn2(sql2);
|
|
3576
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
3697
|
+
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve3, reject);
|
|
3577
3698
|
});
|
|
3578
3699
|
if (uncaughtError)
|
|
3579
3700
|
throw uncaughtError;
|
|
@@ -3630,8 +3751,8 @@ function Postgres(a, b2) {
|
|
|
3630
3751
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
3631
3752
|
}
|
|
3632
3753
|
function cancel(query) {
|
|
3633
|
-
return new Promise((
|
|
3634
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
3754
|
+
return new Promise((resolve3, reject) => {
|
|
3755
|
+
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());
|
|
3635
3756
|
});
|
|
3636
3757
|
}
|
|
3637
3758
|
async function end({ timeout = null } = {}) {
|
|
@@ -3650,11 +3771,11 @@ function Postgres(a, b2) {
|
|
|
3650
3771
|
async function close() {
|
|
3651
3772
|
await Promise.all(connections.map((c) => c.end()));
|
|
3652
3773
|
}
|
|
3653
|
-
async function destroy(
|
|
3774
|
+
async function destroy(resolve3) {
|
|
3654
3775
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
3655
3776
|
while (queries.length)
|
|
3656
3777
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
3657
|
-
|
|
3778
|
+
resolve3();
|
|
3658
3779
|
}
|
|
3659
3780
|
function connect(c, query) {
|
|
3660
3781
|
move(c, connecting);
|
|
@@ -3838,9 +3959,9 @@ __export(doctor_exports, {
|
|
|
3838
3959
|
__testing: () => __testing,
|
|
3839
3960
|
runDoctor: () => runDoctor
|
|
3840
3961
|
});
|
|
3841
|
-
import { existsSync as
|
|
3962
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
|
|
3842
3963
|
import { homedir as homedir4 } from "os";
|
|
3843
|
-
import { join as
|
|
3964
|
+
import { join as join19 } from "path";
|
|
3844
3965
|
function redact(name, value) {
|
|
3845
3966
|
if (!value) return "(empty)";
|
|
3846
3967
|
if (SECRET_VARS.has(name)) {
|
|
@@ -3851,14 +3972,14 @@ function redact(name, value) {
|
|
|
3851
3972
|
}
|
|
3852
3973
|
function findMcpJson() {
|
|
3853
3974
|
const candidates = [
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3975
|
+
join19(process.cwd(), ".mcp.json"),
|
|
3976
|
+
join19(homedir4(), ".claude", ".mcp.json"),
|
|
3977
|
+
join19(homedir4(), ".mcp.json")
|
|
3857
3978
|
];
|
|
3858
3979
|
for (const path7 of candidates) {
|
|
3859
|
-
if (!
|
|
3980
|
+
if (!existsSync10(path7)) continue;
|
|
3860
3981
|
try {
|
|
3861
|
-
const raw =
|
|
3982
|
+
const raw = readFileSync11(path7, "utf-8");
|
|
3862
3983
|
const parsed = JSON.parse(raw);
|
|
3863
3984
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
3864
3985
|
if (!papiEntry) continue;
|
|
@@ -4132,17 +4253,17 @@ __export(reset_exports, {
|
|
|
4132
4253
|
removePapiEntry: () => removePapiEntry,
|
|
4133
4254
|
runReset: () => runReset
|
|
4134
4255
|
});
|
|
4135
|
-
import { existsSync as
|
|
4256
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
4136
4257
|
import { homedir as homedir5 } from "os";
|
|
4137
|
-
import { join as
|
|
4258
|
+
import { join as join20 } from "path";
|
|
4138
4259
|
import { createInterface } from "readline/promises";
|
|
4139
4260
|
function findResetTarget() {
|
|
4140
4261
|
for (const path7 of CANDIDATE_PATHS()) {
|
|
4141
|
-
if (!
|
|
4262
|
+
if (!existsSync11(path7)) continue;
|
|
4142
4263
|
let raw;
|
|
4143
4264
|
let parsed;
|
|
4144
4265
|
try {
|
|
4145
|
-
raw =
|
|
4266
|
+
raw = readFileSync12(path7, "utf-8");
|
|
4146
4267
|
parsed = JSON.parse(raw);
|
|
4147
4268
|
} catch {
|
|
4148
4269
|
continue;
|
|
@@ -4230,9 +4351,9 @@ var init_reset = __esm({
|
|
|
4230
4351
|
"src/cli/reset.ts"() {
|
|
4231
4352
|
"use strict";
|
|
4232
4353
|
CANDIDATE_PATHS = () => [
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4354
|
+
join20(process.cwd(), ".mcp.json"),
|
|
4355
|
+
join20(homedir5(), ".claude", ".mcp.json"),
|
|
4356
|
+
join20(homedir5(), ".mcp.json")
|
|
4236
4357
|
];
|
|
4237
4358
|
}
|
|
4238
4359
|
});
|
|
@@ -4243,9 +4364,9 @@ __export(audit_exports, {
|
|
|
4243
4364
|
__testing: () => __testing2,
|
|
4244
4365
|
runAudit: () => runAudit
|
|
4245
4366
|
});
|
|
4246
|
-
import { existsSync as
|
|
4367
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
4247
4368
|
import { homedir as homedir6 } from "os";
|
|
4248
|
-
import { join as
|
|
4369
|
+
import { join as join21 } from "path";
|
|
4249
4370
|
function safeListDirs(dir) {
|
|
4250
4371
|
try {
|
|
4251
4372
|
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
@@ -4261,10 +4382,10 @@ function safeListFiles(dir, ext) {
|
|
|
4261
4382
|
}
|
|
4262
4383
|
}
|
|
4263
4384
|
function readMcp(projectPath) {
|
|
4264
|
-
const path7 =
|
|
4265
|
-
if (!
|
|
4385
|
+
const path7 = join21(projectPath, ".mcp.json");
|
|
4386
|
+
if (!existsSync12(path7)) return { servers: [] };
|
|
4266
4387
|
try {
|
|
4267
|
-
const parsed = JSON.parse(
|
|
4388
|
+
const parsed = JSON.parse(readFileSync13(path7, "utf-8"));
|
|
4268
4389
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4269
4390
|
const servers = Object.keys(mcpServers);
|
|
4270
4391
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4296,18 +4417,18 @@ function auditProjectSync(projectPath, name) {
|
|
|
4296
4417
|
path: projectPath,
|
|
4297
4418
|
papiProjectId,
|
|
4298
4419
|
mcpServers: servers,
|
|
4299
|
-
skills: safeListDirs(
|
|
4300
|
-
agentSkills: safeListDirs(
|
|
4301
|
-
agents: safeListFiles(
|
|
4302
|
-
hooks: safeListFiles(
|
|
4420
|
+
skills: safeListDirs(join21(projectPath, ".claude", "skills")),
|
|
4421
|
+
agentSkills: safeListDirs(join21(projectPath, ".agents", "skills")),
|
|
4422
|
+
agents: safeListFiles(join21(projectPath, ".claude", "agents"), ".md"),
|
|
4423
|
+
hooks: safeListFiles(join21(projectPath, ".claude", "hooks"), ".sh")
|
|
4303
4424
|
};
|
|
4304
4425
|
}
|
|
4305
4426
|
function discoverProjects() {
|
|
4306
4427
|
const out = [];
|
|
4307
4428
|
for (const root of PROJECT_ROOTS) {
|
|
4308
4429
|
for (const name of safeListDirs(root)) {
|
|
4309
|
-
const path7 =
|
|
4310
|
-
if (
|
|
4430
|
+
const path7 = join21(root, name);
|
|
4431
|
+
if (existsSync12(join21(path7, ".mcp.json")) || existsSync12(join21(path7, ".claude"))) {
|
|
4311
4432
|
out.push({ name, path: path7 });
|
|
4312
4433
|
}
|
|
4313
4434
|
}
|
|
@@ -4318,9 +4439,9 @@ function readGlobalSkills() {
|
|
|
4318
4439
|
return safeListDirs(GLOBAL_SKILLS_DIR);
|
|
4319
4440
|
}
|
|
4320
4441
|
function readGlobalMcpServers() {
|
|
4321
|
-
if (!
|
|
4442
|
+
if (!existsSync12(GLOBAL_CLAUDE_JSON)) return [];
|
|
4322
4443
|
try {
|
|
4323
|
-
const parsed = JSON.parse(
|
|
4444
|
+
const parsed = JSON.parse(readFileSync13(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4324
4445
|
const servers = parsed.mcpServers ?? {};
|
|
4325
4446
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4326
4447
|
} catch {
|
|
@@ -4472,9 +4593,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
|
|
|
4472
4593
|
var init_audit = __esm({
|
|
4473
4594
|
"src/cli/audit.ts"() {
|
|
4474
4595
|
"use strict";
|
|
4475
|
-
PROJECT_ROOTS = [
|
|
4476
|
-
GLOBAL_SKILLS_DIR =
|
|
4477
|
-
GLOBAL_CLAUDE_JSON =
|
|
4596
|
+
PROJECT_ROOTS = [join21(homedir6(), "Ai-App-Projects"), join21(homedir6(), "android-projects")];
|
|
4597
|
+
GLOBAL_SKILLS_DIR = join21(homedir6(), ".claude", "skills");
|
|
4598
|
+
GLOBAL_CLAUDE_JSON = join21(homedir6(), ".claude.json");
|
|
4478
4599
|
IDLE_WINDOW_DAYS = 30;
|
|
4479
4600
|
GLOBALIZE_THRESHOLD = 3;
|
|
4480
4601
|
__testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
|
|
@@ -4486,8 +4607,8 @@ var setup_exports = {};
|
|
|
4486
4607
|
__export(setup_exports, {
|
|
4487
4608
|
runSetup: () => runSetup
|
|
4488
4609
|
});
|
|
4489
|
-
import { existsSync as
|
|
4490
|
-
import { join as
|
|
4610
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync6, chmodSync as chmodSync2, statSync as statSync6 } from "fs";
|
|
4611
|
+
import { join as join22 } from "path";
|
|
4491
4612
|
function baseUrl() {
|
|
4492
4613
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
4493
4614
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
@@ -4516,14 +4637,14 @@ async function postJson(url, body) {
|
|
|
4516
4637
|
return { status: res.status, data };
|
|
4517
4638
|
}
|
|
4518
4639
|
function sleep(ms) {
|
|
4519
|
-
return new Promise((
|
|
4640
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4520
4641
|
}
|
|
4521
4642
|
function writeMcpJson(opts) {
|
|
4522
|
-
const path7 =
|
|
4643
|
+
const path7 = join22(process.cwd(), ".mcp.json");
|
|
4523
4644
|
let parsed = {};
|
|
4524
|
-
if (
|
|
4645
|
+
if (existsSync13(path7)) {
|
|
4525
4646
|
try {
|
|
4526
|
-
parsed = JSON.parse(
|
|
4647
|
+
parsed = JSON.parse(readFileSync14(path7, "utf-8"));
|
|
4527
4648
|
} catch {
|
|
4528
4649
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
4529
4650
|
}
|
|
@@ -4546,7 +4667,7 @@ function writeMcpJson(opts) {
|
|
|
4546
4667
|
parsed.mcpServers = mcpServers;
|
|
4547
4668
|
writeFileSync6(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
4548
4669
|
try {
|
|
4549
|
-
const mode =
|
|
4670
|
+
const mode = statSync6(path7).mode & 511;
|
|
4550
4671
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
4551
4672
|
} catch {
|
|
4552
4673
|
}
|
|
@@ -4654,9 +4775,9 @@ var init_setup = __esm({
|
|
|
4654
4775
|
});
|
|
4655
4776
|
|
|
4656
4777
|
// src/index.ts
|
|
4657
|
-
import { readFileSync as
|
|
4658
|
-
import { dirname as
|
|
4659
|
-
import { fileURLToPath as
|
|
4778
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
4779
|
+
import { dirname as dirname6, join as join23 } from "path";
|
|
4780
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4660
4781
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4661
4782
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4662
4783
|
import {
|
|
@@ -4688,6 +4809,8 @@ var HELP_FOOTER_MD = `
|
|
|
4688
4809
|
`;
|
|
4689
4810
|
|
|
4690
4811
|
// src/config.ts
|
|
4812
|
+
var STRATEGY_REVIEW_OFFER_GAP = 5;
|
|
4813
|
+
var STRATEGY_REVIEW_BLOCK_GAP = 7;
|
|
4691
4814
|
function loadConfig() {
|
|
4692
4815
|
const projectArgIdx = process.argv.indexOf("--project");
|
|
4693
4816
|
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
@@ -7653,18 +7776,36 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
7653
7776
|
}
|
|
7654
7777
|
|
|
7655
7778
|
// src/server.ts
|
|
7656
|
-
import { readFileSync as
|
|
7779
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7657
7780
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
7658
|
-
import { join as
|
|
7659
|
-
import { fileURLToPath as
|
|
7781
|
+
import { join as join18, dirname as dirname5 } from "path";
|
|
7782
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7660
7783
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7661
7784
|
import {
|
|
7662
7785
|
CallToolRequestSchema,
|
|
7663
7786
|
ListToolsRequestSchema,
|
|
7664
7787
|
ListPromptsRequestSchema,
|
|
7665
|
-
GetPromptRequestSchema
|
|
7788
|
+
GetPromptRequestSchema,
|
|
7789
|
+
ListResourcesRequestSchema,
|
|
7790
|
+
ListResourceTemplatesRequestSchema,
|
|
7791
|
+
ReadResourceRequestSchema
|
|
7666
7792
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7667
7793
|
|
|
7794
|
+
// src/universal-frame.ts
|
|
7795
|
+
var UNIVERSAL_FRAME = `PAPI gives this project a structured plan \u2192 build \u2192 review cycle, persisted across sessions. Follow it:
|
|
7796
|
+
|
|
7797
|
+
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.
|
|
7798
|
+
|
|
7799
|
+
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.
|
|
7800
|
+
|
|
7801
|
+
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.
|
|
7802
|
+
|
|
7803
|
+
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.
|
|
7804
|
+
|
|
7805
|
+
5. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
|
|
7806
|
+
|
|
7807
|
+
PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.`;
|
|
7808
|
+
|
|
7668
7809
|
// src/lib/response.ts
|
|
7669
7810
|
function textResponse(text, usage) {
|
|
7670
7811
|
const result = {
|
|
@@ -10985,7 +11126,7 @@ ${lines.join("\n")}`;
|
|
|
10985
11126
|
console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
|
|
10986
11127
|
const gap = health.cyclesSinceLastStrategyReview;
|
|
10987
11128
|
const lastReviewCycle = health.totalCycles - gap;
|
|
10988
|
-
const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap <
|
|
11129
|
+
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\`).`;
|
|
10989
11130
|
let ctx2 = {
|
|
10990
11131
|
mode,
|
|
10991
11132
|
cycleNumber: health.totalCycles,
|
|
@@ -11175,7 +11316,7 @@ ${logLines}`);
|
|
|
11175
11316
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
11176
11317
|
const gapFull = health.cyclesSinceLastStrategyReview;
|
|
11177
11318
|
const lastReviewCycleFull = health.totalCycles - gapFull;
|
|
11178
|
-
const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull <
|
|
11319
|
+
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\`).`;
|
|
11179
11320
|
let ctx = {
|
|
11180
11321
|
mode,
|
|
11181
11322
|
cycleNumber: health.totalCycles,
|
|
@@ -11779,15 +11920,15 @@ Run \`review_submit\` to clear them, or pass \`force: true\` to bypass this bloc
|
|
|
11779
11920
|
console.error(`[plan] ${note}`);
|
|
11780
11921
|
}
|
|
11781
11922
|
const gap = health.cyclesSinceLastStrategyReview;
|
|
11782
|
-
if (gap >=
|
|
11923
|
+
if (gap >= STRATEGY_REVIEW_BLOCK_GAP && !force) {
|
|
11783
11924
|
const lastReviewCycle = cycleNumber - gap;
|
|
11784
11925
|
throw new Error(
|
|
11785
|
-
`Strategy Review gate \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). A strategy review is required every
|
|
11926
|
+
`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.
|
|
11786
11927
|
|
|
11787
11928
|
Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
11788
11929
|
);
|
|
11789
11930
|
}
|
|
11790
|
-
if (gap >=
|
|
11931
|
+
if (gap >= STRATEGY_REVIEW_BLOCK_GAP && force) {
|
|
11791
11932
|
const lastReviewCycle = cycleNumber - gap;
|
|
11792
11933
|
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.
|
|
11793
11934
|
|
|
@@ -12360,8 +12501,6 @@ function getState(callerKey) {
|
|
|
12360
12501
|
function callerKeyFromConfig(config2) {
|
|
12361
12502
|
return config2.projectId ?? config2.userId ?? void 0;
|
|
12362
12503
|
}
|
|
12363
|
-
var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
|
|
12364
|
-
var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
|
|
12365
12504
|
var REVIEW_LIST_GUARD_WINDOW_MS = 15 * 60 * 1e3;
|
|
12366
12505
|
var FAILURE_WINDOW_MS = 30 * 60 * 1e3;
|
|
12367
12506
|
var FAILURE_RATE_THRESHOLD = 8;
|
|
@@ -12401,37 +12540,20 @@ function getProjectConnectionBanner(projectName, projectSlug) {
|
|
|
12401
12540
|
function detectContextDegradation(now = Date.now(), callerKey) {
|
|
12402
12541
|
const state = getState(callerKey);
|
|
12403
12542
|
if (state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
12404
|
-
return
|
|
12543
|
+
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.`;
|
|
12405
12544
|
}
|
|
12406
12545
|
const cutoff = now - FAILURE_WINDOW_MS;
|
|
12407
12546
|
const recentFailures = state.failureTimestamps.filter((t) => t >= cutoff).length;
|
|
12408
12547
|
if (recentFailures >= FAILURE_RATE_THRESHOLD) {
|
|
12409
12548
|
const mins = Math.round(FAILURE_WINDOW_MS / 6e4);
|
|
12410
|
-
return
|
|
12549
|
+
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.`;
|
|
12411
12550
|
}
|
|
12412
12551
|
return null;
|
|
12413
12552
|
}
|
|
12414
12553
|
async function buildSessionGuidance(callerKey) {
|
|
12415
|
-
const state = getState(callerKey);
|
|
12416
12554
|
const signals = [];
|
|
12417
12555
|
const degradation = detectContextDegradation(Date.now(), callerKey);
|
|
12418
12556
|
if (degradation) signals.push(degradation);
|
|
12419
|
-
if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
|
|
12420
|
-
signals.push(
|
|
12421
|
-
`${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
|
|
12422
|
-
);
|
|
12423
|
-
}
|
|
12424
|
-
if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
|
|
12425
|
-
const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
|
|
12426
|
-
signals.push(
|
|
12427
|
-
`${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
|
|
12428
|
-
);
|
|
12429
|
-
}
|
|
12430
|
-
if (state.releaseSinceLastOrient) {
|
|
12431
|
-
signals.push(
|
|
12432
|
-
"Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
|
|
12433
|
-
);
|
|
12434
|
-
}
|
|
12435
12557
|
return signals.slice(0, 3);
|
|
12436
12558
|
}
|
|
12437
12559
|
|
|
@@ -12583,6 +12705,7 @@ function formatPlanResult(result) {
|
|
|
12583
12705
|
);
|
|
12584
12706
|
}
|
|
12585
12707
|
const lines = [];
|
|
12708
|
+
if (result.projectBanner) lines.push(`> ${result.projectBanner}`, "");
|
|
12586
12709
|
lines.push(`${result.strategyReviewWarning}${pullLine}**${modeLabel} Mode \u2014 ${cycleLabel}**`);
|
|
12587
12710
|
if (result.writeSummary) {
|
|
12588
12711
|
const ws = result.writeSummary;
|
|
@@ -12692,7 +12815,9 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
12692
12815
|
source: "mcp-server",
|
|
12693
12816
|
confirmCancellations: args.confirm_cancellations === true
|
|
12694
12817
|
}, tracker);
|
|
12695
|
-
const
|
|
12818
|
+
const planProjectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
12819
|
+
const projectBanner = planProjectInfo ? getProjectConnectionBanner(planProjectInfo.name, planProjectInfo.slug) ?? void 0 : void 0;
|
|
12820
|
+
const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs, projectBanner });
|
|
12696
12821
|
return {
|
|
12697
12822
|
...response,
|
|
12698
12823
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -15847,8 +15972,8 @@ ${existing}` : entry;
|
|
|
15847
15972
|
}
|
|
15848
15973
|
|
|
15849
15974
|
// src/services/setup.ts
|
|
15850
|
-
import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2 } from "fs/promises";
|
|
15851
|
-
import { join as
|
|
15975
|
+
import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
|
|
15976
|
+
import { join as join7, basename, extname, dirname as dirname3 } from "path";
|
|
15852
15977
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
15853
15978
|
|
|
15854
15979
|
// src/lib/detect-codebase.ts
|
|
@@ -15933,6 +16058,159 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
|
15933
16058
|
return out;
|
|
15934
16059
|
}
|
|
15935
16060
|
|
|
16061
|
+
// src/lib/design-bundle.ts
|
|
16062
|
+
import { readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync4 } from "fs";
|
|
16063
|
+
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
16064
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16065
|
+
var DESIGN_ASSETS = [
|
|
16066
|
+
{ srcRel: join5("agents", "frontend-design-engineer.md"), destRel: join5(".claude", "agents", "frontend-design-engineer.md"), executable: false },
|
|
16067
|
+
{ srcRel: join5("skills", "design-critique", "SKILL.md"), destRel: join5(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
|
|
16068
|
+
{ srcRel: join5("hooks", "frontend-design-guard.sh"), destRel: join5(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
|
|
16069
|
+
];
|
|
16070
|
+
var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
|
|
16071
|
+
function resolveDesignAssetsDir() {
|
|
16072
|
+
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
16073
|
+
for (let i = 0; i < 5; i++) {
|
|
16074
|
+
const candidate = join5(dir, "design-assets");
|
|
16075
|
+
if (existsSync4(join5(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
|
|
16076
|
+
const parent = resolve2(dir, "..");
|
|
16077
|
+
if (parent === dir) break;
|
|
16078
|
+
dir = parent;
|
|
16079
|
+
}
|
|
16080
|
+
return void 0;
|
|
16081
|
+
}
|
|
16082
|
+
function planDesignInstall(projectRoot, opts = {}) {
|
|
16083
|
+
const assetsDir = resolveDesignAssetsDir();
|
|
16084
|
+
if (!assetsDir) return [];
|
|
16085
|
+
const out = [];
|
|
16086
|
+
for (const asset of DESIGN_ASSETS) {
|
|
16087
|
+
const srcAbs = join5(assetsDir, asset.srcRel);
|
|
16088
|
+
if (!existsSync4(srcAbs)) continue;
|
|
16089
|
+
const dest = projectRoot ? join5(projectRoot, asset.destRel) : asset.destRel;
|
|
16090
|
+
if (opts.skipExisting && projectRoot && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
16091
|
+
out.push({ dest, content: readFileSync2(srcAbs, "utf8"), executable: asset.executable });
|
|
16092
|
+
}
|
|
16093
|
+
return out;
|
|
16094
|
+
}
|
|
16095
|
+
|
|
16096
|
+
// src/lib/skill-detection.ts
|
|
16097
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync3, statSync as statSync5 } from "fs";
|
|
16098
|
+
import { join as join6 } from "path";
|
|
16099
|
+
function readPackageJson(projectRoot) {
|
|
16100
|
+
const path7 = join6(projectRoot, "package.json");
|
|
16101
|
+
if (!existsSync5(path7)) return null;
|
|
16102
|
+
try {
|
|
16103
|
+
const raw = readFileSync3(path7, "utf-8");
|
|
16104
|
+
return JSON.parse(raw);
|
|
16105
|
+
} catch {
|
|
16106
|
+
return null;
|
|
16107
|
+
}
|
|
16108
|
+
}
|
|
16109
|
+
function allDeps(pkg) {
|
|
16110
|
+
if (!pkg) return {};
|
|
16111
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
16112
|
+
}
|
|
16113
|
+
function hasDependencyMatching(deps, pattern) {
|
|
16114
|
+
for (const name of Object.keys(deps)) {
|
|
16115
|
+
if (pattern.test(name)) return true;
|
|
16116
|
+
}
|
|
16117
|
+
return false;
|
|
16118
|
+
}
|
|
16119
|
+
var FRONTEND_DEP_PATTERN = /^(react|react-dom|next|vue|svelte|preact|solid-js|astro|nuxt|tailwindcss)$|^@(sveltejs|angular|remix-run)\//;
|
|
16120
|
+
function detectsFrontendStack(projectRoot) {
|
|
16121
|
+
return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
|
|
16122
|
+
}
|
|
16123
|
+
function hasGitHubWorkflows(projectRoot) {
|
|
16124
|
+
const dir = join6(projectRoot, ".github", "workflows");
|
|
16125
|
+
if (!existsSync5(dir)) return false;
|
|
16126
|
+
try {
|
|
16127
|
+
const entries = readdirSync4(dir);
|
|
16128
|
+
return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
16129
|
+
} catch {
|
|
16130
|
+
return false;
|
|
16131
|
+
}
|
|
16132
|
+
}
|
|
16133
|
+
function envExampleMentionsStaging(projectRoot) {
|
|
16134
|
+
const path7 = join6(projectRoot, ".env.example");
|
|
16135
|
+
if (!existsSync5(path7)) return false;
|
|
16136
|
+
try {
|
|
16137
|
+
const raw = readFileSync3(path7, "utf-8");
|
|
16138
|
+
return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
|
|
16139
|
+
} catch {
|
|
16140
|
+
return false;
|
|
16141
|
+
}
|
|
16142
|
+
}
|
|
16143
|
+
function hasVercelConfig(projectRoot) {
|
|
16144
|
+
if (existsSync5(join6(projectRoot, "vercel.json"))) return true;
|
|
16145
|
+
const vercelDir = join6(projectRoot, ".vercel");
|
|
16146
|
+
if (!existsSync5(vercelDir)) return false;
|
|
16147
|
+
try {
|
|
16148
|
+
return statSync5(vercelDir).isDirectory();
|
|
16149
|
+
} catch {
|
|
16150
|
+
return false;
|
|
16151
|
+
}
|
|
16152
|
+
}
|
|
16153
|
+
function scanForSkillSignals(projectRoot) {
|
|
16154
|
+
const proposals = [];
|
|
16155
|
+
const pkg = readPackageJson(projectRoot);
|
|
16156
|
+
const deps = allDeps(pkg);
|
|
16157
|
+
if (hasGitHubWorkflows(projectRoot)) {
|
|
16158
|
+
proposals.push({
|
|
16159
|
+
id: "gh-actions-debug",
|
|
16160
|
+
name: "GitHub Actions debugger",
|
|
16161
|
+
rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
|
|
16162
|
+
hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
|
|
16163
|
+
});
|
|
16164
|
+
}
|
|
16165
|
+
if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
|
|
16166
|
+
proposals.push({
|
|
16167
|
+
id: "error-tracking",
|
|
16168
|
+
name: "Error tracking helper",
|
|
16169
|
+
rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
|
|
16170
|
+
hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
|
|
16171
|
+
});
|
|
16172
|
+
}
|
|
16173
|
+
if (hasVercelConfig(projectRoot)) {
|
|
16174
|
+
proposals.push({
|
|
16175
|
+
id: "read-vercel-logs",
|
|
16176
|
+
name: "Vercel deploy logs",
|
|
16177
|
+
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.",
|
|
16178
|
+
hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
|
|
16179
|
+
});
|
|
16180
|
+
}
|
|
16181
|
+
if (envExampleMentionsStaging(projectRoot)) {
|
|
16182
|
+
proposals.push({
|
|
16183
|
+
id: "staging-environment",
|
|
16184
|
+
name: "Staging-environment helper",
|
|
16185
|
+
rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
|
|
16186
|
+
hint: 'Search Claude Code skill registry for "staging" or define your own.'
|
|
16187
|
+
});
|
|
16188
|
+
}
|
|
16189
|
+
if (detectsFrontendStack(projectRoot)) {
|
|
16190
|
+
proposals.push({
|
|
16191
|
+
id: "frontend-design-engineer",
|
|
16192
|
+
name: "Frontend design engineer (agent + critique)",
|
|
16193
|
+
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.",
|
|
16194
|
+
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)."
|
|
16195
|
+
});
|
|
16196
|
+
}
|
|
16197
|
+
return proposals;
|
|
16198
|
+
}
|
|
16199
|
+
function formatSkillProposals(proposals) {
|
|
16200
|
+
if (proposals.length === 0) return "";
|
|
16201
|
+
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._"];
|
|
16202
|
+
for (const p of proposals) {
|
|
16203
|
+
lines.push("");
|
|
16204
|
+
lines.push(`### ${p.name}`);
|
|
16205
|
+
lines.push(p.rationale);
|
|
16206
|
+
if (p.hint) {
|
|
16207
|
+
lines.push(`_${p.hint}_`);
|
|
16208
|
+
}
|
|
16209
|
+
lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
|
|
16210
|
+
}
|
|
16211
|
+
return "\n" + lines.join("\n");
|
|
16212
|
+
}
|
|
16213
|
+
|
|
15936
16214
|
// src/templates.ts
|
|
15937
16215
|
var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
|
|
15938
16216
|
|
|
@@ -16335,6 +16613,9 @@ function substitute(template, vars) {
|
|
|
16335
16613
|
}
|
|
16336
16614
|
return result;
|
|
16337
16615
|
}
|
|
16616
|
+
function shouldWriteClaudeMd(clientName) {
|
|
16617
|
+
return !clientName || /claude/i.test(clientName);
|
|
16618
|
+
}
|
|
16338
16619
|
async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
16339
16620
|
const isPg = config2.adapterType === "pg" || config2.adapterType === "proxy";
|
|
16340
16621
|
const vars = {
|
|
@@ -16350,7 +16631,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16350
16631
|
await mkdir(config2.papiDir, { recursive: true });
|
|
16351
16632
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
16352
16633
|
const content = substitute(template, vars);
|
|
16353
|
-
await writeFile2(
|
|
16634
|
+
await writeFile2(join7(config2.papiDir, filename), content, "utf-8");
|
|
16354
16635
|
}
|
|
16355
16636
|
}
|
|
16356
16637
|
} else {
|
|
@@ -16368,13 +16649,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16368
16649
|
const useCollector = config2.adapterType === "proxy";
|
|
16369
16650
|
const docsRel = "docs";
|
|
16370
16651
|
const commandsRel = ".claude/commands";
|
|
16371
|
-
const commandsDir = useCollector ? commandsRel :
|
|
16372
|
-
const docsDir = useCollector ? docsRel :
|
|
16652
|
+
const commandsDir = useCollector ? commandsRel : join7(config2.projectRoot, ".claude", "commands");
|
|
16653
|
+
const docsDir = useCollector ? docsRel : join7(config2.projectRoot, "docs");
|
|
16373
16654
|
if (!useCollector) {
|
|
16374
16655
|
await mkdir(commandsDir, { recursive: true });
|
|
16375
16656
|
await mkdir(docsDir, { recursive: true });
|
|
16376
16657
|
}
|
|
16377
|
-
const claudeMdPath = useCollector ? "CLAUDE.md" :
|
|
16658
|
+
const claudeMdPath = useCollector ? "CLAUDE.md" : join7(config2.projectRoot, "CLAUDE.md");
|
|
16378
16659
|
let claudeMdExists = false;
|
|
16379
16660
|
if (!useCollector) {
|
|
16380
16661
|
try {
|
|
@@ -16383,7 +16664,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16383
16664
|
} catch {
|
|
16384
16665
|
}
|
|
16385
16666
|
}
|
|
16386
|
-
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` :
|
|
16667
|
+
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join7(docsDir, "INDEX.md");
|
|
16387
16668
|
let docsIndexExists = false;
|
|
16388
16669
|
if (!useCollector) {
|
|
16389
16670
|
try {
|
|
@@ -16393,16 +16674,16 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16393
16674
|
}
|
|
16394
16675
|
}
|
|
16395
16676
|
const scaffoldFiles = {
|
|
16396
|
-
[useCollector ? `${commandsRel}/papi-audit.md` :
|
|
16397
|
-
[useCollector ? `${commandsRel}/test.md` :
|
|
16398
|
-
[useCollector ? `${docsRel}/README.md` :
|
|
16677
|
+
[useCollector ? `${commandsRel}/papi-audit.md` : join7(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
|
|
16678
|
+
[useCollector ? `${commandsRel}/test.md` : join7(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
|
|
16679
|
+
[useCollector ? `${docsRel}/README.md` : join7(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
|
|
16399
16680
|
};
|
|
16400
16681
|
if (!docsIndexExists) {
|
|
16401
16682
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
16402
16683
|
}
|
|
16403
|
-
if (!claudeMdExists) {
|
|
16684
|
+
if (!claudeMdExists && shouldWriteClaudeMd(input.clientName)) {
|
|
16404
16685
|
scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_STUB, vars);
|
|
16405
|
-
} else {
|
|
16686
|
+
} else if (claudeMdExists) {
|
|
16406
16687
|
try {
|
|
16407
16688
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
16408
16689
|
if (!existing.includes("AGENTS.md")) {
|
|
@@ -16415,14 +16696,14 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16415
16696
|
const bundleRoot = useCollector ? "" : config2.projectRoot;
|
|
16416
16697
|
for (const [dest, content] of Object.entries(planBundleInstall(bundleRoot, input.projectName, { skipExisting: true }))) {
|
|
16417
16698
|
if (!useCollector) {
|
|
16418
|
-
await mkdir(
|
|
16699
|
+
await mkdir(dirname3(dest), { recursive: true });
|
|
16419
16700
|
}
|
|
16420
16701
|
scaffoldFiles[dest] = content;
|
|
16421
16702
|
}
|
|
16422
16703
|
if (useCollector) {
|
|
16423
16704
|
scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
|
|
16424
16705
|
} else {
|
|
16425
|
-
const cursorDir =
|
|
16706
|
+
const cursorDir = join7(config2.projectRoot, ".cursor");
|
|
16426
16707
|
let cursorDetected = false;
|
|
16427
16708
|
try {
|
|
16428
16709
|
await access2(cursorDir);
|
|
@@ -16430,8 +16711,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16430
16711
|
} catch {
|
|
16431
16712
|
}
|
|
16432
16713
|
if (cursorDetected) {
|
|
16433
|
-
const cursorRulesDir =
|
|
16434
|
-
const cursorRulesPath =
|
|
16714
|
+
const cursorRulesDir = join7(cursorDir, "rules");
|
|
16715
|
+
const cursorRulesPath = join7(cursorRulesDir, "papi.mdc");
|
|
16435
16716
|
await mkdir(cursorRulesDir, { recursive: true });
|
|
16436
16717
|
try {
|
|
16437
16718
|
await access2(cursorRulesPath);
|
|
@@ -16449,6 +16730,19 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16449
16730
|
await writeFile2(filepath, content, "utf-8");
|
|
16450
16731
|
}
|
|
16451
16732
|
}
|
|
16733
|
+
if (!useCollector && detectsFrontendStack(config2.projectRoot)) {
|
|
16734
|
+
for (const entry of planDesignInstall(config2.projectRoot, { skipExisting: true })) {
|
|
16735
|
+
await mkdir(dirname3(entry.dest), { recursive: true });
|
|
16736
|
+
await writeFile2(entry.dest, entry.content, "utf-8");
|
|
16737
|
+
if (entry.executable) {
|
|
16738
|
+
try {
|
|
16739
|
+
await chmod(entry.dest, 493);
|
|
16740
|
+
} catch {
|
|
16741
|
+
}
|
|
16742
|
+
}
|
|
16743
|
+
}
|
|
16744
|
+
await ensureDesignHookRegistered(config2.projectRoot);
|
|
16745
|
+
}
|
|
16452
16746
|
if (!isPg) {
|
|
16453
16747
|
await adapter2.writePhases([{
|
|
16454
16748
|
id: "phase-0",
|
|
@@ -16473,7 +16767,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16473
16767
|
}
|
|
16474
16768
|
var PAPI_PERMISSION = "mcp__papi__*";
|
|
16475
16769
|
async function ensurePapiPermission(projectRoot) {
|
|
16476
|
-
const settingsPath =
|
|
16770
|
+
const settingsPath = join7(projectRoot, ".claude", "settings.json");
|
|
16477
16771
|
try {
|
|
16478
16772
|
let settings = {};
|
|
16479
16773
|
try {
|
|
@@ -16492,7 +16786,43 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
16492
16786
|
if (!allow.includes(PAPI_PERMISSION)) {
|
|
16493
16787
|
allow.push(PAPI_PERMISSION);
|
|
16494
16788
|
}
|
|
16495
|
-
await mkdir(
|
|
16789
|
+
await mkdir(join7(projectRoot, ".claude"), { recursive: true });
|
|
16790
|
+
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
16791
|
+
} catch {
|
|
16792
|
+
}
|
|
16793
|
+
}
|
|
16794
|
+
async function ensureDesignHookRegistered(projectRoot) {
|
|
16795
|
+
const settingsPath = join7(projectRoot, ".claude", "settings.json");
|
|
16796
|
+
try {
|
|
16797
|
+
let settings = {};
|
|
16798
|
+
try {
|
|
16799
|
+
settings = JSON.parse(await readFile4(settingsPath, "utf-8"));
|
|
16800
|
+
} catch {
|
|
16801
|
+
}
|
|
16802
|
+
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
16803
|
+
settings.hooks = {};
|
|
16804
|
+
}
|
|
16805
|
+
const hooks = settings.hooks;
|
|
16806
|
+
if (!Array.isArray(hooks.PreToolUse)) {
|
|
16807
|
+
hooks.PreToolUse = [];
|
|
16808
|
+
}
|
|
16809
|
+
const pre = hooks.PreToolUse;
|
|
16810
|
+
for (const matcher of ["Edit", "Write"]) {
|
|
16811
|
+
let entry = pre.find((e) => e && e["matcher"] === matcher);
|
|
16812
|
+
if (!entry) {
|
|
16813
|
+
entry = { matcher, hooks: [] };
|
|
16814
|
+
pre.push(entry);
|
|
16815
|
+
}
|
|
16816
|
+
if (!Array.isArray(entry["hooks"])) {
|
|
16817
|
+
entry["hooks"] = [];
|
|
16818
|
+
}
|
|
16819
|
+
const chain = entry["hooks"];
|
|
16820
|
+
const already = chain.some((h) => h && h["command"] === DESIGN_HOOK_COMMAND);
|
|
16821
|
+
if (!already) {
|
|
16822
|
+
chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
|
|
16823
|
+
}
|
|
16824
|
+
}
|
|
16825
|
+
await mkdir(join7(projectRoot, ".claude"), { recursive: true });
|
|
16496
16826
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
16497
16827
|
} catch {
|
|
16498
16828
|
}
|
|
@@ -16581,7 +16911,7 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
16581
16911
|
}
|
|
16582
16912
|
}
|
|
16583
16913
|
}
|
|
16584
|
-
if (conventionsText?.trim()) {
|
|
16914
|
+
if (conventionsText?.trim() && shouldWriteClaudeMd(input.clientName)) {
|
|
16585
16915
|
const conventionsBlock = `${CONVENTIONS_SENTINEL}
|
|
16586
16916
|
${conventionsText.trim()}
|
|
16587
16917
|
`;
|
|
@@ -16596,7 +16926,7 @@ ${conventionsText.trim()}
|
|
|
16596
16926
|
);
|
|
16597
16927
|
} else {
|
|
16598
16928
|
try {
|
|
16599
|
-
const claudeMdPath =
|
|
16929
|
+
const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
|
|
16600
16930
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
16601
16931
|
if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
|
|
16602
16932
|
warnings.push(
|
|
@@ -16683,13 +17013,13 @@ async function scanCodebase(projectRoot) {
|
|
|
16683
17013
|
}
|
|
16684
17014
|
let packageJson;
|
|
16685
17015
|
try {
|
|
16686
|
-
const content = await readFile4(
|
|
17016
|
+
const content = await readFile4(join7(projectRoot, "package.json"), "utf-8");
|
|
16687
17017
|
packageJson = JSON.parse(content);
|
|
16688
17018
|
} catch {
|
|
16689
17019
|
}
|
|
16690
17020
|
let readme;
|
|
16691
17021
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
16692
|
-
const content = await safeReadFile(
|
|
17022
|
+
const content = await safeReadFile(join7(projectRoot, name), 5e3);
|
|
16693
17023
|
if (content) {
|
|
16694
17024
|
readme = content;
|
|
16695
17025
|
break;
|
|
@@ -16699,7 +17029,7 @@ async function scanCodebase(projectRoot) {
|
|
|
16699
17029
|
let totalFiles = topLevelFiles.length;
|
|
16700
17030
|
for (const dir of topLevelDirs) {
|
|
16701
17031
|
try {
|
|
16702
|
-
const entries = await readdir(
|
|
17032
|
+
const entries = await readdir(join7(projectRoot, dir), { withFileTypes: true });
|
|
16703
17033
|
const files = entries.filter((e) => e.isFile());
|
|
16704
17034
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
16705
17035
|
totalFiles += files.length;
|
|
@@ -17026,7 +17356,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17026
17356
|
}
|
|
17027
17357
|
}
|
|
17028
17358
|
}
|
|
17029
|
-
{
|
|
17359
|
+
if (shouldWriteClaudeMd(input.clientName)) {
|
|
17030
17360
|
const dogfoodSection = [
|
|
17031
17361
|
"",
|
|
17032
17362
|
"## Dogfood Logging",
|
|
@@ -17046,7 +17376,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17046
17376
|
collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
|
|
17047
17377
|
} else {
|
|
17048
17378
|
try {
|
|
17049
|
-
const claudeMdPath =
|
|
17379
|
+
const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
|
|
17050
17380
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
17051
17381
|
if (!existing.includes("Dogfood Logging")) {
|
|
17052
17382
|
await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
|
|
@@ -17091,7 +17421,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17091
17421
|
cursorScaffolded = true;
|
|
17092
17422
|
} else {
|
|
17093
17423
|
try {
|
|
17094
|
-
await access2(
|
|
17424
|
+
await access2(join7(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
17095
17425
|
cursorScaffolded = true;
|
|
17096
17426
|
} catch {
|
|
17097
17427
|
}
|
|
@@ -17111,11 +17441,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17111
17441
|
}
|
|
17112
17442
|
async function ensureMcpJsonGitignored(projectRoot) {
|
|
17113
17443
|
try {
|
|
17114
|
-
await access2(
|
|
17444
|
+
await access2(join7(projectRoot, ".git"));
|
|
17115
17445
|
} catch {
|
|
17116
17446
|
return void 0;
|
|
17117
17447
|
}
|
|
17118
|
-
const gitignorePath =
|
|
17448
|
+
const gitignorePath = join7(projectRoot, ".gitignore");
|
|
17119
17449
|
let existing = "";
|
|
17120
17450
|
try {
|
|
17121
17451
|
existing = await readFile4(gitignorePath, "utf-8");
|
|
@@ -17260,7 +17590,8 @@ function extractInput(args) {
|
|
|
17260
17590
|
codebaseScan: args.codebase_scan && typeof args.codebase_scan === "object" ? args.codebase_scan : void 0
|
|
17261
17591
|
};
|
|
17262
17592
|
}
|
|
17263
|
-
function formatSuccessResponse(result, constraints) {
|
|
17593
|
+
function formatSuccessResponse(result, constraints, writesClaudeMd = true) {
|
|
17594
|
+
const harnessFiles = writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
|
|
17264
17595
|
const prefix = result.createdProject ? `PAPI project "${result.projectName}" initialised and ` : "";
|
|
17265
17596
|
const briefRegenNote = result.briefRegenerated ? `
|
|
17266
17597
|
|
|
@@ -17277,7 +17608,7 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
|
|
|
17277
17608
|
${[created, skipped].filter(Boolean).join(", ")}.${idea}`;
|
|
17278
17609
|
})() : '\n\nNo starter tasks yet. Describe what you want to build, or seed the board with `idea "<what you want to build>"`, so your first `plan` has something to work from.';
|
|
17279
17610
|
const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
|
|
17280
|
-
const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside
|
|
17611
|
+
const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside your `AGENTS.md` harness." : "";
|
|
17281
17612
|
const gitignoreNote = result.gitignoreNote ? `
|
|
17282
17613
|
|
|
17283
17614
|
\u{1F512} ${result.gitignoreNote}` : "";
|
|
@@ -17289,7 +17620,7 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
|
17289
17620
|
return textResponse(
|
|
17290
17621
|
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}
|
|
17291
17622
|
|
|
17292
|
-
**Important:** Setup created/modified files (
|
|
17623
|
+
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
|
|
17293
17624
|
|
|
17294
17625
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
17295
17626
|
|
|
@@ -17298,7 +17629,7 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
|
|
|
17298
17629
|
Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
|
|
17299
17630
|
);
|
|
17300
17631
|
}
|
|
17301
|
-
async function handleSetup(adapter2, config2, args) {
|
|
17632
|
+
async function handleSetup(adapter2, config2, args, clientName) {
|
|
17302
17633
|
const toolMode = args.mode;
|
|
17303
17634
|
const REQUIRED_FIELDS = ["project_name"];
|
|
17304
17635
|
const missing = REQUIRED_FIELDS.filter((f) => !args[f] || typeof args[f] === "string" && !args[f].trim());
|
|
@@ -17310,6 +17641,8 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
17310
17641
|
);
|
|
17311
17642
|
}
|
|
17312
17643
|
const input = extractInput(args);
|
|
17644
|
+
input.clientName = clientName;
|
|
17645
|
+
const writesClaudeMd = !clientName || /claude/i.test(clientName);
|
|
17313
17646
|
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate");
|
|
17314
17647
|
try {
|
|
17315
17648
|
if (toolMode === "apply") {
|
|
@@ -17321,7 +17654,7 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
17321
17654
|
tracker.mark("apply_setup_writeback");
|
|
17322
17655
|
const result = await applySetup(adapter2, config2, input, briefResponse, adSeedResponse, conventionsResponse, initialTasksResponse);
|
|
17323
17656
|
tracker.mark("apply_format_response");
|
|
17324
|
-
return formatSuccessResponse(result, args.constraints);
|
|
17657
|
+
return formatSuccessResponse(result, args.constraints, writesClaudeMd);
|
|
17325
17658
|
}
|
|
17326
17659
|
{
|
|
17327
17660
|
tracker.mark("prepare_setup");
|
|
@@ -17495,10 +17828,127 @@ ${result.initialTasksPrompt.user}
|
|
|
17495
17828
|
}
|
|
17496
17829
|
}
|
|
17497
17830
|
|
|
17831
|
+
// src/lib/model-routing.ts
|
|
17832
|
+
var TIER_SPECS = {
|
|
17833
|
+
cheap: {
|
|
17834
|
+
label: "Cheap / fast",
|
|
17835
|
+
rationale: "XS\u2013S work \u2014 single-file fixes, UI polish, mechanical edits. A small model is faster and cheaper with no quality loss here.",
|
|
17836
|
+
examples: "Anthropic Claude Haiku \xB7 OpenAI GPT-5 mini \xB7 Google Gemini Flash \xB7 Mistral/Groq small"
|
|
17837
|
+
},
|
|
17838
|
+
capable: {
|
|
17839
|
+
label: "Capable",
|
|
17840
|
+
rationale: "M\u2013L work \u2014 multi-file features, data/plumbing, ambiguous handoffs. Needs a strong general-purpose model.",
|
|
17841
|
+
examples: "Anthropic Claude Sonnet \xB7 OpenAI GPT-5 \xB7 Google Gemini Pro \xB7 Mistral Large"
|
|
17842
|
+
},
|
|
17843
|
+
frontier: {
|
|
17844
|
+
label: "Frontier",
|
|
17845
|
+
rationale: "XL work \u2014 architecture, cross-cutting refactors, the hardest reasoning. Reach for your most capable model.",
|
|
17846
|
+
examples: "Anthropic Claude Opus \xB7 OpenAI GPT-5 Pro / o-series \xB7 Google Gemini Ultra"
|
|
17847
|
+
}
|
|
17848
|
+
};
|
|
17849
|
+
function tierForComplexity(complexity) {
|
|
17850
|
+
switch ((complexity ?? "").trim()) {
|
|
17851
|
+
case "XS":
|
|
17852
|
+
case "S":
|
|
17853
|
+
case "Small":
|
|
17854
|
+
return "cheap";
|
|
17855
|
+
case "XL":
|
|
17856
|
+
return "frontier";
|
|
17857
|
+
case "M":
|
|
17858
|
+
case "Medium":
|
|
17859
|
+
case "L":
|
|
17860
|
+
case "Large":
|
|
17861
|
+
return "capable";
|
|
17862
|
+
default:
|
|
17863
|
+
return "capable";
|
|
17864
|
+
}
|
|
17865
|
+
}
|
|
17866
|
+
function modelTierTag(complexity) {
|
|
17867
|
+
return `\u{1F916} ${TIER_SPECS[tierForComplexity(complexity)].label}`;
|
|
17868
|
+
}
|
|
17869
|
+
function formatModelRecommendation(complexity) {
|
|
17870
|
+
const spec = TIER_SPECS[tierForComplexity(complexity)];
|
|
17871
|
+
return `
|
|
17872
|
+
|
|
17873
|
+
---
|
|
17874
|
+
|
|
17875
|
+
**\u{1F916} Suggested model tier: ${spec.label}**
|
|
17876
|
+
${spec.rationale}
|
|
17877
|
+
Examples (use whichever provider you have): ${spec.examples}.
|
|
17878
|
+
_Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
|
|
17879
|
+
}
|
|
17880
|
+
|
|
17498
17881
|
// src/services/build.ts
|
|
17499
17882
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
17500
|
-
import { readdirSync as
|
|
17501
|
-
import { join as
|
|
17883
|
+
import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
|
|
17884
|
+
import { join as join10 } from "path";
|
|
17885
|
+
|
|
17886
|
+
// src/lib/harness-capability.ts
|
|
17887
|
+
var HARNESS_REGISTRY = {
|
|
17888
|
+
// Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
|
|
17889
|
+
"claude-code": { build: true, label: "Claude Code" },
|
|
17890
|
+
"opencode": { build: true, label: "opencode" },
|
|
17891
|
+
"zcode": { build: true, label: "zcode" },
|
|
17892
|
+
"codex": { build: true, label: "Codex" },
|
|
17893
|
+
"cursor": { build: true, label: "Cursor" },
|
|
17894
|
+
// Cloud harnesses with code sandboxes — their agent builds in-sandbox over HTTP+OAuth.
|
|
17895
|
+
"lovable": { build: true, label: "Lovable" },
|
|
17896
|
+
"bolt": { build: true, label: "Bolt" },
|
|
17897
|
+
"replit": { build: true, label: "Replit" },
|
|
17898
|
+
// Chat-only surfaces, no sandbox — planning only.
|
|
17899
|
+
"chatgpt": { build: false, label: "ChatGPT" },
|
|
17900
|
+
"claude.ai": { build: false, label: "Claude.ai" },
|
|
17901
|
+
"claude-desktop": { build: false, label: "Claude Desktop" }
|
|
17902
|
+
};
|
|
17903
|
+
var UNKNOWN_DEFAULT = { build: false, label: "your tool" };
|
|
17904
|
+
function detectHarness(clientName) {
|
|
17905
|
+
const raw = clientName?.trim() || null;
|
|
17906
|
+
if (!raw) {
|
|
17907
|
+
return { ...UNKNOWN_DEFAULT, raw: null, key: null, known: false };
|
|
17908
|
+
}
|
|
17909
|
+
const norm = raw.toLowerCase();
|
|
17910
|
+
const key = HARNESS_REGISTRY[norm] ? norm : Object.keys(HARNESS_REGISTRY).find((k) => norm.includes(k)) ?? null;
|
|
17911
|
+
if (!key) {
|
|
17912
|
+
return { ...UNKNOWN_DEFAULT, label: raw, raw, key: null, known: false };
|
|
17913
|
+
}
|
|
17914
|
+
return { ...HARNESS_REGISTRY[key], raw, key, known: true };
|
|
17915
|
+
}
|
|
17916
|
+
|
|
17917
|
+
// src/lib/harness-build-steps.ts
|
|
17918
|
+
init_git();
|
|
17919
|
+
function startBuildSteps(branch, base, taskId) {
|
|
17920
|
+
return [
|
|
17921
|
+
`PAPI is connected over HTTP and can't reach your files \u2014 your harness runs git in its own sandbox. To start ${taskId}:`,
|
|
17922
|
+
` git fetch origin`,
|
|
17923
|
+
` git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${base} 2>/dev/null || git checkout -b ${branch}`,
|
|
17924
|
+
`Then implement the task on branch \`${branch}\`.`
|
|
17925
|
+
];
|
|
17926
|
+
}
|
|
17927
|
+
function completeBuildSteps(branch, base, taskId, title) {
|
|
17928
|
+
const message = `${taskId}: ${title}`.replace(/["`\\]/g, "'");
|
|
17929
|
+
return [
|
|
17930
|
+
`To ship ${taskId} from your sandbox:`,
|
|
17931
|
+
` git add -A`,
|
|
17932
|
+
` git commit -m "${message}"`,
|
|
17933
|
+
` git push -u origin ${branch}`,
|
|
17934
|
+
`Then open a PR from \`${branch}\` into \`${base}\`.`
|
|
17935
|
+
];
|
|
17936
|
+
}
|
|
17937
|
+
function hostedBranchName(taskId, module, cycleNumber) {
|
|
17938
|
+
return module && cycleNumber > 0 ? cycleBranchName(cycleNumber, module) : taskBranchName(taskId);
|
|
17939
|
+
}
|
|
17940
|
+
function hostedStartSteps(clientName, taskId, module, cycleNumber, base) {
|
|
17941
|
+
if (!detectHarness(clientName).build) return null;
|
|
17942
|
+
const branch = hostedBranchName(taskId, module, cycleNumber);
|
|
17943
|
+
return { branch, steps: startBuildSteps(branch, base, taskId) };
|
|
17944
|
+
}
|
|
17945
|
+
function hostedCompleteSteps(clientName, taskId, module, cycleNumber, base, title) {
|
|
17946
|
+
if (!detectHarness(clientName).build) return null;
|
|
17947
|
+
const branch = hostedBranchName(taskId, module, cycleNumber);
|
|
17948
|
+
return completeBuildSteps(branch, base, taskId, title);
|
|
17949
|
+
}
|
|
17950
|
+
|
|
17951
|
+
// src/services/build.ts
|
|
17502
17952
|
init_git();
|
|
17503
17953
|
|
|
17504
17954
|
// src/lib/owns-local-workspace.ts
|
|
@@ -17506,16 +17956,16 @@ init_git();
|
|
|
17506
17956
|
|
|
17507
17957
|
// src/services/release.ts
|
|
17508
17958
|
import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
17509
|
-
import { join as
|
|
17959
|
+
import { join as join9 } from "path";
|
|
17510
17960
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
17511
17961
|
|
|
17512
17962
|
// src/lib/install-id.ts
|
|
17513
17963
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
17514
|
-
import { mkdirSync, readFileSync as
|
|
17964
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync, chmodSync } from "fs";
|
|
17515
17965
|
import { homedir as homedir2 } from "os";
|
|
17516
|
-
import { join as
|
|
17517
|
-
var PAPI_HOME_DIR =
|
|
17518
|
-
var INSTALL_ID_FILE =
|
|
17966
|
+
import { join as join8 } from "path";
|
|
17967
|
+
var PAPI_HOME_DIR = join8(homedir2(), ".papi");
|
|
17968
|
+
var INSTALL_ID_FILE = join8(PAPI_HOME_DIR, "install-id.json");
|
|
17519
17969
|
var cachedInstallId = null;
|
|
17520
17970
|
function isValidUuid(s) {
|
|
17521
17971
|
return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
@@ -17523,7 +17973,7 @@ function isValidUuid(s) {
|
|
|
17523
17973
|
function getInstallId() {
|
|
17524
17974
|
if (cachedInstallId) return cachedInstallId;
|
|
17525
17975
|
try {
|
|
17526
|
-
const raw =
|
|
17976
|
+
const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
|
|
17527
17977
|
const parsed = JSON.parse(raw);
|
|
17528
17978
|
if (isValidUuid(parsed.install_id)) {
|
|
17529
17979
|
cachedInstallId = parsed.install_id;
|
|
@@ -17922,6 +18372,64 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
17922
18372
|
}
|
|
17923
18373
|
return { resolvedCycleNum, warnings, force, skipVersion };
|
|
17924
18374
|
}
|
|
18375
|
+
async function contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, options) {
|
|
18376
|
+
if (!isGhAvailable()) {
|
|
18377
|
+
throw new Error(
|
|
18378
|
+
`Release blocked \u2014 opening a release PR needs the GitHub CLI (\`gh\`) authenticated locally. Install and \`gh auth login\`, then re-run \`release\`. (As a contributor you cannot release to ${productionBaseBranch} directly; the owner merges your PR.)`
|
|
18379
|
+
);
|
|
18380
|
+
}
|
|
18381
|
+
const cycleNum = await resolveCycleToClose(adapter2, version, options?.callerUserId);
|
|
18382
|
+
const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, productionBaseBranch);
|
|
18383
|
+
if (branches.length === 0) {
|
|
18384
|
+
throw new Error(
|
|
18385
|
+
`Release blocked \u2014 no cycle branch found to open a PR from (cycle ${cycleNum || "?"}). Make sure your work is committed on a \`feat/cycle-\u2026\` branch and pushed, then re-run \`release\`.`
|
|
18386
|
+
);
|
|
18387
|
+
}
|
|
18388
|
+
const prs = [];
|
|
18389
|
+
for (const branch of branches) {
|
|
18390
|
+
const push = gitPush(config2.projectRoot, branch);
|
|
18391
|
+
if (!push.success) {
|
|
18392
|
+
prs.push({ branch, url: null, error: `push failed: ${push.message}` });
|
|
18393
|
+
continue;
|
|
18394
|
+
}
|
|
18395
|
+
const existing = getPullRequestUrl(config2.projectRoot, branch);
|
|
18396
|
+
if (existing) {
|
|
18397
|
+
prs.push({ branch, url: existing });
|
|
18398
|
+
continue;
|
|
18399
|
+
}
|
|
18400
|
+
const title = `Release ${version}: ${branch}`;
|
|
18401
|
+
const body = `Contributor release PR for cycle ${cycleNum || "?"} (\`${branch}\`).
|
|
18402
|
+
|
|
18403
|
+
Opened by a non-owner editor via \`release\` (task-2244). The project owner reviews and merges this into \`${productionBaseBranch}\`; the contributor's own cycle is already marked complete in PAPI.`;
|
|
18404
|
+
const created = createPullRequest(config2.projectRoot, branch, productionBaseBranch, title, body);
|
|
18405
|
+
prs.push(
|
|
18406
|
+
created.success ? { branch, url: created.message.trim() || getPullRequestUrl(config2.projectRoot, branch) } : { branch, url: null, error: created.message }
|
|
18407
|
+
);
|
|
18408
|
+
}
|
|
18409
|
+
if (!prs.some((p) => p.url)) {
|
|
18410
|
+
const detail = prs.map((p) => `${p.branch}: ${p.error ?? "no PR url returned"}`).join("; ");
|
|
18411
|
+
throw new Error(`Release blocked \u2014 could not open any release PR. ${detail}`);
|
|
18412
|
+
}
|
|
18413
|
+
if (adapter2?.recordContributorReleasePr) {
|
|
18414
|
+
for (const p of prs) {
|
|
18415
|
+
if (!p.url) continue;
|
|
18416
|
+
try {
|
|
18417
|
+
await adapter2.recordContributorReleasePr({
|
|
18418
|
+
prUrl: p.url,
|
|
18419
|
+
branch: p.branch,
|
|
18420
|
+
cycle: cycleNum > 0 ? cycleNum : null,
|
|
18421
|
+
contributorUserId: options?.callerUserId ?? null
|
|
18422
|
+
});
|
|
18423
|
+
} catch (err) {
|
|
18424
|
+
console.error(
|
|
18425
|
+
`[release] recordContributorReleasePr failed for ${p.url} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
|
|
18426
|
+
);
|
|
18427
|
+
}
|
|
18428
|
+
}
|
|
18429
|
+
}
|
|
18430
|
+
const closed = await closeCycleState(config2, adapter2, version, void 0, options);
|
|
18431
|
+
return { prs, cycleClosed: closed.resolvedCycleNum, warnings: closed.warnings };
|
|
18432
|
+
}
|
|
17925
18433
|
async function createRelease(config2, branch, version, adapter2, cycleNum, options) {
|
|
17926
18434
|
const collector = new FileWriteCollector();
|
|
17927
18435
|
if (!isGitAvailable()) {
|
|
@@ -18009,7 +18517,7 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
18009
18517
|
}
|
|
18010
18518
|
}
|
|
18011
18519
|
const latestTag = getLatestTag(config2.projectRoot);
|
|
18012
|
-
const changelogPath =
|
|
18520
|
+
const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
|
|
18013
18521
|
if (!latestTag) {
|
|
18014
18522
|
const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
18015
18523
|
if (config2.adapterType === "proxy") {
|
|
@@ -18296,11 +18804,50 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
18296
18804
|
const resolutionNote = gate.resolutionError ? `
|
|
18297
18805
|
|
|
18298
18806
|
Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
|
|
18299
|
-
|
|
18300
|
-
|
|
18807
|
+
tracker.mark("contributor-role-gate");
|
|
18808
|
+
const callerRole = adapter2.getContributorRole && gate.callerUserId ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
18809
|
+
if (callerRole !== "editor") {
|
|
18810
|
+
const roleNote = callerRole === "viewer" ? `Your role on this project is "viewer", which cannot release. Ask the owner (or an editor) to release, or ask for editor access.` : `Your identity does not match this project's owner, and you do not have an editor role to open a release PR. If you are a contributor, ask the owner for editor access (or push your branch and open a PR manually). If you ARE the owner on a local (pg) setup, set PAPI_USER_ID to your account UUID in .mcp.json; on the hosted/proxy setup your identity comes from your API key \u2014 check you are using YOUR key for YOUR project.`;
|
|
18811
|
+
return errorResponse(
|
|
18812
|
+
`Release to ${productionBaseBranch} is restricted to the project owner.
|
|
18301
18813
|
|
|
18302
|
-
|
|
18303
|
-
|
|
18814
|
+
` + roleNote + `
|
|
18815
|
+
|
|
18816
|
+
Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
18817
|
+
);
|
|
18818
|
+
}
|
|
18819
|
+
tracker.mark("contributor-auto-pr");
|
|
18820
|
+
try {
|
|
18821
|
+
const cr = await contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, {
|
|
18822
|
+
force: force ?? false,
|
|
18823
|
+
callerUserId: gate.callerUserId
|
|
18824
|
+
});
|
|
18825
|
+
const opened = cr.prs.filter((p) => p.url);
|
|
18826
|
+
const failed = cr.prs.filter((p) => !p.url);
|
|
18827
|
+
const cyclePart = cr.cycleClosed > 0 ? `Cycle ${cr.cycleClosed}` : "Your cycle";
|
|
18828
|
+
const prLines = opened.map((p) => `- \`${p.branch}\` \u2192 ${p.url}`);
|
|
18829
|
+
const failLines = failed.map((p) => `- \`${p.branch}\` \u2014 \u26A0\uFE0F ${p.error ?? "no PR opened"}`);
|
|
18830
|
+
const warnBlock = cr.warnings.length > 0 ? `
|
|
18831
|
+
\u26A0\uFE0F Warnings: ${cr.warnings.join("; ")}
|
|
18832
|
+
` : "";
|
|
18833
|
+
return textResponse(
|
|
18834
|
+
`## Release ${version} \u2014 PR opened for owner review
|
|
18835
|
+
|
|
18836
|
+
You released as an **editor**, so PAPI opened a pull request to \`${productionBaseBranch}\` instead of merging directly. The owner reviews and merges it.
|
|
18837
|
+
|
|
18838
|
+
**Pull request(s):**
|
|
18839
|
+
${prLines.join("\n")}
|
|
18840
|
+
` + (failLines.length > 0 ? `
|
|
18841
|
+
**Could not open:**
|
|
18842
|
+
${failLines.join("\n")}
|
|
18843
|
+
` : "") + warnBlock + `
|
|
18844
|
+
${cyclePart} is now marked **complete** in PAPI \u2014 your work is shipped from your side. Any changes the owner requests come forward as a fast-follow in your next cycle; your closed cycle is not rewound.
|
|
18845
|
+
|
|
18846
|
+
Next: run \`plan\` to start your next cycle.`
|
|
18847
|
+
);
|
|
18848
|
+
} catch (err) {
|
|
18849
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
18850
|
+
}
|
|
18304
18851
|
}
|
|
18305
18852
|
}
|
|
18306
18853
|
if (isHostedTransport()) {
|
|
@@ -18665,7 +19212,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
18665
19212
|
if (modified.length === 0) {
|
|
18666
19213
|
return "Auto-commit: skipped (no working-tree changes).";
|
|
18667
19214
|
}
|
|
18668
|
-
const
|
|
19215
|
+
const dirname7 = (p) => {
|
|
18669
19216
|
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
18670
19217
|
return idx > 0 ? p.slice(0, idx) : "";
|
|
18671
19218
|
};
|
|
@@ -18677,7 +19224,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
18677
19224
|
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.`;
|
|
18678
19225
|
}
|
|
18679
19226
|
const untracked = getUntrackedFiles(cwd);
|
|
18680
|
-
const scopedDirs = [...new Set(scoped.map(
|
|
19227
|
+
const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
|
|
18681
19228
|
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
18682
19229
|
const scopedSet = new Set(scoped);
|
|
18683
19230
|
const adjacentUntracked = untracked.filter(
|
|
@@ -18702,8 +19249,13 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
18702
19249
|
}
|
|
18703
19250
|
return safeRun(() => stageAllAndCommit(cwd, message));
|
|
18704
19251
|
}
|
|
18705
|
-
function pushAndCreatePR(config2, taskId, taskTitle) {
|
|
19252
|
+
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
18706
19253
|
const lines = [];
|
|
19254
|
+
if (!hasLocalWorkspace()) {
|
|
19255
|
+
const steps = hostedCompleteSteps(clientName, taskId, module, cycleNumber ?? 0, config2.baseBranch, taskTitle);
|
|
19256
|
+
if (steps) lines.push(...steps);
|
|
19257
|
+
return lines;
|
|
19258
|
+
}
|
|
18707
19259
|
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
18708
19260
|
return lines;
|
|
18709
19261
|
}
|
|
@@ -18888,7 +19440,7 @@ async function describeTask(adapter2, taskId) {
|
|
|
18888
19440
|
}
|
|
18889
19441
|
return { task };
|
|
18890
19442
|
}
|
|
18891
|
-
async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
19443
|
+
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
18892
19444
|
const task = await adapter2.getTask(taskId);
|
|
18893
19445
|
if (!task) {
|
|
18894
19446
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
@@ -18930,6 +19482,12 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
18930
19482
|
const branchLines = [];
|
|
18931
19483
|
if (options.light) {
|
|
18932
19484
|
branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
|
|
19485
|
+
} else if (!hasLocalWorkspace()) {
|
|
19486
|
+
const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
|
|
19487
|
+
const hosted = hostedStartSteps(clientName, taskId, task.module, cycleHealth?.totalCycles ?? 0, config2.baseBranch);
|
|
19488
|
+
if (hosted) {
|
|
19489
|
+
branchLines.push(...hosted.steps);
|
|
19490
|
+
}
|
|
18933
19491
|
} else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
18934
19492
|
const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
|
|
18935
19493
|
const cycleNumber = cycleHealth?.totalCycles ?? 0;
|
|
@@ -18998,6 +19556,18 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
18998
19556
|
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
18999
19557
|
}
|
|
19000
19558
|
} else {
|
|
19559
|
+
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19560
|
+
if (baseBranch !== config2.baseBranch) {
|
|
19561
|
+
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
19562
|
+
}
|
|
19563
|
+
const trackedDirty = getTrackedModifiedFiles(config2.projectRoot);
|
|
19564
|
+
if (trackedDirty.length > 0 && currentBranch !== baseBranch) {
|
|
19565
|
+
const shown = trackedDirty.slice(0, 5).join(", ");
|
|
19566
|
+
const more = trackedDirty.length > 5 ? ` (+${trackedDirty.length - 5} more)` : "";
|
|
19567
|
+
throw new Error(
|
|
19568
|
+
`build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first, or run build_execute from '${baseBranch}'. Dirty: ${shown}${more}.`
|
|
19569
|
+
);
|
|
19570
|
+
}
|
|
19001
19571
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19002
19572
|
const { toStash, preservedDocs } = selectAutostashPaths(
|
|
19003
19573
|
getModifiedFiles(config2.projectRoot),
|
|
@@ -19026,10 +19596,6 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
19026
19596
|
}
|
|
19027
19597
|
}
|
|
19028
19598
|
}
|
|
19029
|
-
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19030
|
-
if (baseBranch !== config2.baseBranch) {
|
|
19031
|
-
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
19032
|
-
}
|
|
19033
19599
|
const featureBranchExistsLocally = branchExists(config2.projectRoot, featureBranch);
|
|
19034
19600
|
const featureBranchOnOrigin = !!originCycleBranch && originCycleBranch === featureBranch;
|
|
19035
19601
|
const featureBranchExists = featureBranchExistsLocally || featureBranchOnOrigin;
|
|
@@ -19125,16 +19691,16 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
19125
19691
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
19126
19692
|
return;
|
|
19127
19693
|
}
|
|
19128
|
-
const papiDir =
|
|
19129
|
-
if (!
|
|
19694
|
+
const papiDir = join10(projectRoot, ".papi");
|
|
19695
|
+
if (!existsSync6(papiDir)) {
|
|
19130
19696
|
mkdirSync2(papiDir, { recursive: true });
|
|
19131
19697
|
}
|
|
19132
|
-
const scopePath =
|
|
19698
|
+
const scopePath = join10(papiDir, "active-task-scope.txt");
|
|
19133
19699
|
writeFileSync2(scopePath, content, "utf-8");
|
|
19134
19700
|
}
|
|
19135
19701
|
function clearActiveTaskScope(projectRoot) {
|
|
19136
|
-
const scopePath =
|
|
19137
|
-
if (
|
|
19702
|
+
const scopePath = join10(projectRoot, ".papi", "active-task-scope.txt");
|
|
19703
|
+
if (existsSync6(scopePath)) {
|
|
19138
19704
|
unlinkSync(scopePath);
|
|
19139
19705
|
}
|
|
19140
19706
|
}
|
|
@@ -19154,7 +19720,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
19154
19720
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
19155
19721
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
19156
19722
|
try {
|
|
19157
|
-
const content =
|
|
19723
|
+
const content = readFileSync5(absolutePath, "utf-8").slice(0, 2e3);
|
|
19158
19724
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19159
19725
|
if (fmMatch) {
|
|
19160
19726
|
const fm = fmMatch[1];
|
|
@@ -19201,7 +19767,7 @@ Fix the deploy first, then re-run build_execute complete with a 2xx verification
|
|
|
19201
19767
|
);
|
|
19202
19768
|
}
|
|
19203
19769
|
}
|
|
19204
|
-
async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
19770
|
+
async function completeBuild(adapter2, config2, taskId, input, options = {}, clientName) {
|
|
19205
19771
|
const task = await adapter2.getTask(taskId);
|
|
19206
19772
|
if (!task) {
|
|
19207
19773
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
@@ -19479,7 +20045,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19479
20045
|
if (options.light) {
|
|
19480
20046
|
prLines.push("Light mode: skipping push and PR creation.");
|
|
19481
20047
|
} else if (config2.autoCommit && input.completed === "yes") {
|
|
19482
|
-
prLines = pushAndCreatePR(config2, taskId, task.title);
|
|
20048
|
+
prLines = pushAndCreatePR(config2, taskId, task.title, clientName, task.module, cycleNumber);
|
|
19483
20049
|
}
|
|
19484
20050
|
const allTasks = await adapter2.queryBoard();
|
|
19485
20051
|
warnIfEmpty("queryBoard (cycle progress)", allTasks);
|
|
@@ -19499,14 +20065,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19499
20065
|
let docWarning;
|
|
19500
20066
|
try {
|
|
19501
20067
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
19502
|
-
const docsDir =
|
|
19503
|
-
if (
|
|
20068
|
+
const docsDir = join10(config2.projectRoot, "docs");
|
|
20069
|
+
if (existsSync6(docsDir)) {
|
|
19504
20070
|
const scanDir = (dir, depth = 0) => {
|
|
19505
20071
|
if (depth > 8) return [];
|
|
19506
|
-
const entries =
|
|
20072
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
19507
20073
|
const files = [];
|
|
19508
20074
|
for (const e of entries) {
|
|
19509
|
-
const full =
|
|
20075
|
+
const full = join10(dir, e.name);
|
|
19510
20076
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
19511
20077
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
19512
20078
|
}
|
|
@@ -19521,7 +20087,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19521
20087
|
const failed = [];
|
|
19522
20088
|
for (const docPath of unregistered) {
|
|
19523
20089
|
try {
|
|
19524
|
-
const meta = extractDocMeta(
|
|
20090
|
+
const meta = extractDocMeta(join10(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
19525
20091
|
await adapter2.registerDoc({
|
|
19526
20092
|
title: meta.title,
|
|
19527
20093
|
type: meta.type,
|
|
@@ -19598,6 +20164,9 @@ var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
|
|
|
19598
20164
|
var MODULE_INSTRUCTIONS = {
|
|
19599
20165
|
Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
|
|
19600
20166
|
|
|
20167
|
+
**STEP 0 \u2014 Dispatch the design agent for the visual layer (do NOT hand-write UI in the main build context).**
|
|
20168
|
+
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.)
|
|
20169
|
+
|
|
19601
20170
|
**STEP 1 \u2014 BEFORE writing any code:**
|
|
19602
20171
|
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.)
|
|
19603
20172
|
|
|
@@ -19837,7 +20406,7 @@ function isResearchOrSpike(task) {
|
|
|
19837
20406
|
}
|
|
19838
20407
|
function formatListItem(task) {
|
|
19839
20408
|
const base = `- **${task.id}:** ${task.title}
|
|
19840
|
-
Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity}`;
|
|
20409
|
+
Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity} | ${modelTierTag(task.complexity)}`;
|
|
19841
20410
|
if (isResearchOrSpike(task)) {
|
|
19842
20411
|
return base + "\n _Research/spike \u2014 still needed?_ **(a)** build_execute | **(b)** fast-close (answered) | **(c)** deprioritise | **(d)** discuss";
|
|
19843
20412
|
}
|
|
@@ -19959,7 +20528,7 @@ async function handleBuildDescribe(adapter2, args) {
|
|
|
19959
20528
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
19960
20529
|
}
|
|
19961
20530
|
}
|
|
19962
|
-
async function handleBuildExecute(adapter2, config2, args) {
|
|
20531
|
+
async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
19963
20532
|
const taskId = args.task_id;
|
|
19964
20533
|
if (!taskId) {
|
|
19965
20534
|
return errorResponse("task_id is required.");
|
|
@@ -19983,15 +20552,20 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
19983
20552
|
}
|
|
19984
20553
|
}
|
|
19985
20554
|
if (hasReportFields(args)) {
|
|
19986
|
-
return handleExecuteComplete(adapter2, config2, taskId, args, light);
|
|
20555
|
+
return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
|
|
19987
20556
|
}
|
|
19988
20557
|
const tracker = new ProgressTracker("start_build");
|
|
19989
20558
|
try {
|
|
19990
|
-
const result = await startBuild(adapter2, config2, taskId, { light });
|
|
20559
|
+
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
19991
20560
|
tracker.mark("start_decorate_handoff");
|
|
19992
20561
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
19993
20562
|
const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
|
|
19994
|
-
const
|
|
20563
|
+
const projectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
20564
|
+
const projectBanner = projectInfo ? getProjectConnectionBanner(projectInfo.name, projectInfo.slug) : null;
|
|
20565
|
+
const projectLine = projectBanner ? `> ${projectBanner}
|
|
20566
|
+
|
|
20567
|
+
` : "";
|
|
20568
|
+
const header = `${projectLine}${branchInfo}**Executing ${result.task.id}: ${result.task.title}** \u2014 marked In Progress.
|
|
19995
20569
|
|
|
19996
20570
|
---
|
|
19997
20571
|
|
|
@@ -20034,7 +20608,8 @@ ${entries}`;
|
|
|
20034
20608
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
20035
20609
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
20036
20610
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
20037
|
-
|
|
20611
|
+
const modelNote = formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity);
|
|
20612
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
|
|
20038
20613
|
} catch (err) {
|
|
20039
20614
|
if (isNoHandoffError(err)) {
|
|
20040
20615
|
const lines = [
|
|
@@ -20070,7 +20645,7 @@ ${entries}`;
|
|
|
20070
20645
|
}));
|
|
20071
20646
|
}
|
|
20072
20647
|
}
|
|
20073
|
-
async function handleExecuteComplete(adapter2, config2, taskId, args, light = false) {
|
|
20648
|
+
async function handleExecuteComplete(adapter2, config2, taskId, args, light = false, clientName) {
|
|
20074
20649
|
const completed = args.completed;
|
|
20075
20650
|
const effort = args.effort;
|
|
20076
20651
|
const estimatedEffort = args.estimated_effort;
|
|
@@ -20148,7 +20723,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
20148
20723
|
})),
|
|
20149
20724
|
productionVerification,
|
|
20150
20725
|
preview
|
|
20151
|
-
}, { light });
|
|
20726
|
+
}, { light }, clientName);
|
|
20152
20727
|
tracker.mark("complete_format");
|
|
20153
20728
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
20154
20729
|
for (const learningId of resolvesLearnings) {
|
|
@@ -20279,14 +20854,12 @@ function formatCompleteResult(result) {
|
|
|
20279
20854
|
} else {
|
|
20280
20855
|
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.`);
|
|
20281
20856
|
}
|
|
20282
|
-
lines.push("", "*Tip: Start a new chat for your next build to keep context fresh and avoid hitting context window limits.*");
|
|
20283
20857
|
} else {
|
|
20284
20858
|
if (hasDiscoveredIssues) {
|
|
20285
20859
|
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.");
|
|
20286
20860
|
} else {
|
|
20287
20861
|
lines.push("", "All cycle tasks complete. Run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
|
|
20288
20862
|
}
|
|
20289
|
-
lines.push("", "*Tip: Start a new chat for your next session to keep context fresh.*");
|
|
20290
20863
|
}
|
|
20291
20864
|
return lines.join("\n");
|
|
20292
20865
|
}
|
|
@@ -20875,13 +21448,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
20875
21448
|
init_git();
|
|
20876
21449
|
|
|
20877
21450
|
// src/services/reconcile.ts
|
|
20878
|
-
import { readFileSync as
|
|
20879
|
-
import { join as
|
|
21451
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
21452
|
+
import { join as join11 } from "path";
|
|
20880
21453
|
function loadDocsIndex(projectRoot) {
|
|
20881
21454
|
if (!hasLocalWorkspace()) return "";
|
|
20882
21455
|
try {
|
|
20883
|
-
const indexPath =
|
|
20884
|
-
const raw =
|
|
21456
|
+
const indexPath = join11(projectRoot, "docs", "INDEX.md");
|
|
21457
|
+
const raw = readFileSync6(indexPath, "utf8");
|
|
20885
21458
|
const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
|
|
20886
21459
|
if (rows.length === 0) return "";
|
|
20887
21460
|
const entries = rows.map((row) => {
|
|
@@ -21456,8 +22029,8 @@ Produce your analysis and structured output above. Present Part 1 to the user an
|
|
|
21456
22029
|
}
|
|
21457
22030
|
|
|
21458
22031
|
// src/tools/review.ts
|
|
21459
|
-
import { existsSync as
|
|
21460
|
-
import { join as
|
|
22032
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
22033
|
+
import { join as join12 } from "path";
|
|
21461
22034
|
init_git();
|
|
21462
22035
|
|
|
21463
22036
|
// src/services/review.ts
|
|
@@ -21669,11 +22242,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
|
21669
22242
|
${diff}
|
|
21670
22243
|
\`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
|
|
21671
22244
|
let projectContext = "";
|
|
21672
|
-
const ctxPath =
|
|
21673
|
-
if (
|
|
22245
|
+
const ctxPath = join12(config2.projectRoot, ".agents", "papi-context.md");
|
|
22246
|
+
if (existsSync7(ctxPath)) {
|
|
21674
22247
|
try {
|
|
21675
22248
|
projectContext = `### Project context (.agents/papi-context.md)
|
|
21676
|
-
${
|
|
22249
|
+
${readFileSync7(ctxPath, "utf-8")}
|
|
21677
22250
|
|
|
21678
22251
|
`;
|
|
21679
22252
|
} catch {
|
|
@@ -21828,8 +22401,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
21828
22401
|
};
|
|
21829
22402
|
}
|
|
21830
22403
|
const details = [];
|
|
21831
|
-
const papiDir =
|
|
21832
|
-
if (
|
|
22404
|
+
const papiDir = join12(config2.projectRoot, ".papi");
|
|
22405
|
+
if (existsSync7(papiDir)) {
|
|
21833
22406
|
try {
|
|
21834
22407
|
const commitResult = stageDirAndCommit(
|
|
21835
22408
|
config2.projectRoot,
|
|
@@ -22859,8 +23432,9 @@ async function getHealthSummary(adapter2) {
|
|
|
22859
23432
|
const cycleNumber = health.totalCycles;
|
|
22860
23433
|
const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
|
|
22861
23434
|
const reviewDue = health.strategyReviewDue;
|
|
22862
|
-
const reviewGateBlocking = cyclesSinceReview >=
|
|
22863
|
-
const
|
|
23435
|
+
const reviewGateBlocking = cyclesSinceReview >= STRATEGY_REVIEW_BLOCK_GAP;
|
|
23436
|
+
const reviewAvailable = cyclesSinceReview >= STRATEGY_REVIEW_OFFER_GAP && !reviewGateBlocking;
|
|
23437
|
+
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}`;
|
|
22864
23438
|
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
22865
23439
|
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
22866
23440
|
const statusCounts = countByStatus(nonDeferredTasks);
|
|
@@ -23139,115 +23713,9 @@ function formatUnblockSection(candidates) {
|
|
|
23139
23713
|
return lines.join("\n");
|
|
23140
23714
|
}
|
|
23141
23715
|
|
|
23142
|
-
// src/lib/skill-detection.ts
|
|
23143
|
-
import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
23144
|
-
import { join as join11 } from "path";
|
|
23145
|
-
function readPackageJson(projectRoot) {
|
|
23146
|
-
const path7 = join11(projectRoot, "package.json");
|
|
23147
|
-
if (!existsSync6(path7)) return null;
|
|
23148
|
-
try {
|
|
23149
|
-
const raw = readFileSync6(path7, "utf-8");
|
|
23150
|
-
return JSON.parse(raw);
|
|
23151
|
-
} catch {
|
|
23152
|
-
return null;
|
|
23153
|
-
}
|
|
23154
|
-
}
|
|
23155
|
-
function allDeps(pkg) {
|
|
23156
|
-
if (!pkg) return {};
|
|
23157
|
-
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
23158
|
-
}
|
|
23159
|
-
function hasDependencyMatching(deps, pattern) {
|
|
23160
|
-
for (const name of Object.keys(deps)) {
|
|
23161
|
-
if (pattern.test(name)) return true;
|
|
23162
|
-
}
|
|
23163
|
-
return false;
|
|
23164
|
-
}
|
|
23165
|
-
function hasGitHubWorkflows(projectRoot) {
|
|
23166
|
-
const dir = join11(projectRoot, ".github", "workflows");
|
|
23167
|
-
if (!existsSync6(dir)) return false;
|
|
23168
|
-
try {
|
|
23169
|
-
const entries = readdirSync5(dir);
|
|
23170
|
-
return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
23171
|
-
} catch {
|
|
23172
|
-
return false;
|
|
23173
|
-
}
|
|
23174
|
-
}
|
|
23175
|
-
function envExampleMentionsStaging(projectRoot) {
|
|
23176
|
-
const path7 = join11(projectRoot, ".env.example");
|
|
23177
|
-
if (!existsSync6(path7)) return false;
|
|
23178
|
-
try {
|
|
23179
|
-
const raw = readFileSync6(path7, "utf-8");
|
|
23180
|
-
return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
|
|
23181
|
-
} catch {
|
|
23182
|
-
return false;
|
|
23183
|
-
}
|
|
23184
|
-
}
|
|
23185
|
-
function hasVercelConfig(projectRoot) {
|
|
23186
|
-
if (existsSync6(join11(projectRoot, "vercel.json"))) return true;
|
|
23187
|
-
const vercelDir = join11(projectRoot, ".vercel");
|
|
23188
|
-
if (!existsSync6(vercelDir)) return false;
|
|
23189
|
-
try {
|
|
23190
|
-
return statSync4(vercelDir).isDirectory();
|
|
23191
|
-
} catch {
|
|
23192
|
-
return false;
|
|
23193
|
-
}
|
|
23194
|
-
}
|
|
23195
|
-
function scanForSkillSignals(projectRoot) {
|
|
23196
|
-
const proposals = [];
|
|
23197
|
-
const pkg = readPackageJson(projectRoot);
|
|
23198
|
-
const deps = allDeps(pkg);
|
|
23199
|
-
if (hasGitHubWorkflows(projectRoot)) {
|
|
23200
|
-
proposals.push({
|
|
23201
|
-
id: "gh-actions-debug",
|
|
23202
|
-
name: "GitHub Actions debugger",
|
|
23203
|
-
rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
|
|
23204
|
-
hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
|
|
23205
|
-
});
|
|
23206
|
-
}
|
|
23207
|
-
if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
|
|
23208
|
-
proposals.push({
|
|
23209
|
-
id: "error-tracking",
|
|
23210
|
-
name: "Error tracking helper",
|
|
23211
|
-
rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
|
|
23212
|
-
hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
|
|
23213
|
-
});
|
|
23214
|
-
}
|
|
23215
|
-
if (hasVercelConfig(projectRoot)) {
|
|
23216
|
-
proposals.push({
|
|
23217
|
-
id: "read-vercel-logs",
|
|
23218
|
-
name: "Vercel deploy logs",
|
|
23219
|
-
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.",
|
|
23220
|
-
hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
|
|
23221
|
-
});
|
|
23222
|
-
}
|
|
23223
|
-
if (envExampleMentionsStaging(projectRoot)) {
|
|
23224
|
-
proposals.push({
|
|
23225
|
-
id: "staging-environment",
|
|
23226
|
-
name: "Staging-environment helper",
|
|
23227
|
-
rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
|
|
23228
|
-
hint: 'Search Claude Code skill registry for "staging" or define your own.'
|
|
23229
|
-
});
|
|
23230
|
-
}
|
|
23231
|
-
return proposals;
|
|
23232
|
-
}
|
|
23233
|
-
function formatSkillProposals(proposals) {
|
|
23234
|
-
if (proposals.length === 0) return "";
|
|
23235
|
-
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._"];
|
|
23236
|
-
for (const p of proposals) {
|
|
23237
|
-
lines.push("");
|
|
23238
|
-
lines.push(`### ${p.name}`);
|
|
23239
|
-
lines.push(p.rationale);
|
|
23240
|
-
if (p.hint) {
|
|
23241
|
-
lines.push(`_${p.hint}_`);
|
|
23242
|
-
}
|
|
23243
|
-
lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
|
|
23244
|
-
}
|
|
23245
|
-
return "\n" + lines.join("\n");
|
|
23246
|
-
}
|
|
23247
|
-
|
|
23248
23716
|
// src/tools/agent-list.ts
|
|
23249
23717
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
23250
|
-
import { join as
|
|
23718
|
+
import { join as join13 } from "path";
|
|
23251
23719
|
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.";
|
|
23252
23720
|
function parseAgentFrontmatter(content) {
|
|
23253
23721
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -23259,7 +23727,7 @@ function parseAgentFrontmatter(content) {
|
|
|
23259
23727
|
return { name: nameMatch?.[1].trim(), description };
|
|
23260
23728
|
}
|
|
23261
23729
|
async function listAgents(projectRoot) {
|
|
23262
|
-
const agentsDir =
|
|
23730
|
+
const agentsDir = join13(projectRoot, ".claude", "agents");
|
|
23263
23731
|
let files;
|
|
23264
23732
|
try {
|
|
23265
23733
|
files = await readdir2(agentsDir);
|
|
@@ -23270,7 +23738,7 @@ async function listAgents(projectRoot) {
|
|
|
23270
23738
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
23271
23739
|
let content;
|
|
23272
23740
|
try {
|
|
23273
|
-
content = await readFile7(
|
|
23741
|
+
content = await readFile7(join13(agentsDir, file), "utf-8");
|
|
23274
23742
|
} catch {
|
|
23275
23743
|
continue;
|
|
23276
23744
|
}
|
|
@@ -23278,7 +23746,7 @@ async function listAgents(projectRoot) {
|
|
|
23278
23746
|
agents.push({
|
|
23279
23747
|
name: meta?.name ?? file.replace(/\.md$/, ""),
|
|
23280
23748
|
description: meta?.description ?? "",
|
|
23281
|
-
path:
|
|
23749
|
+
path: join13(".claude", "agents", file)
|
|
23282
23750
|
});
|
|
23283
23751
|
}
|
|
23284
23752
|
agents.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
@@ -23319,8 +23787,8 @@ ${formatted}`);
|
|
|
23319
23787
|
}
|
|
23320
23788
|
|
|
23321
23789
|
// src/tools/doc-registry.ts
|
|
23322
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
23323
|
-
import { join as
|
|
23790
|
+
import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
23791
|
+
import { join as join14, relative } from "path";
|
|
23324
23792
|
import { homedir as homedir3 } from "os";
|
|
23325
23793
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
23326
23794
|
var docRegisterTool = {
|
|
@@ -23517,7 +23985,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
23517
23985
|
const lines = docs.map((d) => {
|
|
23518
23986
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
23519
23987
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
23520
|
-
const missingNote = root && d.path && !
|
|
23988
|
+
const missingNote = root && d.path && !existsSync8(join14(root, d.path)) ? `
|
|
23521
23989
|
> \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.` : "";
|
|
23522
23990
|
return `### ${d.title}
|
|
23523
23991
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -23531,12 +23999,12 @@ ${d.summary}
|
|
|
23531
23999
|
${lines.join("\n---\n\n")}`);
|
|
23532
24000
|
}
|
|
23533
24001
|
function scanMdFiles(dir, rootDir) {
|
|
23534
|
-
if (!
|
|
24002
|
+
if (!existsSync8(dir)) return [];
|
|
23535
24003
|
const files = [];
|
|
23536
24004
|
try {
|
|
23537
24005
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
23538
24006
|
for (const entry of entries) {
|
|
23539
|
-
const full =
|
|
24007
|
+
const full = join14(dir, entry.name);
|
|
23540
24008
|
if (entry.isDirectory()) {
|
|
23541
24009
|
files.push(...scanMdFiles(full, rootDir));
|
|
23542
24010
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -23549,7 +24017,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
23549
24017
|
}
|
|
23550
24018
|
function extractTitle(filePath) {
|
|
23551
24019
|
try {
|
|
23552
|
-
const content =
|
|
24020
|
+
const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
|
|
23553
24021
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
23554
24022
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
23555
24023
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -23570,17 +24038,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
23570
24038
|
const includePlans = args.include_plans ?? false;
|
|
23571
24039
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
23572
24040
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
23573
|
-
const docsDir =
|
|
24041
|
+
const docsDir = join14(config2.projectRoot, "docs");
|
|
23574
24042
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
23575
24043
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
23576
24044
|
let unregisteredPlans = [];
|
|
23577
24045
|
if (includePlans) {
|
|
23578
|
-
const plansDir =
|
|
23579
|
-
if (
|
|
24046
|
+
const plansDir = join14(homedir3(), ".claude", "plans");
|
|
24047
|
+
if (existsSync8(plansDir)) {
|
|
23580
24048
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
23581
24049
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
23582
24050
|
path: f,
|
|
23583
|
-
title: extractTitle(
|
|
24051
|
+
title: extractTitle(join14(plansDir, f.replace("plans/", "")))
|
|
23584
24052
|
}));
|
|
23585
24053
|
}
|
|
23586
24054
|
}
|
|
@@ -23591,7 +24059,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
23591
24059
|
if (unregisteredDocs.length > 0) {
|
|
23592
24060
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
23593
24061
|
for (const f of unregisteredDocs) {
|
|
23594
|
-
const title = extractTitle(
|
|
24062
|
+
const title = extractTitle(join14(config2.projectRoot, f));
|
|
23595
24063
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
23596
24064
|
}
|
|
23597
24065
|
}
|
|
@@ -23701,6 +24169,22 @@ ${userNotes}` : referenceLine;
|
|
|
23701
24169
|
);
|
|
23702
24170
|
}
|
|
23703
24171
|
|
|
24172
|
+
// src/lib/harness-guidance.ts
|
|
24173
|
+
function buildLoopRunsHere(i) {
|
|
24174
|
+
if (i.forceLocal) return true;
|
|
24175
|
+
if (i.harness.known) return i.harness.build;
|
|
24176
|
+
if (i.envHintsNoGit) return false;
|
|
24177
|
+
return i.localWorkspace;
|
|
24178
|
+
}
|
|
24179
|
+
function nextActionQualifier(harness) {
|
|
24180
|
+
const where = harness.known ? harness.label : "your tool";
|
|
24181
|
+
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._`;
|
|
24182
|
+
}
|
|
24183
|
+
function connectOnlyRoleNote(harness) {
|
|
24184
|
+
if (harness.build || !harness.known) return null;
|
|
24185
|
+
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.`;
|
|
24186
|
+
}
|
|
24187
|
+
|
|
23704
24188
|
// src/lib/enrichment.ts
|
|
23705
24189
|
function headingSlug(line) {
|
|
23706
24190
|
const match = line.match(/^##\s+(.+?)(?:\s*\([^)]*\))?\s*$/);
|
|
@@ -23786,8 +24270,8 @@ async function verifyProject(adapter2) {
|
|
|
23786
24270
|
// src/tools/orient.ts
|
|
23787
24271
|
import { execFile as execFile2 } from "child_process";
|
|
23788
24272
|
import { promisify as promisify2 } from "util";
|
|
23789
|
-
import { readFileSync as
|
|
23790
|
-
import { join as
|
|
24273
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync9 } from "fs";
|
|
24274
|
+
import { join as join15 } from "path";
|
|
23791
24275
|
var execFileAsync2 = promisify2(execFile2);
|
|
23792
24276
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
23793
24277
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -23832,7 +24316,7 @@ async function runWithLimit(concurrency, tasks) {
|
|
|
23832
24316
|
}
|
|
23833
24317
|
var orientTool = {
|
|
23834
24318
|
name: "orient",
|
|
23835
|
-
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.
|
|
24319
|
+
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).",
|
|
23836
24320
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
23837
24321
|
inputSchema: {
|
|
23838
24322
|
type: "object",
|
|
@@ -23840,7 +24324,7 @@ var orientTool = {
|
|
|
23840
24324
|
environment: {
|
|
23841
24325
|
type: "string",
|
|
23842
24326
|
enum: ["local-cli", "hosted", "api", "unknown"],
|
|
23843
|
-
description:
|
|
24327
|
+
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).`
|
|
23844
24328
|
},
|
|
23845
24329
|
deep_housekeeping: {
|
|
23846
24330
|
type: "boolean",
|
|
@@ -23884,7 +24368,7 @@ function formatSynthesisParagraph(opts) {
|
|
|
23884
24368
|
parts.push(`Next: ${cleanMode}`);
|
|
23885
24369
|
return parts.join(" ");
|
|
23886
24370
|
}
|
|
23887
|
-
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = [], projectName, teamSummary) {
|
|
24371
|
+
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = [], projectName, teamSummary, clientName) {
|
|
23888
24372
|
const lines = [];
|
|
23889
24373
|
const cycleIsComplete = health.latestCycleStatus === "complete";
|
|
23890
24374
|
const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
|
|
@@ -23906,6 +24390,12 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
23906
24390
|
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
23907
24391
|
lines.push("");
|
|
23908
24392
|
}
|
|
24393
|
+
const harness = detectHarness(clientName);
|
|
24394
|
+
const roleNote = connectOnlyRoleNote(harness);
|
|
24395
|
+
if (roleNote) {
|
|
24396
|
+
lines.push(`> ${roleNote}`);
|
|
24397
|
+
lines.push("");
|
|
24398
|
+
}
|
|
23909
24399
|
if (buildInfo.warnings.length > 0) {
|
|
23910
24400
|
for (const w of buildInfo.warnings) {
|
|
23911
24401
|
lines.push(`> ${w}`);
|
|
@@ -23917,11 +24407,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
23917
24407
|
lines.push("");
|
|
23918
24408
|
}
|
|
23919
24409
|
const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
|
|
23923
|
-
|
|
23924
|
-
|
|
24410
|
+
const loopRunsHere = buildLoopRunsHere({
|
|
24411
|
+
harness,
|
|
24412
|
+
localWorkspace: hasLocalWorkspace(),
|
|
24413
|
+
forceLocal: environment === "local-cli",
|
|
24414
|
+
envHintsNoGit: GIT_DEPENDENT_ENVS.has(environment)
|
|
24415
|
+
});
|
|
24416
|
+
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
24417
|
+
if (isGitDependentRec && !loopRunsHere) {
|
|
24418
|
+
lines.push(`> ${nextActionQualifier(harness)}`);
|
|
23925
24419
|
}
|
|
23926
24420
|
lines.push("");
|
|
23927
24421
|
lines.push(`**Strategy Review:** ${health.reviewWarning}`);
|
|
@@ -24087,8 +24581,8 @@ async function getLatestGitTag(projectRoot) {
|
|
|
24087
24581
|
}
|
|
24088
24582
|
async function checkNpmVersionDrift() {
|
|
24089
24583
|
try {
|
|
24090
|
-
const pkgPath =
|
|
24091
|
-
const pkg = JSON.parse(
|
|
24584
|
+
const pkgPath = join15(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
24585
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
24092
24586
|
const localVersion = pkg.version;
|
|
24093
24587
|
const packageName = pkg.name;
|
|
24094
24588
|
const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
|
|
@@ -24205,7 +24699,23 @@ async function computeReleaseHistory(adapter2) {
|
|
|
24205
24699
|
if (released.length === 0) return void 0;
|
|
24206
24700
|
return `**Recent releases:** ${released.join(" \xB7 ")}`;
|
|
24207
24701
|
}
|
|
24208
|
-
|
|
24702
|
+
function formatResolvedFeedback(items) {
|
|
24703
|
+
if (items.length === 0) return "";
|
|
24704
|
+
const MAX = 5;
|
|
24705
|
+
const shown = items.slice(0, MAX);
|
|
24706
|
+
const lines = ["\n\n## \u2705 Your feedback shipped"];
|
|
24707
|
+
for (const it of shown) {
|
|
24708
|
+
const kind = it.type === "idea" ? "Idea" : "Bug";
|
|
24709
|
+
const verb = it.status === "wont_fix" ? "closed" : "resolved";
|
|
24710
|
+
const desc = it.description.length > 90 ? `${it.description.slice(0, 87)}\u2026` : it.description;
|
|
24711
|
+
const note = it.resolutionNote?.trim() ? ` \u2014 ${it.resolutionNote.trim()}` : "";
|
|
24712
|
+
lines.push(`- **${kind}** "${desc}" was ${verb}${note}`);
|
|
24713
|
+
}
|
|
24714
|
+
if (items.length > MAX) lines.push(`- \u2026and ${items.length - MAX} more`);
|
|
24715
|
+
lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
|
|
24716
|
+
return lines.join("\n");
|
|
24717
|
+
}
|
|
24718
|
+
async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
24209
24719
|
const environment = normaliseEnvironment(args.environment);
|
|
24210
24720
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
24211
24721
|
const fullEnrichment = args.full === true || deepHousekeeping;
|
|
@@ -24249,6 +24759,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24249
24759
|
backlogHandoffCount: allTasks.filter((t) => t.cycle !== currentCycle).length
|
|
24250
24760
|
};
|
|
24251
24761
|
const sharedActiveDecisionsPromise = adapter2.getActiveDecisions();
|
|
24762
|
+
const resolvedFeedbackPromise = adapter2.getUnnotifiedResolvedFeedback ? adapter2.getUnnotifiedResolvedFeedback(config2.userId).catch(() => []) : Promise.resolve([]);
|
|
24252
24763
|
const [
|
|
24253
24764
|
proxyVersionOutcome,
|
|
24254
24765
|
p1BacklogOutcome,
|
|
@@ -24528,7 +25039,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24528
25039
|
// task-1652: deep housekeeping — opt-in sweep (board, unrecorded commits, unregistered docs)
|
|
24529
25040
|
// task-1865: also detects stale skill forks vs the @papi-ai/skills registry.
|
|
24530
25041
|
tracked("deep-housekeeping", async () => {
|
|
24531
|
-
if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
25042
|
+
if (!deepHousekeeping) return { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
24532
25043
|
let reconciliationNote2 = "";
|
|
24533
25044
|
try {
|
|
24534
25045
|
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
@@ -24540,6 +25051,22 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24540
25051
|
}
|
|
24541
25052
|
} catch {
|
|
24542
25053
|
}
|
|
25054
|
+
let mergedInProgressNote2 = "";
|
|
25055
|
+
try {
|
|
25056
|
+
if (hasLocalWorkspace()) {
|
|
25057
|
+
const merged = detectMergedInProgress(config2.projectRoot, config2.baseBranch, allTasks);
|
|
25058
|
+
if (merged.length > 0) {
|
|
25059
|
+
const lines = ["\n\n## Merged but In Progress"];
|
|
25060
|
+
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):`);
|
|
25061
|
+
for (const m of merged) {
|
|
25062
|
+
const ref = m.pr ? `${m.pr} (\`${m.commit}\`)` : `\`${m.commit}\``;
|
|
25063
|
+
lines.push(`- \u26A0\uFE0F **${m.displayId}** \u2014 merged via ${ref}: ${m.subject}`);
|
|
25064
|
+
}
|
|
25065
|
+
mergedInProgressNote2 = lines.join("\n");
|
|
25066
|
+
}
|
|
25067
|
+
}
|
|
25068
|
+
} catch {
|
|
25069
|
+
}
|
|
24543
25070
|
let unrecordedNote2 = "";
|
|
24544
25071
|
try {
|
|
24545
25072
|
const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
|
|
@@ -24564,7 +25091,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24564
25091
|
let unregisteredDocsNote2 = "";
|
|
24565
25092
|
try {
|
|
24566
25093
|
if (adapter2.searchDocs && hasLocalWorkspace()) {
|
|
24567
|
-
const docsDir =
|
|
25094
|
+
const docsDir = join15(config2.projectRoot, "docs");
|
|
24568
25095
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
24569
25096
|
if (docsFiles.length > 0) {
|
|
24570
25097
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -24591,7 +25118,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24591
25118
|
}
|
|
24592
25119
|
} catch {
|
|
24593
25120
|
}
|
|
24594
|
-
return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
|
|
25121
|
+
return { reconciliationNote: reconciliationNote2, mergedInProgressNote: mergedInProgressNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
|
|
24595
25122
|
})
|
|
24596
25123
|
]);
|
|
24597
25124
|
const proxyWarning = proxyVersionOutcome.status === "fulfilled" ? proxyVersionOutcome.value : void 0;
|
|
@@ -24627,7 +25154,13 @@ ${versionDrift}` : "";
|
|
|
24627
25154
|
const projectName = projectBannerResult.name;
|
|
24628
25155
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
24629
25156
|
const deliveryShapeNote = deliveryShapeOutcome.status === "fulfilled" ? deliveryShapeOutcome.value : "";
|
|
24630
|
-
const
|
|
25157
|
+
const resolvedFeedback = await resolvedFeedbackPromise;
|
|
25158
|
+
const feedbackResolvedNote = formatResolvedFeedback(resolvedFeedback);
|
|
25159
|
+
if (resolvedFeedback.length > 0 && adapter2.markFeedbackNotified) {
|
|
25160
|
+
void adapter2.markFeedbackNotified(resolvedFeedback.map((f) => f.id), config2.userId).catch(() => {
|
|
25161
|
+
});
|
|
25162
|
+
}
|
|
25163
|
+
const { reconciliationNote, mergedInProgressNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
24631
25164
|
let enrichmentNote = "";
|
|
24632
25165
|
const enrichmentCollector = new FileWriteCollector();
|
|
24633
25166
|
try {
|
|
@@ -24674,8 +25207,8 @@ ${section}`;
|
|
|
24674
25207
|
tracked("release-history", () => computeReleaseHistory(adapter2))().catch(() => void 0)
|
|
24675
25208
|
]);
|
|
24676
25209
|
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
24677
|
-
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`).*";
|
|
24678
|
-
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
25210
|
+
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`).*";
|
|
25211
|
+
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);
|
|
24679
25212
|
} catch (err) {
|
|
24680
25213
|
const message = err instanceof Error ? err.message : String(err);
|
|
24681
25214
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -24706,9 +25239,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector) {
|
|
|
24706
25239
|
|
|
24707
25240
|
\u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
24708
25241
|
}
|
|
24709
|
-
const claudeMdPath =
|
|
24710
|
-
if (!
|
|
24711
|
-
const content =
|
|
25242
|
+
const claudeMdPath = join15(projectRoot, "CLAUDE.md");
|
|
25243
|
+
if (!existsSync9(claudeMdPath)) return "";
|
|
25244
|
+
const content = readFileSync9(claudeMdPath, "utf-8");
|
|
24712
25245
|
const additions = [];
|
|
24713
25246
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
24714
25247
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
|
|
@@ -25414,7 +25947,7 @@ ${result.userMessage}
|
|
|
25414
25947
|
|
|
25415
25948
|
// src/services/scope-brief.ts
|
|
25416
25949
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
25417
|
-
import { join as
|
|
25950
|
+
import { join as join16, dirname as dirname4 } from "path";
|
|
25418
25951
|
import Anthropic from "@anthropic-ai/sdk";
|
|
25419
25952
|
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.
|
|
25420
25953
|
|
|
@@ -25469,13 +26002,13 @@ async function runScopeBrief(adapter2, input) {
|
|
|
25469
26002
|
}
|
|
25470
26003
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
25471
26004
|
const relPath = `docs/scopes/${slug}.md`;
|
|
25472
|
-
const absPath =
|
|
26005
|
+
const absPath = join16(input.projectRoot, relPath);
|
|
25473
26006
|
const docBody = addFrontmatter(docContent, task, input.cycleNumber);
|
|
25474
26007
|
const collector = new FileWriteCollector();
|
|
25475
26008
|
if (input.adapterType === "proxy") {
|
|
25476
26009
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
25477
26010
|
} else {
|
|
25478
|
-
mkdirSync3(
|
|
26011
|
+
mkdirSync3(dirname4(absPath), { recursive: true });
|
|
25479
26012
|
writeFileSync4(absPath, docBody, "utf-8");
|
|
25480
26013
|
}
|
|
25481
26014
|
const taskCount = countSubTasks(docContent);
|
|
@@ -26213,21 +26746,90 @@ async function handleTaskUnclaim(adapter2, config2, args) {
|
|
|
26213
26746
|
return textResponse(`\u2705 Released **${result.id}** (${result.title}) back to the shared Pool.`);
|
|
26214
26747
|
}
|
|
26215
26748
|
|
|
26749
|
+
// src/tools/task-move.ts
|
|
26750
|
+
var taskMoveTool = {
|
|
26751
|
+
name: "task_move",
|
|
26752
|
+
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.",
|
|
26753
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
26754
|
+
inputSchema: {
|
|
26755
|
+
type: "object",
|
|
26756
|
+
properties: {
|
|
26757
|
+
task_id: { type: "string", description: 'The task to move, e.g. "task-2292".' },
|
|
26758
|
+
target_project: {
|
|
26759
|
+
type: "string",
|
|
26760
|
+
description: 'The destination project \u2014 its slug (e.g. "papi-ui") or UUID. Must be a project you own.'
|
|
26761
|
+
},
|
|
26762
|
+
confirm: {
|
|
26763
|
+
type: "boolean",
|
|
26764
|
+
description: "Set true to perform the move. Omit (or false) to get a preview of what would happen."
|
|
26765
|
+
}
|
|
26766
|
+
},
|
|
26767
|
+
required: ["task_id", "target_project"]
|
|
26768
|
+
}
|
|
26769
|
+
};
|
|
26770
|
+
function requireStr(value) {
|
|
26771
|
+
const s = typeof value === "string" ? value.trim() : "";
|
|
26772
|
+
return s.length > 0 ? s : null;
|
|
26773
|
+
}
|
|
26774
|
+
async function handleTaskMove(adapter2, config2, args) {
|
|
26775
|
+
const taskId = requireStr(args.task_id);
|
|
26776
|
+
if (!taskId) return errorResponse('A task_id is required. Example: task_move task_id="task-42" target_project="other-project" confirm=true');
|
|
26777
|
+
const targetProject = requireStr(args.target_project);
|
|
26778
|
+
if (!targetProject) return errorResponse("A target_project (slug or UUID) is required \u2014 the project you want to move the task into.");
|
|
26779
|
+
if (typeof adapter2.moveTask !== "function") {
|
|
26780
|
+
return errorResponse("Cross-project move is not available on this adapter.");
|
|
26781
|
+
}
|
|
26782
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
26783
|
+
const callerUserId = gate.callerUserId;
|
|
26784
|
+
if (!callerUserId) {
|
|
26785
|
+
const note = gate.resolutionError ? ` (${gate.resolutionError})` : "";
|
|
26786
|
+
return errorResponse(
|
|
26787
|
+
"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."
|
|
26788
|
+
);
|
|
26789
|
+
}
|
|
26790
|
+
const task = await adapter2.getTask(taskId);
|
|
26791
|
+
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.`);
|
|
26792
|
+
if (args.confirm !== true) {
|
|
26793
|
+
return textResponse(
|
|
26794
|
+
`\u26A0\uFE0F **Preview \u2014 no changes made.**
|
|
26795
|
+
|
|
26796
|
+
This will move **${taskId}** (${task.title}) into project \`${targetProject}\`:
|
|
26797
|
+
- it gets a NEW task id in the target (the current "${taskId}" is freed in this project)
|
|
26798
|
+
- its build reports, comments, and history move with it
|
|
26799
|
+
- its cycle assignment is cleared (it lands in the target's backlog)
|
|
26800
|
+
- you must own both this project and \`${targetProject}\`
|
|
26801
|
+
|
|
26802
|
+
This is a cross-project write and cannot be auto-undone. To proceed:
|
|
26803
|
+
\`task_move task_id="${taskId}" target_project="${targetProject}" confirm=true\``
|
|
26804
|
+
);
|
|
26805
|
+
}
|
|
26806
|
+
try {
|
|
26807
|
+
const res = await adapter2.moveTask(taskId, targetProject, callerUserId);
|
|
26808
|
+
return textResponse(
|
|
26809
|
+
`\u2705 Moved **${taskId}** from ${res.sourceName} to ${res.targetName} as **${res.newDisplayId}**.
|
|
26810
|
+
|
|
26811
|
+
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.`
|
|
26812
|
+
);
|
|
26813
|
+
} catch (err) {
|
|
26814
|
+
return errorResponse(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26815
|
+
}
|
|
26816
|
+
}
|
|
26817
|
+
|
|
26216
26818
|
// src/services/harness-inventory.ts
|
|
26217
26819
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
26218
|
-
import { join as
|
|
26820
|
+
import { join as join17 } from "path";
|
|
26219
26821
|
import { createHash as createHash3 } from "crypto";
|
|
26220
26822
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
26221
26823
|
async function computeFingerprint(root) {
|
|
26222
26824
|
const parts = [];
|
|
26223
26825
|
for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
|
|
26224
|
-
const dir =
|
|
26826
|
+
const dir = join17(root, sub);
|
|
26225
26827
|
try {
|
|
26226
26828
|
const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
|
|
26227
26829
|
for (const name of names) {
|
|
26228
26830
|
let mtime = "";
|
|
26229
26831
|
try {
|
|
26230
|
-
mtime = String(Math.floor((await stat3(
|
|
26832
|
+
mtime = String(Math.floor((await stat3(join17(dir, name))).mtimeMs));
|
|
26231
26833
|
} catch {
|
|
26232
26834
|
}
|
|
26233
26835
|
parts.push(`${sub}/${name}:${mtime}`);
|
|
@@ -26246,7 +26848,7 @@ async function computeFingerprint(root) {
|
|
|
26246
26848
|
}
|
|
26247
26849
|
async function readSkillDescription(skillDir) {
|
|
26248
26850
|
try {
|
|
26249
|
-
const content = await readFile8(
|
|
26851
|
+
const content = await readFile8(join17(skillDir, "SKILL.md"), "utf-8");
|
|
26250
26852
|
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26251
26853
|
if (!fm) return void 0;
|
|
26252
26854
|
const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
|
|
@@ -26266,17 +26868,17 @@ async function scanInventory(root, toolDefs) {
|
|
|
26266
26868
|
version = loadManifest().packageVersion;
|
|
26267
26869
|
} catch {
|
|
26268
26870
|
}
|
|
26269
|
-
const skillsDir =
|
|
26871
|
+
const skillsDir = join17(root, ".claude", "skills");
|
|
26270
26872
|
try {
|
|
26271
26873
|
const dirents = await readdir3(skillsDir, { withFileTypes: true });
|
|
26272
26874
|
for (const d of dirents.filter((e) => e.isDirectory())) {
|
|
26273
26875
|
entries.push({
|
|
26274
26876
|
kind: "skill",
|
|
26275
26877
|
name: d.name,
|
|
26276
|
-
description: await readSkillDescription(
|
|
26878
|
+
description: await readSkillDescription(join17(skillsDir, d.name)),
|
|
26277
26879
|
version,
|
|
26278
26880
|
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
26279
|
-
path:
|
|
26881
|
+
path: join17(".claude", "skills", d.name)
|
|
26280
26882
|
});
|
|
26281
26883
|
}
|
|
26282
26884
|
} catch {
|
|
@@ -26292,9 +26894,9 @@ async function scanInventory(root, toolDefs) {
|
|
|
26292
26894
|
}
|
|
26293
26895
|
const present = /* @__PURE__ */ new Set();
|
|
26294
26896
|
try {
|
|
26295
|
-
for (const f of (await readdir3(
|
|
26897
|
+
for (const f of (await readdir3(join17(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
|
|
26296
26898
|
present.add(f);
|
|
26297
|
-
entries.push({ kind: "hook", name: f, status: "ok", path:
|
|
26899
|
+
entries.push({ kind: "hook", name: f, status: "ok", path: join17(".claude", "hooks", f) });
|
|
26298
26900
|
}
|
|
26299
26901
|
} catch {
|
|
26300
26902
|
}
|
|
@@ -26373,6 +26975,130 @@ async function handleInventorySync(adapter2, config2, args, toolDefs) {
|
|
|
26373
26975
|
return textResponse(lines.join("\n"));
|
|
26374
26976
|
}
|
|
26375
26977
|
|
|
26978
|
+
// src/resources.ts
|
|
26979
|
+
var SCHEME = "mcp://papi";
|
|
26980
|
+
var RESOURCE_TEMPLATES = [
|
|
26981
|
+
{
|
|
26982
|
+
uriTemplate: `${SCHEME}/decisions/{adId}`,
|
|
26983
|
+
name: "Active Decision",
|
|
26984
|
+
description: "A single Active Decision (e.g. AD-12) \u2014 title, confidence, body, and supersession status.",
|
|
26985
|
+
mimeType: "text/markdown"
|
|
26986
|
+
},
|
|
26987
|
+
{
|
|
26988
|
+
uriTemplate: `${SCHEME}/build-reports/cycle-{cycle}`,
|
|
26989
|
+
name: "Cycle build reports",
|
|
26990
|
+
description: "Every build report recorded for a given cycle number (e.g. cycle-310).",
|
|
26991
|
+
mimeType: "text/markdown"
|
|
26992
|
+
},
|
|
26993
|
+
{
|
|
26994
|
+
uriTemplate: `${SCHEME}/cycles/{cycle}`,
|
|
26995
|
+
name: "Cycle summary",
|
|
26996
|
+
description: "Summary, task count, effort, and carry-forward for a given cycle number.",
|
|
26997
|
+
mimeType: "text/markdown"
|
|
26998
|
+
},
|
|
26999
|
+
{
|
|
27000
|
+
uriTemplate: `${SCHEME}/handoffs/{taskId}`,
|
|
27001
|
+
name: "Build handoff",
|
|
27002
|
+
description: "The read-only BUILD HANDOFF for a task (e.g. task-1801).",
|
|
27003
|
+
mimeType: "text/markdown"
|
|
27004
|
+
}
|
|
27005
|
+
];
|
|
27006
|
+
async function listPapiResources(adapter2) {
|
|
27007
|
+
const out = [];
|
|
27008
|
+
try {
|
|
27009
|
+
const ads = await adapter2.getActiveDecisions();
|
|
27010
|
+
for (const d of ads.filter((d2) => !d2.superseded).slice(0, 50)) {
|
|
27011
|
+
out.push({
|
|
27012
|
+
uri: `${SCHEME}/decisions/${d.id}`,
|
|
27013
|
+
name: `${d.id}: ${d.title}`,
|
|
27014
|
+
description: `Active Decision [${d.confidence}]`,
|
|
27015
|
+
mimeType: "text/markdown"
|
|
27016
|
+
});
|
|
27017
|
+
}
|
|
27018
|
+
} catch {
|
|
27019
|
+
}
|
|
27020
|
+
try {
|
|
27021
|
+
const log2 = await adapter2.getCycleLog(10);
|
|
27022
|
+
for (const e of log2) {
|
|
27023
|
+
out.push({
|
|
27024
|
+
uri: `${SCHEME}/cycles/${e.cycleNumber}`,
|
|
27025
|
+
name: `Cycle ${e.cycleNumber} summary`,
|
|
27026
|
+
description: e.title,
|
|
27027
|
+
mimeType: "text/markdown"
|
|
27028
|
+
});
|
|
27029
|
+
out.push({
|
|
27030
|
+
uri: `${SCHEME}/build-reports/cycle-${e.cycleNumber}`,
|
|
27031
|
+
name: `Cycle ${e.cycleNumber} build reports`,
|
|
27032
|
+
mimeType: "text/markdown"
|
|
27033
|
+
});
|
|
27034
|
+
}
|
|
27035
|
+
} catch {
|
|
27036
|
+
}
|
|
27037
|
+
return out;
|
|
27038
|
+
}
|
|
27039
|
+
function parseCycleNumber(raw) {
|
|
27040
|
+
const m = raw.match(/(\d+)\s*$/);
|
|
27041
|
+
return m ? parseInt(m[1], 10) : NaN;
|
|
27042
|
+
}
|
|
27043
|
+
async function readPapiResource(adapter2, uri) {
|
|
27044
|
+
if (!uri.startsWith(`${SCHEME}/`)) {
|
|
27045
|
+
throw new Error(`Unknown resource URI: ${uri}. Expected ${SCHEME}/...`);
|
|
27046
|
+
}
|
|
27047
|
+
const path7 = uri.slice(`${SCHEME}/`.length);
|
|
27048
|
+
const slash = path7.indexOf("/");
|
|
27049
|
+
const kind = slash === -1 ? path7 : path7.slice(0, slash);
|
|
27050
|
+
const rest = slash === -1 ? "" : path7.slice(slash + 1);
|
|
27051
|
+
const md = (text) => ({ uri, mimeType: "text/markdown", text });
|
|
27052
|
+
switch (kind) {
|
|
27053
|
+
case "decisions": {
|
|
27054
|
+
const adId = rest.trim();
|
|
27055
|
+
if (!adId) throw new Error(`Resource URI missing an AD id: ${uri}`);
|
|
27056
|
+
const ads = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
27057
|
+
const target = ads.find((d) => d.id === adId);
|
|
27058
|
+
if (!target) throw new Error(`Active Decision not found in this project: ${adId}`);
|
|
27059
|
+
const supersededNote = target.superseded ? ` [SUPERSEDED by ${target.supersededBy ?? "unknown"}]` : "";
|
|
27060
|
+
const supersedes = ads.filter((d) => d.supersededBy === target.id).map((d) => d.id);
|
|
27061
|
+
const chain = supersedes.length > 0 ? `
|
|
27062
|
+
|
|
27063
|
+
_Supersedes: ${supersedes.join(", ")}_` : "";
|
|
27064
|
+
return md(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
|
|
27065
|
+
|
|
27066
|
+
${target.body}${chain}`);
|
|
27067
|
+
}
|
|
27068
|
+
case "build-reports": {
|
|
27069
|
+
const cycle = parseCycleNumber(rest);
|
|
27070
|
+
if (Number.isNaN(cycle)) throw new Error(`Resource URI missing a cycle number: ${uri}`);
|
|
27071
|
+
const reports = (await adapter2.getBuildReportsSince(cycle)).filter((r) => r.cycle === cycle);
|
|
27072
|
+
return md(`# Build Reports \u2014 Cycle ${cycle} (${reports.length})
|
|
27073
|
+
|
|
27074
|
+
${formatBuildReports(reports)}`);
|
|
27075
|
+
}
|
|
27076
|
+
case "cycles": {
|
|
27077
|
+
const cycle = parseCycleNumber(rest);
|
|
27078
|
+
if (Number.isNaN(cycle)) throw new Error(`Resource URI missing a cycle number: ${uri}`);
|
|
27079
|
+
const entry = (await adapter2.getCycleLogSince(cycle)).find((e) => e.cycleNumber === cycle);
|
|
27080
|
+
if (!entry) throw new Error(`No cycle log entry found in this project for cycle ${cycle}`);
|
|
27081
|
+
return md(formatCycleLog([entry]));
|
|
27082
|
+
}
|
|
27083
|
+
case "handoffs": {
|
|
27084
|
+
const taskId = rest.trim();
|
|
27085
|
+
if (!taskId) throw new Error(`Resource URI missing a task id: ${uri}`);
|
|
27086
|
+
const task = await adapter2.getTask(taskId);
|
|
27087
|
+
if (!task) throw new Error(`Task not found in this project: ${taskId}`);
|
|
27088
|
+
if (!task.buildHandoff) {
|
|
27089
|
+
return md(`# ${taskId}: ${task.title}
|
|
27090
|
+
|
|
27091
|
+
_No BUILD HANDOFF recorded for this task._`);
|
|
27092
|
+
}
|
|
27093
|
+
return md(`# BUILD HANDOFF \u2014 ${taskId}: ${task.title}
|
|
27094
|
+
|
|
27095
|
+
${serializeBuildHandoff(task.buildHandoff)}`);
|
|
27096
|
+
}
|
|
27097
|
+
default:
|
|
27098
|
+
throw new Error(`Unknown resource family "${kind}" in ${uri}. Known: decisions, build-reports, cycles, handoffs.`);
|
|
27099
|
+
}
|
|
27100
|
+
}
|
|
27101
|
+
|
|
26376
27102
|
// src/services/metering.ts
|
|
26377
27103
|
var TIER_ALLOWANCES = {
|
|
26378
27104
|
free: 1e3,
|
|
@@ -26593,32 +27319,36 @@ var PAPI_TOOLS = [
|
|
|
26593
27319
|
contributorListTool,
|
|
26594
27320
|
taskClaimTool,
|
|
26595
27321
|
taskUnclaimTool,
|
|
27322
|
+
taskMoveTool,
|
|
26596
27323
|
inventorySyncTool
|
|
26597
27324
|
];
|
|
26598
27325
|
function getToolMetadata() {
|
|
26599
27326
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
26600
27327
|
}
|
|
26601
27328
|
function createServer(adapter2, config2) {
|
|
26602
|
-
const __pkgFilename =
|
|
26603
|
-
const __pkgDir =
|
|
27329
|
+
const __pkgFilename = fileURLToPath3(import.meta.url);
|
|
27330
|
+
const __pkgDir = dirname5(__pkgFilename);
|
|
26604
27331
|
let serverVersion = "unknown";
|
|
26605
27332
|
try {
|
|
26606
|
-
const pkg = JSON.parse(
|
|
27333
|
+
const pkg = JSON.parse(readFileSync10(join18(__pkgDir, "..", "package.json"), "utf-8"));
|
|
26607
27334
|
serverVersion = pkg.version ?? "unknown";
|
|
26608
27335
|
} catch {
|
|
26609
27336
|
}
|
|
26610
27337
|
const server2 = new Server(
|
|
26611
27338
|
{ name: "papi", version: serverVersion },
|
|
26612
|
-
|
|
27339
|
+
// task-2305: `instructions` is surfaced in the MCP initialize response, so every
|
|
27340
|
+
// connecting host LLM gets PAPI's cycle frame on connect — zero config, any harness.
|
|
27341
|
+
// task-1801: `resources` capability for the PAPI read surface exposed as MCP resources.
|
|
27342
|
+
{ capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: UNIVERSAL_FRAME }
|
|
26613
27343
|
);
|
|
26614
27344
|
if (config2.adapterType === "md") {
|
|
26615
27345
|
process.stderr.write(
|
|
26616
27346
|
"\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"
|
|
26617
27347
|
);
|
|
26618
27348
|
}
|
|
26619
|
-
const __filename =
|
|
26620
|
-
const __dirname2 =
|
|
26621
|
-
const skillsDir =
|
|
27349
|
+
const __filename = fileURLToPath3(import.meta.url);
|
|
27350
|
+
const __dirname2 = dirname5(__filename);
|
|
27351
|
+
const skillsDir = join18(__dirname2, "..", "skills");
|
|
26622
27352
|
function parseSkillFrontmatter(content) {
|
|
26623
27353
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26624
27354
|
if (!match) return null;
|
|
@@ -26636,7 +27366,7 @@ function createServer(adapter2, config2) {
|
|
|
26636
27366
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
26637
27367
|
const prompts = [];
|
|
26638
27368
|
for (const file of mdFiles) {
|
|
26639
|
-
const content = await readFile9(
|
|
27369
|
+
const content = await readFile9(join18(skillsDir, file), "utf-8");
|
|
26640
27370
|
const meta = parseSkillFrontmatter(content);
|
|
26641
27371
|
if (meta) {
|
|
26642
27372
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -26652,7 +27382,7 @@ function createServer(adapter2, config2) {
|
|
|
26652
27382
|
try {
|
|
26653
27383
|
const files = await readdir4(skillsDir);
|
|
26654
27384
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
26655
|
-
const content = await readFile9(
|
|
27385
|
+
const content = await readFile9(join18(skillsDir, file), "utf-8");
|
|
26656
27386
|
const meta = parseSkillFrontmatter(content);
|
|
26657
27387
|
if (meta?.name === name) {
|
|
26658
27388
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -26666,6 +27396,15 @@ function createServer(adapter2, config2) {
|
|
|
26666
27396
|
}
|
|
26667
27397
|
throw new Error(`Prompt not found: ${name}`);
|
|
26668
27398
|
});
|
|
27399
|
+
server2.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
27400
|
+
resources: await listPapiResources(adapter2)
|
|
27401
|
+
}));
|
|
27402
|
+
server2.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
27403
|
+
resourceTemplates: RESOURCE_TEMPLATES
|
|
27404
|
+
}));
|
|
27405
|
+
server2.setRequestHandler(ReadResourceRequestSchema, async (request) => ({
|
|
27406
|
+
contents: [await readPapiResource(adapter2, request.params.uri)]
|
|
27407
|
+
}));
|
|
26669
27408
|
server2.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
26670
27409
|
tools: [...PAPI_TOOLS]
|
|
26671
27410
|
}));
|
|
@@ -26717,13 +27456,13 @@ function createServer(adapter2, config2) {
|
|
|
26717
27456
|
case "board_edit":
|
|
26718
27457
|
return handleBoardEdit(adapter2, safeArgs);
|
|
26719
27458
|
case "setup":
|
|
26720
|
-
return handleSetup(adapter2, config2, safeArgs);
|
|
27459
|
+
return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26721
27460
|
case "build_list":
|
|
26722
27461
|
return handleBuildList(adapter2, config2);
|
|
26723
27462
|
case "build_describe":
|
|
26724
27463
|
return handleBuildDescribe(adapter2, safeArgs);
|
|
26725
27464
|
case "build_execute":
|
|
26726
|
-
return handleBuildExecute(adapter2, config2, safeArgs);
|
|
27465
|
+
return handleBuildExecute(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26727
27466
|
case "build_cancel":
|
|
26728
27467
|
return handleBuildCancel(adapter2, safeArgs);
|
|
26729
27468
|
case "idea":
|
|
@@ -26745,9 +27484,9 @@ function createServer(adapter2, config2) {
|
|
|
26745
27484
|
case "init":
|
|
26746
27485
|
return handleInit(config2, safeArgs);
|
|
26747
27486
|
case "orient":
|
|
26748
|
-
return handleOrient(adapter2, config2, safeArgs);
|
|
27487
|
+
return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26749
27488
|
case "papi":
|
|
26750
|
-
return handleOrient(adapter2, config2, safeArgs);
|
|
27489
|
+
return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26751
27490
|
// alias (task-2223)
|
|
26752
27491
|
case "hierarchy_update":
|
|
26753
27492
|
return handleHierarchyUpdate(adapter2, safeArgs);
|
|
@@ -26789,6 +27528,8 @@ function createServer(adapter2, config2) {
|
|
|
26789
27528
|
return handleContributorList(adapter2, config2, safeArgs);
|
|
26790
27529
|
case "task_claim":
|
|
26791
27530
|
return handleTaskClaim(adapter2, config2, safeArgs);
|
|
27531
|
+
case "task_move":
|
|
27532
|
+
return handleTaskMove(adapter2, config2, safeArgs);
|
|
26792
27533
|
case "task_unclaim":
|
|
26793
27534
|
return handleTaskUnclaim(adapter2, config2, safeArgs);
|
|
26794
27535
|
case "inventory_sync":
|
|
@@ -26857,17 +27598,28 @@ ${usageLine(decision.usage)}`;
|
|
|
26857
27598
|
}
|
|
26858
27599
|
const telemetryProjectId = resolveTelemetryProjectId(config2);
|
|
26859
27600
|
if (telemetryProjectId) {
|
|
26860
|
-
|
|
26861
|
-
|
|
26862
|
-
|
|
27601
|
+
const adapterEmit = adapter2.emitTelemetry?.bind(adapter2) ?? null;
|
|
27602
|
+
if (adapterEmit) {
|
|
27603
|
+
adapterEmit({
|
|
27604
|
+
projectId: telemetryProjectId,
|
|
27605
|
+
toolName: name,
|
|
27606
|
+
eventType: "tool_call",
|
|
27607
|
+
metadata: { duration_ms: elapsed, adapter_type: config2.adapterType }
|
|
27608
|
+
});
|
|
27609
|
+
} else {
|
|
27610
|
+
emitToolCall(telemetryProjectId, name, elapsed, {
|
|
27611
|
+
adapter_type: config2.adapterType
|
|
27612
|
+
});
|
|
27613
|
+
}
|
|
26863
27614
|
const isApplyMode = safeArgs.mode === "apply";
|
|
26864
27615
|
if (!isError) {
|
|
26865
|
-
|
|
26866
|
-
|
|
26867
|
-
|
|
26868
|
-
|
|
26869
|
-
|
|
26870
|
-
|
|
27616
|
+
const milestone = name === "setup" && isApplyMode ? "setup_completed" : name === "plan" && isApplyMode ? "plan_completed" : name === "release" ? "release_completed" : null;
|
|
27617
|
+
if (milestone) {
|
|
27618
|
+
if (adapterEmit) {
|
|
27619
|
+
adapterEmit({ projectId: telemetryProjectId, toolName: milestone, eventType: "milestone" });
|
|
27620
|
+
} else {
|
|
27621
|
+
emitMilestone(telemetryProjectId, milestone);
|
|
27622
|
+
}
|
|
26871
27623
|
}
|
|
26872
27624
|
}
|
|
26873
27625
|
}
|
|
@@ -27297,10 +28049,10 @@ async function dispatchRequest(args) {
|
|
|
27297
28049
|
}
|
|
27298
28050
|
|
|
27299
28051
|
// src/index.ts
|
|
27300
|
-
var __dirname =
|
|
28052
|
+
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
27301
28053
|
var pkgVersion = "unknown";
|
|
27302
28054
|
try {
|
|
27303
|
-
const pkg = JSON.parse(
|
|
28055
|
+
const pkg = JSON.parse(readFileSync15(join23(__dirname, "..", "package.json"), "utf-8"));
|
|
27304
28056
|
pkgVersion = pkg.version;
|
|
27305
28057
|
} catch {
|
|
27306
28058
|
}
|