@papi-ai/server 0.7.40 → 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 +65 -0
- package/dist/index.js +872 -329
- package/package.json +3 -2
- package/skills/papi-cycle/papi-advanced/SKILL.md +5 -10
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ __export(git_exports, {
|
|
|
22
22
|
cycleBranchName: () => cycleBranchName,
|
|
23
23
|
deleteLocalBranch: () => deleteLocalBranch,
|
|
24
24
|
detectBoardMismatches: () => detectBoardMismatches,
|
|
25
|
+
detectMergedInProgress: () => detectMergedInProgress,
|
|
25
26
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
26
27
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
27
28
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
@@ -63,6 +64,7 @@ __export(git_exports, {
|
|
|
63
64
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
64
65
|
pickModuleCycleBranch: () => pickModuleCycleBranch,
|
|
65
66
|
resolveBaseBranch: () => resolveBaseBranch,
|
|
67
|
+
resolveDefaultBranch: () => resolveDefaultBranch,
|
|
66
68
|
runAutoCommit: () => runAutoCommit,
|
|
67
69
|
squashMergePullRequest: () => squashMergePullRequest,
|
|
68
70
|
stageAllAndCommit: () => stageAllAndCommit,
|
|
@@ -660,6 +662,22 @@ function resolveBaseBranch(cwd, preferred) {
|
|
|
660
662
|
if (preferred !== "master" && branchExists(cwd, "master")) return "master";
|
|
661
663
|
return preferred;
|
|
662
664
|
}
|
|
665
|
+
function resolveDefaultBranch(cwd, preferred) {
|
|
666
|
+
try {
|
|
667
|
+
const ref = execFileSync(
|
|
668
|
+
"git",
|
|
669
|
+
["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
670
|
+
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
671
|
+
).trim();
|
|
672
|
+
const name = ref.replace(/^origin\//, "");
|
|
673
|
+
if (name && branchExists(cwd, name)) return name;
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
for (const candidate of [preferred, "main", "master", "trunk"]) {
|
|
677
|
+
if (candidate && branchExists(cwd, candidate)) return candidate;
|
|
678
|
+
}
|
|
679
|
+
return preferred;
|
|
680
|
+
}
|
|
663
681
|
function detectBoardMismatches(cwd, tasks) {
|
|
664
682
|
const empty = { codeAhead: [], staleInProgress: [] };
|
|
665
683
|
if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
|
|
@@ -693,6 +711,46 @@ function detectBoardMismatches(cwd, tasks) {
|
|
|
693
711
|
return empty;
|
|
694
712
|
}
|
|
695
713
|
}
|
|
714
|
+
function escapeRegexLiteral(s) {
|
|
715
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
716
|
+
}
|
|
717
|
+
function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
718
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
719
|
+
const inProgress = tasks.filter((t) => t.status === "In Progress");
|
|
720
|
+
if (inProgress.length === 0) return [];
|
|
721
|
+
const base = resolveDefaultBranch(cwd, preferredBase);
|
|
722
|
+
if (!branchExists(cwd, base)) return [];
|
|
723
|
+
let raw;
|
|
724
|
+
try {
|
|
725
|
+
raw = execFileSync(
|
|
726
|
+
"git",
|
|
727
|
+
["log", base, "--format=%h%x01%s", "-n", "1000"],
|
|
728
|
+
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
729
|
+
);
|
|
730
|
+
} catch {
|
|
731
|
+
return [];
|
|
732
|
+
}
|
|
733
|
+
const commits = raw.split("\n").map((line) => {
|
|
734
|
+
const idx = line.indexOf("");
|
|
735
|
+
if (idx === -1) return null;
|
|
736
|
+
return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
|
|
737
|
+
}).filter((c) => c !== null && c.hash !== "");
|
|
738
|
+
const hits = [];
|
|
739
|
+
for (const task of inProgress) {
|
|
740
|
+
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
|
|
741
|
+
const hit = commits.find((c) => re.test(c.subject));
|
|
742
|
+
if (!hit) continue;
|
|
743
|
+
const prMatch = hit.subject.match(/#(\d+)/);
|
|
744
|
+
hits.push({
|
|
745
|
+
displayId: task.displayId,
|
|
746
|
+
title: task.title,
|
|
747
|
+
commit: hit.hash,
|
|
748
|
+
subject: hit.subject,
|
|
749
|
+
pr: prMatch ? `#${prMatch[1]}` : null
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return hits;
|
|
753
|
+
}
|
|
696
754
|
function detectUnrecordedCommits(cwd, baseBranch) {
|
|
697
755
|
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
698
756
|
const latestTag = getLatestTag(cwd);
|
|
@@ -1256,6 +1314,13 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1256
1314
|
updateTaskStatus(id, status) {
|
|
1257
1315
|
return this.invoke("updateTaskStatus", [id, status]);
|
|
1258
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
|
+
}
|
|
1259
1324
|
// task-2155 (MU-3 task B): hosted/proxy claim write-path. The data-proxy gates
|
|
1260
1325
|
// these via WRITE_METHODS (active editor may write, viewer cannot) and binds the
|
|
1261
1326
|
// assignee to the bearer-derived caller server-side — the assigneeId passed here
|
|
@@ -1712,9 +1777,9 @@ var init_query = __esm({
|
|
|
1712
1777
|
CLOSE = {};
|
|
1713
1778
|
Query = class extends Promise {
|
|
1714
1779
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
1715
|
-
let
|
|
1780
|
+
let resolve3, reject;
|
|
1716
1781
|
super((a, b2) => {
|
|
1717
|
-
|
|
1782
|
+
resolve3 = a;
|
|
1718
1783
|
reject = b2;
|
|
1719
1784
|
});
|
|
1720
1785
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -1725,7 +1790,7 @@ var init_query = __esm({
|
|
|
1725
1790
|
this.options = options;
|
|
1726
1791
|
this.state = null;
|
|
1727
1792
|
this.statement = null;
|
|
1728
|
-
this.resolve = (x) => (this.active = false,
|
|
1793
|
+
this.resolve = (x) => (this.active = false, resolve3(x));
|
|
1729
1794
|
this.reject = (x) => (this.active = false, reject(x));
|
|
1730
1795
|
this.active = false;
|
|
1731
1796
|
this.cancelled = null;
|
|
@@ -1773,12 +1838,12 @@ var init_query = __esm({
|
|
|
1773
1838
|
if (this.executed && !this.active)
|
|
1774
1839
|
return { done: true };
|
|
1775
1840
|
prev && prev();
|
|
1776
|
-
const promise = new Promise((
|
|
1841
|
+
const promise = new Promise((resolve3, reject) => {
|
|
1777
1842
|
this.cursorFn = (value) => {
|
|
1778
|
-
|
|
1843
|
+
resolve3({ value, done: false });
|
|
1779
1844
|
return new Promise((r) => prev = r);
|
|
1780
1845
|
};
|
|
1781
|
-
this.resolve = () => (this.active = false,
|
|
1846
|
+
this.resolve = () => (this.active = false, resolve3({ done: true }));
|
|
1782
1847
|
this.reject = (x) => (this.active = false, reject(x));
|
|
1783
1848
|
});
|
|
1784
1849
|
this.execute();
|
|
@@ -2376,12 +2441,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
2376
2441
|
x.on("drain", drain);
|
|
2377
2442
|
return x;
|
|
2378
2443
|
}
|
|
2379
|
-
async function cancel({ pid, secret },
|
|
2444
|
+
async function cancel({ pid, secret }, resolve3, reject) {
|
|
2380
2445
|
try {
|
|
2381
2446
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
2382
2447
|
await connect();
|
|
2383
2448
|
socket.once("error", reject);
|
|
2384
|
-
socket.once("close",
|
|
2449
|
+
socket.once("close", resolve3);
|
|
2385
2450
|
} catch (error2) {
|
|
2386
2451
|
reject(error2);
|
|
2387
2452
|
}
|
|
@@ -3398,7 +3463,7 @@ var init_subscribe = __esm({
|
|
|
3398
3463
|
// ../../node_modules/postgres/src/large.js
|
|
3399
3464
|
import Stream2 from "stream";
|
|
3400
3465
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3401
|
-
return new Promise(async (
|
|
3466
|
+
return new Promise(async (resolve3, reject) => {
|
|
3402
3467
|
await sql.begin(async (sql2) => {
|
|
3403
3468
|
let finish;
|
|
3404
3469
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -3424,7 +3489,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3424
3489
|
) seek
|
|
3425
3490
|
`
|
|
3426
3491
|
};
|
|
3427
|
-
|
|
3492
|
+
resolve3(lo);
|
|
3428
3493
|
return new Promise(async (r) => finish = r);
|
|
3429
3494
|
async function readable({
|
|
3430
3495
|
highWaterMark = 2048 * 8,
|
|
@@ -3589,8 +3654,8 @@ function Postgres(a, b2) {
|
|
|
3589
3654
|
}
|
|
3590
3655
|
async function reserve() {
|
|
3591
3656
|
const queue = queue_default();
|
|
3592
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
3593
|
-
const query = { reserve:
|
|
3657
|
+
const c = open.length ? open.shift() : await new Promise((resolve3, reject) => {
|
|
3658
|
+
const query = { reserve: resolve3, reject };
|
|
3594
3659
|
queries.push(query);
|
|
3595
3660
|
closed.length && connect(closed.shift(), query);
|
|
3596
3661
|
});
|
|
@@ -3627,9 +3692,9 @@ function Postgres(a, b2) {
|
|
|
3627
3692
|
let uncaughtError, result;
|
|
3628
3693
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
3629
3694
|
try {
|
|
3630
|
-
result = await new Promise((
|
|
3695
|
+
result = await new Promise((resolve3, reject) => {
|
|
3631
3696
|
const x = fn2(sql2);
|
|
3632
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
3697
|
+
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve3, reject);
|
|
3633
3698
|
});
|
|
3634
3699
|
if (uncaughtError)
|
|
3635
3700
|
throw uncaughtError;
|
|
@@ -3686,8 +3751,8 @@ function Postgres(a, b2) {
|
|
|
3686
3751
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
3687
3752
|
}
|
|
3688
3753
|
function cancel(query) {
|
|
3689
|
-
return new Promise((
|
|
3690
|
-
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());
|
|
3691
3756
|
});
|
|
3692
3757
|
}
|
|
3693
3758
|
async function end({ timeout = null } = {}) {
|
|
@@ -3706,11 +3771,11 @@ function Postgres(a, b2) {
|
|
|
3706
3771
|
async function close() {
|
|
3707
3772
|
await Promise.all(connections.map((c) => c.end()));
|
|
3708
3773
|
}
|
|
3709
|
-
async function destroy(
|
|
3774
|
+
async function destroy(resolve3) {
|
|
3710
3775
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
3711
3776
|
while (queries.length)
|
|
3712
3777
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
3713
|
-
|
|
3778
|
+
resolve3();
|
|
3714
3779
|
}
|
|
3715
3780
|
function connect(c, query) {
|
|
3716
3781
|
move(c, connecting);
|
|
@@ -3894,9 +3959,9 @@ __export(doctor_exports, {
|
|
|
3894
3959
|
__testing: () => __testing,
|
|
3895
3960
|
runDoctor: () => runDoctor
|
|
3896
3961
|
});
|
|
3897
|
-
import { existsSync as
|
|
3962
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
|
|
3898
3963
|
import { homedir as homedir4 } from "os";
|
|
3899
|
-
import { join as
|
|
3964
|
+
import { join as join19 } from "path";
|
|
3900
3965
|
function redact(name, value) {
|
|
3901
3966
|
if (!value) return "(empty)";
|
|
3902
3967
|
if (SECRET_VARS.has(name)) {
|
|
@@ -3907,14 +3972,14 @@ function redact(name, value) {
|
|
|
3907
3972
|
}
|
|
3908
3973
|
function findMcpJson() {
|
|
3909
3974
|
const candidates = [
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3975
|
+
join19(process.cwd(), ".mcp.json"),
|
|
3976
|
+
join19(homedir4(), ".claude", ".mcp.json"),
|
|
3977
|
+
join19(homedir4(), ".mcp.json")
|
|
3913
3978
|
];
|
|
3914
3979
|
for (const path7 of candidates) {
|
|
3915
|
-
if (!
|
|
3980
|
+
if (!existsSync10(path7)) continue;
|
|
3916
3981
|
try {
|
|
3917
|
-
const raw =
|
|
3982
|
+
const raw = readFileSync11(path7, "utf-8");
|
|
3918
3983
|
const parsed = JSON.parse(raw);
|
|
3919
3984
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
3920
3985
|
if (!papiEntry) continue;
|
|
@@ -4188,17 +4253,17 @@ __export(reset_exports, {
|
|
|
4188
4253
|
removePapiEntry: () => removePapiEntry,
|
|
4189
4254
|
runReset: () => runReset
|
|
4190
4255
|
});
|
|
4191
|
-
import { existsSync as
|
|
4256
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
4192
4257
|
import { homedir as homedir5 } from "os";
|
|
4193
|
-
import { join as
|
|
4258
|
+
import { join as join20 } from "path";
|
|
4194
4259
|
import { createInterface } from "readline/promises";
|
|
4195
4260
|
function findResetTarget() {
|
|
4196
4261
|
for (const path7 of CANDIDATE_PATHS()) {
|
|
4197
|
-
if (!
|
|
4262
|
+
if (!existsSync11(path7)) continue;
|
|
4198
4263
|
let raw;
|
|
4199
4264
|
let parsed;
|
|
4200
4265
|
try {
|
|
4201
|
-
raw =
|
|
4266
|
+
raw = readFileSync12(path7, "utf-8");
|
|
4202
4267
|
parsed = JSON.parse(raw);
|
|
4203
4268
|
} catch {
|
|
4204
4269
|
continue;
|
|
@@ -4286,9 +4351,9 @@ var init_reset = __esm({
|
|
|
4286
4351
|
"src/cli/reset.ts"() {
|
|
4287
4352
|
"use strict";
|
|
4288
4353
|
CANDIDATE_PATHS = () => [
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4354
|
+
join20(process.cwd(), ".mcp.json"),
|
|
4355
|
+
join20(homedir5(), ".claude", ".mcp.json"),
|
|
4356
|
+
join20(homedir5(), ".mcp.json")
|
|
4292
4357
|
];
|
|
4293
4358
|
}
|
|
4294
4359
|
});
|
|
@@ -4299,9 +4364,9 @@ __export(audit_exports, {
|
|
|
4299
4364
|
__testing: () => __testing2,
|
|
4300
4365
|
runAudit: () => runAudit
|
|
4301
4366
|
});
|
|
4302
|
-
import { existsSync as
|
|
4367
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
4303
4368
|
import { homedir as homedir6 } from "os";
|
|
4304
|
-
import { join as
|
|
4369
|
+
import { join as join21 } from "path";
|
|
4305
4370
|
function safeListDirs(dir) {
|
|
4306
4371
|
try {
|
|
4307
4372
|
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
@@ -4317,10 +4382,10 @@ function safeListFiles(dir, ext) {
|
|
|
4317
4382
|
}
|
|
4318
4383
|
}
|
|
4319
4384
|
function readMcp(projectPath) {
|
|
4320
|
-
const path7 =
|
|
4321
|
-
if (!
|
|
4385
|
+
const path7 = join21(projectPath, ".mcp.json");
|
|
4386
|
+
if (!existsSync12(path7)) return { servers: [] };
|
|
4322
4387
|
try {
|
|
4323
|
-
const parsed = JSON.parse(
|
|
4388
|
+
const parsed = JSON.parse(readFileSync13(path7, "utf-8"));
|
|
4324
4389
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4325
4390
|
const servers = Object.keys(mcpServers);
|
|
4326
4391
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4352,18 +4417,18 @@ function auditProjectSync(projectPath, name) {
|
|
|
4352
4417
|
path: projectPath,
|
|
4353
4418
|
papiProjectId,
|
|
4354
4419
|
mcpServers: servers,
|
|
4355
|
-
skills: safeListDirs(
|
|
4356
|
-
agentSkills: safeListDirs(
|
|
4357
|
-
agents: safeListFiles(
|
|
4358
|
-
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")
|
|
4359
4424
|
};
|
|
4360
4425
|
}
|
|
4361
4426
|
function discoverProjects() {
|
|
4362
4427
|
const out = [];
|
|
4363
4428
|
for (const root of PROJECT_ROOTS) {
|
|
4364
4429
|
for (const name of safeListDirs(root)) {
|
|
4365
|
-
const path7 =
|
|
4366
|
-
if (
|
|
4430
|
+
const path7 = join21(root, name);
|
|
4431
|
+
if (existsSync12(join21(path7, ".mcp.json")) || existsSync12(join21(path7, ".claude"))) {
|
|
4367
4432
|
out.push({ name, path: path7 });
|
|
4368
4433
|
}
|
|
4369
4434
|
}
|
|
@@ -4374,9 +4439,9 @@ function readGlobalSkills() {
|
|
|
4374
4439
|
return safeListDirs(GLOBAL_SKILLS_DIR);
|
|
4375
4440
|
}
|
|
4376
4441
|
function readGlobalMcpServers() {
|
|
4377
|
-
if (!
|
|
4442
|
+
if (!existsSync12(GLOBAL_CLAUDE_JSON)) return [];
|
|
4378
4443
|
try {
|
|
4379
|
-
const parsed = JSON.parse(
|
|
4444
|
+
const parsed = JSON.parse(readFileSync13(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4380
4445
|
const servers = parsed.mcpServers ?? {};
|
|
4381
4446
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4382
4447
|
} catch {
|
|
@@ -4528,9 +4593,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
|
|
|
4528
4593
|
var init_audit = __esm({
|
|
4529
4594
|
"src/cli/audit.ts"() {
|
|
4530
4595
|
"use strict";
|
|
4531
|
-
PROJECT_ROOTS = [
|
|
4532
|
-
GLOBAL_SKILLS_DIR =
|
|
4533
|
-
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");
|
|
4534
4599
|
IDLE_WINDOW_DAYS = 30;
|
|
4535
4600
|
GLOBALIZE_THRESHOLD = 3;
|
|
4536
4601
|
__testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
|
|
@@ -4542,8 +4607,8 @@ var setup_exports = {};
|
|
|
4542
4607
|
__export(setup_exports, {
|
|
4543
4608
|
runSetup: () => runSetup
|
|
4544
4609
|
});
|
|
4545
|
-
import { existsSync as
|
|
4546
|
-
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";
|
|
4547
4612
|
function baseUrl() {
|
|
4548
4613
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
4549
4614
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
@@ -4572,14 +4637,14 @@ async function postJson(url, body) {
|
|
|
4572
4637
|
return { status: res.status, data };
|
|
4573
4638
|
}
|
|
4574
4639
|
function sleep(ms) {
|
|
4575
|
-
return new Promise((
|
|
4640
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4576
4641
|
}
|
|
4577
4642
|
function writeMcpJson(opts) {
|
|
4578
|
-
const path7 =
|
|
4643
|
+
const path7 = join22(process.cwd(), ".mcp.json");
|
|
4579
4644
|
let parsed = {};
|
|
4580
|
-
if (
|
|
4645
|
+
if (existsSync13(path7)) {
|
|
4581
4646
|
try {
|
|
4582
|
-
parsed = JSON.parse(
|
|
4647
|
+
parsed = JSON.parse(readFileSync14(path7, "utf-8"));
|
|
4583
4648
|
} catch {
|
|
4584
4649
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
4585
4650
|
}
|
|
@@ -4602,7 +4667,7 @@ function writeMcpJson(opts) {
|
|
|
4602
4667
|
parsed.mcpServers = mcpServers;
|
|
4603
4668
|
writeFileSync6(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
4604
4669
|
try {
|
|
4605
|
-
const mode =
|
|
4670
|
+
const mode = statSync6(path7).mode & 511;
|
|
4606
4671
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
4607
4672
|
} catch {
|
|
4608
4673
|
}
|
|
@@ -4710,9 +4775,9 @@ var init_setup = __esm({
|
|
|
4710
4775
|
});
|
|
4711
4776
|
|
|
4712
4777
|
// src/index.ts
|
|
4713
|
-
import { readFileSync as
|
|
4714
|
-
import { dirname as
|
|
4715
|
-
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";
|
|
4716
4781
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4717
4782
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4718
4783
|
import {
|
|
@@ -4744,6 +4809,8 @@ var HELP_FOOTER_MD = `
|
|
|
4744
4809
|
`;
|
|
4745
4810
|
|
|
4746
4811
|
// src/config.ts
|
|
4812
|
+
var STRATEGY_REVIEW_OFFER_GAP = 5;
|
|
4813
|
+
var STRATEGY_REVIEW_BLOCK_GAP = 7;
|
|
4747
4814
|
function loadConfig() {
|
|
4748
4815
|
const projectArgIdx = process.argv.indexOf("--project");
|
|
4749
4816
|
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
@@ -7709,18 +7776,36 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
7709
7776
|
}
|
|
7710
7777
|
|
|
7711
7778
|
// src/server.ts
|
|
7712
|
-
import { readFileSync as
|
|
7779
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7713
7780
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
7714
|
-
import { join as
|
|
7715
|
-
import { fileURLToPath as
|
|
7781
|
+
import { join as join18, dirname as dirname5 } from "path";
|
|
7782
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7716
7783
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7717
7784
|
import {
|
|
7718
7785
|
CallToolRequestSchema,
|
|
7719
7786
|
ListToolsRequestSchema,
|
|
7720
7787
|
ListPromptsRequestSchema,
|
|
7721
|
-
GetPromptRequestSchema
|
|
7788
|
+
GetPromptRequestSchema,
|
|
7789
|
+
ListResourcesRequestSchema,
|
|
7790
|
+
ListResourceTemplatesRequestSchema,
|
|
7791
|
+
ReadResourceRequestSchema
|
|
7722
7792
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7723
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
|
+
|
|
7724
7809
|
// src/lib/response.ts
|
|
7725
7810
|
function textResponse(text, usage) {
|
|
7726
7811
|
const result = {
|
|
@@ -11041,7 +11126,7 @@ ${lines.join("\n")}`;
|
|
|
11041
11126
|
console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
|
|
11042
11127
|
const gap = health.cyclesSinceLastStrategyReview;
|
|
11043
11128
|
const lastReviewCycle = health.totalCycles - gap;
|
|
11044
|
-
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\`).`;
|
|
11045
11130
|
let ctx2 = {
|
|
11046
11131
|
mode,
|
|
11047
11132
|
cycleNumber: health.totalCycles,
|
|
@@ -11231,7 +11316,7 @@ ${logLines}`);
|
|
|
11231
11316
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
11232
11317
|
const gapFull = health.cyclesSinceLastStrategyReview;
|
|
11233
11318
|
const lastReviewCycleFull = health.totalCycles - gapFull;
|
|
11234
|
-
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\`).`;
|
|
11235
11320
|
let ctx = {
|
|
11236
11321
|
mode,
|
|
11237
11322
|
cycleNumber: health.totalCycles,
|
|
@@ -11835,15 +11920,15 @@ Run \`review_submit\` to clear them, or pass \`force: true\` to bypass this bloc
|
|
|
11835
11920
|
console.error(`[plan] ${note}`);
|
|
11836
11921
|
}
|
|
11837
11922
|
const gap = health.cyclesSinceLastStrategyReview;
|
|
11838
|
-
if (gap >=
|
|
11923
|
+
if (gap >= STRATEGY_REVIEW_BLOCK_GAP && !force) {
|
|
11839
11924
|
const lastReviewCycle = cycleNumber - gap;
|
|
11840
11925
|
throw new Error(
|
|
11841
|
-
`Strategy Review gate \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). A strategy review is required every
|
|
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.
|
|
11842
11927
|
|
|
11843
11928
|
Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
11844
11929
|
);
|
|
11845
11930
|
}
|
|
11846
|
-
if (gap >=
|
|
11931
|
+
if (gap >= STRATEGY_REVIEW_BLOCK_GAP && force) {
|
|
11847
11932
|
const lastReviewCycle = cycleNumber - gap;
|
|
11848
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.
|
|
11849
11934
|
|
|
@@ -12416,8 +12501,6 @@ function getState(callerKey) {
|
|
|
12416
12501
|
function callerKeyFromConfig(config2) {
|
|
12417
12502
|
return config2.projectId ?? config2.userId ?? void 0;
|
|
12418
12503
|
}
|
|
12419
|
-
var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
|
|
12420
|
-
var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
|
|
12421
12504
|
var REVIEW_LIST_GUARD_WINDOW_MS = 15 * 60 * 1e3;
|
|
12422
12505
|
var FAILURE_WINDOW_MS = 30 * 60 * 1e3;
|
|
12423
12506
|
var FAILURE_RATE_THRESHOLD = 8;
|
|
@@ -12457,37 +12540,20 @@ function getProjectConnectionBanner(projectName, projectSlug) {
|
|
|
12457
12540
|
function detectContextDegradation(now = Date.now(), callerKey) {
|
|
12458
12541
|
const state = getState(callerKey);
|
|
12459
12542
|
if (state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
12460
|
-
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.`;
|
|
12461
12544
|
}
|
|
12462
12545
|
const cutoff = now - FAILURE_WINDOW_MS;
|
|
12463
12546
|
const recentFailures = state.failureTimestamps.filter((t) => t >= cutoff).length;
|
|
12464
12547
|
if (recentFailures >= FAILURE_RATE_THRESHOLD) {
|
|
12465
12548
|
const mins = Math.round(FAILURE_WINDOW_MS / 6e4);
|
|
12466
|
-
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.`;
|
|
12467
12550
|
}
|
|
12468
12551
|
return null;
|
|
12469
12552
|
}
|
|
12470
12553
|
async function buildSessionGuidance(callerKey) {
|
|
12471
|
-
const state = getState(callerKey);
|
|
12472
12554
|
const signals = [];
|
|
12473
12555
|
const degradation = detectContextDegradation(Date.now(), callerKey);
|
|
12474
12556
|
if (degradation) signals.push(degradation);
|
|
12475
|
-
if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
|
|
12476
|
-
signals.push(
|
|
12477
|
-
`${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
|
|
12478
|
-
);
|
|
12479
|
-
}
|
|
12480
|
-
if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
|
|
12481
|
-
const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
|
|
12482
|
-
signals.push(
|
|
12483
|
-
`${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
|
|
12484
|
-
);
|
|
12485
|
-
}
|
|
12486
|
-
if (state.releaseSinceLastOrient) {
|
|
12487
|
-
signals.push(
|
|
12488
|
-
"Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
|
|
12489
|
-
);
|
|
12490
|
-
}
|
|
12491
12557
|
return signals.slice(0, 3);
|
|
12492
12558
|
}
|
|
12493
12559
|
|
|
@@ -15906,8 +15972,8 @@ ${existing}` : entry;
|
|
|
15906
15972
|
}
|
|
15907
15973
|
|
|
15908
15974
|
// src/services/setup.ts
|
|
15909
|
-
import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2 } from "fs/promises";
|
|
15910
|
-
import { join as
|
|
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";
|
|
15911
15977
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
15912
15978
|
|
|
15913
15979
|
// src/lib/detect-codebase.ts
|
|
@@ -15992,6 +16058,159 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
|
15992
16058
|
return out;
|
|
15993
16059
|
}
|
|
15994
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
|
+
|
|
15995
16214
|
// src/templates.ts
|
|
15996
16215
|
var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
|
|
15997
16216
|
|
|
@@ -16412,7 +16631,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16412
16631
|
await mkdir(config2.papiDir, { recursive: true });
|
|
16413
16632
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
16414
16633
|
const content = substitute(template, vars);
|
|
16415
|
-
await writeFile2(
|
|
16634
|
+
await writeFile2(join7(config2.papiDir, filename), content, "utf-8");
|
|
16416
16635
|
}
|
|
16417
16636
|
}
|
|
16418
16637
|
} else {
|
|
@@ -16430,13 +16649,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16430
16649
|
const useCollector = config2.adapterType === "proxy";
|
|
16431
16650
|
const docsRel = "docs";
|
|
16432
16651
|
const commandsRel = ".claude/commands";
|
|
16433
|
-
const commandsDir = useCollector ? commandsRel :
|
|
16434
|
-
const docsDir = useCollector ? docsRel :
|
|
16652
|
+
const commandsDir = useCollector ? commandsRel : join7(config2.projectRoot, ".claude", "commands");
|
|
16653
|
+
const docsDir = useCollector ? docsRel : join7(config2.projectRoot, "docs");
|
|
16435
16654
|
if (!useCollector) {
|
|
16436
16655
|
await mkdir(commandsDir, { recursive: true });
|
|
16437
16656
|
await mkdir(docsDir, { recursive: true });
|
|
16438
16657
|
}
|
|
16439
|
-
const claudeMdPath = useCollector ? "CLAUDE.md" :
|
|
16658
|
+
const claudeMdPath = useCollector ? "CLAUDE.md" : join7(config2.projectRoot, "CLAUDE.md");
|
|
16440
16659
|
let claudeMdExists = false;
|
|
16441
16660
|
if (!useCollector) {
|
|
16442
16661
|
try {
|
|
@@ -16445,7 +16664,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16445
16664
|
} catch {
|
|
16446
16665
|
}
|
|
16447
16666
|
}
|
|
16448
|
-
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` :
|
|
16667
|
+
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join7(docsDir, "INDEX.md");
|
|
16449
16668
|
let docsIndexExists = false;
|
|
16450
16669
|
if (!useCollector) {
|
|
16451
16670
|
try {
|
|
@@ -16455,9 +16674,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16455
16674
|
}
|
|
16456
16675
|
}
|
|
16457
16676
|
const scaffoldFiles = {
|
|
16458
|
-
[useCollector ? `${commandsRel}/papi-audit.md` :
|
|
16459
|
-
[useCollector ? `${commandsRel}/test.md` :
|
|
16460
|
-
[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)
|
|
16461
16680
|
};
|
|
16462
16681
|
if (!docsIndexExists) {
|
|
16463
16682
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
@@ -16477,14 +16696,14 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16477
16696
|
const bundleRoot = useCollector ? "" : config2.projectRoot;
|
|
16478
16697
|
for (const [dest, content] of Object.entries(planBundleInstall(bundleRoot, input.projectName, { skipExisting: true }))) {
|
|
16479
16698
|
if (!useCollector) {
|
|
16480
|
-
await mkdir(
|
|
16699
|
+
await mkdir(dirname3(dest), { recursive: true });
|
|
16481
16700
|
}
|
|
16482
16701
|
scaffoldFiles[dest] = content;
|
|
16483
16702
|
}
|
|
16484
16703
|
if (useCollector) {
|
|
16485
16704
|
scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
|
|
16486
16705
|
} else {
|
|
16487
|
-
const cursorDir =
|
|
16706
|
+
const cursorDir = join7(config2.projectRoot, ".cursor");
|
|
16488
16707
|
let cursorDetected = false;
|
|
16489
16708
|
try {
|
|
16490
16709
|
await access2(cursorDir);
|
|
@@ -16492,8 +16711,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16492
16711
|
} catch {
|
|
16493
16712
|
}
|
|
16494
16713
|
if (cursorDetected) {
|
|
16495
|
-
const cursorRulesDir =
|
|
16496
|
-
const cursorRulesPath =
|
|
16714
|
+
const cursorRulesDir = join7(cursorDir, "rules");
|
|
16715
|
+
const cursorRulesPath = join7(cursorRulesDir, "papi.mdc");
|
|
16497
16716
|
await mkdir(cursorRulesDir, { recursive: true });
|
|
16498
16717
|
try {
|
|
16499
16718
|
await access2(cursorRulesPath);
|
|
@@ -16511,6 +16730,19 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16511
16730
|
await writeFile2(filepath, content, "utf-8");
|
|
16512
16731
|
}
|
|
16513
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
|
+
}
|
|
16514
16746
|
if (!isPg) {
|
|
16515
16747
|
await adapter2.writePhases([{
|
|
16516
16748
|
id: "phase-0",
|
|
@@ -16535,7 +16767,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16535
16767
|
}
|
|
16536
16768
|
var PAPI_PERMISSION = "mcp__papi__*";
|
|
16537
16769
|
async function ensurePapiPermission(projectRoot) {
|
|
16538
|
-
const settingsPath =
|
|
16770
|
+
const settingsPath = join7(projectRoot, ".claude", "settings.json");
|
|
16539
16771
|
try {
|
|
16540
16772
|
let settings = {};
|
|
16541
16773
|
try {
|
|
@@ -16554,7 +16786,43 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
16554
16786
|
if (!allow.includes(PAPI_PERMISSION)) {
|
|
16555
16787
|
allow.push(PAPI_PERMISSION);
|
|
16556
16788
|
}
|
|
16557
|
-
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 });
|
|
16558
16826
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
16559
16827
|
} catch {
|
|
16560
16828
|
}
|
|
@@ -16658,7 +16926,7 @@ ${conventionsText.trim()}
|
|
|
16658
16926
|
);
|
|
16659
16927
|
} else {
|
|
16660
16928
|
try {
|
|
16661
|
-
const claudeMdPath =
|
|
16929
|
+
const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
|
|
16662
16930
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
16663
16931
|
if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
|
|
16664
16932
|
warnings.push(
|
|
@@ -16745,13 +17013,13 @@ async function scanCodebase(projectRoot) {
|
|
|
16745
17013
|
}
|
|
16746
17014
|
let packageJson;
|
|
16747
17015
|
try {
|
|
16748
|
-
const content = await readFile4(
|
|
17016
|
+
const content = await readFile4(join7(projectRoot, "package.json"), "utf-8");
|
|
16749
17017
|
packageJson = JSON.parse(content);
|
|
16750
17018
|
} catch {
|
|
16751
17019
|
}
|
|
16752
17020
|
let readme;
|
|
16753
17021
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
16754
|
-
const content = await safeReadFile(
|
|
17022
|
+
const content = await safeReadFile(join7(projectRoot, name), 5e3);
|
|
16755
17023
|
if (content) {
|
|
16756
17024
|
readme = content;
|
|
16757
17025
|
break;
|
|
@@ -16761,7 +17029,7 @@ async function scanCodebase(projectRoot) {
|
|
|
16761
17029
|
let totalFiles = topLevelFiles.length;
|
|
16762
17030
|
for (const dir of topLevelDirs) {
|
|
16763
17031
|
try {
|
|
16764
|
-
const entries = await readdir(
|
|
17032
|
+
const entries = await readdir(join7(projectRoot, dir), { withFileTypes: true });
|
|
16765
17033
|
const files = entries.filter((e) => e.isFile());
|
|
16766
17034
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
16767
17035
|
totalFiles += files.length;
|
|
@@ -17108,7 +17376,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17108
17376
|
collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
|
|
17109
17377
|
} else {
|
|
17110
17378
|
try {
|
|
17111
|
-
const claudeMdPath =
|
|
17379
|
+
const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
|
|
17112
17380
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
17113
17381
|
if (!existing.includes("Dogfood Logging")) {
|
|
17114
17382
|
await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
|
|
@@ -17153,7 +17421,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17153
17421
|
cursorScaffolded = true;
|
|
17154
17422
|
} else {
|
|
17155
17423
|
try {
|
|
17156
|
-
await access2(
|
|
17424
|
+
await access2(join7(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
17157
17425
|
cursorScaffolded = true;
|
|
17158
17426
|
} catch {
|
|
17159
17427
|
}
|
|
@@ -17173,11 +17441,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17173
17441
|
}
|
|
17174
17442
|
async function ensureMcpJsonGitignored(projectRoot) {
|
|
17175
17443
|
try {
|
|
17176
|
-
await access2(
|
|
17444
|
+
await access2(join7(projectRoot, ".git"));
|
|
17177
17445
|
} catch {
|
|
17178
17446
|
return void 0;
|
|
17179
17447
|
}
|
|
17180
|
-
const gitignorePath =
|
|
17448
|
+
const gitignorePath = join7(projectRoot, ".gitignore");
|
|
17181
17449
|
let existing = "";
|
|
17182
17450
|
try {
|
|
17183
17451
|
existing = await readFile4(gitignorePath, "utf-8");
|
|
@@ -17560,10 +17828,127 @@ ${result.initialTasksPrompt.user}
|
|
|
17560
17828
|
}
|
|
17561
17829
|
}
|
|
17562
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
|
+
|
|
17563
17881
|
// src/services/build.ts
|
|
17564
17882
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
17565
|
-
import { readdirSync as
|
|
17566
|
-
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
|
|
17567
17952
|
init_git();
|
|
17568
17953
|
|
|
17569
17954
|
// src/lib/owns-local-workspace.ts
|
|
@@ -17571,16 +17956,16 @@ init_git();
|
|
|
17571
17956
|
|
|
17572
17957
|
// src/services/release.ts
|
|
17573
17958
|
import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
17574
|
-
import { join as
|
|
17959
|
+
import { join as join9 } from "path";
|
|
17575
17960
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
17576
17961
|
|
|
17577
17962
|
// src/lib/install-id.ts
|
|
17578
17963
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
17579
|
-
import { mkdirSync, readFileSync as
|
|
17964
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync, chmodSync } from "fs";
|
|
17580
17965
|
import { homedir as homedir2 } from "os";
|
|
17581
|
-
import { join as
|
|
17582
|
-
var PAPI_HOME_DIR =
|
|
17583
|
-
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");
|
|
17584
17969
|
var cachedInstallId = null;
|
|
17585
17970
|
function isValidUuid(s) {
|
|
17586
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);
|
|
@@ -17588,7 +17973,7 @@ function isValidUuid(s) {
|
|
|
17588
17973
|
function getInstallId() {
|
|
17589
17974
|
if (cachedInstallId) return cachedInstallId;
|
|
17590
17975
|
try {
|
|
17591
|
-
const raw =
|
|
17976
|
+
const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
|
|
17592
17977
|
const parsed = JSON.parse(raw);
|
|
17593
17978
|
if (isValidUuid(parsed.install_id)) {
|
|
17594
17979
|
cachedInstallId = parsed.install_id;
|
|
@@ -18132,7 +18517,7 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
18132
18517
|
}
|
|
18133
18518
|
}
|
|
18134
18519
|
const latestTag = getLatestTag(config2.projectRoot);
|
|
18135
|
-
const changelogPath =
|
|
18520
|
+
const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
|
|
18136
18521
|
if (!latestTag) {
|
|
18137
18522
|
const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
18138
18523
|
if (config2.adapterType === "proxy") {
|
|
@@ -18827,7 +19212,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
18827
19212
|
if (modified.length === 0) {
|
|
18828
19213
|
return "Auto-commit: skipped (no working-tree changes).";
|
|
18829
19214
|
}
|
|
18830
|
-
const
|
|
19215
|
+
const dirname7 = (p) => {
|
|
18831
19216
|
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
18832
19217
|
return idx > 0 ? p.slice(0, idx) : "";
|
|
18833
19218
|
};
|
|
@@ -18839,7 +19224,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
18839
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.`;
|
|
18840
19225
|
}
|
|
18841
19226
|
const untracked = getUntrackedFiles(cwd);
|
|
18842
|
-
const scopedDirs = [...new Set(scoped.map(
|
|
19227
|
+
const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
|
|
18843
19228
|
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
18844
19229
|
const scopedSet = new Set(scoped);
|
|
18845
19230
|
const adjacentUntracked = untracked.filter(
|
|
@@ -18864,8 +19249,13 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
18864
19249
|
}
|
|
18865
19250
|
return safeRun(() => stageAllAndCommit(cwd, message));
|
|
18866
19251
|
}
|
|
18867
|
-
function pushAndCreatePR(config2, taskId, taskTitle) {
|
|
19252
|
+
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
18868
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
|
+
}
|
|
18869
19259
|
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
18870
19260
|
return lines;
|
|
18871
19261
|
}
|
|
@@ -19050,7 +19440,7 @@ async function describeTask(adapter2, taskId) {
|
|
|
19050
19440
|
}
|
|
19051
19441
|
return { task };
|
|
19052
19442
|
}
|
|
19053
|
-
async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
19443
|
+
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
19054
19444
|
const task = await adapter2.getTask(taskId);
|
|
19055
19445
|
if (!task) {
|
|
19056
19446
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
@@ -19092,6 +19482,12 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
19092
19482
|
const branchLines = [];
|
|
19093
19483
|
if (options.light) {
|
|
19094
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
|
+
}
|
|
19095
19491
|
} else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
19096
19492
|
const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
|
|
19097
19493
|
const cycleNumber = cycleHealth?.totalCycles ?? 0;
|
|
@@ -19295,16 +19691,16 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
19295
19691
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
19296
19692
|
return;
|
|
19297
19693
|
}
|
|
19298
|
-
const papiDir =
|
|
19299
|
-
if (!
|
|
19694
|
+
const papiDir = join10(projectRoot, ".papi");
|
|
19695
|
+
if (!existsSync6(papiDir)) {
|
|
19300
19696
|
mkdirSync2(papiDir, { recursive: true });
|
|
19301
19697
|
}
|
|
19302
|
-
const scopePath =
|
|
19698
|
+
const scopePath = join10(papiDir, "active-task-scope.txt");
|
|
19303
19699
|
writeFileSync2(scopePath, content, "utf-8");
|
|
19304
19700
|
}
|
|
19305
19701
|
function clearActiveTaskScope(projectRoot) {
|
|
19306
|
-
const scopePath =
|
|
19307
|
-
if (
|
|
19702
|
+
const scopePath = join10(projectRoot, ".papi", "active-task-scope.txt");
|
|
19703
|
+
if (existsSync6(scopePath)) {
|
|
19308
19704
|
unlinkSync(scopePath);
|
|
19309
19705
|
}
|
|
19310
19706
|
}
|
|
@@ -19324,7 +19720,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
19324
19720
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
19325
19721
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
19326
19722
|
try {
|
|
19327
|
-
const content =
|
|
19723
|
+
const content = readFileSync5(absolutePath, "utf-8").slice(0, 2e3);
|
|
19328
19724
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19329
19725
|
if (fmMatch) {
|
|
19330
19726
|
const fm = fmMatch[1];
|
|
@@ -19371,7 +19767,7 @@ Fix the deploy first, then re-run build_execute complete with a 2xx verification
|
|
|
19371
19767
|
);
|
|
19372
19768
|
}
|
|
19373
19769
|
}
|
|
19374
|
-
async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
19770
|
+
async function completeBuild(adapter2, config2, taskId, input, options = {}, clientName) {
|
|
19375
19771
|
const task = await adapter2.getTask(taskId);
|
|
19376
19772
|
if (!task) {
|
|
19377
19773
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
@@ -19649,7 +20045,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19649
20045
|
if (options.light) {
|
|
19650
20046
|
prLines.push("Light mode: skipping push and PR creation.");
|
|
19651
20047
|
} else if (config2.autoCommit && input.completed === "yes") {
|
|
19652
|
-
prLines = pushAndCreatePR(config2, taskId, task.title);
|
|
20048
|
+
prLines = pushAndCreatePR(config2, taskId, task.title, clientName, task.module, cycleNumber);
|
|
19653
20049
|
}
|
|
19654
20050
|
const allTasks = await adapter2.queryBoard();
|
|
19655
20051
|
warnIfEmpty("queryBoard (cycle progress)", allTasks);
|
|
@@ -19669,14 +20065,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19669
20065
|
let docWarning;
|
|
19670
20066
|
try {
|
|
19671
20067
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
19672
|
-
const docsDir =
|
|
19673
|
-
if (
|
|
20068
|
+
const docsDir = join10(config2.projectRoot, "docs");
|
|
20069
|
+
if (existsSync6(docsDir)) {
|
|
19674
20070
|
const scanDir = (dir, depth = 0) => {
|
|
19675
20071
|
if (depth > 8) return [];
|
|
19676
|
-
const entries =
|
|
20072
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
19677
20073
|
const files = [];
|
|
19678
20074
|
for (const e of entries) {
|
|
19679
|
-
const full =
|
|
20075
|
+
const full = join10(dir, e.name);
|
|
19680
20076
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
19681
20077
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
19682
20078
|
}
|
|
@@ -19691,7 +20087,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
19691
20087
|
const failed = [];
|
|
19692
20088
|
for (const docPath of unregistered) {
|
|
19693
20089
|
try {
|
|
19694
|
-
const meta = extractDocMeta(
|
|
20090
|
+
const meta = extractDocMeta(join10(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
19695
20091
|
await adapter2.registerDoc({
|
|
19696
20092
|
title: meta.title,
|
|
19697
20093
|
type: meta.type,
|
|
@@ -19768,6 +20164,9 @@ var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
|
|
|
19768
20164
|
var MODULE_INSTRUCTIONS = {
|
|
19769
20165
|
Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
|
|
19770
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
|
+
|
|
19771
20170
|
**STEP 1 \u2014 BEFORE writing any code:**
|
|
19772
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.)
|
|
19773
20172
|
|
|
@@ -20007,7 +20406,7 @@ function isResearchOrSpike(task) {
|
|
|
20007
20406
|
}
|
|
20008
20407
|
function formatListItem(task) {
|
|
20009
20408
|
const base = `- **${task.id}:** ${task.title}
|
|
20010
|
-
Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity}`;
|
|
20409
|
+
Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity} | ${modelTierTag(task.complexity)}`;
|
|
20011
20410
|
if (isResearchOrSpike(task)) {
|
|
20012
20411
|
return base + "\n _Research/spike \u2014 still needed?_ **(a)** build_execute | **(b)** fast-close (answered) | **(c)** deprioritise | **(d)** discuss";
|
|
20013
20412
|
}
|
|
@@ -20129,7 +20528,7 @@ async function handleBuildDescribe(adapter2, args) {
|
|
|
20129
20528
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
20130
20529
|
}
|
|
20131
20530
|
}
|
|
20132
|
-
async function handleBuildExecute(adapter2, config2, args) {
|
|
20531
|
+
async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
20133
20532
|
const taskId = args.task_id;
|
|
20134
20533
|
if (!taskId) {
|
|
20135
20534
|
return errorResponse("task_id is required.");
|
|
@@ -20153,11 +20552,11 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
20153
20552
|
}
|
|
20154
20553
|
}
|
|
20155
20554
|
if (hasReportFields(args)) {
|
|
20156
|
-
return handleExecuteComplete(adapter2, config2, taskId, args, light);
|
|
20555
|
+
return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
|
|
20157
20556
|
}
|
|
20158
20557
|
const tracker = new ProgressTracker("start_build");
|
|
20159
20558
|
try {
|
|
20160
|
-
const result = await startBuild(adapter2, config2, taskId, { light });
|
|
20559
|
+
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
20161
20560
|
tracker.mark("start_decorate_handoff");
|
|
20162
20561
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
20163
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") : "";
|
|
@@ -20209,7 +20608,8 @@ ${entries}`;
|
|
|
20209
20608
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
20210
20609
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
20211
20610
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
20212
|
-
|
|
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);
|
|
20213
20613
|
} catch (err) {
|
|
20214
20614
|
if (isNoHandoffError(err)) {
|
|
20215
20615
|
const lines = [
|
|
@@ -20245,7 +20645,7 @@ ${entries}`;
|
|
|
20245
20645
|
}));
|
|
20246
20646
|
}
|
|
20247
20647
|
}
|
|
20248
|
-
async function handleExecuteComplete(adapter2, config2, taskId, args, light = false) {
|
|
20648
|
+
async function handleExecuteComplete(adapter2, config2, taskId, args, light = false, clientName) {
|
|
20249
20649
|
const completed = args.completed;
|
|
20250
20650
|
const effort = args.effort;
|
|
20251
20651
|
const estimatedEffort = args.estimated_effort;
|
|
@@ -20323,7 +20723,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
20323
20723
|
})),
|
|
20324
20724
|
productionVerification,
|
|
20325
20725
|
preview
|
|
20326
|
-
}, { light });
|
|
20726
|
+
}, { light }, clientName);
|
|
20327
20727
|
tracker.mark("complete_format");
|
|
20328
20728
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
20329
20729
|
for (const learningId of resolvesLearnings) {
|
|
@@ -20454,14 +20854,12 @@ function formatCompleteResult(result) {
|
|
|
20454
20854
|
} else {
|
|
20455
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.`);
|
|
20456
20856
|
}
|
|
20457
|
-
lines.push("", "*Tip: Start a new chat for your next build to keep context fresh and avoid hitting context window limits.*");
|
|
20458
20857
|
} else {
|
|
20459
20858
|
if (hasDiscoveredIssues) {
|
|
20460
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.");
|
|
20461
20860
|
} else {
|
|
20462
20861
|
lines.push("", "All cycle tasks complete. Run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
|
|
20463
20862
|
}
|
|
20464
|
-
lines.push("", "*Tip: Start a new chat for your next session to keep context fresh.*");
|
|
20465
20863
|
}
|
|
20466
20864
|
return lines.join("\n");
|
|
20467
20865
|
}
|
|
@@ -21050,13 +21448,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
21050
21448
|
init_git();
|
|
21051
21449
|
|
|
21052
21450
|
// src/services/reconcile.ts
|
|
21053
|
-
import { readFileSync as
|
|
21054
|
-
import { join as
|
|
21451
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
21452
|
+
import { join as join11 } from "path";
|
|
21055
21453
|
function loadDocsIndex(projectRoot) {
|
|
21056
21454
|
if (!hasLocalWorkspace()) return "";
|
|
21057
21455
|
try {
|
|
21058
|
-
const indexPath =
|
|
21059
|
-
const raw =
|
|
21456
|
+
const indexPath = join11(projectRoot, "docs", "INDEX.md");
|
|
21457
|
+
const raw = readFileSync6(indexPath, "utf8");
|
|
21060
21458
|
const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
|
|
21061
21459
|
if (rows.length === 0) return "";
|
|
21062
21460
|
const entries = rows.map((row) => {
|
|
@@ -21631,8 +22029,8 @@ Produce your analysis and structured output above. Present Part 1 to the user an
|
|
|
21631
22029
|
}
|
|
21632
22030
|
|
|
21633
22031
|
// src/tools/review.ts
|
|
21634
|
-
import { existsSync as
|
|
21635
|
-
import { join as
|
|
22032
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
22033
|
+
import { join as join12 } from "path";
|
|
21636
22034
|
init_git();
|
|
21637
22035
|
|
|
21638
22036
|
// src/services/review.ts
|
|
@@ -21844,11 +22242,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
|
21844
22242
|
${diff}
|
|
21845
22243
|
\`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
|
|
21846
22244
|
let projectContext = "";
|
|
21847
|
-
const ctxPath =
|
|
21848
|
-
if (
|
|
22245
|
+
const ctxPath = join12(config2.projectRoot, ".agents", "papi-context.md");
|
|
22246
|
+
if (existsSync7(ctxPath)) {
|
|
21849
22247
|
try {
|
|
21850
22248
|
projectContext = `### Project context (.agents/papi-context.md)
|
|
21851
|
-
${
|
|
22249
|
+
${readFileSync7(ctxPath, "utf-8")}
|
|
21852
22250
|
|
|
21853
22251
|
`;
|
|
21854
22252
|
} catch {
|
|
@@ -22003,8 +22401,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
22003
22401
|
};
|
|
22004
22402
|
}
|
|
22005
22403
|
const details = [];
|
|
22006
|
-
const papiDir =
|
|
22007
|
-
if (
|
|
22404
|
+
const papiDir = join12(config2.projectRoot, ".papi");
|
|
22405
|
+
if (existsSync7(papiDir)) {
|
|
22008
22406
|
try {
|
|
22009
22407
|
const commitResult = stageDirAndCommit(
|
|
22010
22408
|
config2.projectRoot,
|
|
@@ -23034,8 +23432,9 @@ async function getHealthSummary(adapter2) {
|
|
|
23034
23432
|
const cycleNumber = health.totalCycles;
|
|
23035
23433
|
const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
|
|
23036
23434
|
const reviewDue = health.strategyReviewDue;
|
|
23037
|
-
const reviewGateBlocking = cyclesSinceReview >=
|
|
23038
|
-
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}`;
|
|
23039
23438
|
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
23040
23439
|
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
23041
23440
|
const statusCounts = countByStatus(nonDeferredTasks);
|
|
@@ -23314,115 +23713,9 @@ function formatUnblockSection(candidates) {
|
|
|
23314
23713
|
return lines.join("\n");
|
|
23315
23714
|
}
|
|
23316
23715
|
|
|
23317
|
-
// src/lib/skill-detection.ts
|
|
23318
|
-
import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
23319
|
-
import { join as join11 } from "path";
|
|
23320
|
-
function readPackageJson(projectRoot) {
|
|
23321
|
-
const path7 = join11(projectRoot, "package.json");
|
|
23322
|
-
if (!existsSync6(path7)) return null;
|
|
23323
|
-
try {
|
|
23324
|
-
const raw = readFileSync6(path7, "utf-8");
|
|
23325
|
-
return JSON.parse(raw);
|
|
23326
|
-
} catch {
|
|
23327
|
-
return null;
|
|
23328
|
-
}
|
|
23329
|
-
}
|
|
23330
|
-
function allDeps(pkg) {
|
|
23331
|
-
if (!pkg) return {};
|
|
23332
|
-
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
23333
|
-
}
|
|
23334
|
-
function hasDependencyMatching(deps, pattern) {
|
|
23335
|
-
for (const name of Object.keys(deps)) {
|
|
23336
|
-
if (pattern.test(name)) return true;
|
|
23337
|
-
}
|
|
23338
|
-
return false;
|
|
23339
|
-
}
|
|
23340
|
-
function hasGitHubWorkflows(projectRoot) {
|
|
23341
|
-
const dir = join11(projectRoot, ".github", "workflows");
|
|
23342
|
-
if (!existsSync6(dir)) return false;
|
|
23343
|
-
try {
|
|
23344
|
-
const entries = readdirSync5(dir);
|
|
23345
|
-
return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
23346
|
-
} catch {
|
|
23347
|
-
return false;
|
|
23348
|
-
}
|
|
23349
|
-
}
|
|
23350
|
-
function envExampleMentionsStaging(projectRoot) {
|
|
23351
|
-
const path7 = join11(projectRoot, ".env.example");
|
|
23352
|
-
if (!existsSync6(path7)) return false;
|
|
23353
|
-
try {
|
|
23354
|
-
const raw = readFileSync6(path7, "utf-8");
|
|
23355
|
-
return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
|
|
23356
|
-
} catch {
|
|
23357
|
-
return false;
|
|
23358
|
-
}
|
|
23359
|
-
}
|
|
23360
|
-
function hasVercelConfig(projectRoot) {
|
|
23361
|
-
if (existsSync6(join11(projectRoot, "vercel.json"))) return true;
|
|
23362
|
-
const vercelDir = join11(projectRoot, ".vercel");
|
|
23363
|
-
if (!existsSync6(vercelDir)) return false;
|
|
23364
|
-
try {
|
|
23365
|
-
return statSync4(vercelDir).isDirectory();
|
|
23366
|
-
} catch {
|
|
23367
|
-
return false;
|
|
23368
|
-
}
|
|
23369
|
-
}
|
|
23370
|
-
function scanForSkillSignals(projectRoot) {
|
|
23371
|
-
const proposals = [];
|
|
23372
|
-
const pkg = readPackageJson(projectRoot);
|
|
23373
|
-
const deps = allDeps(pkg);
|
|
23374
|
-
if (hasGitHubWorkflows(projectRoot)) {
|
|
23375
|
-
proposals.push({
|
|
23376
|
-
id: "gh-actions-debug",
|
|
23377
|
-
name: "GitHub Actions debugger",
|
|
23378
|
-
rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
|
|
23379
|
-
hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
|
|
23380
|
-
});
|
|
23381
|
-
}
|
|
23382
|
-
if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
|
|
23383
|
-
proposals.push({
|
|
23384
|
-
id: "error-tracking",
|
|
23385
|
-
name: "Error tracking helper",
|
|
23386
|
-
rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
|
|
23387
|
-
hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
|
|
23388
|
-
});
|
|
23389
|
-
}
|
|
23390
|
-
if (hasVercelConfig(projectRoot)) {
|
|
23391
|
-
proposals.push({
|
|
23392
|
-
id: "read-vercel-logs",
|
|
23393
|
-
name: "Vercel deploy logs",
|
|
23394
|
-
rationale: "Detected vercel.json or .vercel/ \u2014 a skill for pulling deploy + runtime logs from Vercel directly into Claude Code helps when builds fail or runtime errors land.",
|
|
23395
|
-
hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
|
|
23396
|
-
});
|
|
23397
|
-
}
|
|
23398
|
-
if (envExampleMentionsStaging(projectRoot)) {
|
|
23399
|
-
proposals.push({
|
|
23400
|
-
id: "staging-environment",
|
|
23401
|
-
name: "Staging-environment helper",
|
|
23402
|
-
rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
|
|
23403
|
-
hint: 'Search Claude Code skill registry for "staging" or define your own.'
|
|
23404
|
-
});
|
|
23405
|
-
}
|
|
23406
|
-
return proposals;
|
|
23407
|
-
}
|
|
23408
|
-
function formatSkillProposals(proposals) {
|
|
23409
|
-
if (proposals.length === 0) return "";
|
|
23410
|
-
const lines = ["", "## Skill proposals", "_PAPI scanned your project once and noticed tooling that often pairs with a Claude Code skill. Each proposal is one-shot \u2014 they won't resurface._"];
|
|
23411
|
-
for (const p of proposals) {
|
|
23412
|
-
lines.push("");
|
|
23413
|
-
lines.push(`### ${p.name}`);
|
|
23414
|
-
lines.push(p.rationale);
|
|
23415
|
-
if (p.hint) {
|
|
23416
|
-
lines.push(`_${p.hint}_`);
|
|
23417
|
-
}
|
|
23418
|
-
lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
|
|
23419
|
-
}
|
|
23420
|
-
return "\n" + lines.join("\n");
|
|
23421
|
-
}
|
|
23422
|
-
|
|
23423
23716
|
// src/tools/agent-list.ts
|
|
23424
23717
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
23425
|
-
import { join as
|
|
23718
|
+
import { join as join13 } from "path";
|
|
23426
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.";
|
|
23427
23720
|
function parseAgentFrontmatter(content) {
|
|
23428
23721
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -23434,7 +23727,7 @@ function parseAgentFrontmatter(content) {
|
|
|
23434
23727
|
return { name: nameMatch?.[1].trim(), description };
|
|
23435
23728
|
}
|
|
23436
23729
|
async function listAgents(projectRoot) {
|
|
23437
|
-
const agentsDir =
|
|
23730
|
+
const agentsDir = join13(projectRoot, ".claude", "agents");
|
|
23438
23731
|
let files;
|
|
23439
23732
|
try {
|
|
23440
23733
|
files = await readdir2(agentsDir);
|
|
@@ -23445,7 +23738,7 @@ async function listAgents(projectRoot) {
|
|
|
23445
23738
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
23446
23739
|
let content;
|
|
23447
23740
|
try {
|
|
23448
|
-
content = await readFile7(
|
|
23741
|
+
content = await readFile7(join13(agentsDir, file), "utf-8");
|
|
23449
23742
|
} catch {
|
|
23450
23743
|
continue;
|
|
23451
23744
|
}
|
|
@@ -23453,7 +23746,7 @@ async function listAgents(projectRoot) {
|
|
|
23453
23746
|
agents.push({
|
|
23454
23747
|
name: meta?.name ?? file.replace(/\.md$/, ""),
|
|
23455
23748
|
description: meta?.description ?? "",
|
|
23456
|
-
path:
|
|
23749
|
+
path: join13(".claude", "agents", file)
|
|
23457
23750
|
});
|
|
23458
23751
|
}
|
|
23459
23752
|
agents.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
@@ -23494,8 +23787,8 @@ ${formatted}`);
|
|
|
23494
23787
|
}
|
|
23495
23788
|
|
|
23496
23789
|
// src/tools/doc-registry.ts
|
|
23497
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
23498
|
-
import { join as
|
|
23790
|
+
import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
23791
|
+
import { join as join14, relative } from "path";
|
|
23499
23792
|
import { homedir as homedir3 } from "os";
|
|
23500
23793
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
23501
23794
|
var docRegisterTool = {
|
|
@@ -23692,7 +23985,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
23692
23985
|
const lines = docs.map((d) => {
|
|
23693
23986
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
23694
23987
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
23695
|
-
const missingNote = root && d.path && !
|
|
23988
|
+
const missingNote = root && d.path && !existsSync8(join14(root, d.path)) ? `
|
|
23696
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.` : "";
|
|
23697
23990
|
return `### ${d.title}
|
|
23698
23991
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -23706,12 +23999,12 @@ ${d.summary}
|
|
|
23706
23999
|
${lines.join("\n---\n\n")}`);
|
|
23707
24000
|
}
|
|
23708
24001
|
function scanMdFiles(dir, rootDir) {
|
|
23709
|
-
if (!
|
|
24002
|
+
if (!existsSync8(dir)) return [];
|
|
23710
24003
|
const files = [];
|
|
23711
24004
|
try {
|
|
23712
24005
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
23713
24006
|
for (const entry of entries) {
|
|
23714
|
-
const full =
|
|
24007
|
+
const full = join14(dir, entry.name);
|
|
23715
24008
|
if (entry.isDirectory()) {
|
|
23716
24009
|
files.push(...scanMdFiles(full, rootDir));
|
|
23717
24010
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -23724,7 +24017,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
23724
24017
|
}
|
|
23725
24018
|
function extractTitle(filePath) {
|
|
23726
24019
|
try {
|
|
23727
|
-
const content =
|
|
24020
|
+
const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
|
|
23728
24021
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
23729
24022
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
23730
24023
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -23745,17 +24038,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
23745
24038
|
const includePlans = args.include_plans ?? false;
|
|
23746
24039
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
23747
24040
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
23748
|
-
const docsDir =
|
|
24041
|
+
const docsDir = join14(config2.projectRoot, "docs");
|
|
23749
24042
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
23750
24043
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
23751
24044
|
let unregisteredPlans = [];
|
|
23752
24045
|
if (includePlans) {
|
|
23753
|
-
const plansDir =
|
|
23754
|
-
if (
|
|
24046
|
+
const plansDir = join14(homedir3(), ".claude", "plans");
|
|
24047
|
+
if (existsSync8(plansDir)) {
|
|
23755
24048
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
23756
24049
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
23757
24050
|
path: f,
|
|
23758
|
-
title: extractTitle(
|
|
24051
|
+
title: extractTitle(join14(plansDir, f.replace("plans/", "")))
|
|
23759
24052
|
}));
|
|
23760
24053
|
}
|
|
23761
24054
|
}
|
|
@@ -23766,7 +24059,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
23766
24059
|
if (unregisteredDocs.length > 0) {
|
|
23767
24060
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
23768
24061
|
for (const f of unregisteredDocs) {
|
|
23769
|
-
const title = extractTitle(
|
|
24062
|
+
const title = extractTitle(join14(config2.projectRoot, f));
|
|
23770
24063
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
23771
24064
|
}
|
|
23772
24065
|
}
|
|
@@ -23876,6 +24169,22 @@ ${userNotes}` : referenceLine;
|
|
|
23876
24169
|
);
|
|
23877
24170
|
}
|
|
23878
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
|
+
|
|
23879
24188
|
// src/lib/enrichment.ts
|
|
23880
24189
|
function headingSlug(line) {
|
|
23881
24190
|
const match = line.match(/^##\s+(.+?)(?:\s*\([^)]*\))?\s*$/);
|
|
@@ -23961,8 +24270,8 @@ async function verifyProject(adapter2) {
|
|
|
23961
24270
|
// src/tools/orient.ts
|
|
23962
24271
|
import { execFile as execFile2 } from "child_process";
|
|
23963
24272
|
import { promisify as promisify2 } from "util";
|
|
23964
|
-
import { readFileSync as
|
|
23965
|
-
import { join as
|
|
24273
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync9 } from "fs";
|
|
24274
|
+
import { join as join15 } from "path";
|
|
23966
24275
|
var execFileAsync2 = promisify2(execFile2);
|
|
23967
24276
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
23968
24277
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -24007,7 +24316,7 @@ async function runWithLimit(concurrency, tasks) {
|
|
|
24007
24316
|
}
|
|
24008
24317
|
var orientTool = {
|
|
24009
24318
|
name: "orient",
|
|
24010
|
-
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.
|
|
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).",
|
|
24011
24320
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
24012
24321
|
inputSchema: {
|
|
24013
24322
|
type: "object",
|
|
@@ -24015,7 +24324,7 @@ var orientTool = {
|
|
|
24015
24324
|
environment: {
|
|
24016
24325
|
type: "string",
|
|
24017
24326
|
enum: ["local-cli", "hosted", "api", "unknown"],
|
|
24018
|
-
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).`
|
|
24019
24328
|
},
|
|
24020
24329
|
deep_housekeeping: {
|
|
24021
24330
|
type: "boolean",
|
|
@@ -24059,7 +24368,7 @@ function formatSynthesisParagraph(opts) {
|
|
|
24059
24368
|
parts.push(`Next: ${cleanMode}`);
|
|
24060
24369
|
return parts.join(" ");
|
|
24061
24370
|
}
|
|
24062
|
-
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) {
|
|
24063
24372
|
const lines = [];
|
|
24064
24373
|
const cycleIsComplete = health.latestCycleStatus === "complete";
|
|
24065
24374
|
const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
|
|
@@ -24081,6 +24390,12 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
24081
24390
|
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
24082
24391
|
lines.push("");
|
|
24083
24392
|
}
|
|
24393
|
+
const harness = detectHarness(clientName);
|
|
24394
|
+
const roleNote = connectOnlyRoleNote(harness);
|
|
24395
|
+
if (roleNote) {
|
|
24396
|
+
lines.push(`> ${roleNote}`);
|
|
24397
|
+
lines.push("");
|
|
24398
|
+
}
|
|
24084
24399
|
if (buildInfo.warnings.length > 0) {
|
|
24085
24400
|
for (const w of buildInfo.warnings) {
|
|
24086
24401
|
lines.push(`> ${w}`);
|
|
@@ -24092,11 +24407,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
24092
24407
|
lines.push("");
|
|
24093
24408
|
}
|
|
24094
24409
|
const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
|
|
24095
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
|
|
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)}`);
|
|
24100
24419
|
}
|
|
24101
24420
|
lines.push("");
|
|
24102
24421
|
lines.push(`**Strategy Review:** ${health.reviewWarning}`);
|
|
@@ -24262,8 +24581,8 @@ async function getLatestGitTag(projectRoot) {
|
|
|
24262
24581
|
}
|
|
24263
24582
|
async function checkNpmVersionDrift() {
|
|
24264
24583
|
try {
|
|
24265
|
-
const pkgPath =
|
|
24266
|
-
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"));
|
|
24267
24586
|
const localVersion = pkg.version;
|
|
24268
24587
|
const packageName = pkg.name;
|
|
24269
24588
|
const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
|
|
@@ -24396,7 +24715,7 @@ function formatResolvedFeedback(items) {
|
|
|
24396
24715
|
lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
|
|
24397
24716
|
return lines.join("\n");
|
|
24398
24717
|
}
|
|
24399
|
-
async function handleOrient(adapter2, config2, args = {}) {
|
|
24718
|
+
async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
24400
24719
|
const environment = normaliseEnvironment(args.environment);
|
|
24401
24720
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
24402
24721
|
const fullEnrichment = args.full === true || deepHousekeeping;
|
|
@@ -24720,7 +25039,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24720
25039
|
// task-1652: deep housekeeping — opt-in sweep (board, unrecorded commits, unregistered docs)
|
|
24721
25040
|
// task-1865: also detects stale skill forks vs the @papi-ai/skills registry.
|
|
24722
25041
|
tracked("deep-housekeeping", async () => {
|
|
24723
|
-
if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
25042
|
+
if (!deepHousekeeping) return { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
24724
25043
|
let reconciliationNote2 = "";
|
|
24725
25044
|
try {
|
|
24726
25045
|
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
@@ -24732,6 +25051,22 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24732
25051
|
}
|
|
24733
25052
|
} catch {
|
|
24734
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
|
+
}
|
|
24735
25070
|
let unrecordedNote2 = "";
|
|
24736
25071
|
try {
|
|
24737
25072
|
const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
|
|
@@ -24756,7 +25091,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24756
25091
|
let unregisteredDocsNote2 = "";
|
|
24757
25092
|
try {
|
|
24758
25093
|
if (adapter2.searchDocs && hasLocalWorkspace()) {
|
|
24759
|
-
const docsDir =
|
|
25094
|
+
const docsDir = join15(config2.projectRoot, "docs");
|
|
24760
25095
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
24761
25096
|
if (docsFiles.length > 0) {
|
|
24762
25097
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -24783,7 +25118,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24783
25118
|
}
|
|
24784
25119
|
} catch {
|
|
24785
25120
|
}
|
|
24786
|
-
return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
|
|
25121
|
+
return { reconciliationNote: reconciliationNote2, mergedInProgressNote: mergedInProgressNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
|
|
24787
25122
|
})
|
|
24788
25123
|
]);
|
|
24789
25124
|
const proxyWarning = proxyVersionOutcome.status === "fulfilled" ? proxyVersionOutcome.value : void 0;
|
|
@@ -24825,7 +25160,7 @@ ${versionDrift}` : "";
|
|
|
24825
25160
|
void adapter2.markFeedbackNotified(resolvedFeedback.map((f) => f.id), config2.userId).catch(() => {
|
|
24826
25161
|
});
|
|
24827
25162
|
}
|
|
24828
|
-
const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
25163
|
+
const { reconciliationNote, mergedInProgressNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
24829
25164
|
let enrichmentNote = "";
|
|
24830
25165
|
const enrichmentCollector = new FileWriteCollector();
|
|
24831
25166
|
try {
|
|
@@ -24872,8 +25207,8 @@ ${section}`;
|
|
|
24872
25207
|
tracked("release-history", () => computeReleaseHistory(adapter2))().catch(() => void 0)
|
|
24873
25208
|
]);
|
|
24874
25209
|
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
24875
|
-
const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
|
|
24876
|
-
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
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);
|
|
24877
25212
|
} catch (err) {
|
|
24878
25213
|
const message = err instanceof Error ? err.message : String(err);
|
|
24879
25214
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -24904,9 +25239,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector) {
|
|
|
24904
25239
|
|
|
24905
25240
|
\u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
24906
25241
|
}
|
|
24907
|
-
const claudeMdPath =
|
|
24908
|
-
if (!
|
|
24909
|
-
const content =
|
|
25242
|
+
const claudeMdPath = join15(projectRoot, "CLAUDE.md");
|
|
25243
|
+
if (!existsSync9(claudeMdPath)) return "";
|
|
25244
|
+
const content = readFileSync9(claudeMdPath, "utf-8");
|
|
24910
25245
|
const additions = [];
|
|
24911
25246
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
24912
25247
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
|
|
@@ -25612,7 +25947,7 @@ ${result.userMessage}
|
|
|
25612
25947
|
|
|
25613
25948
|
// src/services/scope-brief.ts
|
|
25614
25949
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
25615
|
-
import { join as
|
|
25950
|
+
import { join as join16, dirname as dirname4 } from "path";
|
|
25616
25951
|
import Anthropic from "@anthropic-ai/sdk";
|
|
25617
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.
|
|
25618
25953
|
|
|
@@ -25667,13 +26002,13 @@ async function runScopeBrief(adapter2, input) {
|
|
|
25667
26002
|
}
|
|
25668
26003
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
25669
26004
|
const relPath = `docs/scopes/${slug}.md`;
|
|
25670
|
-
const absPath =
|
|
26005
|
+
const absPath = join16(input.projectRoot, relPath);
|
|
25671
26006
|
const docBody = addFrontmatter(docContent, task, input.cycleNumber);
|
|
25672
26007
|
const collector = new FileWriteCollector();
|
|
25673
26008
|
if (input.adapterType === "proxy") {
|
|
25674
26009
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
25675
26010
|
} else {
|
|
25676
|
-
mkdirSync3(
|
|
26011
|
+
mkdirSync3(dirname4(absPath), { recursive: true });
|
|
25677
26012
|
writeFileSync4(absPath, docBody, "utf-8");
|
|
25678
26013
|
}
|
|
25679
26014
|
const taskCount = countSubTasks(docContent);
|
|
@@ -26411,21 +26746,90 @@ async function handleTaskUnclaim(adapter2, config2, args) {
|
|
|
26411
26746
|
return textResponse(`\u2705 Released **${result.id}** (${result.title}) back to the shared Pool.`);
|
|
26412
26747
|
}
|
|
26413
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
|
+
|
|
26414
26818
|
// src/services/harness-inventory.ts
|
|
26415
26819
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
26416
|
-
import { join as
|
|
26820
|
+
import { join as join17 } from "path";
|
|
26417
26821
|
import { createHash as createHash3 } from "crypto";
|
|
26418
26822
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
26419
26823
|
async function computeFingerprint(root) {
|
|
26420
26824
|
const parts = [];
|
|
26421
26825
|
for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
|
|
26422
|
-
const dir =
|
|
26826
|
+
const dir = join17(root, sub);
|
|
26423
26827
|
try {
|
|
26424
26828
|
const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
|
|
26425
26829
|
for (const name of names) {
|
|
26426
26830
|
let mtime = "";
|
|
26427
26831
|
try {
|
|
26428
|
-
mtime = String(Math.floor((await stat3(
|
|
26832
|
+
mtime = String(Math.floor((await stat3(join17(dir, name))).mtimeMs));
|
|
26429
26833
|
} catch {
|
|
26430
26834
|
}
|
|
26431
26835
|
parts.push(`${sub}/${name}:${mtime}`);
|
|
@@ -26444,7 +26848,7 @@ async function computeFingerprint(root) {
|
|
|
26444
26848
|
}
|
|
26445
26849
|
async function readSkillDescription(skillDir) {
|
|
26446
26850
|
try {
|
|
26447
|
-
const content = await readFile8(
|
|
26851
|
+
const content = await readFile8(join17(skillDir, "SKILL.md"), "utf-8");
|
|
26448
26852
|
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26449
26853
|
if (!fm) return void 0;
|
|
26450
26854
|
const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
|
|
@@ -26464,17 +26868,17 @@ async function scanInventory(root, toolDefs) {
|
|
|
26464
26868
|
version = loadManifest().packageVersion;
|
|
26465
26869
|
} catch {
|
|
26466
26870
|
}
|
|
26467
|
-
const skillsDir =
|
|
26871
|
+
const skillsDir = join17(root, ".claude", "skills");
|
|
26468
26872
|
try {
|
|
26469
26873
|
const dirents = await readdir3(skillsDir, { withFileTypes: true });
|
|
26470
26874
|
for (const d of dirents.filter((e) => e.isDirectory())) {
|
|
26471
26875
|
entries.push({
|
|
26472
26876
|
kind: "skill",
|
|
26473
26877
|
name: d.name,
|
|
26474
|
-
description: await readSkillDescription(
|
|
26878
|
+
description: await readSkillDescription(join17(skillsDir, d.name)),
|
|
26475
26879
|
version,
|
|
26476
26880
|
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
26477
|
-
path:
|
|
26881
|
+
path: join17(".claude", "skills", d.name)
|
|
26478
26882
|
});
|
|
26479
26883
|
}
|
|
26480
26884
|
} catch {
|
|
@@ -26490,9 +26894,9 @@ async function scanInventory(root, toolDefs) {
|
|
|
26490
26894
|
}
|
|
26491
26895
|
const present = /* @__PURE__ */ new Set();
|
|
26492
26896
|
try {
|
|
26493
|
-
for (const f of (await readdir3(
|
|
26897
|
+
for (const f of (await readdir3(join17(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
|
|
26494
26898
|
present.add(f);
|
|
26495
|
-
entries.push({ kind: "hook", name: f, status: "ok", path:
|
|
26899
|
+
entries.push({ kind: "hook", name: f, status: "ok", path: join17(".claude", "hooks", f) });
|
|
26496
26900
|
}
|
|
26497
26901
|
} catch {
|
|
26498
26902
|
}
|
|
@@ -26571,6 +26975,130 @@ async function handleInventorySync(adapter2, config2, args, toolDefs) {
|
|
|
26571
26975
|
return textResponse(lines.join("\n"));
|
|
26572
26976
|
}
|
|
26573
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
|
+
|
|
26574
27102
|
// src/services/metering.ts
|
|
26575
27103
|
var TIER_ALLOWANCES = {
|
|
26576
27104
|
free: 1e3,
|
|
@@ -26791,32 +27319,36 @@ var PAPI_TOOLS = [
|
|
|
26791
27319
|
contributorListTool,
|
|
26792
27320
|
taskClaimTool,
|
|
26793
27321
|
taskUnclaimTool,
|
|
27322
|
+
taskMoveTool,
|
|
26794
27323
|
inventorySyncTool
|
|
26795
27324
|
];
|
|
26796
27325
|
function getToolMetadata() {
|
|
26797
27326
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
26798
27327
|
}
|
|
26799
27328
|
function createServer(adapter2, config2) {
|
|
26800
|
-
const __pkgFilename =
|
|
26801
|
-
const __pkgDir =
|
|
27329
|
+
const __pkgFilename = fileURLToPath3(import.meta.url);
|
|
27330
|
+
const __pkgDir = dirname5(__pkgFilename);
|
|
26802
27331
|
let serverVersion = "unknown";
|
|
26803
27332
|
try {
|
|
26804
|
-
const pkg = JSON.parse(
|
|
27333
|
+
const pkg = JSON.parse(readFileSync10(join18(__pkgDir, "..", "package.json"), "utf-8"));
|
|
26805
27334
|
serverVersion = pkg.version ?? "unknown";
|
|
26806
27335
|
} catch {
|
|
26807
27336
|
}
|
|
26808
27337
|
const server2 = new Server(
|
|
26809
27338
|
{ name: "papi", version: serverVersion },
|
|
26810
|
-
|
|
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 }
|
|
26811
27343
|
);
|
|
26812
27344
|
if (config2.adapterType === "md") {
|
|
26813
27345
|
process.stderr.write(
|
|
26814
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"
|
|
26815
27347
|
);
|
|
26816
27348
|
}
|
|
26817
|
-
const __filename =
|
|
26818
|
-
const __dirname2 =
|
|
26819
|
-
const skillsDir =
|
|
27349
|
+
const __filename = fileURLToPath3(import.meta.url);
|
|
27350
|
+
const __dirname2 = dirname5(__filename);
|
|
27351
|
+
const skillsDir = join18(__dirname2, "..", "skills");
|
|
26820
27352
|
function parseSkillFrontmatter(content) {
|
|
26821
27353
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26822
27354
|
if (!match) return null;
|
|
@@ -26834,7 +27366,7 @@ function createServer(adapter2, config2) {
|
|
|
26834
27366
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
26835
27367
|
const prompts = [];
|
|
26836
27368
|
for (const file of mdFiles) {
|
|
26837
|
-
const content = await readFile9(
|
|
27369
|
+
const content = await readFile9(join18(skillsDir, file), "utf-8");
|
|
26838
27370
|
const meta = parseSkillFrontmatter(content);
|
|
26839
27371
|
if (meta) {
|
|
26840
27372
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -26850,7 +27382,7 @@ function createServer(adapter2, config2) {
|
|
|
26850
27382
|
try {
|
|
26851
27383
|
const files = await readdir4(skillsDir);
|
|
26852
27384
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
26853
|
-
const content = await readFile9(
|
|
27385
|
+
const content = await readFile9(join18(skillsDir, file), "utf-8");
|
|
26854
27386
|
const meta = parseSkillFrontmatter(content);
|
|
26855
27387
|
if (meta?.name === name) {
|
|
26856
27388
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -26864,6 +27396,15 @@ function createServer(adapter2, config2) {
|
|
|
26864
27396
|
}
|
|
26865
27397
|
throw new Error(`Prompt not found: ${name}`);
|
|
26866
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
|
+
}));
|
|
26867
27408
|
server2.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
26868
27409
|
tools: [...PAPI_TOOLS]
|
|
26869
27410
|
}));
|
|
@@ -26921,7 +27462,7 @@ function createServer(adapter2, config2) {
|
|
|
26921
27462
|
case "build_describe":
|
|
26922
27463
|
return handleBuildDescribe(adapter2, safeArgs);
|
|
26923
27464
|
case "build_execute":
|
|
26924
|
-
return handleBuildExecute(adapter2, config2, safeArgs);
|
|
27465
|
+
return handleBuildExecute(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26925
27466
|
case "build_cancel":
|
|
26926
27467
|
return handleBuildCancel(adapter2, safeArgs);
|
|
26927
27468
|
case "idea":
|
|
@@ -26943,9 +27484,9 @@ function createServer(adapter2, config2) {
|
|
|
26943
27484
|
case "init":
|
|
26944
27485
|
return handleInit(config2, safeArgs);
|
|
26945
27486
|
case "orient":
|
|
26946
|
-
return handleOrient(adapter2, config2, safeArgs);
|
|
27487
|
+
return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26947
27488
|
case "papi":
|
|
26948
|
-
return handleOrient(adapter2, config2, safeArgs);
|
|
27489
|
+
return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26949
27490
|
// alias (task-2223)
|
|
26950
27491
|
case "hierarchy_update":
|
|
26951
27492
|
return handleHierarchyUpdate(adapter2, safeArgs);
|
|
@@ -26987,6 +27528,8 @@ function createServer(adapter2, config2) {
|
|
|
26987
27528
|
return handleContributorList(adapter2, config2, safeArgs);
|
|
26988
27529
|
case "task_claim":
|
|
26989
27530
|
return handleTaskClaim(adapter2, config2, safeArgs);
|
|
27531
|
+
case "task_move":
|
|
27532
|
+
return handleTaskMove(adapter2, config2, safeArgs);
|
|
26990
27533
|
case "task_unclaim":
|
|
26991
27534
|
return handleTaskUnclaim(adapter2, config2, safeArgs);
|
|
26992
27535
|
case "inventory_sync":
|
|
@@ -27506,10 +28049,10 @@ async function dispatchRequest(args) {
|
|
|
27506
28049
|
}
|
|
27507
28050
|
|
|
27508
28051
|
// src/index.ts
|
|
27509
|
-
var __dirname =
|
|
28052
|
+
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
27510
28053
|
var pkgVersion = "unknown";
|
|
27511
28054
|
try {
|
|
27512
|
-
const pkg = JSON.parse(
|
|
28055
|
+
const pkg = JSON.parse(readFileSync15(join23(__dirname, "..", "package.json"), "utf-8"));
|
|
27513
28056
|
pkgVersion = pkg.version;
|
|
27514
28057
|
} catch {
|
|
27515
28058
|
}
|