@papi-ai/server 0.6.1 → 0.7.0-alpha.1
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/README.md +26 -130
- package/dist/index.js +1618 -1038
- package/dist/prompts.js +9 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7381,6 +7381,13 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7381
7381
|
UPDATE strategy_recommendations
|
|
7382
7382
|
SET status = 'actioned', actioned_cycle = ${cycleNumber}, updated_at = now()
|
|
7383
7383
|
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
7384
|
+
`;
|
|
7385
|
+
}
|
|
7386
|
+
async dismissRecommendation(id, reason) {
|
|
7387
|
+
await this.sql`
|
|
7388
|
+
UPDATE strategy_recommendations
|
|
7389
|
+
SET status = 'actioned', dismissal_reason = ${reason}, updated_at = now()
|
|
7390
|
+
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
7384
7391
|
`;
|
|
7385
7392
|
}
|
|
7386
7393
|
// -------------------------------------------------------------------------
|
|
@@ -8067,7 +8074,48 @@ var init_proxy_adapter = __esm({
|
|
|
8067
8074
|
constructor(config2) {
|
|
8068
8075
|
this.endpoint = config2.endpoint.replace(/\/$/, "");
|
|
8069
8076
|
this.apiKey = config2.apiKey;
|
|
8070
|
-
this.projectId = config2.projectId;
|
|
8077
|
+
this.projectId = config2.projectId ?? "";
|
|
8078
|
+
}
|
|
8079
|
+
/** Resolved project ID — available after ensureProject() completes. */
|
|
8080
|
+
getProjectId() {
|
|
8081
|
+
return this.projectId;
|
|
8082
|
+
}
|
|
8083
|
+
/**
|
|
8084
|
+
* Ensure the authenticated user has a project. If none exists, auto-creates one.
|
|
8085
|
+
* Returns the project ID (existing or newly created). Idempotent and race-safe.
|
|
8086
|
+
*
|
|
8087
|
+
* Call this before any other adapter method when PAPI_PROJECT_ID is not configured.
|
|
8088
|
+
*/
|
|
8089
|
+
async ensureProject(projectName, repoUrl) {
|
|
8090
|
+
const url = `${this.endpoint}/ensure-project`;
|
|
8091
|
+
const response = await fetch(url, {
|
|
8092
|
+
method: "POST",
|
|
8093
|
+
headers: {
|
|
8094
|
+
"Content-Type": "application/json",
|
|
8095
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
8096
|
+
},
|
|
8097
|
+
body: JSON.stringify({
|
|
8098
|
+
...projectName ? { projectName } : {},
|
|
8099
|
+
...repoUrl ? { repoUrl } : {}
|
|
8100
|
+
})
|
|
8101
|
+
});
|
|
8102
|
+
if (!response.ok) {
|
|
8103
|
+
const errorBody = await response.text();
|
|
8104
|
+
let message;
|
|
8105
|
+
try {
|
|
8106
|
+
const parsed = JSON.parse(errorBody);
|
|
8107
|
+
message = parsed.error ?? errorBody;
|
|
8108
|
+
} catch {
|
|
8109
|
+
message = errorBody;
|
|
8110
|
+
}
|
|
8111
|
+
throw new Error(`Auto-provision failed (${response.status}): ${message}`);
|
|
8112
|
+
}
|
|
8113
|
+
const body = await response.json();
|
|
8114
|
+
if (!body.ok && body.error) {
|
|
8115
|
+
throw new Error(`Auto-provision failed: ${body.error}`);
|
|
8116
|
+
}
|
|
8117
|
+
this.projectId = body.projectId;
|
|
8118
|
+
return { projectId: body.projectId, projectName: body.projectName, created: body.created };
|
|
8071
8119
|
}
|
|
8072
8120
|
/**
|
|
8073
8121
|
* Send an adapter method call to the proxy Edge Function.
|
|
@@ -8097,6 +8145,20 @@ var init_proxy_adapter = __esm({
|
|
|
8097
8145
|
} catch {
|
|
8098
8146
|
message = errorBody;
|
|
8099
8147
|
}
|
|
8148
|
+
if (response.status === 401) {
|
|
8149
|
+
throw new Error(
|
|
8150
|
+
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
8151
|
+
Check PAPI_DATA_API_KEY in your .mcp.json config. You can regenerate it from the PAPI dashboard.
|
|
8152
|
+
(${response.status} on ${method}: ${message})`
|
|
8153
|
+
);
|
|
8154
|
+
}
|
|
8155
|
+
if (response.status === 403 || response.status === 404) {
|
|
8156
|
+
throw new Error(
|
|
8157
|
+
`Auth: Project not found or access denied \u2014 PAPI_PROJECT_ID may be wrong.
|
|
8158
|
+
Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI dashboard settings.
|
|
8159
|
+
(${response.status} on ${method}: ${message})`
|
|
8160
|
+
);
|
|
8161
|
+
}
|
|
8100
8162
|
throw new Error(`Proxy error (${response.status}) on ${method}: ${message}`);
|
|
8101
8163
|
}
|
|
8102
8164
|
const body = await response.json();
|
|
@@ -8120,6 +8182,27 @@ var init_proxy_adapter = __esm({
|
|
|
8120
8182
|
return false;
|
|
8121
8183
|
}
|
|
8122
8184
|
}
|
|
8185
|
+
/**
|
|
8186
|
+
* Validate API key and project access against the proxy.
|
|
8187
|
+
* Returns HTTP status so callers can distinguish 401 (bad key) from 403/404 (bad project).
|
|
8188
|
+
* Status 0 means a network error occurred.
|
|
8189
|
+
*/
|
|
8190
|
+
async probeAuth(projectId) {
|
|
8191
|
+
try {
|
|
8192
|
+
const response = await fetch(`${this.endpoint}/invoke`, {
|
|
8193
|
+
method: "POST",
|
|
8194
|
+
headers: {
|
|
8195
|
+
"Content-Type": "application/json",
|
|
8196
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
8197
|
+
},
|
|
8198
|
+
body: JSON.stringify({ projectId, method: "projectExists", args: [] }),
|
|
8199
|
+
signal: AbortSignal.timeout(5e3)
|
|
8200
|
+
});
|
|
8201
|
+
return { ok: response.ok, status: response.status };
|
|
8202
|
+
} catch {
|
|
8203
|
+
return { ok: false, status: 0 };
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8123
8206
|
// --- Planning & Health ---
|
|
8124
8207
|
readPlanningLog() {
|
|
8125
8208
|
return this.invoke("readPlanningLog");
|
|
@@ -8370,6 +8453,9 @@ var init_proxy_adapter = __esm({
|
|
|
8370
8453
|
projectExists() {
|
|
8371
8454
|
return this.invoke("projectExists");
|
|
8372
8455
|
}
|
|
8456
|
+
getBuildReportCountForTask(taskId) {
|
|
8457
|
+
return this.invoke("getBuildReportCountForTask", [taskId]);
|
|
8458
|
+
}
|
|
8373
8459
|
// --- Atomic plan write-back ---
|
|
8374
8460
|
planWriteBack(payload) {
|
|
8375
8461
|
return this.invoke("planWriteBack", [payload]);
|
|
@@ -8378,1042 +8464,1191 @@ var init_proxy_adapter = __esm({
|
|
|
8378
8464
|
}
|
|
8379
8465
|
});
|
|
8380
8466
|
|
|
8381
|
-
// src/
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8467
|
+
// src/lib/git.ts
|
|
8468
|
+
var git_exports = {};
|
|
8469
|
+
__export(git_exports, {
|
|
8470
|
+
AUTO_WRITTEN_PATHS: () => AUTO_WRITTEN_PATHS,
|
|
8471
|
+
branchExists: () => branchExists,
|
|
8472
|
+
checkoutBranch: () => checkoutBranch,
|
|
8473
|
+
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
8474
|
+
createPullRequest: () => createPullRequest,
|
|
8475
|
+
createTag: () => createTag,
|
|
8476
|
+
deleteLocalBranch: () => deleteLocalBranch,
|
|
8477
|
+
detectBoardMismatches: () => detectBoardMismatches,
|
|
8478
|
+
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
8479
|
+
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
8480
|
+
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
8481
|
+
getCurrentBranch: () => getCurrentBranch,
|
|
8482
|
+
getFilesChangedFromBase: () => getFilesChangedFromBase,
|
|
8483
|
+
getHeadCommitSha: () => getHeadCommitSha,
|
|
8484
|
+
getLatestTag: () => getLatestTag,
|
|
8485
|
+
getOriginRepoSlug: () => getOriginRepoSlug,
|
|
8486
|
+
getUnmergedBranches: () => getUnmergedBranches,
|
|
8487
|
+
gitPull: () => gitPull,
|
|
8488
|
+
gitPush: () => gitPush,
|
|
8489
|
+
hasRemote: () => hasRemote,
|
|
8490
|
+
hasUncommittedChanges: () => hasUncommittedChanges,
|
|
8491
|
+
hasUnpushedCommits: () => hasUnpushedCommits,
|
|
8492
|
+
isGhAvailable: () => isGhAvailable,
|
|
8493
|
+
isGitAvailable: () => isGitAvailable,
|
|
8494
|
+
isGitRepo: () => isGitRepo,
|
|
8495
|
+
mergePullRequest: () => mergePullRequest,
|
|
8496
|
+
resolveBaseBranch: () => resolveBaseBranch,
|
|
8497
|
+
runAutoCommit: () => runAutoCommit,
|
|
8498
|
+
stageAllAndCommit: () => stageAllAndCommit,
|
|
8499
|
+
stageDirAndCommit: () => stageDirAndCommit,
|
|
8500
|
+
tagExists: () => tagExists,
|
|
8501
|
+
taskBranchName: () => taskBranchName,
|
|
8502
|
+
withBaseBranchSync: () => withBaseBranchSync
|
|
8503
|
+
});
|
|
8504
|
+
import { execFileSync } from "child_process";
|
|
8505
|
+
function isGitAvailable() {
|
|
8506
|
+
try {
|
|
8507
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
8508
|
+
return true;
|
|
8509
|
+
} catch {
|
|
8510
|
+
return false;
|
|
8393
8511
|
}
|
|
8394
|
-
const anthropicApiKey = process.env.PAPI_API_KEY ?? "";
|
|
8395
|
-
const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
|
|
8396
|
-
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
8397
|
-
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
8398
|
-
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
8399
|
-
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
8400
|
-
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
8401
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
8402
|
-
const explicitAdapter = process.env.PAPI_ADAPTER;
|
|
8403
|
-
const adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
|
|
8404
|
-
return {
|
|
8405
|
-
projectRoot,
|
|
8406
|
-
papiDir: path.join(projectRoot, ".papi"),
|
|
8407
|
-
anthropicApiKey,
|
|
8408
|
-
autoCommit: autoCommit2,
|
|
8409
|
-
baseBranch,
|
|
8410
|
-
autoPR,
|
|
8411
|
-
adapterType,
|
|
8412
|
-
papiEndpoint,
|
|
8413
|
-
lightMode
|
|
8414
|
-
};
|
|
8415
8512
|
}
|
|
8416
|
-
|
|
8417
|
-
// src/adapter-factory.ts
|
|
8418
|
-
init_dist2();
|
|
8419
|
-
import path2 from "path";
|
|
8420
|
-
import { execSync } from "child_process";
|
|
8421
|
-
function detectUserId() {
|
|
8513
|
+
function isGitRepo(cwd) {
|
|
8422
8514
|
try {
|
|
8423
|
-
|
|
8424
|
-
|
|
8515
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
8516
|
+
cwd,
|
|
8517
|
+
stdio: "ignore"
|
|
8518
|
+
});
|
|
8519
|
+
return true;
|
|
8425
8520
|
} catch {
|
|
8521
|
+
return false;
|
|
8426
8522
|
}
|
|
8523
|
+
}
|
|
8524
|
+
function stageDirAndCommit(cwd, dir, message) {
|
|
8427
8525
|
try {
|
|
8428
|
-
|
|
8429
|
-
|
|
8526
|
+
execFileSync("git", ["check-ignore", "-q", dir], { cwd });
|
|
8527
|
+
return { committed: false, message: `Skipped commit \u2014 '${dir}' is gitignored.` };
|
|
8430
8528
|
} catch {
|
|
8431
8529
|
}
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
"placeholder",
|
|
8440
|
-
"example.com",
|
|
8441
|
-
"localhost:5432/dbname",
|
|
8442
|
-
"user:password@host"
|
|
8443
|
-
];
|
|
8444
|
-
function validateDatabaseUrl(connectionString) {
|
|
8445
|
-
const lower = connectionString.toLowerCase().trim();
|
|
8446
|
-
if (PLACEHOLDER_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
|
|
8447
|
-
throw new Error(
|
|
8448
|
-
"DATABASE_URL contains a placeholder value and is not configured.\nReplace it with your actual Supabase connection string in .mcp.json.\nIf you don't have one yet, contact the PAPI admin for access."
|
|
8449
|
-
);
|
|
8530
|
+
execFileSync("git", ["add", dir], { cwd });
|
|
8531
|
+
const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
8532
|
+
cwd,
|
|
8533
|
+
encoding: "utf-8"
|
|
8534
|
+
}).trim();
|
|
8535
|
+
if (!staged) {
|
|
8536
|
+
return { committed: false, message: "No changes to commit." };
|
|
8450
8537
|
}
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8538
|
+
execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
|
|
8539
|
+
return { committed: true, message };
|
|
8540
|
+
}
|
|
8541
|
+
function stageAllAndCommit(cwd, message) {
|
|
8542
|
+
execFileSync("git", ["add", "."], { cwd });
|
|
8543
|
+
const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
8544
|
+
cwd,
|
|
8545
|
+
encoding: "utf-8"
|
|
8546
|
+
}).trim();
|
|
8547
|
+
if (!staged) {
|
|
8548
|
+
return { committed: false, message: "No changes to commit." };
|
|
8457
8549
|
}
|
|
8550
|
+
execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
|
|
8551
|
+
return { committed: true, message };
|
|
8458
8552
|
}
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8553
|
+
function getCurrentBranch(cwd) {
|
|
8554
|
+
try {
|
|
8555
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
8556
|
+
cwd,
|
|
8557
|
+
encoding: "utf-8"
|
|
8558
|
+
}).trim();
|
|
8559
|
+
} catch {
|
|
8560
|
+
return null;
|
|
8561
|
+
}
|
|
8462
8562
|
}
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
_connectionStatus = "offline";
|
|
8469
|
-
return new MdFileAdapter(papiDir);
|
|
8470
|
-
case "pg": {
|
|
8471
|
-
const { PgAdapter: PgAdapter2, PgPapiAdapter: PgPapiAdapter2, configFromEnv: configFromEnv2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
8472
|
-
const projectId = process.env["PAPI_PROJECT_ID"];
|
|
8473
|
-
if (!projectId) {
|
|
8474
|
-
throw new Error(
|
|
8475
|
-
"PAPI_PROJECT_ID is required when using PostgreSQL storage. Set it to the UUID of your project in the database."
|
|
8476
|
-
);
|
|
8477
|
-
}
|
|
8478
|
-
const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv2();
|
|
8479
|
-
validateDatabaseUrl(config2.connectionString);
|
|
8480
|
-
try {
|
|
8481
|
-
const { ensureSchema: ensureSchema2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
8482
|
-
await ensureSchema2(config2);
|
|
8483
|
-
} catch (err) {
|
|
8484
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8485
|
-
console.error(`[papi] \u2717 Schema creation failed: ${msg}`);
|
|
8486
|
-
console.error("[papi] Check DATABASE_URL and Supabase access.");
|
|
8487
|
-
}
|
|
8488
|
-
try {
|
|
8489
|
-
const pgAdapter = new PgAdapter2(config2);
|
|
8490
|
-
const existing = await pgAdapter.getProject(projectId);
|
|
8491
|
-
if (!existing) {
|
|
8492
|
-
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? "";
|
|
8493
|
-
const slug = path2.basename(projectRoot) || "unnamed";
|
|
8494
|
-
let userId = process.env["PAPI_USER_ID"] ?? void 0;
|
|
8495
|
-
if (!userId) {
|
|
8496
|
-
userId = detectUserId();
|
|
8497
|
-
if (userId) {
|
|
8498
|
-
console.error(`[papi] Auto-detected user identity: ${userId}`);
|
|
8499
|
-
console.error("[papi] Set PAPI_USER_ID in .mcp.json to make this explicit.");
|
|
8500
|
-
} else {
|
|
8501
|
-
console.error("[papi] \u26A0 No PAPI_USER_ID set and auto-detection failed.");
|
|
8502
|
-
console.error("[papi] Project will have no user scope \u2014 it may be visible to all dashboard users.");
|
|
8503
|
-
console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
|
|
8504
|
-
}
|
|
8505
|
-
}
|
|
8506
|
-
await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
|
|
8507
|
-
}
|
|
8508
|
-
await pgAdapter.close();
|
|
8509
|
-
} catch {
|
|
8510
|
-
}
|
|
8511
|
-
const adapter2 = new PgPapiAdapter2(config2, projectId);
|
|
8512
|
-
try {
|
|
8513
|
-
await adapter2.initRls();
|
|
8514
|
-
} catch {
|
|
8515
|
-
}
|
|
8516
|
-
const connected = await adapter2.probeConnection();
|
|
8517
|
-
if (connected) {
|
|
8518
|
-
_connectionStatus = "connected";
|
|
8519
|
-
console.error("[papi] \u2713 Supabase connected");
|
|
8520
|
-
} else {
|
|
8521
|
-
_connectionStatus = "degraded";
|
|
8522
|
-
console.error("[papi] \u2717 Supabase unreachable \u2014 running in degraded mode, data may be stale");
|
|
8523
|
-
console.error("[papi] Check your DATABASE_URL in .mcp.json \u2014 is the connection string correct?");
|
|
8524
|
-
}
|
|
8525
|
-
return adapter2;
|
|
8526
|
-
}
|
|
8527
|
-
case "proxy": {
|
|
8528
|
-
const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
|
|
8529
|
-
const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app";
|
|
8530
|
-
const projectId = process.env["PAPI_PROJECT_ID"];
|
|
8531
|
-
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
8532
|
-
if (!projectId && !dataApiKey) {
|
|
8533
|
-
throw new Error(
|
|
8534
|
-
`PAPI needs an account to store your project data.
|
|
8535
|
-
|
|
8536
|
-
Get started in 3 steps:
|
|
8537
|
-
1. Sign up at ${dashboardUrl}/login
|
|
8538
|
-
2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
|
|
8539
|
-
3. Download the config, place it in your project root, and restart Claude Code
|
|
8540
|
-
|
|
8541
|
-
Already have an account? Make sure PAPI_DATA_API_KEY and PAPI_PROJECT_ID are set in your .mcp.json env config.`
|
|
8542
|
-
);
|
|
8543
|
-
}
|
|
8544
|
-
if (!projectId) {
|
|
8545
|
-
throw new Error(
|
|
8546
|
-
`PAPI_PROJECT_ID is required.
|
|
8547
|
-
Visit ${dashboardUrl}/onboard to generate your config, or add PAPI_PROJECT_ID to your .mcp.json env config.`
|
|
8548
|
-
);
|
|
8549
|
-
}
|
|
8550
|
-
const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
|
|
8551
|
-
if (!dataApiKey) {
|
|
8552
|
-
throw new Error(
|
|
8553
|
-
`PAPI_DATA_API_KEY is required for proxy mode.
|
|
8554
|
-
To get your API key:
|
|
8555
|
-
1. Sign up or sign in at ${dashboardUrl}/login
|
|
8556
|
-
2. Your API key is shown on the onboarding page (save it \u2014 shown only once)
|
|
8557
|
-
3. Add PAPI_DATA_API_KEY to your .mcp.json env config
|
|
8558
|
-
If you already have a key, set it in your MCP configuration.`
|
|
8559
|
-
);
|
|
8560
|
-
}
|
|
8561
|
-
const adapter2 = new ProxyPapiAdapter2({
|
|
8562
|
-
endpoint: dataEndpoint,
|
|
8563
|
-
apiKey: dataApiKey,
|
|
8564
|
-
projectId
|
|
8565
|
-
});
|
|
8566
|
-
const connected = await adapter2.probeConnection();
|
|
8567
|
-
if (connected) {
|
|
8568
|
-
_connectionStatus = "connected";
|
|
8569
|
-
console.error("[papi] \u2713 Data proxy connected");
|
|
8570
|
-
} else {
|
|
8571
|
-
_connectionStatus = "degraded";
|
|
8572
|
-
console.error("[papi] \u2717 Data proxy unreachable \u2014 running in degraded mode");
|
|
8573
|
-
console.error("[papi] Check your PAPI_DATA_ENDPOINT configuration.");
|
|
8574
|
-
}
|
|
8575
|
-
return adapter2;
|
|
8576
|
-
}
|
|
8577
|
-
default: {
|
|
8578
|
-
const _exhaustive = adapterType;
|
|
8579
|
-
throw new Error(
|
|
8580
|
-
`Unknown PAPI_ADAPTER value: "${_exhaustive}". Valid options: "md", "pg", "proxy".`
|
|
8581
|
-
);
|
|
8563
|
+
function hasUncommittedChanges(cwd, ignore) {
|
|
8564
|
+
try {
|
|
8565
|
+
const args = ["status", "--porcelain"];
|
|
8566
|
+
if (ignore?.length) {
|
|
8567
|
+
args.push("--", ".", ...ignore.map((p) => `:!${p}`));
|
|
8582
8568
|
}
|
|
8569
|
+
const status = execFileSync("git", args, {
|
|
8570
|
+
cwd,
|
|
8571
|
+
encoding: "utf-8"
|
|
8572
|
+
}).trim();
|
|
8573
|
+
return status.length > 0;
|
|
8574
|
+
} catch {
|
|
8575
|
+
return false;
|
|
8583
8576
|
}
|
|
8584
8577
|
}
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
ListPromptsRequestSchema,
|
|
8595
|
-
GetPromptRequestSchema
|
|
8596
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
8597
|
-
|
|
8598
|
-
// src/lib/response.ts
|
|
8599
|
-
function textResponse(text, usage) {
|
|
8600
|
-
const result = {
|
|
8601
|
-
content: [{ type: "text", text }]
|
|
8602
|
-
};
|
|
8603
|
-
if (usage) {
|
|
8604
|
-
result._usage = usage;
|
|
8578
|
+
function branchExists(cwd, branch) {
|
|
8579
|
+
try {
|
|
8580
|
+
execFileSync("git", ["rev-parse", "--verify", branch], {
|
|
8581
|
+
cwd,
|
|
8582
|
+
stdio: "ignore"
|
|
8583
|
+
});
|
|
8584
|
+
return true;
|
|
8585
|
+
} catch {
|
|
8586
|
+
return false;
|
|
8605
8587
|
}
|
|
8606
|
-
return result;
|
|
8607
8588
|
}
|
|
8608
|
-
function
|
|
8609
|
-
|
|
8589
|
+
function checkoutBranch(cwd, branch) {
|
|
8590
|
+
try {
|
|
8591
|
+
execFileSync("git", ["checkout", branch], { cwd, encoding: "utf-8" });
|
|
8592
|
+
return { success: true, message: `Checked out branch '${branch}'.` };
|
|
8593
|
+
} catch {
|
|
8594
|
+
return { success: false, message: `Failed to checkout branch '${branch}'.` };
|
|
8595
|
+
}
|
|
8610
8596
|
}
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
// src/lib/formatters.ts
|
|
8619
|
-
function formatActiveDecisionsForPlan(decisions) {
|
|
8620
|
-
if (decisions.length === 0) return "No active decisions.";
|
|
8621
|
-
return decisions.filter((d) => !d.superseded).map((d) => `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
|
|
8622
|
-
|
|
8623
|
-
${d.body}`).join("\n\n");
|
|
8597
|
+
function createAndCheckoutBranch(cwd, branch) {
|
|
8598
|
+
try {
|
|
8599
|
+
execFileSync("git", ["checkout", "-b", branch], { cwd, encoding: "utf-8" });
|
|
8600
|
+
return { success: true, message: `Created and checked out branch '${branch}'.` };
|
|
8601
|
+
} catch {
|
|
8602
|
+
return { success: false, message: `Failed to create branch '${branch}'.` };
|
|
8603
|
+
}
|
|
8624
8604
|
}
|
|
8625
|
-
function
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
return `- ${d.id}: ${d.title} [${d.confidence}${lifecycle}]${summaryLine}`;
|
|
8636
|
-
}).join("\n");
|
|
8605
|
+
function hasRemote(cwd, remote = "origin") {
|
|
8606
|
+
try {
|
|
8607
|
+
const remotes = execFileSync("git", ["remote"], {
|
|
8608
|
+
cwd,
|
|
8609
|
+
encoding: "utf-8"
|
|
8610
|
+
}).trim();
|
|
8611
|
+
return remotes.split("\n").includes(remote);
|
|
8612
|
+
} catch {
|
|
8613
|
+
return false;
|
|
8614
|
+
}
|
|
8637
8615
|
}
|
|
8638
|
-
function
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
const
|
|
8645
|
-
return
|
|
8616
|
+
function gitPull(cwd) {
|
|
8617
|
+
try {
|
|
8618
|
+
execFileSync("git", ["pull"], { cwd, encoding: "utf-8", timeout: GIT_NETWORK_TIMEOUT_MS });
|
|
8619
|
+
return { success: true, message: "Pulled latest changes." };
|
|
8620
|
+
} catch (err) {
|
|
8621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8622
|
+
const isTimeout = msg.includes("ETIMEDOUT") || msg.includes("killed");
|
|
8623
|
+
return {
|
|
8624
|
+
success: false,
|
|
8625
|
+
message: isTimeout ? "Pull timed out after 30s." : `Pull failed: ${msg}`
|
|
8626
|
+
};
|
|
8646
8627
|
}
|
|
8647
|
-
return "";
|
|
8648
8628
|
}
|
|
8649
|
-
function
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8629
|
+
function gitPush(cwd, branch) {
|
|
8630
|
+
try {
|
|
8631
|
+
execFileSync("git", ["push", "-u", "origin", branch], {
|
|
8632
|
+
cwd,
|
|
8633
|
+
encoding: "utf-8",
|
|
8634
|
+
timeout: GIT_NETWORK_TIMEOUT_MS
|
|
8635
|
+
});
|
|
8636
|
+
return { success: true, message: `Pushed branch '${branch}' to origin.` };
|
|
8637
|
+
} catch (err) {
|
|
8638
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8639
|
+
const isTimeout = msg.includes("ETIMEDOUT") || msg.includes("killed");
|
|
8640
|
+
return {
|
|
8641
|
+
success: false,
|
|
8642
|
+
message: isTimeout ? `Push timed out after 30s.` : `Push failed: ${msg}`
|
|
8643
|
+
};
|
|
8656
8644
|
}
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8645
|
+
}
|
|
8646
|
+
function isGhAvailable() {
|
|
8647
|
+
try {
|
|
8648
|
+
execFileSync("gh", ["--version"], { stdio: "ignore" });
|
|
8649
|
+
return true;
|
|
8650
|
+
} catch {
|
|
8651
|
+
return false;
|
|
8661
8652
|
}
|
|
8662
|
-
return parts.join(", ");
|
|
8663
8653
|
}
|
|
8664
|
-
function
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8654
|
+
function getOriginRepoSlug(cwd) {
|
|
8655
|
+
try {
|
|
8656
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
8657
|
+
cwd,
|
|
8658
|
+
encoding: "utf-8"
|
|
8659
|
+
}).trim();
|
|
8660
|
+
const sshMatch = url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
8661
|
+
if (sshMatch) return sshMatch[1];
|
|
8662
|
+
const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
8663
|
+
if (httpsMatch) return httpsMatch[1];
|
|
8664
|
+
return null;
|
|
8665
|
+
} catch {
|
|
8666
|
+
return null;
|
|
8667
|
+
}
|
|
8678
8668
|
}
|
|
8679
|
-
function
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
const trimmed = r.architectureNotes.length > 150 ? r.architectureNotes.slice(0, 150) + "..." : r.architectureNotes;
|
|
8686
|
-
parts.push(` _Delivered:_ ${trimmed}`);
|
|
8687
|
-
}
|
|
8688
|
-
if (r.filesChanged && r.filesChanged.length > 0) {
|
|
8689
|
-
parts.push(` _Files:_ ${r.filesChanged.slice(0, 5).join(", ")}${r.filesChanged.length > 5 ? ` (+${r.filesChanged.length - 5} more)` : ""}`);
|
|
8669
|
+
function createPullRequest(cwd, branch, baseBranch, title, body) {
|
|
8670
|
+
try {
|
|
8671
|
+
const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
|
|
8672
|
+
const repo = getOriginRepoSlug(cwd);
|
|
8673
|
+
if (repo) {
|
|
8674
|
+
args.push("--repo", repo);
|
|
8690
8675
|
}
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
].join("\n");
|
|
8700
|
-
}
|
|
8701
|
-
function formatCycleLog(entries) {
|
|
8702
|
-
if (entries.length === 0) return "No cycle log entries yet.";
|
|
8703
|
-
return entries.map(
|
|
8704
|
-
(e) => `### Cycle ${e.cycleNumber} \u2014 ${e.title}
|
|
8705
|
-
|
|
8706
|
-
${e.content}` + (e.carryForward ? `
|
|
8707
|
-
|
|
8708
|
-
**Carry Forward:** ${e.carryForward}` : "") + (e.notes ? `
|
|
8709
|
-
|
|
8710
|
-
**Cycle Notes:** ${e.notes}` : "")
|
|
8711
|
-
).join("\n\n---\n\n");
|
|
8712
|
-
}
|
|
8713
|
-
var PLAN_EXCLUDED_STATUSES = /* @__PURE__ */ new Set(["Done", "Cancelled", "Archived", "Deferred"]);
|
|
8714
|
-
var PLAN_NOTES_MAX_LENGTH = 300;
|
|
8715
|
-
function truncateNotes(notes, maxLen) {
|
|
8716
|
-
if (!notes) return "";
|
|
8717
|
-
if (notes.length <= maxLen) return notes;
|
|
8718
|
-
return notes.slice(0, maxLen) + "...";
|
|
8719
|
-
}
|
|
8720
|
-
var ACTIVE_WORK_STATUSES = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
|
|
8721
|
-
function isRecentTask(task, currentCycle) {
|
|
8722
|
-
if (!task.reviewed) return true;
|
|
8723
|
-
if (ACTIVE_WORK_STATUSES.has(task.status)) return true;
|
|
8724
|
-
if (task.createdCycle === void 0) return true;
|
|
8725
|
-
if (task.createdCycle >= currentCycle - 1) return true;
|
|
8726
|
-
if (task.cycle !== void 0 && task.cycle >= currentCycle) return true;
|
|
8727
|
-
return false;
|
|
8676
|
+
const output = execFileSync("gh", args, { cwd, encoding: "utf-8" }).trim();
|
|
8677
|
+
return { success: true, message: output };
|
|
8678
|
+
} catch (err) {
|
|
8679
|
+
return {
|
|
8680
|
+
success: false,
|
|
8681
|
+
message: `PR creation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
8682
|
+
};
|
|
8683
|
+
}
|
|
8728
8684
|
}
|
|
8729
|
-
function
|
|
8730
|
-
|
|
8731
|
-
const typeTag = task.taskType && task.taskType !== "task" ? ` | ${task.taskType}` : "";
|
|
8732
|
-
return `- ${task.id}: ${task.title} [${task.status} | ${task.priority} | ${task.complexity}${typeTag}${handoffTag}]`;
|
|
8685
|
+
function sleepSync(ms) {
|
|
8686
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
8733
8687
|
}
|
|
8734
|
-
function
|
|
8735
|
-
const
|
|
8736
|
-
const
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
|
|
8740
|
-
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
8741
|
-
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
|
|
8742
|
-
Notes: ${notes}` : ""}`;
|
|
8743
|
-
}
|
|
8744
|
-
function formatBoardForPlan(tasks, filters, currentCycle) {
|
|
8745
|
-
if (tasks.length === 0) return "No tasks on the board.";
|
|
8746
|
-
let active = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status));
|
|
8747
|
-
const excludedCount = tasks.length - active.length;
|
|
8748
|
-
if (filters) {
|
|
8749
|
-
if (filters.phase) active = active.filter((t) => t.phase === filters.phase);
|
|
8750
|
-
if (filters.module) active = active.filter((t) => t.module === filters.module);
|
|
8751
|
-
if (filters.epic) active = active.filter((t) => t.epic === filters.epic);
|
|
8752
|
-
if (filters.priority) active = active.filter((t) => t.priority === filters.priority);
|
|
8753
|
-
}
|
|
8754
|
-
const userFilteredCount = tasks.length - excludedCount - active.length;
|
|
8755
|
-
const filterParts = [];
|
|
8756
|
-
if (excludedCount > 0) filterParts.push(`${excludedCount} completed filtered`);
|
|
8757
|
-
if (userFilteredCount > 0) filterParts.push(`${userFilteredCount} excluded by filters`);
|
|
8758
|
-
const filterSuffix = filterParts.length > 0 ? filterParts.join(", ") : "";
|
|
8759
|
-
if (active.length === 0) {
|
|
8760
|
-
return `No active tasks on the board (${filterSuffix || "all filtered"}).`;
|
|
8761
|
-
}
|
|
8762
|
-
const byCounts = (statuses) => statuses.map((s) => {
|
|
8763
|
-
const n = active.filter((t) => t.status === s).length;
|
|
8764
|
-
return n > 0 ? `${n} ${s.toLowerCase()}` : null;
|
|
8765
|
-
}).filter(Boolean).join(", ");
|
|
8766
|
-
const summary = `Board: ${active.length} active tasks (${byCounts(["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"])})` + (filterSuffix ? ` \u2014 ${filterSuffix}` : "");
|
|
8767
|
-
if (currentCycle === void 0) {
|
|
8768
|
-
const formatted = active.map(formatDetailedTask).join("\n\n");
|
|
8769
|
-
return `${summary}
|
|
8770
|
-
|
|
8771
|
-
${formatted}`;
|
|
8688
|
+
function mergePullRequest(cwd, branch) {
|
|
8689
|
+
const repo = getOriginRepoSlug(cwd);
|
|
8690
|
+
const baseArgs = ["pr", "merge", branch, "--merge", "--delete-branch"];
|
|
8691
|
+
if (repo) {
|
|
8692
|
+
baseArgs.push("--repo", repo);
|
|
8772
8693
|
}
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8694
|
+
for (let attempt = 1; attempt <= MERGE_MAX_RETRIES; attempt++) {
|
|
8695
|
+
try {
|
|
8696
|
+
execFileSync("gh", baseArgs, { cwd, encoding: "utf-8" });
|
|
8697
|
+
return { success: true, message: `Merged PR for '${branch}' and deleted branch.` };
|
|
8698
|
+
} catch (err) {
|
|
8699
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8700
|
+
const isNotMergeable = msg.includes("not mergeable");
|
|
8701
|
+
if (isNotMergeable && attempt < MERGE_MAX_RETRIES) {
|
|
8702
|
+
sleepSync(MERGE_RETRY_DELAY_MS);
|
|
8703
|
+
continue;
|
|
8704
|
+
}
|
|
8705
|
+
return { success: false, message: `PR merge failed: ${msg}` };
|
|
8780
8706
|
}
|
|
8781
8707
|
}
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8708
|
+
return { success: false, message: "PR merge failed: max retries exceeded" };
|
|
8709
|
+
}
|
|
8710
|
+
function deleteLocalBranch(cwd, branch) {
|
|
8711
|
+
try {
|
|
8712
|
+
execFileSync("git", ["branch", "-d", branch], { cwd, encoding: "utf-8" });
|
|
8713
|
+
return { success: true, message: `Deleted local branch '${branch}'.` };
|
|
8714
|
+
} catch (err) {
|
|
8715
|
+
return {
|
|
8716
|
+
success: false,
|
|
8717
|
+
message: `Failed to delete local branch '${branch}': ${err instanceof Error ? err.message : String(err)}`
|
|
8718
|
+
};
|
|
8791
8719
|
}
|
|
8792
|
-
return sections.join("\n\n");
|
|
8793
8720
|
}
|
|
8794
|
-
function
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8721
|
+
function tagExists(cwd, tag) {
|
|
8722
|
+
try {
|
|
8723
|
+
execFileSync("git", ["rev-parse", "--verify", `refs/tags/${tag}`], {
|
|
8724
|
+
cwd,
|
|
8725
|
+
stdio: "ignore"
|
|
8726
|
+
});
|
|
8727
|
+
return true;
|
|
8728
|
+
} catch {
|
|
8729
|
+
return false;
|
|
8730
|
+
}
|
|
8803
8731
|
}
|
|
8804
|
-
function
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8732
|
+
function createTag(cwd, tag, message) {
|
|
8733
|
+
try {
|
|
8734
|
+
execFileSync("git", ["tag", "-a", tag, "-m", message], { cwd, encoding: "utf-8" });
|
|
8735
|
+
return { success: true, message: `Created tag '${tag}'.` };
|
|
8736
|
+
} catch (err) {
|
|
8737
|
+
return {
|
|
8738
|
+
success: false,
|
|
8739
|
+
message: `Failed to create tag: ${err instanceof Error ? err.message : String(err)}`
|
|
8740
|
+
};
|
|
8741
|
+
}
|
|
8809
8742
|
}
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8743
|
+
function getLatestTag(cwd) {
|
|
8744
|
+
try {
|
|
8745
|
+
return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
|
|
8746
|
+
cwd,
|
|
8747
|
+
encoding: "utf-8"
|
|
8748
|
+
}).trim() || null;
|
|
8749
|
+
} catch {
|
|
8750
|
+
return null;
|
|
8818
8751
|
}
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
const
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
snapshots.push({
|
|
8831
|
-
cycle: sn,
|
|
8832
|
-
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8833
|
-
accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
|
|
8834
|
-
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
8835
|
-
});
|
|
8752
|
+
}
|
|
8753
|
+
function getCommitsSinceTag(cwd, tag) {
|
|
8754
|
+
try {
|
|
8755
|
+
const output = execFileSync(
|
|
8756
|
+
"git",
|
|
8757
|
+
["log", `${tag}..HEAD`, "--oneline"],
|
|
8758
|
+
{ cwd, encoding: "utf-8" }
|
|
8759
|
+
).trim();
|
|
8760
|
+
return output ? output.split("\n") : [];
|
|
8761
|
+
} catch {
|
|
8762
|
+
return [];
|
|
8836
8763
|
}
|
|
8837
|
-
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
8838
|
-
return snapshots;
|
|
8839
8764
|
}
|
|
8840
|
-
function
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8765
|
+
async function withBaseBranchSync(config2, fn) {
|
|
8766
|
+
const warnings = [];
|
|
8767
|
+
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
8768
|
+
return { result: await fn(), warnings };
|
|
8769
|
+
}
|
|
8770
|
+
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
8771
|
+
if (baseBranch !== config2.baseBranch) {
|
|
8772
|
+
warnings.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
8773
|
+
}
|
|
8774
|
+
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
8775
|
+
const needsBranchSwitch = currentBranch !== null && currentBranch !== baseBranch;
|
|
8776
|
+
let previousBranch = null;
|
|
8777
|
+
if (needsBranchSwitch && hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
8778
|
+
warnings.push("Skipping pull \u2014 uncommitted changes detected. Board data may be stale.");
|
|
8779
|
+
} else if (needsBranchSwitch) {
|
|
8780
|
+
const checkout = checkoutBranch(config2.projectRoot, baseBranch);
|
|
8781
|
+
if (!checkout.success) {
|
|
8782
|
+
warnings.push(`Could not switch to ${baseBranch}: ${checkout.message} Board data may be stale.`);
|
|
8783
|
+
} else {
|
|
8784
|
+
previousBranch = currentBranch;
|
|
8785
|
+
if (hasRemote(config2.projectRoot)) {
|
|
8786
|
+
const pull = gitPull(config2.projectRoot);
|
|
8787
|
+
if (!pull.success && config2.abortOnConflict && /conflict/i.test(pull.message)) {
|
|
8788
|
+
checkoutBranch(config2.projectRoot, previousBranch);
|
|
8789
|
+
return { result: void 0, warnings, abort: pull.message };
|
|
8790
|
+
}
|
|
8791
|
+
warnings.push(pull.success ? `Synced from ${baseBranch}.` : `Pull failed: ${pull.message}`);
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
} else if (hasRemote(config2.projectRoot)) {
|
|
8795
|
+
const pull = gitPull(config2.projectRoot);
|
|
8796
|
+
if (!pull.success && config2.abortOnConflict && /conflict/i.test(pull.message)) {
|
|
8797
|
+
return { result: void 0, warnings, abort: pull.message };
|
|
8851
8798
|
}
|
|
8799
|
+
warnings.push(pull.success ? `Synced from ${baseBranch}.` : `Pull failed: ${pull.message}`);
|
|
8852
8800
|
}
|
|
8853
|
-
const
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
const avgEffort = Math.round(
|
|
8857
|
-
recentVelocities.reduce((sum, v) => sum + v.effortPoints, 0) / recentVelocities.length * 10
|
|
8858
|
-
) / 10;
|
|
8859
|
-
lines.push("");
|
|
8860
|
-
lines.push("**Cycle Sizing (effort points \u2014 primary signal)**");
|
|
8861
|
-
lines.push(`- Last ${recentVelocities.length} cycles: ${recentVelocities.map((v) => `S${v.cycle}=${v.effortPoints}pts`).join(", ")}`);
|
|
8862
|
-
lines.push(`- Average: ${avgEffort} effort points/cycle (XS=1, S=2, M=3, L=5, XL=8)`);
|
|
8863
|
-
lines.push(`- Use average as a reference, not a target \u2014 size cycles based on what the selected tasks actually require.`);
|
|
8801
|
+
const result = await fn();
|
|
8802
|
+
if (previousBranch) {
|
|
8803
|
+
checkoutBranch(config2.projectRoot, previousBranch);
|
|
8864
8804
|
}
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8805
|
+
return { result, warnings };
|
|
8806
|
+
}
|
|
8807
|
+
function hasUnpushedCommits(cwd) {
|
|
8808
|
+
try {
|
|
8809
|
+
const output = execFileSync(
|
|
8810
|
+
"git",
|
|
8811
|
+
["rev-list", "@{u}..HEAD", "--count"],
|
|
8812
|
+
{ cwd, encoding: "utf-8" }
|
|
8813
|
+
).trim();
|
|
8814
|
+
return parseInt(output, 10) > 0;
|
|
8815
|
+
} catch {
|
|
8816
|
+
return false;
|
|
8873
8817
|
}
|
|
8874
|
-
return lines.join("\n");
|
|
8875
8818
|
}
|
|
8876
|
-
function
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
const velocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
|
|
8880
|
-
if (velocities.length >= 2) {
|
|
8881
|
-
const recent = velocities.slice(-5);
|
|
8882
|
-
const avg = recent.reduce((sum, v) => sum + v.completed, 0) / recent.length;
|
|
8883
|
-
const latest = recent[recent.length - 1];
|
|
8884
|
-
lines.push("**Cycle History (5-cycle avg)**");
|
|
8885
|
-
lines.push(`- Average: ${avg.toFixed(1)} tasks/cycle`);
|
|
8886
|
-
lines.push(`- Latest (Cycle ${latest.cycle}): ${latest.completed} tasks, ${latest.effortPoints} effort points`);
|
|
8887
|
-
}
|
|
8819
|
+
function ensureLatestDevelop(cwd, baseBranch) {
|
|
8820
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) {
|
|
8821
|
+
return { pulled: false };
|
|
8888
8822
|
}
|
|
8889
|
-
const
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
if (activeTasks.length > 0) {
|
|
8893
|
-
const highPriority = activeTasks.filter(
|
|
8894
|
-
(t) => t.priority === "P1 High" || t.priority === "P0 Critical"
|
|
8895
|
-
).length;
|
|
8896
|
-
const ratio = (highPriority / activeTasks.length * 100).toFixed(0);
|
|
8897
|
-
const health = highPriority <= 3 ? "healthy" : highPriority <= 6 ? "moderate" : "top-heavy";
|
|
8898
|
-
if (lines.length > 0) lines.push("");
|
|
8899
|
-
lines.push("**Backlog Health**");
|
|
8900
|
-
lines.push(`- ${activeTasks.length} items, ${highPriority} high-priority (${ratio}%) \u2014 ${health}`);
|
|
8901
|
-
}
|
|
8902
|
-
if (snapshots.length > 0) {
|
|
8903
|
-
const accuracies = snapshots.flatMap((s) => s.accuracy).sort((a, b2) => a.cycle - b2.cycle);
|
|
8904
|
-
if (accuracies.length >= 3) {
|
|
8905
|
-
const recent = accuracies.slice(-5);
|
|
8906
|
-
const avgBias = recent.reduce((sum, a) => sum + a.bias, 0) / recent.length;
|
|
8907
|
-
const biasDir = avgBias > 0.3 ? "over-estimates" : avgBias < -0.3 ? "under-estimates" : "balanced";
|
|
8908
|
-
const avgMatch = recent.reduce((sum, a) => sum + a.matchRate, 0) / recent.length;
|
|
8909
|
-
if (lines.length > 0) lines.push("");
|
|
8910
|
-
lines.push("**Estimation Trend (5-cycle)**");
|
|
8911
|
-
lines.push(`- Avg match rate: ${avgMatch.toFixed(0)}%`);
|
|
8912
|
-
lines.push(`- Avg bias: ${avgBias >= 0 ? "+" : ""}${avgBias.toFixed(1)} \u2014 ${biasDir}`);
|
|
8913
|
-
}
|
|
8914
|
-
}
|
|
8915
|
-
return lines.join("\n");
|
|
8916
|
-
}
|
|
8917
|
-
function formatBuildPatterns(patterns) {
|
|
8918
|
-
const sections = [];
|
|
8919
|
-
if (patterns.recurringSurprises.length > 0) {
|
|
8920
|
-
const items = patterns.recurringSurprises.map((s) => `- "${s.text}" \u2014 ${s.count} occurrences (cycles ${s.cycles.join(", ")})`).join("\n");
|
|
8921
|
-
sections.push(`**Recurring Surprises**
|
|
8922
|
-
${items}`);
|
|
8923
|
-
}
|
|
8924
|
-
if (patterns.estimationBias !== "none") {
|
|
8925
|
-
const direction = patterns.estimationBias === "under" ? "under-estimated (actual effort exceeded estimates)" : "over-estimated (actual effort was less than estimates)";
|
|
8926
|
-
sections.push(`**Estimation Bias**
|
|
8927
|
-
Tasks are consistently ${direction} \u2014 ${patterns.estimationBiasRate}% of recent builds.`);
|
|
8928
|
-
}
|
|
8929
|
-
const scopeKeys = Object.keys(patterns.scopeAccuracyBreakdown);
|
|
8930
|
-
if (scopeKeys.length > 0) {
|
|
8931
|
-
const items = scopeKeys.map((k) => `- ${k}: ${patterns.scopeAccuracyBreakdown[k]}`).join("\n");
|
|
8932
|
-
sections.push(`**Scope Accuracy**
|
|
8933
|
-
${items}`);
|
|
8934
|
-
}
|
|
8935
|
-
if (patterns.untriagedIssues.length > 0) {
|
|
8936
|
-
const items = patterns.untriagedIssues.map((issue) => `- ${issue}`).join("\n");
|
|
8937
|
-
sections.push(`**Discovered Issues (untriaged)**
|
|
8938
|
-
${items}`);
|
|
8939
|
-
}
|
|
8940
|
-
return sections.join("\n\n");
|
|
8941
|
-
}
|
|
8942
|
-
function formatReviewPatterns(patterns) {
|
|
8943
|
-
const sections = [];
|
|
8944
|
-
if (patterns.recurringFeedback.length > 0) {
|
|
8945
|
-
const items = patterns.recurringFeedback.map((f) => `- "${f.text}" \u2014 ${f.count} occurrences (${f.taskIds.join(", ")})`).join("\n");
|
|
8946
|
-
sections.push(`**Recurring Review Feedback**
|
|
8947
|
-
${items}`);
|
|
8948
|
-
}
|
|
8949
|
-
const verdictKeys = Object.keys(patterns.verdictBreakdown);
|
|
8950
|
-
if (verdictKeys.length > 0) {
|
|
8951
|
-
const items = verdictKeys.map((k) => `- ${k}: ${patterns.verdictBreakdown[k]}`).join("\n");
|
|
8952
|
-
sections.push(`**Verdict Breakdown**
|
|
8953
|
-
${items}`);
|
|
8954
|
-
}
|
|
8955
|
-
if (patterns.requestChangesRate >= 50) {
|
|
8956
|
-
sections.push(`**High Rework Rate**
|
|
8957
|
-
${patterns.requestChangesRate}% of recent reviews requested changes.`);
|
|
8958
|
-
}
|
|
8959
|
-
return sections.join("\n\n");
|
|
8960
|
-
}
|
|
8961
|
-
function formatReviews(reviews) {
|
|
8962
|
-
if (reviews.length === 0) return "";
|
|
8963
|
-
return reviews.map(
|
|
8964
|
-
(r) => `### ${r.taskId} \u2014 ${r.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance"} \u2014 ${r.date}
|
|
8965
|
-
|
|
8966
|
-
- **Verdict:** ${r.verdict}
|
|
8967
|
-
- **Reviewer:** ${r.reviewer}
|
|
8968
|
-
- **Comments:** ${r.comments}`
|
|
8969
|
-
).join("\n\n---\n\n");
|
|
8970
|
-
}
|
|
8971
|
-
function formatTaskComments(comments, taskIds, heading = "## Task Comments") {
|
|
8972
|
-
const relevant = comments.filter((c) => taskIds.has(c.taskId));
|
|
8973
|
-
if (relevant.length === 0) return "";
|
|
8974
|
-
const byTask = /* @__PURE__ */ new Map();
|
|
8975
|
-
for (const c of relevant) {
|
|
8976
|
-
const list = byTask.get(c.taskId) ?? [];
|
|
8977
|
-
list.push(c);
|
|
8978
|
-
byTask.set(c.taskId, list);
|
|
8979
|
-
}
|
|
8980
|
-
const lines = ["", heading];
|
|
8981
|
-
for (const [taskId, taskComments] of byTask) {
|
|
8982
|
-
for (const c of taskComments.slice(0, 3)) {
|
|
8983
|
-
const date = c.createdAt.split("T")[0];
|
|
8984
|
-
const text = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
|
|
8985
|
-
lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${text}"`);
|
|
8986
|
-
}
|
|
8987
|
-
}
|
|
8988
|
-
return lines.join("\n");
|
|
8989
|
-
}
|
|
8990
|
-
function formatDiscoveryCanvas(canvas) {
|
|
8991
|
-
const sections = [];
|
|
8992
|
-
if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
|
|
8993
|
-
sections.push("**Landscape & References:**");
|
|
8994
|
-
for (const ref of canvas.landscapeReferences) {
|
|
8995
|
-
const url = ref.url ? ` (${ref.url})` : "";
|
|
8996
|
-
const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
|
|
8997
|
-
sections.push(`- ${ref.name}${url}${notes}`);
|
|
8998
|
-
}
|
|
8823
|
+
const current = getCurrentBranch(cwd);
|
|
8824
|
+
if (!current || current !== baseBranch) {
|
|
8825
|
+
return { pulled: false, warning: `Skipping pull \u2014 not on ${baseBranch} (on ${current ?? "unknown"}).` };
|
|
8999
8826
|
}
|
|
9000
|
-
if (
|
|
9001
|
-
|
|
9002
|
-
for (const j of canvas.userJourneys) {
|
|
9003
|
-
const priority = j.priority ? ` [${j.priority}]` : "";
|
|
9004
|
-
sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
|
|
9005
|
-
}
|
|
8827
|
+
if (hasUncommittedChanges(cwd, AUTO_WRITTEN_PATHS)) {
|
|
8828
|
+
return { pulled: false, warning: "Skipping pull \u2014 uncommitted changes detected." };
|
|
9006
8829
|
}
|
|
9007
|
-
if (
|
|
9008
|
-
|
|
8830
|
+
if (hasUnpushedCommits(cwd)) {
|
|
8831
|
+
return { pulled: false, warning: "Skipping pull \u2014 unpushed commits on current branch." };
|
|
9009
8832
|
}
|
|
9010
|
-
if (
|
|
9011
|
-
|
|
9012
|
-
for (const a of canvas.assumptionsOpenQuestions) {
|
|
9013
|
-
const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
|
|
9014
|
-
sections.push(`- [${a.status}] ${a.text}${evidence}`);
|
|
9015
|
-
}
|
|
8833
|
+
if (!hasRemote(cwd)) {
|
|
8834
|
+
return { pulled: false };
|
|
9016
8835
|
}
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
|
|
9021
|
-
sections.push(`- ${s.signal}${metric}`);
|
|
9022
|
-
}
|
|
8836
|
+
const result = gitPull(cwd);
|
|
8837
|
+
if (!result.success) {
|
|
8838
|
+
return { pulled: false, warning: `Pull failed \u2014 using local data. ${result.message}` };
|
|
9023
8839
|
}
|
|
9024
|
-
return
|
|
8840
|
+
return { pulled: true };
|
|
9025
8841
|
}
|
|
9026
|
-
|
|
9027
|
-
// src/lib/git.ts
|
|
9028
|
-
import { execFileSync } from "child_process";
|
|
9029
|
-
var AUTO_WRITTEN_PATHS = [".papi/*"];
|
|
9030
|
-
function isGitAvailable() {
|
|
8842
|
+
function getUnmergedBranches(cwd, baseBranch) {
|
|
9031
8843
|
try {
|
|
9032
|
-
|
|
9033
|
-
|
|
8844
|
+
const output = execFileSync(
|
|
8845
|
+
"git",
|
|
8846
|
+
["branch", "--no-merged", baseBranch],
|
|
8847
|
+
{ cwd, encoding: "utf-8" }
|
|
8848
|
+
).trim();
|
|
8849
|
+
if (!output) return [];
|
|
8850
|
+
return output.split("\n").map((b2) => b2.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
9034
8851
|
} catch {
|
|
9035
|
-
return
|
|
8852
|
+
return [];
|
|
9036
8853
|
}
|
|
9037
8854
|
}
|
|
9038
|
-
function
|
|
8855
|
+
function resolveBaseBranch(cwd, preferred) {
|
|
8856
|
+
if (branchExists(cwd, preferred)) return preferred;
|
|
8857
|
+
if (preferred !== "main" && branchExists(cwd, "main")) return "main";
|
|
8858
|
+
if (preferred !== "master" && branchExists(cwd, "master")) return "master";
|
|
8859
|
+
return preferred;
|
|
8860
|
+
}
|
|
8861
|
+
function detectBoardMismatches(cwd, tasks) {
|
|
8862
|
+
const empty = { codeAhead: [], staleInProgress: [] };
|
|
8863
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
|
|
9039
8864
|
try {
|
|
9040
|
-
execFileSync("git", ["
|
|
8865
|
+
const mergedOutput = execFileSync("git", ["branch", "--merged", "HEAD"], {
|
|
9041
8866
|
cwd,
|
|
9042
|
-
|
|
8867
|
+
encoding: "utf-8",
|
|
8868
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
9043
8869
|
});
|
|
9044
|
-
|
|
8870
|
+
const allOutput = execFileSync("git", ["branch"], {
|
|
8871
|
+
cwd,
|
|
8872
|
+
encoding: "utf-8",
|
|
8873
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
8874
|
+
});
|
|
8875
|
+
const parseBranches = (raw) => new Set(raw.split("\n").map((l) => l.trim().replace(/^\* /, "")).filter(Boolean));
|
|
8876
|
+
const mergedBranches = parseBranches(mergedOutput);
|
|
8877
|
+
const allBranches = parseBranches(allOutput);
|
|
8878
|
+
const codeAhead = [];
|
|
8879
|
+
const staleInProgress = [];
|
|
8880
|
+
for (const task of tasks) {
|
|
8881
|
+
const branch = `feat/${task.displayId}`;
|
|
8882
|
+
if (task.status === "Backlog" && mergedBranches.has(branch)) {
|
|
8883
|
+
codeAhead.push({ displayId: task.displayId, title: task.title, branch });
|
|
8884
|
+
}
|
|
8885
|
+
if (task.status === "In Progress" && !allBranches.has(branch)) {
|
|
8886
|
+
staleInProgress.push({ displayId: task.displayId, title: task.title });
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
return { codeAhead, staleInProgress };
|
|
9045
8890
|
} catch {
|
|
9046
|
-
return
|
|
8891
|
+
return empty;
|
|
9047
8892
|
}
|
|
9048
8893
|
}
|
|
9049
|
-
function
|
|
8894
|
+
function detectUnrecordedCommits(cwd, baseBranch) {
|
|
8895
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
8896
|
+
const latestTag = getLatestTag(cwd);
|
|
8897
|
+
if (!latestTag) return [];
|
|
9050
8898
|
try {
|
|
9051
|
-
|
|
9052
|
-
|
|
8899
|
+
const output = execFileSync(
|
|
8900
|
+
"git",
|
|
8901
|
+
["log", `${latestTag}..${baseBranch}`, "--oneline", "--max-count=20"],
|
|
8902
|
+
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
8903
|
+
).trim();
|
|
8904
|
+
if (!output) return [];
|
|
8905
|
+
const CYCLE_PATTERNS = [
|
|
8906
|
+
/feat\(task-\d+\):/,
|
|
8907
|
+
// build_execute commits: feat(task-NNN): title
|
|
8908
|
+
/^[a-f0-9]+ release:/,
|
|
8909
|
+
// release commits
|
|
8910
|
+
/^[a-f0-9]+ Merge /,
|
|
8911
|
+
// merge commits from PRs
|
|
8912
|
+
/chore\(task-/
|
|
8913
|
+
// task-related housekeeping
|
|
8914
|
+
];
|
|
8915
|
+
return output.split("\n").filter((line) => line.trim() && !CYCLE_PATTERNS.some((p) => p.test(line))).map((line) => {
|
|
8916
|
+
const spaceIdx = line.indexOf(" ");
|
|
8917
|
+
return {
|
|
8918
|
+
hash: line.slice(0, spaceIdx),
|
|
8919
|
+
message: line.slice(spaceIdx + 1)
|
|
8920
|
+
};
|
|
8921
|
+
});
|
|
9053
8922
|
} catch {
|
|
8923
|
+
return [];
|
|
9054
8924
|
}
|
|
9055
|
-
execFileSync("git", ["add", dir], { cwd });
|
|
9056
|
-
const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
9057
|
-
cwd,
|
|
9058
|
-
encoding: "utf-8"
|
|
9059
|
-
}).trim();
|
|
9060
|
-
if (!staged) {
|
|
9061
|
-
return { committed: false, message: "No changes to commit." };
|
|
9062
|
-
}
|
|
9063
|
-
execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
|
|
9064
|
-
return { committed: true, message };
|
|
9065
8925
|
}
|
|
9066
|
-
function
|
|
9067
|
-
|
|
9068
|
-
const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
9069
|
-
cwd,
|
|
9070
|
-
encoding: "utf-8"
|
|
9071
|
-
}).trim();
|
|
9072
|
-
if (!staged) {
|
|
9073
|
-
return { committed: false, message: "No changes to commit." };
|
|
9074
|
-
}
|
|
9075
|
-
execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
|
|
9076
|
-
return { committed: true, message };
|
|
8926
|
+
function taskBranchName(taskId) {
|
|
8927
|
+
return `feat/${taskId}`;
|
|
9077
8928
|
}
|
|
9078
|
-
function
|
|
8929
|
+
function getHeadCommitSha(cwd) {
|
|
9079
8930
|
try {
|
|
9080
|
-
return execFileSync("git", ["rev-parse", "
|
|
9081
|
-
cwd,
|
|
9082
|
-
encoding: "utf-8"
|
|
9083
|
-
}).trim();
|
|
8931
|
+
return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
|
|
9084
8932
|
} catch {
|
|
9085
8933
|
return null;
|
|
9086
8934
|
}
|
|
9087
8935
|
}
|
|
9088
|
-
function
|
|
8936
|
+
function runAutoCommit(projectRoot, commitFn) {
|
|
8937
|
+
if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
|
|
8938
|
+
if (!isGitRepo(projectRoot)) return "Auto-commit: skipped (not a git repository).";
|
|
9089
8939
|
try {
|
|
9090
|
-
const
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
}
|
|
9094
|
-
const status = execFileSync("git", args, {
|
|
9095
|
-
cwd,
|
|
9096
|
-
encoding: "utf-8"
|
|
9097
|
-
}).trim();
|
|
9098
|
-
return status.length > 0;
|
|
9099
|
-
} catch {
|
|
9100
|
-
return false;
|
|
8940
|
+
const result = commitFn();
|
|
8941
|
+
return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
|
|
8942
|
+
} catch (err) {
|
|
8943
|
+
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9101
8944
|
}
|
|
9102
8945
|
}
|
|
9103
|
-
function
|
|
8946
|
+
function getFilesChangedFromBase(cwd, baseBranch) {
|
|
9104
8947
|
try {
|
|
9105
|
-
execFileSync("git", ["
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
});
|
|
9109
|
-
return true;
|
|
8948
|
+
const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
|
|
8949
|
+
const output = execFileSync("git", ["diff", "--name-only", mergeBase, "HEAD"], { cwd, encoding: "utf-8" }).trim();
|
|
8950
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
9110
8951
|
} catch {
|
|
9111
|
-
return
|
|
8952
|
+
return [];
|
|
9112
8953
|
}
|
|
9113
8954
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
8955
|
+
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES;
|
|
8956
|
+
var init_git = __esm({
|
|
8957
|
+
"src/lib/git.ts"() {
|
|
8958
|
+
"use strict";
|
|
8959
|
+
AUTO_WRITTEN_PATHS = [".papi/*"];
|
|
8960
|
+
GIT_NETWORK_TIMEOUT_MS = 6e4;
|
|
8961
|
+
MERGE_RETRY_DELAY_MS = 2e3;
|
|
8962
|
+
MERGE_MAX_RETRIES = 3;
|
|
8963
|
+
}
|
|
8964
|
+
});
|
|
8965
|
+
|
|
8966
|
+
// src/index.ts
|
|
8967
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
8968
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
8969
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8970
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8971
|
+
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8972
|
+
import {
|
|
8973
|
+
CallToolRequestSchema as CallToolRequestSchema2,
|
|
8974
|
+
ListToolsRequestSchema as ListToolsRequestSchema2
|
|
8975
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8976
|
+
|
|
8977
|
+
// src/config.ts
|
|
8978
|
+
import path from "path";
|
|
8979
|
+
function loadConfig() {
|
|
8980
|
+
const projectArgIdx = process.argv.indexOf("--project");
|
|
8981
|
+
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
8982
|
+
const projectRoot = configuredRoot ?? process.cwd();
|
|
8983
|
+
if (!configuredRoot) {
|
|
8984
|
+
process.stderr.write(
|
|
8985
|
+
'\nPAPI is running but no project is configured.\nSay "run setup" to get started.\n\n'
|
|
8986
|
+
);
|
|
9120
8987
|
}
|
|
8988
|
+
const anthropicApiKey = process.env.PAPI_API_KEY ?? "";
|
|
8989
|
+
const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
|
|
8990
|
+
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
8991
|
+
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
8992
|
+
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
8993
|
+
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
8994
|
+
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
8995
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
8996
|
+
const explicitAdapter = process.env.PAPI_ADAPTER;
|
|
8997
|
+
const adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
|
|
8998
|
+
return {
|
|
8999
|
+
projectRoot,
|
|
9000
|
+
papiDir: path.join(projectRoot, ".papi"),
|
|
9001
|
+
anthropicApiKey,
|
|
9002
|
+
autoCommit: autoCommit2,
|
|
9003
|
+
baseBranch,
|
|
9004
|
+
autoPR,
|
|
9005
|
+
adapterType,
|
|
9006
|
+
papiEndpoint,
|
|
9007
|
+
lightMode
|
|
9008
|
+
};
|
|
9121
9009
|
}
|
|
9122
|
-
|
|
9010
|
+
|
|
9011
|
+
// src/adapter-factory.ts
|
|
9012
|
+
init_dist2();
|
|
9013
|
+
import path2 from "path";
|
|
9014
|
+
import { execSync } from "child_process";
|
|
9015
|
+
function detectUserId() {
|
|
9123
9016
|
try {
|
|
9124
|
-
|
|
9125
|
-
|
|
9017
|
+
const email = execSync("git config user.email", { encoding: "utf8", timeout: 5e3 }).trim();
|
|
9018
|
+
if (email) return email;
|
|
9126
9019
|
} catch {
|
|
9127
|
-
return { success: false, message: `Failed to create branch '${branch}'.` };
|
|
9128
9020
|
}
|
|
9129
|
-
}
|
|
9130
|
-
function hasRemote(cwd, remote = "origin") {
|
|
9131
9021
|
try {
|
|
9132
|
-
const
|
|
9133
|
-
|
|
9134
|
-
encoding: "utf-8"
|
|
9135
|
-
}).trim();
|
|
9136
|
-
return remotes.split("\n").includes(remote);
|
|
9022
|
+
const ghUser = execSync("gh api user --jq .email", { encoding: "utf8", timeout: 1e4 }).trim();
|
|
9023
|
+
if (ghUser && ghUser !== "null") return ghUser;
|
|
9137
9024
|
} catch {
|
|
9138
|
-
return false;
|
|
9139
9025
|
}
|
|
9026
|
+
return void 0;
|
|
9140
9027
|
}
|
|
9141
|
-
var
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9028
|
+
var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
|
|
9029
|
+
var PLACEHOLDER_PATTERNS = [
|
|
9030
|
+
"<YOUR_DATABASE_URL>",
|
|
9031
|
+
"your-database-url",
|
|
9032
|
+
"your_database_url",
|
|
9033
|
+
"placeholder",
|
|
9034
|
+
"example.com",
|
|
9035
|
+
"localhost:5432/dbname",
|
|
9036
|
+
"user:password@host"
|
|
9037
|
+
];
|
|
9038
|
+
function validateDatabaseUrl(connectionString) {
|
|
9039
|
+
const lower = connectionString.toLowerCase().trim();
|
|
9040
|
+
if (PLACEHOLDER_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
|
|
9041
|
+
throw new Error(
|
|
9042
|
+
"DATABASE_URL contains a placeholder value and is not configured.\nReplace it with your actual Supabase connection string in .mcp.json.\nIf you don't have one yet, contact the PAPI admin for access."
|
|
9043
|
+
);
|
|
9044
|
+
}
|
|
9045
|
+
if (!lower.startsWith("postgres://") && !lower.startsWith("postgresql://")) {
|
|
9046
|
+
throw new Error(
|
|
9047
|
+
`DATABASE_URL must be a PostgreSQL connection string (postgres:// or postgresql://).
|
|
9048
|
+
Got: "${connectionString.slice(0, 30)}..."
|
|
9049
|
+
Check your .mcp.json configuration.`
|
|
9050
|
+
);
|
|
9153
9051
|
}
|
|
9154
9052
|
}
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9053
|
+
var _connectionStatus = "offline";
|
|
9054
|
+
function getConnectionStatus() {
|
|
9055
|
+
return _connectionStatus;
|
|
9056
|
+
}
|
|
9057
|
+
async function createAdapter(optionsOrType, maybePapiDir) {
|
|
9058
|
+
const options = typeof optionsOrType === "string" ? { adapterType: optionsOrType, papiDir: maybePapiDir } : optionsOrType;
|
|
9059
|
+
const { adapterType, papiDir, papiEndpoint } = options;
|
|
9060
|
+
switch (adapterType) {
|
|
9061
|
+
case "md":
|
|
9062
|
+
_connectionStatus = "offline";
|
|
9063
|
+
return new MdFileAdapter(papiDir);
|
|
9064
|
+
case "pg": {
|
|
9065
|
+
const { PgAdapter: PgAdapter2, PgPapiAdapter: PgPapiAdapter2, configFromEnv: configFromEnv2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
9066
|
+
const projectId = process.env["PAPI_PROJECT_ID"];
|
|
9067
|
+
if (!projectId) {
|
|
9068
|
+
throw new Error(
|
|
9069
|
+
"PAPI_PROJECT_ID is required when using PostgreSQL storage. Set it to the UUID of your project in the database."
|
|
9070
|
+
);
|
|
9071
|
+
}
|
|
9072
|
+
const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv2();
|
|
9073
|
+
validateDatabaseUrl(config2.connectionString);
|
|
9074
|
+
try {
|
|
9075
|
+
const { ensureSchema: ensureSchema2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
9076
|
+
await ensureSchema2(config2);
|
|
9077
|
+
} catch (err) {
|
|
9078
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9079
|
+
console.error(`[papi] \u2717 Schema creation failed: ${msg}`);
|
|
9080
|
+
console.error("[papi] Check DATABASE_URL and Supabase access.");
|
|
9081
|
+
}
|
|
9082
|
+
try {
|
|
9083
|
+
const pgAdapter = new PgAdapter2(config2);
|
|
9084
|
+
const existing = await pgAdapter.getProject(projectId);
|
|
9085
|
+
if (!existing) {
|
|
9086
|
+
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? "";
|
|
9087
|
+
const slug = path2.basename(projectRoot) || "unnamed";
|
|
9088
|
+
let userId = process.env["PAPI_USER_ID"] ?? void 0;
|
|
9089
|
+
if (!userId) {
|
|
9090
|
+
userId = detectUserId();
|
|
9091
|
+
if (userId) {
|
|
9092
|
+
console.error(`[papi] Auto-detected user identity: ${userId}`);
|
|
9093
|
+
console.error("[papi] Set PAPI_USER_ID in .mcp.json to make this explicit.");
|
|
9094
|
+
} else {
|
|
9095
|
+
console.error("[papi] \u26A0 No PAPI_USER_ID set and auto-detection failed.");
|
|
9096
|
+
console.error("[papi] Project will have no user scope \u2014 it may be visible to all dashboard users.");
|
|
9097
|
+
console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
|
|
9101
|
+
}
|
|
9102
|
+
await pgAdapter.close();
|
|
9103
|
+
} catch {
|
|
9104
|
+
}
|
|
9105
|
+
const adapter2 = new PgPapiAdapter2(config2, projectId);
|
|
9106
|
+
try {
|
|
9107
|
+
await adapter2.initRls();
|
|
9108
|
+
} catch {
|
|
9109
|
+
}
|
|
9110
|
+
const connected = await adapter2.probeConnection();
|
|
9111
|
+
if (connected) {
|
|
9112
|
+
_connectionStatus = "connected";
|
|
9113
|
+
console.error("[papi] \u2713 Supabase connected");
|
|
9114
|
+
} else {
|
|
9115
|
+
_connectionStatus = "degraded";
|
|
9116
|
+
console.error("[papi] \u2717 Supabase unreachable \u2014 running in degraded mode, data may be stale");
|
|
9117
|
+
console.error("[papi] Check your DATABASE_URL in .mcp.json \u2014 is the connection string correct?");
|
|
9118
|
+
}
|
|
9119
|
+
return adapter2;
|
|
9120
|
+
}
|
|
9121
|
+
case "proxy": {
|
|
9122
|
+
const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
|
|
9123
|
+
const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://getpapi.ai";
|
|
9124
|
+
const projectId = process.env["PAPI_PROJECT_ID"];
|
|
9125
|
+
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
9126
|
+
if (!dataApiKey) {
|
|
9127
|
+
throw new Error(
|
|
9128
|
+
`PAPI needs an account to store your project data.
|
|
9129
|
+
|
|
9130
|
+
Get started in 3 steps:
|
|
9131
|
+
1. Sign up at ${dashboardUrl}/login
|
|
9132
|
+
2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
|
|
9133
|
+
3. Download the config, place it in your project root, and restart Claude Code
|
|
9134
|
+
|
|
9135
|
+
Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json env config.`
|
|
9136
|
+
);
|
|
9137
|
+
}
|
|
9138
|
+
const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
|
|
9139
|
+
const adapter2 = new ProxyPapiAdapter2({
|
|
9140
|
+
endpoint: dataEndpoint,
|
|
9141
|
+
apiKey: dataApiKey,
|
|
9142
|
+
projectId: projectId || void 0
|
|
9143
|
+
});
|
|
9144
|
+
const connected = await adapter2.probeConnection();
|
|
9145
|
+
if (!connected) {
|
|
9146
|
+
_connectionStatus = "degraded";
|
|
9147
|
+
console.error("[papi] \u2717 Data proxy unreachable \u2014 running in degraded mode");
|
|
9148
|
+
console.error("[papi] Check your PAPI_DATA_ENDPOINT configuration.");
|
|
9149
|
+
} else if (projectId) {
|
|
9150
|
+
const auth = await adapter2.probeAuth(projectId);
|
|
9151
|
+
if (!auth.ok) {
|
|
9152
|
+
if (auth.status === 401) {
|
|
9153
|
+
throw new Error(
|
|
9154
|
+
"PAPI_DATA_API_KEY is invalid \u2014 authentication failed.\nCheck the key in your .mcp.json config. You can generate a new key from the PAPI dashboard."
|
|
9155
|
+
);
|
|
9156
|
+
} else if (auth.status === 403 || auth.status === 404) {
|
|
9157
|
+
throw new Error(
|
|
9158
|
+
`PAPI_PROJECT_ID "${projectId}" was not found or you don't have access.
|
|
9159
|
+
Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI dashboard settings.`
|
|
9160
|
+
);
|
|
9161
|
+
} else if (auth.status !== 0) {
|
|
9162
|
+
_connectionStatus = "degraded";
|
|
9163
|
+
console.error(`[papi] \u26A0 Auth check returned ${auth.status} \u2014 running in degraded mode`);
|
|
9164
|
+
}
|
|
9165
|
+
} else {
|
|
9166
|
+
_connectionStatus = "connected";
|
|
9167
|
+
console.error("[papi] \u2713 Data proxy connected");
|
|
9168
|
+
}
|
|
9169
|
+
} else {
|
|
9170
|
+
_connectionStatus = "connected";
|
|
9171
|
+
console.error("[papi] \u2713 Data proxy reachable \u2014 project will be auto-provisioned");
|
|
9172
|
+
}
|
|
9173
|
+
if (!projectId && connected) {
|
|
9174
|
+
try {
|
|
9175
|
+
const { getOriginRepoSlug: getOriginRepoSlug2 } = await Promise.resolve().then(() => (init_git(), git_exports));
|
|
9176
|
+
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
|
|
9177
|
+
const repoSlug = getOriginRepoSlug2(projectRoot);
|
|
9178
|
+
const projectName = repoSlug ? repoSlug.split("/").pop() || path2.basename(projectRoot) || "My Project" : path2.basename(projectRoot) || "My Project";
|
|
9179
|
+
let repoUrl;
|
|
9180
|
+
try {
|
|
9181
|
+
repoUrl = execSync("git remote get-url origin", { cwd: projectRoot, encoding: "utf8", timeout: 3e3 }).trim() || void 0;
|
|
9182
|
+
} catch {
|
|
9183
|
+
}
|
|
9184
|
+
const result = await adapter2.ensureProject(projectName, repoUrl);
|
|
9185
|
+
if (result.created) {
|
|
9186
|
+
console.error(`[papi] \u2713 Project "${result.projectName}" auto-provisioned (${result.projectId})`);
|
|
9187
|
+
console.error("[papi] Tip: add PAPI_PROJECT_ID to .mcp.json to skip this check on future starts.");
|
|
9188
|
+
} else {
|
|
9189
|
+
console.error(`[papi] \u2713 Using project "${result.projectName}" (${result.projectId})`);
|
|
9190
|
+
}
|
|
9191
|
+
} catch (err) {
|
|
9192
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9193
|
+
console.error(`[papi] \u26A0 Auto-provision failed: ${msg}`);
|
|
9194
|
+
console.error("[papi] Set PAPI_PROJECT_ID in .mcp.json to connect to an existing project.");
|
|
9195
|
+
}
|
|
9196
|
+
}
|
|
9197
|
+
return adapter2;
|
|
9198
|
+
}
|
|
9199
|
+
default: {
|
|
9200
|
+
const _exhaustive = adapterType;
|
|
9201
|
+
throw new Error(
|
|
9202
|
+
`Unknown PAPI_ADAPTER value: "${_exhaustive}". Valid options: "md", "pg", "proxy".`
|
|
9203
|
+
);
|
|
9204
|
+
}
|
|
9170
9205
|
}
|
|
9171
9206
|
}
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9207
|
+
|
|
9208
|
+
// src/server.ts
|
|
9209
|
+
import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
9210
|
+
import { join as join10, dirname } from "path";
|
|
9211
|
+
import { fileURLToPath } from "url";
|
|
9212
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9213
|
+
import {
|
|
9214
|
+
CallToolRequestSchema,
|
|
9215
|
+
ListToolsRequestSchema,
|
|
9216
|
+
ListPromptsRequestSchema,
|
|
9217
|
+
GetPromptRequestSchema
|
|
9218
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9219
|
+
|
|
9220
|
+
// src/lib/response.ts
|
|
9221
|
+
function textResponse(text, usage) {
|
|
9222
|
+
const result = {
|
|
9223
|
+
content: [{ type: "text", text }]
|
|
9224
|
+
};
|
|
9225
|
+
if (usage) {
|
|
9226
|
+
result._usage = usage;
|
|
9178
9227
|
}
|
|
9228
|
+
return result;
|
|
9179
9229
|
}
|
|
9180
|
-
function
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9230
|
+
function errorResponse(message) {
|
|
9231
|
+
return { content: [{ type: "text", text: `Error: ${message}` }] };
|
|
9232
|
+
}
|
|
9233
|
+
|
|
9234
|
+
// src/services/plan.ts
|
|
9235
|
+
init_dist2();
|
|
9236
|
+
import { createHash, randomUUID as randomUUID7 } from "crypto";
|
|
9237
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
9238
|
+
import path3 from "path";
|
|
9239
|
+
|
|
9240
|
+
// src/lib/formatters.ts
|
|
9241
|
+
function formatActiveDecisionsForPlan(decisions) {
|
|
9242
|
+
if (decisions.length === 0) return "No active decisions.";
|
|
9243
|
+
return decisions.filter((d) => !d.superseded).map((d) => `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
|
|
9244
|
+
|
|
9245
|
+
${d.body}`).join("\n\n");
|
|
9246
|
+
}
|
|
9247
|
+
function formatActiveDecisionsForReview(decisions) {
|
|
9248
|
+
if (decisions.length === 0) return "No active decisions.";
|
|
9249
|
+
return decisions.map((d) => {
|
|
9250
|
+
const lifecycle = d.outcome && d.outcome !== "pending" ? ` | Outcome: ${d.outcome}` + (d.revisionCount ? ` | ${d.revisionCount} revision(s)` : "") : "";
|
|
9251
|
+
const summary = extractDecisionSummary(d.body);
|
|
9252
|
+
const summaryLine = summary ? `
|
|
9253
|
+
Summary: ${summary}` : "";
|
|
9254
|
+
if (d.superseded) {
|
|
9255
|
+
return `- ${d.id}: ${d.title} [SUPERSEDED by ${d.supersededBy}${lifecycle}]${summaryLine}`;
|
|
9256
|
+
}
|
|
9257
|
+
return `- ${d.id}: ${d.title} [${d.confidence}${lifecycle}]${summaryLine}`;
|
|
9258
|
+
}).join("\n");
|
|
9259
|
+
}
|
|
9260
|
+
function extractDecisionSummary(body) {
|
|
9261
|
+
if (!body) return "";
|
|
9262
|
+
const lines = body.split("\n");
|
|
9263
|
+
for (const line of lines) {
|
|
9264
|
+
const trimmed = line.trim();
|
|
9265
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
|
|
9266
|
+
const clean = trimmed.replace(/\*\*/g, "").replace(/\*/g, "");
|
|
9267
|
+
return clean.length > 150 ? clean.slice(0, 147) + "..." : clean;
|
|
9268
|
+
}
|
|
9269
|
+
return "";
|
|
9270
|
+
}
|
|
9271
|
+
function formatDecisionLifecycleSummary(decisions) {
|
|
9272
|
+
const active = decisions.filter((d) => !d.superseded);
|
|
9273
|
+
if (active.length === 0) return void 0;
|
|
9274
|
+
const counts = {};
|
|
9275
|
+
for (const d of active) {
|
|
9276
|
+
const outcome = d.outcome ?? "pending";
|
|
9277
|
+
counts[outcome] = (counts[outcome] ?? 0) + 1;
|
|
9278
|
+
}
|
|
9279
|
+
const parts = Object.entries(counts).map(([k, v]) => `${v} ${k}`);
|
|
9280
|
+
const revised = active.filter((d) => (d.revisionCount ?? 0) > 0);
|
|
9281
|
+
if (revised.length > 0) {
|
|
9282
|
+
parts.push(`${revised.length} with revisions`);
|
|
9283
|
+
}
|
|
9284
|
+
return parts.join(", ");
|
|
9285
|
+
}
|
|
9286
|
+
function formatBuildReports(reports) {
|
|
9287
|
+
if (reports.length === 0) return "No build reports yet.";
|
|
9288
|
+
return reports.map(
|
|
9289
|
+
(r) => `### ${r.taskName} \u2014 ${r.date} \u2014 Cycle ${r.cycle}
|
|
9290
|
+
|
|
9291
|
+
- **Completed:** ${r.completed}
|
|
9292
|
+
- **Actual Effort:** ${r.actualEffort} vs estimated ${r.estimatedEffort}
|
|
9293
|
+
` + (r.correctionsCount ? `- **Corrections:** ${r.correctionsCount}
|
|
9294
|
+
` : "") + (Array.isArray(r.briefImplications) && r.briefImplications.length ? `- **Brief Implications:** ${r.briefImplications.map((bi) => `[${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("; ")}
|
|
9295
|
+
` : "") + `- **Surprises:** ${r.surprises}
|
|
9296
|
+
- **Discovered Issues:** ${r.discoveredIssues}
|
|
9297
|
+
- **Architecture Notes:** ${r.architectureNotes}` + (r.deadEnds ? `
|
|
9298
|
+
- **Dead Ends:** ${r.deadEnds}` : "")
|
|
9299
|
+
).join("\n\n---\n\n");
|
|
9300
|
+
}
|
|
9301
|
+
function formatRecentlyShippedCapabilities(reports) {
|
|
9302
|
+
const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
|
|
9303
|
+
if (completed.length === 0) return void 0;
|
|
9304
|
+
const lines = completed.map((r) => {
|
|
9305
|
+
const parts = [`- **${r.taskId}:** ${r.taskName}`];
|
|
9306
|
+
if (r.architectureNotes && r.architectureNotes !== "None") {
|
|
9307
|
+
const trimmed = r.architectureNotes.length > 150 ? r.architectureNotes.slice(0, 150) + "..." : r.architectureNotes;
|
|
9308
|
+
parts.push(` _Delivered:_ ${trimmed}`);
|
|
9309
|
+
}
|
|
9310
|
+
if (r.filesChanged && r.filesChanged.length > 0) {
|
|
9311
|
+
parts.push(` _Files:_ ${r.filesChanged.slice(0, 5).join(", ")}${r.filesChanged.length > 5 ? ` (+${r.filesChanged.length - 5} more)` : ""}`);
|
|
9312
|
+
}
|
|
9313
|
+
return parts.join("\n");
|
|
9314
|
+
});
|
|
9315
|
+
return [
|
|
9316
|
+
`${completed.length} task(s) completed in recent cycles:`,
|
|
9317
|
+
"",
|
|
9318
|
+
...lines,
|
|
9319
|
+
"",
|
|
9320
|
+
"Cross-reference candidate tasks against this list. If >80% of a candidate task's scope appears here, recommend cancellation or scope reduction instead of scheduling."
|
|
9321
|
+
].join("\n");
|
|
9322
|
+
}
|
|
9323
|
+
function formatCycleLog(entries) {
|
|
9324
|
+
if (entries.length === 0) return "No cycle log entries yet.";
|
|
9325
|
+
return entries.map(
|
|
9326
|
+
(e) => `### Cycle ${e.cycleNumber} \u2014 ${e.title}
|
|
9327
|
+
|
|
9328
|
+
${e.content}` + (e.carryForward ? `
|
|
9329
|
+
|
|
9330
|
+
**Carry Forward:** ${e.carryForward}` : "") + (e.notes ? `
|
|
9331
|
+
|
|
9332
|
+
**Cycle Notes:** ${e.notes}` : "")
|
|
9333
|
+
).join("\n\n---\n\n");
|
|
9334
|
+
}
|
|
9335
|
+
var PLAN_EXCLUDED_STATUSES = /* @__PURE__ */ new Set(["Done", "Cancelled", "Archived", "Deferred"]);
|
|
9336
|
+
var PLAN_NOTES_MAX_LENGTH = 300;
|
|
9337
|
+
function truncateNotes(notes, maxLen) {
|
|
9338
|
+
if (!notes) return "";
|
|
9339
|
+
if (notes.length <= maxLen) return notes;
|
|
9340
|
+
return notes.slice(0, maxLen) + "...";
|
|
9341
|
+
}
|
|
9342
|
+
var ACTIVE_WORK_STATUSES = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
|
|
9343
|
+
function isRecentTask(task, currentCycle) {
|
|
9344
|
+
if (!task.reviewed) return true;
|
|
9345
|
+
if (ACTIVE_WORK_STATUSES.has(task.status)) return true;
|
|
9346
|
+
if (task.createdCycle === void 0) return true;
|
|
9347
|
+
if (task.createdCycle >= currentCycle - 1) return true;
|
|
9348
|
+
if (task.cycle !== void 0 && task.cycle >= currentCycle) return true;
|
|
9349
|
+
return false;
|
|
9350
|
+
}
|
|
9351
|
+
function formatCompactTask(task) {
|
|
9352
|
+
const handoffTag = task.hasHandoff ? " | \u2713 handoff" : "";
|
|
9353
|
+
const typeTag = task.taskType && task.taskType !== "task" ? ` | ${task.taskType}` : "";
|
|
9354
|
+
return `- ${task.id}: ${task.title} [${task.status} | ${task.priority} | ${task.complexity}${typeTag}${handoffTag}]`;
|
|
9355
|
+
}
|
|
9356
|
+
function formatDetailedTask(t) {
|
|
9357
|
+
const notes = truncateNotes(t.notes, PLAN_NOTES_MAX_LENGTH);
|
|
9358
|
+
const hasHandoff = t.hasHandoff;
|
|
9359
|
+
const typeTag = t.taskType && t.taskType !== "task" ? ` | Type: ${t.taskType}` : "";
|
|
9360
|
+
return `- **${t.id}:** ${t.title}
|
|
9361
|
+
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
|
|
9362
|
+
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
9363
|
+
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
|
|
9364
|
+
Notes: ${notes}` : ""}`;
|
|
9365
|
+
}
|
|
9366
|
+
function formatBoardForPlan(tasks, filters, currentCycle) {
|
|
9367
|
+
if (tasks.length === 0) return "No tasks on the board.";
|
|
9368
|
+
let active = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status));
|
|
9369
|
+
const excludedCount = tasks.length - active.length;
|
|
9370
|
+
if (filters) {
|
|
9371
|
+
if (filters.phase) active = active.filter((t) => t.phase === filters.phase);
|
|
9372
|
+
if (filters.module) active = active.filter((t) => t.module === filters.module);
|
|
9373
|
+
if (filters.epic) active = active.filter((t) => t.epic === filters.epic);
|
|
9374
|
+
if (filters.priority) active = active.filter((t) => t.priority === filters.priority);
|
|
9375
|
+
}
|
|
9376
|
+
const userFilteredCount = tasks.length - excludedCount - active.length;
|
|
9377
|
+
const filterParts = [];
|
|
9378
|
+
if (excludedCount > 0) filterParts.push(`${excludedCount} completed filtered`);
|
|
9379
|
+
if (userFilteredCount > 0) filterParts.push(`${userFilteredCount} excluded by filters`);
|
|
9380
|
+
const filterSuffix = filterParts.length > 0 ? filterParts.join(", ") : "";
|
|
9381
|
+
if (active.length === 0) {
|
|
9382
|
+
return `No active tasks on the board (${filterSuffix || "all filtered"}).`;
|
|
9383
|
+
}
|
|
9384
|
+
const byCounts = (statuses) => statuses.map((s) => {
|
|
9385
|
+
const n = active.filter((t) => t.status === s).length;
|
|
9386
|
+
return n > 0 ? `${n} ${s.toLowerCase()}` : null;
|
|
9387
|
+
}).filter(Boolean).join(", ");
|
|
9388
|
+
const summary = `Board: ${active.length} active tasks (${byCounts(["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"])})` + (filterSuffix ? ` \u2014 ${filterSuffix}` : "");
|
|
9389
|
+
if (currentCycle === void 0) {
|
|
9390
|
+
const formatted = active.map(formatDetailedTask).join("\n\n");
|
|
9391
|
+
return `${summary}
|
|
9392
|
+
|
|
9393
|
+
${formatted}`;
|
|
9394
|
+
}
|
|
9395
|
+
const recent = [];
|
|
9396
|
+
const stable = [];
|
|
9397
|
+
for (const t of active) {
|
|
9398
|
+
if (isRecentTask(t, currentCycle)) {
|
|
9399
|
+
recent.push(t);
|
|
9400
|
+
} else {
|
|
9401
|
+
stable.push(t);
|
|
9402
|
+
}
|
|
9403
|
+
}
|
|
9404
|
+
const sections = [summary];
|
|
9405
|
+
if (recent.length > 0) {
|
|
9406
|
+
sections.push(recent.map(formatDetailedTask).join("\n\n"));
|
|
9407
|
+
}
|
|
9408
|
+
if (stable.length > 0) {
|
|
9409
|
+
sections.push(
|
|
9410
|
+
`**Stable backlog (${stable.length} tasks \u2014 compact):**
|
|
9411
|
+
` + stable.map(formatCompactTask).join("\n")
|
|
9412
|
+
);
|
|
9193
9413
|
}
|
|
9414
|
+
return sections.join("\n\n");
|
|
9194
9415
|
}
|
|
9195
|
-
function
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
} catch (err) {
|
|
9205
|
-
return {
|
|
9206
|
-
success: false,
|
|
9207
|
-
message: `PR creation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
9208
|
-
};
|
|
9209
|
-
}
|
|
9416
|
+
function formatBoardForReview(tasks) {
|
|
9417
|
+
if (tasks.length === 0) return "No tasks on the board.";
|
|
9418
|
+
return tasks.map(
|
|
9419
|
+
(t) => `- **${t.id}:** ${t.title}
|
|
9420
|
+
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}
|
|
9421
|
+
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
9422
|
+
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${t.notes ? `
|
|
9423
|
+
Notes: ${t.notes}` : ""}`
|
|
9424
|
+
).join("\n\n");
|
|
9210
9425
|
}
|
|
9211
|
-
function
|
|
9212
|
-
|
|
9426
|
+
function trendArrow(current, previous, higherIsBetter) {
|
|
9427
|
+
if (previous === void 0) return "";
|
|
9428
|
+
if (current === previous) return " \u2192";
|
|
9429
|
+
const improving = higherIsBetter ? current > previous : current < previous;
|
|
9430
|
+
return improving ? " \u2191" : " \u2193";
|
|
9213
9431
|
}
|
|
9214
|
-
var
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
const
|
|
9218
|
-
const
|
|
9219
|
-
|
|
9220
|
-
|
|
9432
|
+
var EFFORT_MAP2 = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
9433
|
+
function computeSnapshotsFromBuildReports(reports) {
|
|
9434
|
+
if (reports.length === 0) return [];
|
|
9435
|
+
const byCycleMap = /* @__PURE__ */ new Map();
|
|
9436
|
+
for (const r of reports) {
|
|
9437
|
+
const existing = byCycleMap.get(r.cycle) ?? [];
|
|
9438
|
+
existing.push(r);
|
|
9439
|
+
byCycleMap.set(r.cycle, existing);
|
|
9221
9440
|
}
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
}
|
|
9233
|
-
return { success: false, message: `PR merge failed: ${msg}` };
|
|
9441
|
+
const snapshots = [];
|
|
9442
|
+
for (const [sn, cycleReports] of byCycleMap) {
|
|
9443
|
+
const completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
9444
|
+
const total = cycleReports.length;
|
|
9445
|
+
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
9446
|
+
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
9447
|
+
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
9448
|
+
let effortPoints = 0;
|
|
9449
|
+
for (const r of cycleReports) {
|
|
9450
|
+
effortPoints += EFFORT_MAP2[r.actualEffort] ?? 3;
|
|
9234
9451
|
}
|
|
9452
|
+
snapshots.push({
|
|
9453
|
+
cycle: sn,
|
|
9454
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9455
|
+
accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
|
|
9456
|
+
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
9457
|
+
});
|
|
9235
9458
|
}
|
|
9236
|
-
|
|
9459
|
+
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
9460
|
+
return snapshots;
|
|
9237
9461
|
}
|
|
9238
|
-
function
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
};
|
|
9462
|
+
function formatCycleMetrics(snapshots) {
|
|
9463
|
+
if (snapshots.length === 0) return "No methodology metrics yet.";
|
|
9464
|
+
const latest = snapshots[snapshots.length - 1];
|
|
9465
|
+
const previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] : void 0;
|
|
9466
|
+
const lines = [];
|
|
9467
|
+
if (latest.velocity.length > 0) {
|
|
9468
|
+
const latestV = latest.velocity[latest.velocity.length - 1];
|
|
9469
|
+
lines.push("**Cycle Sizing**");
|
|
9470
|
+
lines.push(`- Last cycle: ${latestV.completed} tasks, ${latestV.effortPoints} effort points`);
|
|
9471
|
+
if (latestV.partial > 0 || latestV.failed > 0) {
|
|
9472
|
+
lines.push(`- Partial: ${latestV.partial} | Failed: ${latestV.failed}`);
|
|
9473
|
+
}
|
|
9247
9474
|
}
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9475
|
+
const allVelocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
|
|
9476
|
+
const recentVelocities = allVelocities.slice(-5);
|
|
9477
|
+
if (recentVelocities.length > 0) {
|
|
9478
|
+
const avgEffort = Math.round(
|
|
9479
|
+
recentVelocities.reduce((sum, v) => sum + v.effortPoints, 0) / recentVelocities.length * 10
|
|
9480
|
+
) / 10;
|
|
9481
|
+
lines.push("");
|
|
9482
|
+
lines.push("**Cycle Sizing (effort points \u2014 primary signal)**");
|
|
9483
|
+
lines.push(`- Last ${recentVelocities.length} cycles: ${recentVelocities.map((v) => `S${v.cycle}=${v.effortPoints}pts`).join(", ")}`);
|
|
9484
|
+
lines.push(`- Average: ${avgEffort} effort points/cycle (XS=1, S=2, M=3, L=5, XL=8)`);
|
|
9485
|
+
lines.push(`- Use average as a reference, not a target \u2014 size cycles based on what the selected tasks actually require.`);
|
|
9258
9486
|
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
message: `Failed to create tag: ${err instanceof Error ? err.message : String(err)}`
|
|
9268
|
-
};
|
|
9487
|
+
if (latest.accuracy.length > 0) {
|
|
9488
|
+
const latestA = latest.accuracy[latest.accuracy.length - 1];
|
|
9489
|
+
const prevA = previous?.accuracy[previous.accuracy.length - 1];
|
|
9490
|
+
lines.push("");
|
|
9491
|
+
lines.push("**Estimation Accuracy**");
|
|
9492
|
+
lines.push(`- Match rate: ${latestA.matchRate}%${trendArrow(latestA.matchRate, prevA?.matchRate, true)}`);
|
|
9493
|
+
lines.push(`- MAE: ${latestA.mae}${trendArrow(latestA.mae, prevA?.mae, false)}`);
|
|
9494
|
+
lines.push(`- Bias: ${latestA.bias >= 0 ? "+" : ""}${latestA.bias}${trendArrow(Math.abs(latestA.bias), prevA ? Math.abs(prevA.bias) : void 0, false)}`);
|
|
9269
9495
|
}
|
|
9496
|
+
return lines.join("\n");
|
|
9270
9497
|
}
|
|
9271
|
-
function
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9498
|
+
function formatDerivedMetrics(snapshots, backlogTasks) {
|
|
9499
|
+
const lines = [];
|
|
9500
|
+
if (snapshots.length > 0) {
|
|
9501
|
+
const velocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
|
|
9502
|
+
if (velocities.length >= 2) {
|
|
9503
|
+
const recent = velocities.slice(-5);
|
|
9504
|
+
const avg = recent.reduce((sum, v) => sum + v.completed, 0) / recent.length;
|
|
9505
|
+
const latest = recent[recent.length - 1];
|
|
9506
|
+
lines.push("**Cycle History (5-cycle avg)**");
|
|
9507
|
+
lines.push(`- Average: ${avg.toFixed(1)} tasks/cycle`);
|
|
9508
|
+
lines.push(`- Latest (Cycle ${latest.cycle}): ${latest.completed} tasks, ${latest.effortPoints} effort points`);
|
|
9509
|
+
}
|
|
9279
9510
|
}
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
).
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9511
|
+
const activeTasks = backlogTasks.filter(
|
|
9512
|
+
(t) => t.status === "Backlog" || t.status === "In Cycle" || t.status === "Ready"
|
|
9513
|
+
);
|
|
9514
|
+
if (activeTasks.length > 0) {
|
|
9515
|
+
const highPriority = activeTasks.filter(
|
|
9516
|
+
(t) => t.priority === "P1 High" || t.priority === "P0 Critical"
|
|
9517
|
+
).length;
|
|
9518
|
+
const ratio = (highPriority / activeTasks.length * 100).toFixed(0);
|
|
9519
|
+
const health = highPriority <= 3 ? "healthy" : highPriority <= 6 ? "moderate" : "top-heavy";
|
|
9520
|
+
if (lines.length > 0) lines.push("");
|
|
9521
|
+
lines.push("**Backlog Health**");
|
|
9522
|
+
lines.push(`- ${activeTasks.length} items, ${highPriority} high-priority (${ratio}%) \u2014 ${health}`);
|
|
9523
|
+
}
|
|
9524
|
+
if (snapshots.length > 0) {
|
|
9525
|
+
const accuracies = snapshots.flatMap((s) => s.accuracy).sort((a, b2) => a.cycle - b2.cycle);
|
|
9526
|
+
if (accuracies.length >= 3) {
|
|
9527
|
+
const recent = accuracies.slice(-5);
|
|
9528
|
+
const avgBias = recent.reduce((sum, a) => sum + a.bias, 0) / recent.length;
|
|
9529
|
+
const biasDir = avgBias > 0.3 ? "over-estimates" : avgBias < -0.3 ? "under-estimates" : "balanced";
|
|
9530
|
+
const avgMatch = recent.reduce((sum, a) => sum + a.matchRate, 0) / recent.length;
|
|
9531
|
+
if (lines.length > 0) lines.push("");
|
|
9532
|
+
lines.push("**Estimation Trend (5-cycle)**");
|
|
9533
|
+
lines.push(`- Avg match rate: ${avgMatch.toFixed(0)}%`);
|
|
9534
|
+
lines.push(`- Avg bias: ${avgBias >= 0 ? "+" : ""}${avgBias.toFixed(1)} \u2014 ${biasDir}`);
|
|
9535
|
+
}
|
|
9291
9536
|
}
|
|
9537
|
+
return lines.join("\n");
|
|
9292
9538
|
}
|
|
9293
|
-
|
|
9294
|
-
const
|
|
9295
|
-
if (
|
|
9296
|
-
|
|
9539
|
+
function formatBuildPatterns(patterns) {
|
|
9540
|
+
const sections = [];
|
|
9541
|
+
if (patterns.recurringSurprises.length > 0) {
|
|
9542
|
+
const items = patterns.recurringSurprises.map((s) => `- "${s.text}" \u2014 ${s.count} occurrences (cycles ${s.cycles.join(", ")})`).join("\n");
|
|
9543
|
+
sections.push(`**Recurring Surprises**
|
|
9544
|
+
${items}`);
|
|
9297
9545
|
}
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9546
|
+
if (patterns.estimationBias !== "none") {
|
|
9547
|
+
const direction = patterns.estimationBias === "under" ? "under-estimated (actual effort exceeded estimates)" : "over-estimated (actual effort was less than estimates)";
|
|
9548
|
+
sections.push(`**Estimation Bias**
|
|
9549
|
+
Tasks are consistently ${direction} \u2014 ${patterns.estimationBiasRate}% of recent builds.`);
|
|
9301
9550
|
}
|
|
9302
|
-
const
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
}
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
}
|
|
9322
|
-
} else if (hasRemote(config2.projectRoot)) {
|
|
9323
|
-
const pull = gitPull(config2.projectRoot);
|
|
9324
|
-
if (!pull.success && config2.abortOnConflict && /conflict/i.test(pull.message)) {
|
|
9325
|
-
return { result: void 0, warnings, abort: pull.message };
|
|
9326
|
-
}
|
|
9327
|
-
warnings.push(pull.success ? `Synced from ${baseBranch}.` : `Pull failed: ${pull.message}`);
|
|
9551
|
+
const scopeKeys = Object.keys(patterns.scopeAccuracyBreakdown);
|
|
9552
|
+
if (scopeKeys.length > 0) {
|
|
9553
|
+
const items = scopeKeys.map((k) => `- ${k}: ${patterns.scopeAccuracyBreakdown[k]}`).join("\n");
|
|
9554
|
+
sections.push(`**Scope Accuracy**
|
|
9555
|
+
${items}`);
|
|
9556
|
+
}
|
|
9557
|
+
if (patterns.untriagedIssues.length > 0) {
|
|
9558
|
+
const items = patterns.untriagedIssues.map((issue) => `- ${issue}`).join("\n");
|
|
9559
|
+
sections.push(`**Discovered Issues (untriaged)**
|
|
9560
|
+
${items}`);
|
|
9561
|
+
}
|
|
9562
|
+
return sections.join("\n\n");
|
|
9563
|
+
}
|
|
9564
|
+
function formatReviewPatterns(patterns) {
|
|
9565
|
+
const sections = [];
|
|
9566
|
+
if (patterns.recurringFeedback.length > 0) {
|
|
9567
|
+
const items = patterns.recurringFeedback.map((f) => `- "${f.text}" \u2014 ${f.count} occurrences (${f.taskIds.join(", ")})`).join("\n");
|
|
9568
|
+
sections.push(`**Recurring Review Feedback**
|
|
9569
|
+
${items}`);
|
|
9328
9570
|
}
|
|
9329
|
-
const
|
|
9330
|
-
if (
|
|
9331
|
-
|
|
9571
|
+
const verdictKeys = Object.keys(patterns.verdictBreakdown);
|
|
9572
|
+
if (verdictKeys.length > 0) {
|
|
9573
|
+
const items = verdictKeys.map((k) => `- ${k}: ${patterns.verdictBreakdown[k]}`).join("\n");
|
|
9574
|
+
sections.push(`**Verdict Breakdown**
|
|
9575
|
+
${items}`);
|
|
9332
9576
|
}
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
try {
|
|
9337
|
-
const output = execFileSync(
|
|
9338
|
-
"git",
|
|
9339
|
-
["branch", "--no-merged", baseBranch],
|
|
9340
|
-
{ cwd, encoding: "utf-8" }
|
|
9341
|
-
).trim();
|
|
9342
|
-
if (!output) return [];
|
|
9343
|
-
return output.split("\n").map((b2) => b2.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
9344
|
-
} catch {
|
|
9345
|
-
return [];
|
|
9577
|
+
if (patterns.requestChangesRate >= 50) {
|
|
9578
|
+
sections.push(`**High Rework Rate**
|
|
9579
|
+
${patterns.requestChangesRate}% of recent reviews requested changes.`);
|
|
9346
9580
|
}
|
|
9581
|
+
return sections.join("\n\n");
|
|
9347
9582
|
}
|
|
9348
|
-
function
|
|
9349
|
-
if (
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9583
|
+
function formatReviews(reviews) {
|
|
9584
|
+
if (reviews.length === 0) return "";
|
|
9585
|
+
return reviews.map(
|
|
9586
|
+
(r) => `### ${r.taskId} \u2014 ${r.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance"} \u2014 ${r.date}
|
|
9587
|
+
|
|
9588
|
+
- **Verdict:** ${r.verdict}
|
|
9589
|
+
- **Reviewer:** ${r.reviewer}
|
|
9590
|
+
- **Comments:** ${r.comments}`
|
|
9591
|
+
).join("\n\n---\n\n");
|
|
9353
9592
|
}
|
|
9354
|
-
function
|
|
9355
|
-
const
|
|
9356
|
-
if (
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
const mergedBranches = parseBranches(mergedOutput);
|
|
9370
|
-
const allBranches = parseBranches(allOutput);
|
|
9371
|
-
const codeAhead = [];
|
|
9372
|
-
const staleInProgress = [];
|
|
9373
|
-
for (const task of tasks) {
|
|
9374
|
-
const branch = `feat/${task.displayId}`;
|
|
9375
|
-
if (task.status === "Backlog" && mergedBranches.has(branch)) {
|
|
9376
|
-
codeAhead.push({ displayId: task.displayId, title: task.title, branch });
|
|
9377
|
-
}
|
|
9378
|
-
if (task.status === "In Progress" && !allBranches.has(branch)) {
|
|
9379
|
-
staleInProgress.push({ displayId: task.displayId, title: task.title });
|
|
9380
|
-
}
|
|
9593
|
+
function formatTaskComments(comments, taskIds, heading = "## Task Comments") {
|
|
9594
|
+
const relevant = comments.filter((c) => taskIds.has(c.taskId));
|
|
9595
|
+
if (relevant.length === 0) return "";
|
|
9596
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
9597
|
+
for (const c of relevant) {
|
|
9598
|
+
const list = byTask.get(c.taskId) ?? [];
|
|
9599
|
+
list.push(c);
|
|
9600
|
+
byTask.set(c.taskId, list);
|
|
9601
|
+
}
|
|
9602
|
+
const lines = ["", heading];
|
|
9603
|
+
for (const [taskId, taskComments] of byTask) {
|
|
9604
|
+
for (const c of taskComments.slice(0, 3)) {
|
|
9605
|
+
const date = c.createdAt.split("T")[0];
|
|
9606
|
+
const text = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
|
|
9607
|
+
lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${text}"`);
|
|
9381
9608
|
}
|
|
9382
|
-
return { codeAhead, staleInProgress };
|
|
9383
|
-
} catch {
|
|
9384
|
-
return empty;
|
|
9385
9609
|
}
|
|
9610
|
+
return lines.join("\n");
|
|
9386
9611
|
}
|
|
9387
|
-
function
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9612
|
+
function formatDiscoveryCanvas(canvas) {
|
|
9613
|
+
const sections = [];
|
|
9614
|
+
if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
|
|
9615
|
+
sections.push("**Landscape & References:**");
|
|
9616
|
+
for (const ref of canvas.landscapeReferences) {
|
|
9617
|
+
const url = ref.url ? ` (${ref.url})` : "";
|
|
9618
|
+
const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
|
|
9619
|
+
sections.push(`- ${ref.name}${url}${notes}`);
|
|
9620
|
+
}
|
|
9395
9621
|
}
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
|
|
9403
|
-
} catch (err) {
|
|
9404
|
-
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9622
|
+
if (canvas.userJourneys && canvas.userJourneys.length > 0) {
|
|
9623
|
+
sections.push("**User Journeys:**");
|
|
9624
|
+
for (const j of canvas.userJourneys) {
|
|
9625
|
+
const priority = j.priority ? ` [${j.priority}]` : "";
|
|
9626
|
+
sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
|
|
9627
|
+
}
|
|
9405
9628
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9629
|
+
if (canvas.mvpBoundary) {
|
|
9630
|
+
sections.push("**MVP Boundary:**", canvas.mvpBoundary);
|
|
9631
|
+
}
|
|
9632
|
+
if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
|
|
9633
|
+
sections.push("**Assumptions & Open Questions:**");
|
|
9634
|
+
for (const a of canvas.assumptionsOpenQuestions) {
|
|
9635
|
+
const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
|
|
9636
|
+
sections.push(`- [${a.status}] ${a.text}${evidence}`);
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
if (canvas.successSignals && canvas.successSignals.length > 0) {
|
|
9640
|
+
sections.push("**Success Signals:**");
|
|
9641
|
+
for (const s of canvas.successSignals) {
|
|
9642
|
+
const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
|
|
9643
|
+
sections.push(`- ${s.signal}${metric}`);
|
|
9644
|
+
}
|
|
9414
9645
|
}
|
|
9646
|
+
return sections.length > 0 ? sections.join("\n") : void 0;
|
|
9415
9647
|
}
|
|
9416
9648
|
|
|
9649
|
+
// src/services/plan.ts
|
|
9650
|
+
init_git();
|
|
9651
|
+
|
|
9417
9652
|
// src/lib/horizon.ts
|
|
9418
9653
|
function buildHorizonContext(phases, tasks) {
|
|
9419
9654
|
if (phases.length === 0) return null;
|
|
@@ -9604,6 +9839,9 @@ ACCEPTANCE CRITERIA
|
|
|
9604
9839
|
[ ] [criterion 1]
|
|
9605
9840
|
[ ] [criterion 2]
|
|
9606
9841
|
|
|
9842
|
+
PRE-MORTEM
|
|
9843
|
+
[For projects with 10+ cycles: 1-3 bullet points \u2014 most likely technical blocker, integration risk with adjacent systems, and scope creep signal. Draw from dead_ends and surprises in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.]
|
|
9844
|
+
|
|
9607
9845
|
SECURITY CONSIDERATIONS
|
|
9608
9846
|
[data exposure, secrets handling, auth/access control, dependency risks \u2014 or "None \u2014 no security-relevant changes"]
|
|
9609
9847
|
|
|
@@ -9798,6 +10036,7 @@ Standard planning cycle with full board review.
|
|
|
9798
10036
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
9799
10037
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
9800
10038
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10039
|
+
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
9801
10040
|
**Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
|
|
9802
10041
|
|
|
9803
10042
|
RESEARCH OUTPUT
|
|
@@ -9969,7 +10208,8 @@ Standard planning cycle with full board review.
|
|
|
9969
10208
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
9970
10209
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
9971
10210
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
9972
|
-
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing
|
|
10211
|
+
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10212
|
+
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.`);
|
|
9973
10213
|
if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
|
|
9974
10214
|
if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
|
|
9975
10215
|
if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
|
|
@@ -10632,6 +10872,9 @@ ACCEPTANCE CRITERIA
|
|
|
10632
10872
|
[ ] [criterion 1]
|
|
10633
10873
|
[ ] [criterion 2]
|
|
10634
10874
|
|
|
10875
|
+
PRE-MORTEM
|
|
10876
|
+
[For projects with 10+ cycles: 1-3 bullet points \u2014 most likely technical blocker, integration risk, scope creep signal. Omit for projects with fewer than 10 cycles.]
|
|
10877
|
+
|
|
10635
10878
|
SECURITY CONSIDERATIONS
|
|
10636
10879
|
[or "None \u2014 no security-relevant changes"]
|
|
10637
10880
|
|
|
@@ -11521,7 +11764,7 @@ ${cleanContent}`;
|
|
|
11521
11764
|
pendingRecIds = pending.map((r) => r.id);
|
|
11522
11765
|
} catch {
|
|
11523
11766
|
}
|
|
11524
|
-
const
|
|
11767
|
+
const handoffs = (data.cycleHandoffs ?? []).map((h) => {
|
|
11525
11768
|
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
11526
11769
|
if (parsed && !parsed.createdAt) {
|
|
11527
11770
|
parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -11560,8 +11803,8 @@ ${cleanContent}`;
|
|
|
11560
11803
|
} catch {
|
|
11561
11804
|
}
|
|
11562
11805
|
const effortMapLegacy = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
11563
|
-
const legacyTaskCount =
|
|
11564
|
-
const legacyEffortPoints =
|
|
11806
|
+
const legacyTaskCount = handoffs.length;
|
|
11807
|
+
const legacyEffortPoints = handoffs.reduce((sum, h) => sum + (effortMapLegacy[h.handoff.effort] ?? 3), 0);
|
|
11565
11808
|
const payload = {
|
|
11566
11809
|
cycleNumber: newCycleNumber,
|
|
11567
11810
|
cycleLog: {
|
|
@@ -11611,7 +11854,7 @@ ${cleanContent}`;
|
|
|
11611
11854
|
body: ad.body
|
|
11612
11855
|
})),
|
|
11613
11856
|
pendingRecommendationIds: pendingRecIds,
|
|
11614
|
-
handoffs
|
|
11857
|
+
handoffs,
|
|
11615
11858
|
cycle,
|
|
11616
11859
|
reviewedTaskIds
|
|
11617
11860
|
};
|
|
@@ -11660,8 +11903,12 @@ async function writeBack(adapter2, _mode, cycleNumber, data, contextHashes) {
|
|
|
11660
11903
|
|
|
11661
11904
|
${cleanContent}`;
|
|
11662
11905
|
const effortMap = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
11663
|
-
const
|
|
11664
|
-
|
|
11906
|
+
const legacyHandoffs = (data.cycleHandoffs ?? []).map((h) => {
|
|
11907
|
+
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
11908
|
+
return { taskId: h.taskId, handoff: parsed ?? { effort: "M" } };
|
|
11909
|
+
});
|
|
11910
|
+
const cycleTaskCount = legacyHandoffs.length;
|
|
11911
|
+
const cycleEffortPoints = legacyHandoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
|
|
11665
11912
|
const cycleLogPromise = adapter2.writeCycleLogEntry({
|
|
11666
11913
|
uuid: randomUUID7(),
|
|
11667
11914
|
cycleNumber: newCycleNumber,
|
|
@@ -12063,7 +12310,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12063
12310
|
const [decisions, reports, brief] = await Promise.all([
|
|
12064
12311
|
adapter2.getActiveDecisions(),
|
|
12065
12312
|
adapter2.getRecentBuildReports(10),
|
|
12066
|
-
adapter2.
|
|
12313
|
+
adapter2.readProductBrief()
|
|
12067
12314
|
]);
|
|
12068
12315
|
const northStar = await adapter2.getCurrentNorthStar?.() ?? "";
|
|
12069
12316
|
const userMessage2 = buildHandoffsOnlyUserMessage({
|
|
@@ -12801,6 +13048,27 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
12801
13048
|
adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([])
|
|
12802
13049
|
]);
|
|
12803
13050
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
13051
|
+
const existingAdIds = new Set(decisions.map((d) => d.id));
|
|
13052
|
+
const survivingPendingRecs = [];
|
|
13053
|
+
for (const rec of pendingRecs) {
|
|
13054
|
+
const isOrphaned = (() => {
|
|
13055
|
+
if (rec.target && /^AD-\d+$/.test(rec.target) && !existingAdIds.has(rec.target)) return true;
|
|
13056
|
+
if (rec.type === "ad_update") {
|
|
13057
|
+
const adMatch = rec.content.match(/^(?:delete|resolve|confidence_change|supersede|modify):\s*(AD-\d+)/i);
|
|
13058
|
+
if (adMatch && !existingAdIds.has(adMatch[1])) return true;
|
|
13059
|
+
}
|
|
13060
|
+
return false;
|
|
13061
|
+
})();
|
|
13062
|
+
if (isOrphaned) {
|
|
13063
|
+
try {
|
|
13064
|
+
await adapter2.dismissRecommendation?.(rec.id, "target AD no longer exists");
|
|
13065
|
+
console.error(`[strategy_review] Swept orphaned pending rec ${rec.id} (target: ${rec.target ?? rec.content.slice(0, 50)})`);
|
|
13066
|
+
} catch {
|
|
13067
|
+
}
|
|
13068
|
+
} else {
|
|
13069
|
+
survivingPendingRecs.push(rec);
|
|
13070
|
+
}
|
|
13071
|
+
}
|
|
12804
13072
|
const recentLog = log;
|
|
12805
13073
|
let buildPatternsText;
|
|
12806
13074
|
let reviewPatternsText;
|
|
@@ -12906,7 +13174,7 @@ ${lines.join("\n")}`;
|
|
|
12906
13174
|
try {
|
|
12907
13175
|
const plansDir = join2(homedir(), ".claude", "plans");
|
|
12908
13176
|
if (existsSync(plansDir)) {
|
|
12909
|
-
const lastReviewDate = previousStrategyReviews?.[0]?.
|
|
13177
|
+
const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
|
|
12910
13178
|
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
12911
13179
|
const fullPath = join2(plansDir, f);
|
|
12912
13180
|
const stat2 = statSync(fullPath);
|
|
@@ -13150,7 +13418,7 @@ ${cleanContent}`;
|
|
|
13150
13418
|
} else {
|
|
13151
13419
|
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
13152
13420
|
}
|
|
13153
|
-
const eventType = ad.action === "delete" ? "
|
|
13421
|
+
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
13154
13422
|
try {
|
|
13155
13423
|
await adapter2.appendDecisionEvent({
|
|
13156
13424
|
decisionId: ad.id,
|
|
@@ -13211,7 +13479,25 @@ ${cleanContent}`;
|
|
|
13211
13479
|
try {
|
|
13212
13480
|
const recs = extractRecommendations(data, cycleNumber);
|
|
13213
13481
|
if (recs.length > 0) {
|
|
13214
|
-
await
|
|
13482
|
+
const existingAds = await adapter2.getActiveDecisions().catch(() => []);
|
|
13483
|
+
const existingAdIds = new Set(existingAds.map((ad) => ad.id));
|
|
13484
|
+
const filteredRecs = recs.filter((rec) => {
|
|
13485
|
+
if (rec.target && /^AD-\d+$/.test(rec.target)) {
|
|
13486
|
+
if (!existingAdIds.has(rec.target)) {
|
|
13487
|
+
console.error(`[strategy_review] Skipped ghost recommendation for non-existent ${rec.target}: "${rec.content.slice(0, 80)}"`);
|
|
13488
|
+
return false;
|
|
13489
|
+
}
|
|
13490
|
+
}
|
|
13491
|
+
if (rec.type === "ad_update") {
|
|
13492
|
+
const adMatch = rec.content.match(/^(?:delete|resolve|confidence_change|supersede|modify):\s*(AD-\d+)/i);
|
|
13493
|
+
if (adMatch && !existingAdIds.has(adMatch[1])) {
|
|
13494
|
+
console.error(`[strategy_review] Skipped ghost recommendation for non-existent ${adMatch[1]}: "${rec.content.slice(0, 80)}"`);
|
|
13495
|
+
return false;
|
|
13496
|
+
}
|
|
13497
|
+
}
|
|
13498
|
+
return true;
|
|
13499
|
+
});
|
|
13500
|
+
await Promise.all(filteredRecs.map((rec) => adapter2.writeRecommendation(rec)));
|
|
13215
13501
|
}
|
|
13216
13502
|
} catch {
|
|
13217
13503
|
}
|
|
@@ -13293,6 +13579,17 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
|
|
|
13293
13579
|
let slackWarning;
|
|
13294
13580
|
let writeBackFailed;
|
|
13295
13581
|
let phaseChanges;
|
|
13582
|
+
if (!data) {
|
|
13583
|
+
const marker = "<!-- PAPI_STRUCTURED_OUTPUT -->";
|
|
13584
|
+
const hasMarker = rawOutput.includes(marker);
|
|
13585
|
+
if (hasMarker) {
|
|
13586
|
+
const afterMarker = rawOutput.slice(rawOutput.indexOf(marker) + marker.length, rawOutput.indexOf(marker) + marker.length + 300).trim();
|
|
13587
|
+
writeBackFailed = `Structured output marker found but JSON block was missing or malformed. Content after marker (first 300 chars): "${afterMarker}". Ensure your output includes a valid \`\`\`json block after <!-- PAPI_STRUCTURED_OUTPUT -->.`;
|
|
13588
|
+
} else {
|
|
13589
|
+
const preview = rawOutput.slice(0, 200).trim();
|
|
13590
|
+
writeBackFailed = `No structured output marker found. Expected "<!-- PAPI_STRUCTURED_OUTPUT -->" followed by a \`\`\`json block. Received (first 200 chars): "${preview}". Re-run strategy_review apply with the complete output including both parts.`;
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13296
13593
|
if (data) {
|
|
13297
13594
|
try {
|
|
13298
13595
|
phaseChanges = await writeBack2(adapter2, cycleNumber, data, displayText);
|
|
@@ -13516,8 +13813,8 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
|
|
|
13516
13813
|
let phases = [];
|
|
13517
13814
|
try {
|
|
13518
13815
|
[horizons, stages, phases] = await Promise.all([
|
|
13519
|
-
adapter2.readHorizons(),
|
|
13520
|
-
adapter2.readStages(),
|
|
13816
|
+
adapter2.readHorizons?.() ?? [],
|
|
13817
|
+
adapter2.readStages?.() ?? [],
|
|
13521
13818
|
adapter2.readPhases()
|
|
13522
13819
|
]);
|
|
13523
13820
|
} catch {
|
|
@@ -13650,7 +13947,7 @@ ${cleanContent}`;
|
|
|
13650
13947
|
} else {
|
|
13651
13948
|
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
13652
13949
|
}
|
|
13653
|
-
const eventType = ad.action === "delete" ? "
|
|
13950
|
+
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
13654
13951
|
try {
|
|
13655
13952
|
await adapter2.appendDecisionEvent({
|
|
13656
13953
|
decisionId: ad.id,
|
|
@@ -13763,7 +14060,7 @@ async function captureDecision(adapter2, input) {
|
|
|
13763
14060
|
decisionId: adId,
|
|
13764
14061
|
eventType: adAction === "created" ? "created" : "modified",
|
|
13765
14062
|
cycle: cycleNumber,
|
|
13766
|
-
source: "
|
|
14063
|
+
source: "strategy_change",
|
|
13767
14064
|
sourceRef: `cycle-${cycleNumber}-capture`,
|
|
13768
14065
|
detail: `Captured: ${input.text.slice(0, 200)}`
|
|
13769
14066
|
});
|
|
@@ -14264,10 +14561,10 @@ function formatBoard(result) {
|
|
|
14264
14561
|
t.title,
|
|
14265
14562
|
t.status,
|
|
14266
14563
|
t.cycle != null ? String(t.cycle) : "-",
|
|
14267
|
-
t.phase,
|
|
14268
|
-
t.module,
|
|
14269
|
-
t.epic,
|
|
14270
|
-
t.complexity,
|
|
14564
|
+
t.phase ?? "-",
|
|
14565
|
+
t.module ?? "-",
|
|
14566
|
+
t.epic ?? "-",
|
|
14567
|
+
t.complexity ?? "-",
|
|
14271
14568
|
t.createdAt ?? "-"
|
|
14272
14569
|
]);
|
|
14273
14570
|
const widths = headers.map(
|
|
@@ -14486,7 +14783,45 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
14486
14783
|
// src/services/setup.ts
|
|
14487
14784
|
init_dist2();
|
|
14488
14785
|
import { mkdir, writeFile as writeFile2, readFile as readFile3, readdir, access as access2, stat } from "fs/promises";
|
|
14489
|
-
import { join as
|
|
14786
|
+
import { join as join4, basename, extname } from "path";
|
|
14787
|
+
|
|
14788
|
+
// src/lib/detect-codebase.ts
|
|
14789
|
+
import { existsSync as existsSync2 } from "fs";
|
|
14790
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
14791
|
+
import { join as join3 } from "path";
|
|
14792
|
+
function detectCodebaseType(projectRoot) {
|
|
14793
|
+
if (existsSync2(join3(projectRoot, ".git"))) {
|
|
14794
|
+
return "existing_codebase";
|
|
14795
|
+
}
|
|
14796
|
+
const manifests = [
|
|
14797
|
+
"package.json",
|
|
14798
|
+
"Cargo.toml",
|
|
14799
|
+
"pyproject.toml",
|
|
14800
|
+
"go.mod",
|
|
14801
|
+
"Gemfile",
|
|
14802
|
+
"pom.xml",
|
|
14803
|
+
"build.gradle",
|
|
14804
|
+
"CMakeLists.txt"
|
|
14805
|
+
];
|
|
14806
|
+
for (const manifest of manifests) {
|
|
14807
|
+
if (existsSync2(join3(projectRoot, manifest))) {
|
|
14808
|
+
return "existing_codebase";
|
|
14809
|
+
}
|
|
14810
|
+
}
|
|
14811
|
+
try {
|
|
14812
|
+
const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
|
|
14813
|
+
const fileCount = entries.filter((f) => {
|
|
14814
|
+
try {
|
|
14815
|
+
return statSync2(join3(projectRoot, f)).isFile();
|
|
14816
|
+
} catch {
|
|
14817
|
+
return false;
|
|
14818
|
+
}
|
|
14819
|
+
}).length;
|
|
14820
|
+
if (fileCount > 5) return "existing_codebase";
|
|
14821
|
+
} catch {
|
|
14822
|
+
}
|
|
14823
|
+
return "new_project";
|
|
14824
|
+
}
|
|
14490
14825
|
|
|
14491
14826
|
// src/templates.ts
|
|
14492
14827
|
var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
|
|
@@ -15079,7 +15414,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
15079
15414
|
await mkdir(config2.papiDir, { recursive: true });
|
|
15080
15415
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
15081
15416
|
const content = substitute(template, vars);
|
|
15082
|
-
await writeFile2(
|
|
15417
|
+
await writeFile2(join4(config2.papiDir, filename), content, "utf-8");
|
|
15083
15418
|
}
|
|
15084
15419
|
}
|
|
15085
15420
|
} else {
|
|
@@ -15094,18 +15429,18 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
15094
15429
|
} catch {
|
|
15095
15430
|
}
|
|
15096
15431
|
}
|
|
15097
|
-
const commandsDir =
|
|
15098
|
-
const docsDir =
|
|
15432
|
+
const commandsDir = join4(config2.projectRoot, ".claude", "commands");
|
|
15433
|
+
const docsDir = join4(config2.projectRoot, "docs");
|
|
15099
15434
|
await mkdir(commandsDir, { recursive: true });
|
|
15100
15435
|
await mkdir(docsDir, { recursive: true });
|
|
15101
|
-
const claudeMdPath =
|
|
15436
|
+
const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
|
|
15102
15437
|
let claudeMdExists = false;
|
|
15103
15438
|
try {
|
|
15104
15439
|
await access2(claudeMdPath);
|
|
15105
15440
|
claudeMdExists = true;
|
|
15106
15441
|
} catch {
|
|
15107
15442
|
}
|
|
15108
|
-
const docsIndexPath =
|
|
15443
|
+
const docsIndexPath = join4(docsDir, "INDEX.md");
|
|
15109
15444
|
let docsIndexExists = false;
|
|
15110
15445
|
try {
|
|
15111
15446
|
await access2(docsIndexPath);
|
|
@@ -15113,9 +15448,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
15113
15448
|
} catch {
|
|
15114
15449
|
}
|
|
15115
15450
|
const scaffoldFiles = {
|
|
15116
|
-
[
|
|
15117
|
-
[
|
|
15118
|
-
[
|
|
15451
|
+
[join4(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
|
|
15452
|
+
[join4(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
|
|
15453
|
+
[join4(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
|
|
15119
15454
|
};
|
|
15120
15455
|
if (!docsIndexExists) {
|
|
15121
15456
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
@@ -15132,7 +15467,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
15132
15467
|
} catch {
|
|
15133
15468
|
}
|
|
15134
15469
|
}
|
|
15135
|
-
const cursorDir =
|
|
15470
|
+
const cursorDir = join4(config2.projectRoot, ".cursor");
|
|
15136
15471
|
let cursorDetected = false;
|
|
15137
15472
|
try {
|
|
15138
15473
|
await access2(cursorDir);
|
|
@@ -15140,8 +15475,8 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
15140
15475
|
} catch {
|
|
15141
15476
|
}
|
|
15142
15477
|
if (cursorDetected) {
|
|
15143
|
-
const cursorRulesDir =
|
|
15144
|
-
const cursorRulesPath =
|
|
15478
|
+
const cursorRulesDir = join4(cursorDir, "rules");
|
|
15479
|
+
const cursorRulesPath = join4(cursorRulesDir, "papi.mdc");
|
|
15145
15480
|
await mkdir(cursorRulesDir, { recursive: true });
|
|
15146
15481
|
try {
|
|
15147
15482
|
await access2(cursorRulesPath);
|
|
@@ -15167,7 +15502,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
15167
15502
|
}
|
|
15168
15503
|
var PAPI_PERMISSION = "mcp__papi__*";
|
|
15169
15504
|
async function ensurePapiPermission(projectRoot) {
|
|
15170
|
-
const settingsPath =
|
|
15505
|
+
const settingsPath = join4(projectRoot, ".claude", "settings.json");
|
|
15171
15506
|
try {
|
|
15172
15507
|
let settings = {};
|
|
15173
15508
|
try {
|
|
@@ -15186,14 +15521,14 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
15186
15521
|
if (!allow.includes(PAPI_PERMISSION)) {
|
|
15187
15522
|
allow.push(PAPI_PERMISSION);
|
|
15188
15523
|
}
|
|
15189
|
-
await mkdir(
|
|
15524
|
+
await mkdir(join4(projectRoot, ".claude"), { recursive: true });
|
|
15190
15525
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
15191
15526
|
} catch {
|
|
15192
15527
|
}
|
|
15193
15528
|
}
|
|
15194
15529
|
async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
|
|
15195
15530
|
if (config2.adapterType !== "pg") {
|
|
15196
|
-
await writeFile2(
|
|
15531
|
+
await writeFile2(join4(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
|
|
15197
15532
|
}
|
|
15198
15533
|
await adapter2.updateProductBrief(briefText);
|
|
15199
15534
|
const briefPhases = parsePhases(briefText);
|
|
@@ -15227,7 +15562,12 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
|
|
|
15227
15562
|
if (Array.isArray(ads)) {
|
|
15228
15563
|
for (const ad of ads) {
|
|
15229
15564
|
if (ad.id && ad.body) {
|
|
15230
|
-
|
|
15565
|
+
if (adapter2.upsertActiveDecision) {
|
|
15566
|
+
const title = ad.title || ad.body.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
|
|
15567
|
+
await adapter2.upsertActiveDecision(ad.id, ad.body, title, ad.confidence || "MEDIUM", 0);
|
|
15568
|
+
} else {
|
|
15569
|
+
await adapter2.updateActiveDecision(ad.id, ad.body, 0);
|
|
15570
|
+
}
|
|
15231
15571
|
seededAds++;
|
|
15232
15572
|
}
|
|
15233
15573
|
}
|
|
@@ -15237,7 +15577,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
|
|
|
15237
15577
|
}
|
|
15238
15578
|
if (conventionsText?.trim()) {
|
|
15239
15579
|
try {
|
|
15240
|
-
const claudeMdPath =
|
|
15580
|
+
const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
|
|
15241
15581
|
const existing = await readFile3(claudeMdPath, "utf-8");
|
|
15242
15582
|
await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
|
|
15243
15583
|
} catch {
|
|
@@ -15305,13 +15645,13 @@ async function scanCodebase(projectRoot) {
|
|
|
15305
15645
|
}
|
|
15306
15646
|
let packageJson;
|
|
15307
15647
|
try {
|
|
15308
|
-
const content = await readFile3(
|
|
15648
|
+
const content = await readFile3(join4(projectRoot, "package.json"), "utf-8");
|
|
15309
15649
|
packageJson = JSON.parse(content);
|
|
15310
15650
|
} catch {
|
|
15311
15651
|
}
|
|
15312
15652
|
let readme;
|
|
15313
15653
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
15314
|
-
const content = await safeReadFile(
|
|
15654
|
+
const content = await safeReadFile(join4(projectRoot, name), 5e3);
|
|
15315
15655
|
if (content) {
|
|
15316
15656
|
readme = content;
|
|
15317
15657
|
break;
|
|
@@ -15321,7 +15661,7 @@ async function scanCodebase(projectRoot) {
|
|
|
15321
15661
|
let totalFiles = topLevelFiles.length;
|
|
15322
15662
|
for (const dir of topLevelDirs) {
|
|
15323
15663
|
try {
|
|
15324
|
-
const entries = await readdir(
|
|
15664
|
+
const entries = await readdir(join4(projectRoot, dir), { withFileTypes: true });
|
|
15325
15665
|
const files = entries.filter((e) => e.isFile());
|
|
15326
15666
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
15327
15667
|
totalFiles += files.length;
|
|
@@ -15400,16 +15740,19 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
15400
15740
|
existingBrief = await adapter2.readProductBrief();
|
|
15401
15741
|
} catch {
|
|
15402
15742
|
throw new Error(
|
|
15403
|
-
config2.adapterType === "pg" ? "Could not read Product Brief from database. Check DATABASE_URL and PAPI_PROJECT_ID are set correctly in your MCP config." : "Could not read PRODUCT_BRIEF.md. Ensure .papi/ directory exists."
|
|
15743
|
+
config2.adapterType === "proxy" ? "Could not read Product Brief from PAPI. Check PAPI_DATA_API_KEY and PAPI_PROJECT_ID are set correctly in your MCP config." : config2.adapterType === "pg" ? "Could not read Product Brief from database. Check DATABASE_URL and PAPI_PROJECT_ID are set correctly in your MCP config." : "Could not read PRODUCT_BRIEF.md. Ensure .papi/ directory exists."
|
|
15404
15744
|
);
|
|
15405
15745
|
}
|
|
15406
15746
|
const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
15407
15747
|
if (existingBrief.trim() && !existingBrief.includes(TEMPLATE_MARKER) && !input.force) {
|
|
15408
15748
|
throw new Error("PRODUCT_BRIEF.md already contains a generated Product Brief. Running setup again would overwrite it.\n\nTo proceed anyway, run setup with force: true.");
|
|
15409
15749
|
}
|
|
15750
|
+
const detectedCodebaseType = detectCodebaseType(config2.projectRoot);
|
|
15751
|
+
const autoDetected = input.existingProject === void 0 || input.existingProject === false;
|
|
15752
|
+
const isExistingProject = input.existingProject === true || autoDetected && detectedCodebaseType === "existing_codebase";
|
|
15410
15753
|
let codebaseSummary;
|
|
15411
15754
|
let sourceContents;
|
|
15412
|
-
if (
|
|
15755
|
+
if (isExistingProject) {
|
|
15413
15756
|
const scan = await scanCodebase(config2.projectRoot);
|
|
15414
15757
|
if (input.sources) {
|
|
15415
15758
|
sourceContents = await readSourceFiles(input.sources);
|
|
@@ -15452,7 +15795,7 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
15452
15795
|
constraints: input.constraints
|
|
15453
15796
|
})
|
|
15454
15797
|
} : void 0;
|
|
15455
|
-
const initialTasksPrompt =
|
|
15798
|
+
const initialTasksPrompt = isExistingProject && codebaseSummary ? {
|
|
15456
15799
|
system: INITIAL_TASKS_SYSTEM,
|
|
15457
15800
|
user: buildInitialTasksPrompt({
|
|
15458
15801
|
projectName: input.projectName,
|
|
@@ -15468,7 +15811,9 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
15468
15811
|
adSeedPrompt,
|
|
15469
15812
|
conventionsPrompt,
|
|
15470
15813
|
initialTasksPrompt,
|
|
15471
|
-
codebaseSummary
|
|
15814
|
+
codebaseSummary,
|
|
15815
|
+
detectedCodebaseType,
|
|
15816
|
+
autoDetected: autoDetected && detectedCodebaseType !== "new_project"
|
|
15472
15817
|
};
|
|
15473
15818
|
}
|
|
15474
15819
|
async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
|
|
@@ -15507,7 +15852,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
15507
15852
|
}
|
|
15508
15853
|
}
|
|
15509
15854
|
try {
|
|
15510
|
-
const claudeMdPath =
|
|
15855
|
+
const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
|
|
15511
15856
|
const existing = await readFile3(claudeMdPath, "utf-8");
|
|
15512
15857
|
if (!existing.includes("Dogfood Logging")) {
|
|
15513
15858
|
const dogfoodSection = [
|
|
@@ -15551,7 +15896,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
15551
15896
|
}
|
|
15552
15897
|
let cursorScaffolded = false;
|
|
15553
15898
|
try {
|
|
15554
|
-
await access2(
|
|
15899
|
+
await access2(join4(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
15555
15900
|
cursorScaffolded = true;
|
|
15556
15901
|
} catch {
|
|
15557
15902
|
}
|
|
@@ -15712,14 +16057,25 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
|
|
|
15712
16057
|
if (!args.project_type) inferredDefaults.push(`- **Project type:** defaulted to "${input.projectType}"`);
|
|
15713
16058
|
if (!args.team_size) inferredDefaults.push(`- **Team size:** defaulted to "${input.teamSize}"`);
|
|
15714
16059
|
if (!args.deployment_target) inferredDefaults.push(`- **Deployment:** defaulted to "${input.deploymentTarget}"`);
|
|
15715
|
-
const isExisting =
|
|
16060
|
+
const isExisting = result.detectedCodebaseType === "existing_codebase";
|
|
15716
16061
|
const sections = [
|
|
15717
|
-
isExisting ? `## PAPI Setup \u2014 Adopt Existing Project (Prepare Phase)` : `## PAPI Setup \u2014 Prepare Phase`,
|
|
16062
|
+
isExisting ? `## PAPI Setup \u2014 Adopt Existing Project (Prepare Phase)` : `## PAPI Setup \u2014 New Project (Prepare Phase)`,
|
|
15718
16063
|
"",
|
|
15719
16064
|
result.createdProject ? config2.adapterType === "md" ? `Project "${result.projectName}" scaffolded \u2014 .papi/ directory created.
|
|
15720
16065
|
` : `Project "${result.projectName}" scaffolded \u2014 database tables created.
|
|
15721
16066
|
` : ""
|
|
15722
16067
|
];
|
|
16068
|
+
if (result.autoDetected) {
|
|
16069
|
+
sections.push(
|
|
16070
|
+
`**Codebase detected:** Existing codebase found \u2014 running in adoption mode. If this is wrong, re-run setup with \`existing_project: false\`.`,
|
|
16071
|
+
""
|
|
16072
|
+
);
|
|
16073
|
+
} else if (result.detectedCodebaseType === "new_project" && input.existingProject === void 0) {
|
|
16074
|
+
sections.push(
|
|
16075
|
+
`**Codebase detected:** Fresh project \u2014 running in new project mode. If you have existing code, re-run setup with \`existing_project: true\`.`,
|
|
16076
|
+
""
|
|
16077
|
+
);
|
|
16078
|
+
}
|
|
15723
16079
|
if (inferredDefaults.length > 0) {
|
|
15724
16080
|
sections.push(
|
|
15725
16081
|
`**Defaults applied** (override by re-running setup with these fields):`,
|
|
@@ -15817,9 +16173,10 @@ ${result.initialTasksPrompt.user}
|
|
|
15817
16173
|
init_dist2();
|
|
15818
16174
|
|
|
15819
16175
|
// src/services/build.ts
|
|
16176
|
+
init_git();
|
|
15820
16177
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
15821
|
-
import { readdirSync as
|
|
15822
|
-
import { join as
|
|
16178
|
+
import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
|
|
16179
|
+
import { join as join5 } from "path";
|
|
15823
16180
|
function capitalizeCompleted(value) {
|
|
15824
16181
|
const map = {
|
|
15825
16182
|
yes: "Yes",
|
|
@@ -16220,14 +16577,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16220
16577
|
let docWarning;
|
|
16221
16578
|
try {
|
|
16222
16579
|
if (adapter2.searchDocs) {
|
|
16223
|
-
const docsDir =
|
|
16224
|
-
if (
|
|
16580
|
+
const docsDir = join5(config2.projectRoot, "docs");
|
|
16581
|
+
if (existsSync3(docsDir)) {
|
|
16225
16582
|
const scanDir = (dir, depth = 0) => {
|
|
16226
16583
|
if (depth > 8) return [];
|
|
16227
|
-
const entries =
|
|
16584
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
16228
16585
|
const files = [];
|
|
16229
16586
|
for (const e of entries) {
|
|
16230
|
-
const full =
|
|
16587
|
+
const full = join5(dir, e.name);
|
|
16231
16588
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
16232
16589
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
16233
16590
|
}
|
|
@@ -16242,7 +16599,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16242
16599
|
const failed = [];
|
|
16243
16600
|
for (const docPath of unregistered) {
|
|
16244
16601
|
try {
|
|
16245
|
-
const meta = extractDocMeta(
|
|
16602
|
+
const meta = extractDocMeta(join5(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
16246
16603
|
await adapter2.registerDoc({
|
|
16247
16604
|
title: meta.title,
|
|
16248
16605
|
type: meta.type,
|
|
@@ -16821,6 +17178,9 @@ Reason: ${result.reason}`);
|
|
|
16821
17178
|
}
|
|
16822
17179
|
}
|
|
16823
17180
|
|
|
17181
|
+
// src/tools/idea.ts
|
|
17182
|
+
init_git();
|
|
17183
|
+
|
|
16824
17184
|
// src/services/idea.ts
|
|
16825
17185
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
16826
17186
|
var ROUTING_PATTERNS = [
|
|
@@ -17273,6 +17633,9 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
|
|
|
17273
17633
|
return textResponse(result.message);
|
|
17274
17634
|
}
|
|
17275
17635
|
|
|
17636
|
+
// src/tools/bug.ts
|
|
17637
|
+
init_git();
|
|
17638
|
+
|
|
17276
17639
|
// src/services/bug.ts
|
|
17277
17640
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
17278
17641
|
function resolveCurrentPhase2(phases) {
|
|
@@ -17395,6 +17758,9 @@ async function handleBug(adapter2, config2, args) {
|
|
|
17395
17758
|
return textResponse(`\u{1F41B} ${task.id}: "${task.title}" \u2014 ${severityLabel} bug added to backlog${branchNote}. Will be picked up by next plan.${truncateWarning}`);
|
|
17396
17759
|
}
|
|
17397
17760
|
|
|
17761
|
+
// src/tools/ad-hoc.ts
|
|
17762
|
+
init_git();
|
|
17763
|
+
|
|
17398
17764
|
// src/services/ad-hoc.ts
|
|
17399
17765
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
17400
17766
|
async function recordAdHoc(adapter2, input) {
|
|
@@ -17531,6 +17897,9 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
17531
17897
|
);
|
|
17532
17898
|
}
|
|
17533
17899
|
|
|
17900
|
+
// src/tools/board-reconcile.ts
|
|
17901
|
+
init_git();
|
|
17902
|
+
|
|
17534
17903
|
// src/services/reconcile.ts
|
|
17535
17904
|
async function prepareReconcile(adapter2) {
|
|
17536
17905
|
const health = await adapter2.getCycleHealth();
|
|
@@ -17586,15 +17955,15 @@ async function prepareReconcile(adapter2) {
|
|
|
17586
17955
|
const misaligned = allTasks.filter((t) => {
|
|
17587
17956
|
const mapping = phaseStageMap.get(t.phase ?? "");
|
|
17588
17957
|
if (!mapping) return false;
|
|
17589
|
-
return mapping.stage.status === "
|
|
17958
|
+
return mapping.stage.status === "deferred" || mapping.horizon.status === "deferred";
|
|
17590
17959
|
});
|
|
17591
17960
|
if (misaligned.length > 0) {
|
|
17592
|
-
lines.push("### Hierarchy Misalignment (tasks in
|
|
17961
|
+
lines.push("### Hierarchy Misalignment (tasks in deferred stages/horizons)");
|
|
17593
17962
|
for (const t of misaligned) {
|
|
17594
17963
|
const mapping = phaseStageMap.get(t.phase ?? "");
|
|
17595
17964
|
const stageLabel = mapping?.stage.label ?? "unknown";
|
|
17596
17965
|
const horizonLabel = mapping?.horizon.label ?? "unknown";
|
|
17597
|
-
lines.push(`- **${t.id}:** ${t.title} \u2014 phase "${t.phase}" belongs to **${stageLabel}** (${horizonLabel}), which is
|
|
17966
|
+
lines.push(`- **${t.id}:** ${t.title} \u2014 phase "${t.phase}" belongs to **${stageLabel}** (${horizonLabel}), which is deferred. Consider deferring or reassigning.`);
|
|
17598
17967
|
}
|
|
17599
17968
|
lines.push("");
|
|
17600
17969
|
}
|
|
@@ -17764,7 +18133,7 @@ async function prepareRetriage(adapter2) {
|
|
|
17764
18133
|
lines.push(`**${allTasks.length} tasks** to reassess priority and complexity.`);
|
|
17765
18134
|
lines.push("");
|
|
17766
18135
|
try {
|
|
17767
|
-
const ads = await adapter2.
|
|
18136
|
+
const ads = await adapter2.getActiveDecisions();
|
|
17768
18137
|
if (ads.length > 0) {
|
|
17769
18138
|
lines.push("### Active Decisions (strategic context)");
|
|
17770
18139
|
for (const ad of ads.slice(0, 10)) {
|
|
@@ -18098,6 +18467,53 @@ Assess each task above and produce your retriage output. Then call \`board_recon
|
|
|
18098
18467
|
}
|
|
18099
18468
|
|
|
18100
18469
|
// src/services/health.ts
|
|
18470
|
+
function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
|
|
18471
|
+
if (cycleNumber < 3) return null;
|
|
18472
|
+
const scores = [];
|
|
18473
|
+
const recentSnaps = snapshots.slice(-3);
|
|
18474
|
+
const baselineSnaps = snapshots.slice(-10);
|
|
18475
|
+
if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
|
|
18476
|
+
const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
|
|
18477
|
+
const recentAvg = avg(recentSnaps);
|
|
18478
|
+
const baselineAvg = avg(baselineSnaps);
|
|
18479
|
+
const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
|
|
18480
|
+
scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
|
|
18481
|
+
} else {
|
|
18482
|
+
scores.push({ name: "Velocity", score: 50, weight: 0.25 });
|
|
18483
|
+
}
|
|
18484
|
+
if (recentSnaps.length > 0) {
|
|
18485
|
+
const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
|
|
18486
|
+
scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
|
|
18487
|
+
} else {
|
|
18488
|
+
scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
|
|
18489
|
+
}
|
|
18490
|
+
const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
|
|
18491
|
+
const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
|
|
18492
|
+
scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
|
|
18493
|
+
const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
|
|
18494
|
+
if (backlogTasks.length > 0) {
|
|
18495
|
+
const criticalCount = backlogTasks.filter(
|
|
18496
|
+
(t) => t.priority === "P0 Critical" || t.priority === "P1 High"
|
|
18497
|
+
).length;
|
|
18498
|
+
const criticalRatio = criticalCount / backlogTasks.length;
|
|
18499
|
+
const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
|
|
18500
|
+
scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
|
|
18501
|
+
} else {
|
|
18502
|
+
scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
|
|
18503
|
+
}
|
|
18504
|
+
if (decisionUsage.length > 0) {
|
|
18505
|
+
const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
|
|
18506
|
+
const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
|
|
18507
|
+
scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
|
|
18508
|
+
} else {
|
|
18509
|
+
scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
|
|
18510
|
+
}
|
|
18511
|
+
const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
|
|
18512
|
+
const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
|
|
18513
|
+
const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
|
|
18514
|
+
const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
|
|
18515
|
+
return { score: totalScore, status, reason };
|
|
18516
|
+
}
|
|
18101
18517
|
function countByStatus(tasks) {
|
|
18102
18518
|
const counts = /* @__PURE__ */ new Map();
|
|
18103
18519
|
for (const task of tasks) {
|
|
@@ -18175,8 +18591,8 @@ async function getHealthSummary(adapter2) {
|
|
|
18175
18591
|
}
|
|
18176
18592
|
let metricsSection;
|
|
18177
18593
|
let derivedMetricsSection = "";
|
|
18594
|
+
let snapshots = [];
|
|
18178
18595
|
try {
|
|
18179
|
-
let snapshots = [];
|
|
18180
18596
|
try {
|
|
18181
18597
|
const reports = await adapter2.getRecentBuildReports(50);
|
|
18182
18598
|
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
@@ -18208,8 +18624,10 @@ async function getHealthSummary(adapter2) {
|
|
|
18208
18624
|
}
|
|
18209
18625
|
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
18210
18626
|
let decisionUsageSection = "";
|
|
18627
|
+
let decisionUsageEntries = [];
|
|
18211
18628
|
try {
|
|
18212
18629
|
const usage = await adapter2.getDecisionUsage(cycleNumber);
|
|
18630
|
+
decisionUsageEntries = usage;
|
|
18213
18631
|
if (usage.length > 0) {
|
|
18214
18632
|
const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
|
|
18215
18633
|
if (stale.length > 0) {
|
|
@@ -18264,6 +18682,7 @@ ${lines.join("\n")}`;
|
|
|
18264
18682
|
}
|
|
18265
18683
|
} catch {
|
|
18266
18684
|
}
|
|
18685
|
+
const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
|
|
18267
18686
|
return {
|
|
18268
18687
|
cycleNumber,
|
|
18269
18688
|
latestCycleStatus: health.latestCycleStatus,
|
|
@@ -18281,7 +18700,10 @@ ${lines.join("\n")}`;
|
|
|
18281
18700
|
decisionLifecycleSection,
|
|
18282
18701
|
decisionScoresSection,
|
|
18283
18702
|
contextUtilisationSection,
|
|
18284
|
-
northStarSection
|
|
18703
|
+
northStarSection,
|
|
18704
|
+
healthScore: healthResult?.score ?? null,
|
|
18705
|
+
healthStatus: healthResult?.status ?? null,
|
|
18706
|
+
healthReason: healthResult?.reason ?? null
|
|
18285
18707
|
};
|
|
18286
18708
|
}
|
|
18287
18709
|
|
|
@@ -18378,8 +18800,9 @@ async function handleHealth(adapter2) {
|
|
|
18378
18800
|
}
|
|
18379
18801
|
|
|
18380
18802
|
// src/services/release.ts
|
|
18803
|
+
init_git();
|
|
18381
18804
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
18382
|
-
import { join as
|
|
18805
|
+
import { join as join6 } from "path";
|
|
18383
18806
|
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
18384
18807
|
|
|
18385
18808
|
## v0.1.0-alpha \u2014 Initial Release
|
|
@@ -18470,7 +18893,7 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
18470
18893
|
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
18471
18894
|
changelogContent = generateChangelog(version, commits);
|
|
18472
18895
|
}
|
|
18473
|
-
const changelogPath =
|
|
18896
|
+
const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
|
|
18474
18897
|
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
18475
18898
|
const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
|
|
18476
18899
|
const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
|
|
@@ -18566,8 +18989,9 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
18566
18989
|
}
|
|
18567
18990
|
|
|
18568
18991
|
// src/tools/review.ts
|
|
18569
|
-
import { existsSync as
|
|
18570
|
-
import { join as
|
|
18992
|
+
import { existsSync as existsSync4 } from "fs";
|
|
18993
|
+
import { join as join7 } from "path";
|
|
18994
|
+
init_git();
|
|
18571
18995
|
|
|
18572
18996
|
// src/services/review.ts
|
|
18573
18997
|
init_dist2();
|
|
@@ -18805,8 +19229,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
18805
19229
|
}
|
|
18806
19230
|
const featureBranch = taskBranchName(taskId);
|
|
18807
19231
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
18808
|
-
const papiDir =
|
|
18809
|
-
if (
|
|
19232
|
+
const papiDir = join7(config2.projectRoot, ".papi");
|
|
19233
|
+
if (existsSync4(papiDir)) {
|
|
18810
19234
|
try {
|
|
18811
19235
|
const commitResult = stageDirAndCommit(
|
|
18812
19236
|
config2.projectRoot,
|
|
@@ -19044,6 +19468,7 @@ async function handleInit(config2, args) {
|
|
|
19044
19468
|
const mcpJsonPath = path4.join(projectRoot, ".mcp.json");
|
|
19045
19469
|
const force = args.force === true;
|
|
19046
19470
|
const projectName = args.project_name?.trim() || path4.basename(projectRoot);
|
|
19471
|
+
const usingCwdDefault = !process.env.PAPI_PROJECT_DIR && process.argv.indexOf("--project") === -1;
|
|
19047
19472
|
let existingConfig = null;
|
|
19048
19473
|
try {
|
|
19049
19474
|
await access3(mcpJsonPath);
|
|
@@ -19070,44 +19495,93 @@ Path: ${mcpJsonPath}`
|
|
|
19070
19495
|
}
|
|
19071
19496
|
}
|
|
19072
19497
|
}
|
|
19073
|
-
const
|
|
19074
|
-
const
|
|
19075
|
-
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19498
|
+
const existingApiKey = process.env.PAPI_DATA_API_KEY;
|
|
19499
|
+
const existingProjectId = process.env.PAPI_PROJECT_ID;
|
|
19500
|
+
const isProxyUser = Boolean(existingApiKey) || config2.adapterType === "proxy";
|
|
19501
|
+
const isDatabaseUser = Boolean(process.env.DATABASE_URL) || config2.adapterType === "pg";
|
|
19502
|
+
if (isProxyUser && existingApiKey && existingProjectId) {
|
|
19503
|
+
const mcpConfig = {
|
|
19504
|
+
mcpServers: {
|
|
19505
|
+
papi: {
|
|
19506
|
+
command: "npx",
|
|
19507
|
+
args: ["-y", "@papi-ai/server"],
|
|
19508
|
+
env: {
|
|
19509
|
+
PAPI_PROJECT_ID: existingProjectId,
|
|
19510
|
+
PAPI_DATA_API_KEY: existingApiKey
|
|
19511
|
+
}
|
|
19512
|
+
}
|
|
19513
|
+
}
|
|
19514
|
+
};
|
|
19515
|
+
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
19516
|
+
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
19517
|
+
return textResponse(
|
|
19518
|
+
`# PAPI Initialised \u2014 ${projectName}
|
|
19519
|
+
|
|
19520
|
+
**Config:** \`${mcpJsonPath}\`
|
|
19521
|
+
|
|
19522
|
+
Your existing API key and project ID have been saved to .mcp.json.
|
|
19523
|
+
|
|
19524
|
+
## Next Steps
|
|
19525
|
+
|
|
19526
|
+
1. **Restart your MCP client** to pick up the new config.
|
|
19527
|
+
2. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief and CLAUDE.md.
|
|
19528
|
+
`
|
|
19529
|
+
);
|
|
19530
|
+
}
|
|
19531
|
+
if (isDatabaseUser) {
|
|
19532
|
+
const projectId = randomUUID14();
|
|
19533
|
+
const mcpConfig = {
|
|
19534
|
+
mcpServers: {
|
|
19535
|
+
papi: {
|
|
19536
|
+
command: "npx",
|
|
19537
|
+
args: ["-y", "@papi-ai/server"],
|
|
19538
|
+
env: {
|
|
19539
|
+
PAPI_PROJECT_DIR: projectRoot,
|
|
19540
|
+
PAPI_ADAPTER: "pg",
|
|
19541
|
+
DATABASE_URL: process.env.DATABASE_URL || "<YOUR_DATABASE_URL>",
|
|
19542
|
+
PAPI_PROJECT_ID: projectId
|
|
19543
|
+
}
|
|
19084
19544
|
}
|
|
19085
19545
|
}
|
|
19086
|
-
}
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19546
|
+
};
|
|
19547
|
+
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
19548
|
+
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
19549
|
+
const output2 = [
|
|
19550
|
+
`# PAPI Initialised \u2014 ${projectName}`,
|
|
19551
|
+
"",
|
|
19552
|
+
`**Project ID:** \`${projectId}\``,
|
|
19553
|
+
`**Config:** \`${mcpJsonPath}\``,
|
|
19554
|
+
"",
|
|
19555
|
+
"## Next Steps",
|
|
19556
|
+
"",
|
|
19557
|
+
...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : ["1. **Set your DATABASE_URL** \u2014 replace `<YOUR_DATABASE_URL>` in `.mcp.json` with your Supabase connection string."],
|
|
19558
|
+
"2. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md."
|
|
19559
|
+
].join("\n");
|
|
19560
|
+
return textResponse(output2);
|
|
19561
|
+
}
|
|
19090
19562
|
const output = [
|
|
19091
|
-
`# PAPI
|
|
19563
|
+
`# PAPI \u2014 Account Required`,
|
|
19092
19564
|
"",
|
|
19093
|
-
|
|
19094
|
-
`**Config:** \`${mcpJsonPath}\``,
|
|
19565
|
+
`PAPI needs an account to store your project data.`,
|
|
19095
19566
|
"",
|
|
19096
|
-
"##
|
|
19567
|
+
"## Get Started in 3 Steps",
|
|
19097
19568
|
"",
|
|
19098
|
-
"1. **
|
|
19099
|
-
"2. **
|
|
19100
|
-
"3. **
|
|
19569
|
+
"1. **Sign up** at https://getpapi.ai/login",
|
|
19570
|
+
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
19571
|
+
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
19101
19572
|
"",
|
|
19102
|
-
"
|
|
19573
|
+
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
19574
|
+
"",
|
|
19575
|
+
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
19103
19576
|
].join("\n");
|
|
19104
19577
|
return textResponse(output);
|
|
19105
19578
|
}
|
|
19106
19579
|
|
|
19107
19580
|
// src/tools/orient.ts
|
|
19581
|
+
init_git();
|
|
19108
19582
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
19109
|
-
import { readFileSync as readFileSync2, writeFileSync, existsSync as
|
|
19110
|
-
import { join as
|
|
19583
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync5 } from "fs";
|
|
19584
|
+
import { join as join8 } from "path";
|
|
19111
19585
|
var orientTool = {
|
|
19112
19586
|
name: "orient",
|
|
19113
19587
|
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.",
|
|
@@ -19117,7 +19591,7 @@ var orientTool = {
|
|
|
19117
19591
|
required: []
|
|
19118
19592
|
}
|
|
19119
19593
|
};
|
|
19120
|
-
function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
|
|
19594
|
+
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot) {
|
|
19121
19595
|
const lines = [];
|
|
19122
19596
|
const cycleIsComplete = health.latestCycleStatus === "complete";
|
|
19123
19597
|
const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
|
|
@@ -19156,6 +19630,11 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
|
|
|
19156
19630
|
lines.push(`**North Star:** ${health.northStarSection}`);
|
|
19157
19631
|
lines.push("");
|
|
19158
19632
|
}
|
|
19633
|
+
if (health.healthScore !== null && health.healthStatus !== null) {
|
|
19634
|
+
const icon = health.healthStatus === "GREEN" ? "\u{1F7E2}" : health.healthStatus === "AMBER" ? "\u{1F7E1}" : "\u{1F534}";
|
|
19635
|
+
lines.push(`**Health:** ${icon} ${health.healthStatus} (${health.healthScore}/100) \u2014 ${health.healthReason}`);
|
|
19636
|
+
lines.push("");
|
|
19637
|
+
}
|
|
19159
19638
|
lines.push("## Board");
|
|
19160
19639
|
lines.push(health.boardSummary);
|
|
19161
19640
|
lines.push("");
|
|
@@ -19184,7 +19663,16 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
|
|
|
19184
19663
|
}
|
|
19185
19664
|
if (buildInfo.isEmpty) {
|
|
19186
19665
|
lines.push("## Tasks");
|
|
19187
|
-
|
|
19666
|
+
if (buildInfo.currentCycle === 0) {
|
|
19667
|
+
const codebaseType = projectRoot ? detectCodebaseType(projectRoot) : "new_project";
|
|
19668
|
+
if (codebaseType === "existing_codebase") {
|
|
19669
|
+
lines.push("No tasks found. Existing codebase detected \u2014 run `setup existing_project: true` to analyse your project structure, generate a Product Brief and initial backlog, then `plan` to start your first cycle.");
|
|
19670
|
+
} else {
|
|
19671
|
+
lines.push("No tasks found. Fresh project \u2014 run `setup` to define your Product Brief, then `plan` to create your first cycle.");
|
|
19672
|
+
}
|
|
19673
|
+
} else {
|
|
19674
|
+
lines.push("Board is empty \u2014 run `plan` to create your next cycle.");
|
|
19675
|
+
}
|
|
19188
19676
|
lines.push("");
|
|
19189
19677
|
} else if (buildInfo.noHandoffs) {
|
|
19190
19678
|
lines.push("## Tasks");
|
|
@@ -19222,15 +19710,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
|
|
|
19222
19710
|
async function getHierarchyPosition(adapter2) {
|
|
19223
19711
|
try {
|
|
19224
19712
|
const [horizons, stages, phases, allTasks] = await Promise.all([
|
|
19225
|
-
adapter2.readHorizons(),
|
|
19226
|
-
adapter2.readStages(),
|
|
19713
|
+
adapter2.readHorizons?.() ?? [],
|
|
19714
|
+
adapter2.readStages?.() ?? [],
|
|
19227
19715
|
adapter2.readPhases(),
|
|
19228
19716
|
adapter2.queryBoard()
|
|
19229
19717
|
]);
|
|
19230
19718
|
if (horizons.length === 0) return void 0;
|
|
19231
|
-
const activeHorizon = horizons.find((h) => h.status === "
|
|
19719
|
+
const activeHorizon = horizons.find((h) => h.status === "active") || horizons[0];
|
|
19232
19720
|
const activeStages = stages.filter((s) => s.horizonId === activeHorizon.id);
|
|
19233
|
-
const activeStage = activeStages.find((s) => s.status === "
|
|
19721
|
+
const activeStage = activeStages.find((s) => s.status === "active") || activeStages[0];
|
|
19234
19722
|
if (!activeStage) return void 0;
|
|
19235
19723
|
const stagePhases = phases.filter((p) => p.stageId === activeStage.id);
|
|
19236
19724
|
const activePhases = stagePhases.filter((p) => p.status === "In Progress");
|
|
@@ -19271,7 +19759,7 @@ function getLatestGitTag(projectRoot) {
|
|
|
19271
19759
|
}
|
|
19272
19760
|
function checkNpmVersionDrift() {
|
|
19273
19761
|
try {
|
|
19274
|
-
const pkgPath =
|
|
19762
|
+
const pkgPath = join8(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
19275
19763
|
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
19276
19764
|
const localVersion = pkg.version;
|
|
19277
19765
|
const packageName = pkg.name;
|
|
@@ -19363,6 +19851,19 @@ ${versionDrift}` : "";
|
|
|
19363
19851
|
}
|
|
19364
19852
|
} catch {
|
|
19365
19853
|
}
|
|
19854
|
+
let unrecordedNote = "";
|
|
19855
|
+
try {
|
|
19856
|
+
const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
|
|
19857
|
+
if (unrecorded.length > 0) {
|
|
19858
|
+
const lines = ["\n\n## Unrecorded Work"];
|
|
19859
|
+
lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
|
|
19860
|
+
for (const c of unrecorded) {
|
|
19861
|
+
lines.push(`- \`${c.hash}\` ${c.message}`);
|
|
19862
|
+
}
|
|
19863
|
+
unrecordedNote = lines.join("\n");
|
|
19864
|
+
}
|
|
19865
|
+
} catch {
|
|
19866
|
+
}
|
|
19366
19867
|
let recsNote = "";
|
|
19367
19868
|
try {
|
|
19368
19869
|
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
@@ -19396,15 +19897,15 @@ ${versionDrift}` : "";
|
|
|
19396
19897
|
}
|
|
19397
19898
|
} catch {
|
|
19398
19899
|
}
|
|
19399
|
-
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag) + ttfvNote + reconciliationNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
|
|
19900
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
|
|
19400
19901
|
} catch (err) {
|
|
19401
19902
|
const message = err instanceof Error ? err.message : String(err);
|
|
19402
19903
|
return errorResponse(`Orient failed: ${message}`);
|
|
19403
19904
|
}
|
|
19404
19905
|
}
|
|
19405
19906
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
19406
|
-
const claudeMdPath =
|
|
19407
|
-
if (!
|
|
19907
|
+
const claudeMdPath = join8(projectRoot, "CLAUDE.md");
|
|
19908
|
+
if (!existsSync5(claudeMdPath)) return "";
|
|
19408
19909
|
const content = readFileSync2(claudeMdPath, "utf-8");
|
|
19409
19910
|
const additions = [];
|
|
19410
19911
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
@@ -19487,6 +19988,9 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
19487
19988
|
const available = phases.map((p) => p.label).join(", ");
|
|
19488
19989
|
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"}`);
|
|
19489
19990
|
}
|
|
19991
|
+
if (!status) {
|
|
19992
|
+
return errorResponse("status is required for phase updates.");
|
|
19993
|
+
}
|
|
19490
19994
|
if (phase.status === status) {
|
|
19491
19995
|
return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
19492
19996
|
}
|
|
@@ -19540,6 +20044,9 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
19540
20044
|
const available = horizons.map((h) => h.label).join(", ");
|
|
19541
20045
|
return errorResponse(`Horizon "${name}" not found. Available horizons: ${available || "none"}`);
|
|
19542
20046
|
}
|
|
20047
|
+
if (!status) {
|
|
20048
|
+
return errorResponse("status is required for horizon updates.");
|
|
20049
|
+
}
|
|
19543
20050
|
if (horizon.status === status) {
|
|
19544
20051
|
return textResponse(`Horizon "${horizon.label}" is already "${status}". No change made.`);
|
|
19545
20052
|
}
|
|
@@ -19603,8 +20110,8 @@ async function assembleZoomOutContext(adapter2, cycleNumber, projectRoot) {
|
|
|
19603
20110
|
let hierarchyText = "";
|
|
19604
20111
|
try {
|
|
19605
20112
|
const [horizons, stages, phases] = await Promise.all([
|
|
19606
|
-
adapter2.readHorizons(),
|
|
19607
|
-
adapter2.readStages(),
|
|
20113
|
+
adapter2.readHorizons?.() ?? [],
|
|
20114
|
+
adapter2.readStages?.() ?? [],
|
|
19608
20115
|
adapter2.readPhases()
|
|
19609
20116
|
]);
|
|
19610
20117
|
if (horizons.length > 0) {
|
|
@@ -19891,8 +20398,8 @@ ${result.userMessage}
|
|
|
19891
20398
|
}
|
|
19892
20399
|
|
|
19893
20400
|
// src/tools/doc-registry.ts
|
|
19894
|
-
import { readdirSync as
|
|
19895
|
-
import { join as
|
|
20401
|
+
import { readdirSync as readdirSync4, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
20402
|
+
import { join as join9, relative } from "path";
|
|
19896
20403
|
import { homedir as homedir2 } from "os";
|
|
19897
20404
|
var docRegisterTool = {
|
|
19898
20405
|
name: "doc_register",
|
|
@@ -20033,12 +20540,12 @@ ${d.summary}
|
|
|
20033
20540
|
${lines.join("\n---\n\n")}`);
|
|
20034
20541
|
}
|
|
20035
20542
|
function scanMdFiles(dir, rootDir) {
|
|
20036
|
-
if (!
|
|
20543
|
+
if (!existsSync6(dir)) return [];
|
|
20037
20544
|
const files = [];
|
|
20038
20545
|
try {
|
|
20039
|
-
const entries =
|
|
20546
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20040
20547
|
for (const entry of entries) {
|
|
20041
|
-
const full =
|
|
20548
|
+
const full = join9(dir, entry.name);
|
|
20042
20549
|
if (entry.isDirectory()) {
|
|
20043
20550
|
files.push(...scanMdFiles(full, rootDir));
|
|
20044
20551
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -20067,17 +20574,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
20067
20574
|
const includePlans = args.include_plans ?? false;
|
|
20068
20575
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
20069
20576
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
20070
|
-
const docsDir =
|
|
20577
|
+
const docsDir = join9(config2.projectRoot, "docs");
|
|
20071
20578
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
20072
20579
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
20073
20580
|
let unregisteredPlans = [];
|
|
20074
20581
|
if (includePlans) {
|
|
20075
|
-
const plansDir =
|
|
20076
|
-
if (
|
|
20582
|
+
const plansDir = join9(homedir2(), ".claude", "plans");
|
|
20583
|
+
if (existsSync6(plansDir)) {
|
|
20077
20584
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
20078
20585
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
20079
20586
|
path: f,
|
|
20080
|
-
title: extractTitle(
|
|
20587
|
+
title: extractTitle(join9(plansDir, f.replace("plans/", "")))
|
|
20081
20588
|
}));
|
|
20082
20589
|
}
|
|
20083
20590
|
}
|
|
@@ -20088,7 +20595,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
20088
20595
|
if (unregisteredDocs.length > 0) {
|
|
20089
20596
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
20090
20597
|
for (const f of unregisteredDocs) {
|
|
20091
|
-
const title = extractTitle(
|
|
20598
|
+
const title = extractTitle(join9(config2.projectRoot, f));
|
|
20092
20599
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
20093
20600
|
}
|
|
20094
20601
|
}
|
|
@@ -20252,8 +20759,8 @@ function createServer(adapter2, config2) {
|
|
|
20252
20759
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
20253
20760
|
);
|
|
20254
20761
|
const __filename = fileURLToPath(import.meta.url);
|
|
20255
|
-
const
|
|
20256
|
-
const skillsDir =
|
|
20762
|
+
const __dirname2 = dirname(__filename);
|
|
20763
|
+
const skillsDir = join10(__dirname2, "..", "skills");
|
|
20257
20764
|
function parseSkillFrontmatter(content) {
|
|
20258
20765
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
20259
20766
|
if (!match) return null;
|
|
@@ -20271,7 +20778,7 @@ function createServer(adapter2, config2) {
|
|
|
20271
20778
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
20272
20779
|
const prompts = [];
|
|
20273
20780
|
for (const file of mdFiles) {
|
|
20274
|
-
const content = await readFile5(
|
|
20781
|
+
const content = await readFile5(join10(skillsDir, file), "utf-8");
|
|
20275
20782
|
const meta = parseSkillFrontmatter(content);
|
|
20276
20783
|
if (meta) {
|
|
20277
20784
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -20287,7 +20794,7 @@ function createServer(adapter2, config2) {
|
|
|
20287
20794
|
try {
|
|
20288
20795
|
const files = await readdir2(skillsDir);
|
|
20289
20796
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
20290
|
-
const content = await readFile5(
|
|
20797
|
+
const content = await readFile5(join10(skillsDir, file), "utf-8");
|
|
20291
20798
|
const meta = parseSkillFrontmatter(content);
|
|
20292
20799
|
if (meta?.name === name) {
|
|
20293
20800
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -20354,7 +20861,7 @@ function createServer(adapter2, config2) {
|
|
|
20354
20861
|
return {
|
|
20355
20862
|
content: [{
|
|
20356
20863
|
type: "text",
|
|
20357
|
-
text: `No project found for PAPI_PROJECT_ID \`${
|
|
20864
|
+
text: `No project found for PAPI_PROJECT_ID \`${process.env.PAPI_PROJECT_ID ?? "(not set)"}\`. Run \`setup\` first to initialise your project.`
|
|
20358
20865
|
}]
|
|
20359
20866
|
};
|
|
20360
20867
|
}
|
|
@@ -20488,15 +20995,88 @@ function createServer(adapter2, config2) {
|
|
|
20488
20995
|
}
|
|
20489
20996
|
|
|
20490
20997
|
// src/index.ts
|
|
20998
|
+
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
20999
|
+
var pkgVersion = "unknown";
|
|
21000
|
+
try {
|
|
21001
|
+
const pkg = JSON.parse(readFileSync4(join11(__dirname, "..", "package.json"), "utf-8"));
|
|
21002
|
+
pkgVersion = pkg.version;
|
|
21003
|
+
} catch {
|
|
21004
|
+
}
|
|
21005
|
+
var cliArgs = process.argv.slice(2);
|
|
21006
|
+
if (cliArgs.includes("--help") || cliArgs.includes("-h")) {
|
|
21007
|
+
console.log(`papi-server v${pkgVersion} \u2014 PAPI MCP server
|
|
21008
|
+
|
|
21009
|
+
Usage: npx @papi-ai/server [options]
|
|
21010
|
+
|
|
21011
|
+
Options:
|
|
21012
|
+
--help, -h Show this help message
|
|
21013
|
+
--version, -v Show version number
|
|
21014
|
+
--project <dir> Set the project directory
|
|
21015
|
+
|
|
21016
|
+
Getting started:
|
|
21017
|
+
1. Sign up at https://getpapi.ai/login
|
|
21018
|
+
2. Complete the onboarding wizard to get your API key
|
|
21019
|
+
3. Add the generated config to your MCP client
|
|
21020
|
+
4. Say "run setup" in your AI tool
|
|
21021
|
+
|
|
21022
|
+
Docs: https://getpapi.ai/docs
|
|
21023
|
+
`);
|
|
21024
|
+
process.exit(0);
|
|
21025
|
+
}
|
|
21026
|
+
if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
|
|
21027
|
+
console.log(pkgVersion);
|
|
21028
|
+
process.exit(0);
|
|
21029
|
+
}
|
|
20491
21030
|
process.on("unhandledRejection", (err) => {
|
|
20492
21031
|
console.error("[papi] unhandledRejection (swallowed):", err instanceof Error ? err.message : err);
|
|
20493
21032
|
});
|
|
20494
21033
|
var config = loadConfig();
|
|
20495
|
-
var adapter
|
|
20496
|
-
|
|
20497
|
-
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
|
|
21034
|
+
var adapter;
|
|
21035
|
+
var setupError;
|
|
21036
|
+
try {
|
|
21037
|
+
adapter = await createAdapter({
|
|
21038
|
+
adapterType: config.adapterType,
|
|
21039
|
+
papiDir: config.papiDir,
|
|
21040
|
+
papiEndpoint: config.papiEndpoint
|
|
21041
|
+
});
|
|
21042
|
+
} catch (err) {
|
|
21043
|
+
setupError = err instanceof Error ? err.message : String(err);
|
|
21044
|
+
process.stderr.write(`[papi] Startup error: ${setupError}
|
|
21045
|
+
`);
|
|
21046
|
+
}
|
|
21047
|
+
var server;
|
|
21048
|
+
if (adapter && !setupError) {
|
|
21049
|
+
server = createServer(adapter, config);
|
|
21050
|
+
} else {
|
|
21051
|
+
server = new Server2(
|
|
21052
|
+
{ name: "papi", version: pkgVersion },
|
|
21053
|
+
{ capabilities: { tools: {} } }
|
|
21054
|
+
);
|
|
21055
|
+
const errorMessage = setupError || "Unknown startup error";
|
|
21056
|
+
server.setRequestHandler(ListToolsRequestSchema2, async () => ({
|
|
21057
|
+
tools: [{
|
|
21058
|
+
name: "setup",
|
|
21059
|
+
description: "PAPI is not connected \u2014 run this tool for setup instructions.",
|
|
21060
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
21061
|
+
}]
|
|
21062
|
+
}));
|
|
21063
|
+
server.setRequestHandler(CallToolRequestSchema2, async () => ({
|
|
21064
|
+
content: [{
|
|
21065
|
+
type: "text",
|
|
21066
|
+
text: `# PAPI Connection Error
|
|
21067
|
+
|
|
21068
|
+
${errorMessage}
|
|
21069
|
+
|
|
21070
|
+
## Quick Fix
|
|
21071
|
+
|
|
21072
|
+
If you haven't set up PAPI yet:
|
|
21073
|
+
1. Go to https://getpapi.ai/login and sign up
|
|
21074
|
+
2. Complete the onboarding wizard \u2014 it generates your config
|
|
21075
|
+
3. Copy the config to your project and restart your AI tool
|
|
21076
|
+
|
|
21077
|
+
If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_DATA_API_KEY** are set in your .mcp.json env config.`
|
|
21078
|
+
}]
|
|
21079
|
+
}));
|
|
21080
|
+
}
|
|
20501
21081
|
var transport = new StdioServerTransport();
|
|
20502
21082
|
await server.connect(transport);
|