@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/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/index.ts
8382
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8383
-
8384
- // src/config.ts
8385
- import path from "path";
8386
- function loadConfig() {
8387
- const projectArgIdx = process.argv.indexOf("--project");
8388
- const projectRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
8389
- if (!projectRoot) {
8390
- throw new Error(
8391
- "Project root required. Pass --project /path/to/project or set PAPI_PROJECT_DIR."
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
- const email = execSync("git config user.email", { encoding: "utf8", timeout: 5e3 }).trim();
8424
- if (email) return email;
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
- const ghUser = execSync("gh api user --jq .email", { encoding: "utf8", timeout: 1e4 }).trim();
8429
- if (ghUser && ghUser !== "null") return ghUser;
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
- return void 0;
8433
- }
8434
- var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
8435
- var PLACEHOLDER_PATTERNS = [
8436
- "<YOUR_DATABASE_URL>",
8437
- "your-database-url",
8438
- "your_database_url",
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
- if (!lower.startsWith("postgres://") && !lower.startsWith("postgresql://")) {
8452
- throw new Error(
8453
- `DATABASE_URL must be a PostgreSQL connection string (postgres:// or postgresql://).
8454
- Got: "${connectionString.slice(0, 30)}..."
8455
- Check your .mcp.json configuration.`
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
- var _connectionStatus = "offline";
8460
- function getConnectionStatus() {
8461
- return _connectionStatus;
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
- async function createAdapter(optionsOrType, maybePapiDir) {
8464
- const options = typeof optionsOrType === "string" ? { adapterType: optionsOrType, papiDir: maybePapiDir } : optionsOrType;
8465
- const { adapterType, papiDir, papiEndpoint } = options;
8466
- switch (adapterType) {
8467
- case "md":
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
- // src/server.ts
8587
- import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
8588
- import { join as join9, dirname } from "path";
8589
- import { fileURLToPath } from "url";
8590
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8591
- import {
8592
- CallToolRequestSchema,
8593
- ListToolsRequestSchema,
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 errorResponse(message) {
8609
- return { content: [{ type: "text", text: `Error: ${message}` }] };
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
- // src/services/plan.ts
8613
- init_dist2();
8614
- import { createHash, randomUUID as randomUUID7 } from "crypto";
8615
- import { readFile as readFile2 } from "fs/promises";
8616
- import path3 from "path";
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 formatActiveDecisionsForReview(decisions) {
8626
- if (decisions.length === 0) return "No active decisions.";
8627
- return decisions.map((d) => {
8628
- const lifecycle = d.outcome && d.outcome !== "pending" ? ` | Outcome: ${d.outcome}` + (d.revisionCount ? ` | ${d.revisionCount} revision(s)` : "") : "";
8629
- const summary = extractDecisionSummary(d.body);
8630
- const summaryLine = summary ? `
8631
- Summary: ${summary}` : "";
8632
- if (d.superseded) {
8633
- return `- ${d.id}: ${d.title} [SUPERSEDED by ${d.supersededBy}${lifecycle}]${summaryLine}`;
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 extractDecisionSummary(body) {
8639
- if (!body) return "";
8640
- const lines = body.split("\n");
8641
- for (const line of lines) {
8642
- const trimmed = line.trim();
8643
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
8644
- const clean = trimmed.replace(/\*\*/g, "").replace(/\*/g, "");
8645
- return clean.length > 150 ? clean.slice(0, 147) + "..." : clean;
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 formatDecisionLifecycleSummary(decisions) {
8650
- const active = decisions.filter((d) => !d.superseded);
8651
- if (active.length === 0) return void 0;
8652
- const counts = {};
8653
- for (const d of active) {
8654
- const outcome = d.outcome ?? "pending";
8655
- counts[outcome] = (counts[outcome] ?? 0) + 1;
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
- const parts = Object.entries(counts).map(([k, v]) => `${v} ${k}`);
8658
- const revised = active.filter((d) => (d.revisionCount ?? 0) > 0);
8659
- if (revised.length > 0) {
8660
- parts.push(`${revised.length} with revisions`);
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 formatBuildReports(reports) {
8665
- if (reports.length === 0) return "No build reports yet.";
8666
- return reports.map(
8667
- (r) => `### ${r.taskName} \u2014 ${r.date} \u2014 Cycle ${r.cycle}
8668
-
8669
- - **Completed:** ${r.completed}
8670
- - **Actual Effort:** ${r.actualEffort} vs estimated ${r.estimatedEffort}
8671
- ` + (r.correctionsCount ? `- **Corrections:** ${r.correctionsCount}
8672
- ` : "") + (Array.isArray(r.briefImplications) && r.briefImplications.length ? `- **Brief Implications:** ${r.briefImplications.map((bi) => `[${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("; ")}
8673
- ` : "") + `- **Surprises:** ${r.surprises}
8674
- - **Discovered Issues:** ${r.discoveredIssues}
8675
- - **Architecture Notes:** ${r.architectureNotes}` + (r.deadEnds ? `
8676
- - **Dead Ends:** ${r.deadEnds}` : "")
8677
- ).join("\n\n---\n\n");
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 formatRecentlyShippedCapabilities(reports) {
8680
- const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
8681
- if (completed.length === 0) return void 0;
8682
- const lines = completed.map((r) => {
8683
- const parts = [`- **${r.taskId}:** ${r.taskName}`];
8684
- if (r.architectureNotes && r.architectureNotes !== "None") {
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
- return parts.join("\n");
8692
- });
8693
- return [
8694
- `${completed.length} task(s) completed in recent cycles:`,
8695
- "",
8696
- ...lines,
8697
- "",
8698
- "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."
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 formatCompactTask(task) {
8730
- const handoffTag = task.hasHandoff ? " | \u2713 handoff" : "";
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 formatDetailedTask(t) {
8735
- const notes = truncateNotes(t.notes, PLAN_NOTES_MAX_LENGTH);
8736
- const hasHandoff = t.hasHandoff;
8737
- const typeTag = t.taskType && t.taskType !== "task" ? ` | Type: ${t.taskType}` : "";
8738
- return `- **${t.id}:** ${t.title}
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
- const recent = [];
8774
- const stable = [];
8775
- for (const t of active) {
8776
- if (isRecentTask(t, currentCycle)) {
8777
- recent.push(t);
8778
- } else {
8779
- stable.push(t);
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
- const sections = [summary];
8783
- if (recent.length > 0) {
8784
- sections.push(recent.map(formatDetailedTask).join("\n\n"));
8785
- }
8786
- if (stable.length > 0) {
8787
- sections.push(
8788
- `**Stable backlog (${stable.length} tasks \u2014 compact):**
8789
- ` + stable.map(formatCompactTask).join("\n")
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 formatBoardForReview(tasks) {
8795
- if (tasks.length === 0) return "No tasks on the board.";
8796
- return tasks.map(
8797
- (t) => `- **${t.id}:** ${t.title}
8798
- Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}
8799
- Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
8800
- Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${t.notes ? `
8801
- Notes: ${t.notes}` : ""}`
8802
- ).join("\n\n");
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 trendArrow(current, previous, higherIsBetter) {
8805
- if (previous === void 0) return "";
8806
- if (current === previous) return " \u2192";
8807
- const improving = higherIsBetter ? current > previous : current < previous;
8808
- return improving ? " \u2191" : " \u2193";
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
- var EFFORT_MAP2 = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
8811
- function computeSnapshotsFromBuildReports(reports) {
8812
- if (reports.length === 0) return [];
8813
- const byCycleMap = /* @__PURE__ */ new Map();
8814
- for (const r of reports) {
8815
- const existing = byCycleMap.get(r.cycle) ?? [];
8816
- existing.push(r);
8817
- byCycleMap.set(r.cycle, existing);
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
- const snapshots = [];
8820
- for (const [sn, cycleReports] of byCycleMap) {
8821
- const completed = cycleReports.filter((r) => r.completed === "Yes").length;
8822
- const total = cycleReports.length;
8823
- const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
8824
- const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
8825
- const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
8826
- let effortPoints = 0;
8827
- for (const r of cycleReports) {
8828
- effortPoints += EFFORT_MAP2[r.actualEffort] ?? 3;
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 formatCycleMetrics(snapshots) {
8841
- if (snapshots.length === 0) return "No methodology metrics yet.";
8842
- const latest = snapshots[snapshots.length - 1];
8843
- const previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] : void 0;
8844
- const lines = [];
8845
- if (latest.velocity.length > 0) {
8846
- const latestV = latest.velocity[latest.velocity.length - 1];
8847
- lines.push("**Cycle Sizing**");
8848
- lines.push(`- Last cycle: ${latestV.completed} tasks, ${latestV.effortPoints} effort points`);
8849
- if (latestV.partial > 0 || latestV.failed > 0) {
8850
- lines.push(`- Partial: ${latestV.partial} | Failed: ${latestV.failed}`);
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 allVelocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
8854
- const recentVelocities = allVelocities.slice(-5);
8855
- if (recentVelocities.length > 0) {
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
- if (latest.accuracy.length > 0) {
8866
- const latestA = latest.accuracy[latest.accuracy.length - 1];
8867
- const prevA = previous?.accuracy[previous.accuracy.length - 1];
8868
- lines.push("");
8869
- lines.push("**Estimation Accuracy**");
8870
- lines.push(`- Match rate: ${latestA.matchRate}%${trendArrow(latestA.matchRate, prevA?.matchRate, true)}`);
8871
- lines.push(`- MAE: ${latestA.mae}${trendArrow(latestA.mae, prevA?.mae, false)}`);
8872
- lines.push(`- Bias: ${latestA.bias >= 0 ? "+" : ""}${latestA.bias}${trendArrow(Math.abs(latestA.bias), prevA ? Math.abs(prevA.bias) : void 0, false)}`);
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 formatDerivedMetrics(snapshots, backlogTasks) {
8877
- const lines = [];
8878
- if (snapshots.length > 0) {
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 activeTasks = backlogTasks.filter(
8890
- (t) => t.status === "Backlog" || t.status === "In Cycle" || t.status === "Ready"
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 (canvas.userJourneys && canvas.userJourneys.length > 0) {
9001
- sections.push("**User Journeys:**");
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 (canvas.mvpBoundary) {
9008
- sections.push("**MVP Boundary:**", canvas.mvpBoundary);
8830
+ if (hasUnpushedCommits(cwd)) {
8831
+ return { pulled: false, warning: "Skipping pull \u2014 unpushed commits on current branch." };
9009
8832
  }
9010
- if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
9011
- sections.push("**Assumptions & Open Questions:**");
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
- if (canvas.successSignals && canvas.successSignals.length > 0) {
9018
- sections.push("**Success Signals:**");
9019
- for (const s of canvas.successSignals) {
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 sections.length > 0 ? sections.join("\n") : void 0;
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
- execFileSync("git", ["--version"], { stdio: "ignore" });
9033
- return true;
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 false;
8852
+ return [];
9036
8853
  }
9037
8854
  }
9038
- function isGitRepo(cwd) {
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", ["rev-parse", "--is-inside-work-tree"], {
8865
+ const mergedOutput = execFileSync("git", ["branch", "--merged", "HEAD"], {
9041
8866
  cwd,
9042
- stdio: "ignore"
8867
+ encoding: "utf-8",
8868
+ stdio: ["ignore", "pipe", "ignore"]
9043
8869
  });
9044
- return true;
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 false;
8891
+ return empty;
9047
8892
  }
9048
8893
  }
9049
- function stageDirAndCommit(cwd, dir, message) {
8894
+ function detectUnrecordedCommits(cwd, baseBranch) {
8895
+ if (!isGitAvailable() || !isGitRepo(cwd)) return [];
8896
+ const latestTag = getLatestTag(cwd);
8897
+ if (!latestTag) return [];
9050
8898
  try {
9051
- execFileSync("git", ["check-ignore", "-q", dir], { cwd });
9052
- return { committed: false, message: `Skipped commit \u2014 '${dir}' is gitignored.` };
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 stageAllAndCommit(cwd, message) {
9067
- execFileSync("git", ["add", "."], { cwd });
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 getCurrentBranch(cwd) {
8929
+ function getHeadCommitSha(cwd) {
9079
8930
  try {
9080
- return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
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 hasUncommittedChanges(cwd, ignore) {
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 args = ["status", "--porcelain"];
9091
- if (ignore?.length) {
9092
- args.push("--", ".", ...ignore.map((p) => `:!${p}`));
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 branchExists(cwd, branch) {
8946
+ function getFilesChangedFromBase(cwd, baseBranch) {
9104
8947
  try {
9105
- execFileSync("git", ["rev-parse", "--verify", branch], {
9106
- cwd,
9107
- stdio: "ignore"
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 false;
8952
+ return [];
9112
8953
  }
9113
8954
  }
9114
- function checkoutBranch(cwd, branch) {
9115
- try {
9116
- execFileSync("git", ["checkout", branch], { cwd, encoding: "utf-8" });
9117
- return { success: true, message: `Checked out branch '${branch}'.` };
9118
- } catch {
9119
- return { success: false, message: `Failed to checkout branch '${branch}'.` };
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
- function createAndCheckoutBranch(cwd, branch) {
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
- execFileSync("git", ["checkout", "-b", branch], { cwd, encoding: "utf-8" });
9125
- return { success: true, message: `Created and checked out branch '${branch}'.` };
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 remotes = execFileSync("git", ["remote"], {
9133
- cwd,
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 GIT_NETWORK_TIMEOUT_MS = 6e4;
9142
- function gitPull(cwd) {
9143
- try {
9144
- execFileSync("git", ["pull"], { cwd, encoding: "utf-8", timeout: GIT_NETWORK_TIMEOUT_MS });
9145
- return { success: true, message: "Pulled latest changes." };
9146
- } catch (err) {
9147
- const msg = err instanceof Error ? err.message : String(err);
9148
- const isTimeout = msg.includes("ETIMEDOUT") || msg.includes("killed");
9149
- return {
9150
- success: false,
9151
- message: isTimeout ? "Pull timed out after 30s." : `Pull failed: ${msg}`
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
- function gitPush(cwd, branch) {
9156
- try {
9157
- execFileSync("git", ["push", "-u", "origin", branch], {
9158
- cwd,
9159
- encoding: "utf-8",
9160
- timeout: GIT_NETWORK_TIMEOUT_MS
9161
- });
9162
- return { success: true, message: `Pushed branch '${branch}' to origin.` };
9163
- } catch (err) {
9164
- const msg = err instanceof Error ? err.message : String(err);
9165
- const isTimeout = msg.includes("ETIMEDOUT") || msg.includes("killed");
9166
- return {
9167
- success: false,
9168
- message: isTimeout ? `Push timed out after 30s.` : `Push failed: ${msg}`
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
- function isGhAvailable() {
9173
- try {
9174
- execFileSync("gh", ["--version"], { stdio: "ignore" });
9175
- return true;
9176
- } catch {
9177
- return false;
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 getOriginRepoSlug(cwd) {
9181
- try {
9182
- const url = execFileSync("git", ["remote", "get-url", "origin"], {
9183
- cwd,
9184
- encoding: "utf-8"
9185
- }).trim();
9186
- const sshMatch = url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
9187
- if (sshMatch) return sshMatch[1];
9188
- const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
9189
- if (httpsMatch) return httpsMatch[1];
9190
- return null;
9191
- } catch {
9192
- return null;
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 createPullRequest(cwd, branch, baseBranch, title, body) {
9196
- try {
9197
- const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
9198
- const repo = getOriginRepoSlug(cwd);
9199
- if (repo) {
9200
- args.push("--repo", repo);
9201
- }
9202
- const output = execFileSync("gh", args, { cwd, encoding: "utf-8" }).trim();
9203
- return { success: true, message: output };
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 sleepSync(ms) {
9212
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
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 MERGE_RETRY_DELAY_MS = 2e3;
9215
- var MERGE_MAX_RETRIES = 3;
9216
- function mergePullRequest(cwd, branch) {
9217
- const repo = getOriginRepoSlug(cwd);
9218
- const baseArgs = ["pr", "merge", branch, "--merge", "--delete-branch"];
9219
- if (repo) {
9220
- baseArgs.push("--repo", repo);
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
- for (let attempt = 1; attempt <= MERGE_MAX_RETRIES; attempt++) {
9223
- try {
9224
- execFileSync("gh", baseArgs, { cwd, encoding: "utf-8" });
9225
- return { success: true, message: `Merged PR for '${branch}' and deleted branch.` };
9226
- } catch (err) {
9227
- const msg = err instanceof Error ? err.message : String(err);
9228
- const isNotMergeable = msg.includes("not mergeable");
9229
- if (isNotMergeable && attempt < MERGE_MAX_RETRIES) {
9230
- sleepSync(MERGE_RETRY_DELAY_MS);
9231
- continue;
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
- return { success: false, message: "PR merge failed: max retries exceeded" };
9459
+ snapshots.sort((a, b2) => a.cycle - b2.cycle);
9460
+ return snapshots;
9237
9461
  }
9238
- function deleteLocalBranch(cwd, branch) {
9239
- try {
9240
- execFileSync("git", ["branch", "-d", branch], { cwd, encoding: "utf-8" });
9241
- return { success: true, message: `Deleted local branch '${branch}'.` };
9242
- } catch (err) {
9243
- return {
9244
- success: false,
9245
- message: `Failed to delete local branch '${branch}': ${err instanceof Error ? err.message : String(err)}`
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
- function tagExists(cwd, tag) {
9250
- try {
9251
- execFileSync("git", ["rev-parse", "--verify", `refs/tags/${tag}`], {
9252
- cwd,
9253
- stdio: "ignore"
9254
- });
9255
- return true;
9256
- } catch {
9257
- return false;
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
- function createTag(cwd, tag, message) {
9261
- try {
9262
- execFileSync("git", ["tag", "-a", tag, "-m", message], { cwd, encoding: "utf-8" });
9263
- return { success: true, message: `Created tag '${tag}'.` };
9264
- } catch (err) {
9265
- return {
9266
- success: false,
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 getLatestTag(cwd) {
9272
- try {
9273
- return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
9274
- cwd,
9275
- encoding: "utf-8"
9276
- }).trim() || null;
9277
- } catch {
9278
- return null;
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
- function getCommitsSinceTag(cwd, tag) {
9282
- try {
9283
- const output = execFileSync(
9284
- "git",
9285
- ["log", `${tag}..HEAD`, "--oneline"],
9286
- { cwd, encoding: "utf-8" }
9287
- ).trim();
9288
- return output ? output.split("\n") : [];
9289
- } catch {
9290
- return [];
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
- async function withBaseBranchSync(config2, fn) {
9294
- const warnings = [];
9295
- if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
9296
- return { result: await fn(), warnings };
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
- const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
9299
- if (baseBranch !== config2.baseBranch) {
9300
- warnings.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
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 currentBranch = getCurrentBranch(config2.projectRoot);
9303
- const needsBranchSwitch = currentBranch !== null && currentBranch !== baseBranch;
9304
- let previousBranch = null;
9305
- if (needsBranchSwitch && hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
9306
- warnings.push("Skipping pull \u2014 uncommitted changes detected. Board data may be stale.");
9307
- } else if (needsBranchSwitch) {
9308
- const checkout = checkoutBranch(config2.projectRoot, baseBranch);
9309
- if (!checkout.success) {
9310
- warnings.push(`Could not switch to ${baseBranch}: ${checkout.message} Board data may be stale.`);
9311
- } else {
9312
- previousBranch = currentBranch;
9313
- if (hasRemote(config2.projectRoot)) {
9314
- const pull = gitPull(config2.projectRoot);
9315
- if (!pull.success && config2.abortOnConflict && /conflict/i.test(pull.message)) {
9316
- checkoutBranch(config2.projectRoot, previousBranch);
9317
- return { result: void 0, warnings, abort: pull.message };
9318
- }
9319
- warnings.push(pull.success ? `Synced from ${baseBranch}.` : `Pull failed: ${pull.message}`);
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 result = await fn();
9330
- if (previousBranch) {
9331
- checkoutBranch(config2.projectRoot, previousBranch);
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
- return { result, warnings };
9334
- }
9335
- function getUnmergedBranches(cwd, baseBranch) {
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 resolveBaseBranch(cwd, preferred) {
9349
- if (branchExists(cwd, preferred)) return preferred;
9350
- if (preferred !== "main" && branchExists(cwd, "main")) return "main";
9351
- if (preferred !== "master" && branchExists(cwd, "master")) return "master";
9352
- return preferred;
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 detectBoardMismatches(cwd, tasks) {
9355
- const empty = { codeAhead: [], staleInProgress: [] };
9356
- if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
9357
- try {
9358
- const mergedOutput = execFileSync("git", ["branch", "--merged", "HEAD"], {
9359
- cwd,
9360
- encoding: "utf-8",
9361
- stdio: ["ignore", "pipe", "ignore"]
9362
- });
9363
- const allOutput = execFileSync("git", ["branch"], {
9364
- cwd,
9365
- encoding: "utf-8",
9366
- stdio: ["ignore", "pipe", "ignore"]
9367
- });
9368
- const parseBranches = (raw) => new Set(raw.split("\n").map((l) => l.trim().replace(/^\* /, "")).filter(Boolean));
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 taskBranchName(taskId) {
9388
- return `feat/${taskId}`;
9389
- }
9390
- function getHeadCommitSha(cwd) {
9391
- try {
9392
- return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
9393
- } catch {
9394
- return null;
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
- function runAutoCommit(projectRoot, commitFn) {
9398
- if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
9399
- if (!isGitRepo(projectRoot)) return "Auto-commit: skipped (not a git repository).";
9400
- try {
9401
- const result = commitFn();
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
- function getFilesChangedFromBase(cwd, baseBranch) {
9408
- try {
9409
- const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
9410
- const output = execFileSync("git", ["diff", "--name-only", mergeBase, "HEAD"], { cwd, encoding: "utf-8" }).trim();
9411
- return output ? output.split("\n").filter(Boolean) : [];
9412
- } catch {
9413
- return [];
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 handoffs2 = (data.cycleHandoffs ?? []).map((h) => {
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 = handoffs2.length;
11564
- const legacyEffortPoints = handoffs2.reduce((sum, h) => sum + (effortMapLegacy[h.handoff.effort] ?? 3), 0);
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: handoffs2,
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 cycleTaskCount = handoffs.length;
11664
- const cycleEffortPoints = handoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
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.getProductBrief()
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]?.date ? new Date(previousStrategyReviews[0].date) : /* @__PURE__ */ new Date(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" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
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 Promise.all(recs.map((rec) => adapter2.writeRecommendation(rec)));
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" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
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: "strategy_capture",
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 join3, basename, extname } from "path";
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(join3(config2.papiDir, filename), content, "utf-8");
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 = join3(config2.projectRoot, ".claude", "commands");
15098
- const docsDir = join3(config2.projectRoot, "docs");
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 = join3(config2.projectRoot, "CLAUDE.md");
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 = join3(docsDir, "INDEX.md");
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
- [join3(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
15117
- [join3(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
15118
- [join3(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
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 = join3(config2.projectRoot, ".cursor");
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 = join3(cursorDir, "rules");
15144
- const cursorRulesPath = join3(cursorRulesDir, "papi.mdc");
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 = join3(projectRoot, ".claude", "settings.json");
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(join3(projectRoot, ".claude"), { recursive: true });
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(join3(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
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
- await adapter2.updateActiveDecision(ad.id, ad.body, 0);
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 = join3(config2.projectRoot, "CLAUDE.md");
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(join3(projectRoot, "package.json"), "utf-8");
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(join3(projectRoot, name), 5e3);
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(join3(projectRoot, dir), { withFileTypes: true });
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 (input.existingProject) {
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 = input.existingProject && codebaseSummary ? {
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 = join3(config2.projectRoot, "CLAUDE.md");
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(join3(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
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 = input.existingProject;
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 readdirSync2, existsSync as existsSync2, readFileSync } from "fs";
15822
- import { join as join4 } from "path";
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 = join4(config2.projectRoot, "docs");
16224
- if (existsSync2(docsDir)) {
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 = readdirSync2(dir, { withFileTypes: true });
16584
+ const entries = readdirSync3(dir, { withFileTypes: true });
16228
16585
  const files = [];
16229
16586
  for (const e of entries) {
16230
- const full = join4(dir, e.name);
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(join4(config2.projectRoot, docPath), docPath, cycleNumber);
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 === "Not Started" || mapping.horizon.status === "Not Started";
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 Not Started stages/horizons)");
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 Not Started. Consider deferring or reassigning.`);
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.readActiveDecisions();
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 join5 } from "path";
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 = join5(config2.projectRoot, "CHANGELOG.md");
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 existsSync3 } from "fs";
18570
- import { join as join6 } from "path";
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 = join6(config2.projectRoot, ".papi");
18809
- if (existsSync3(papiDir)) {
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 projectId = randomUUID14();
19074
- const mcpConfig = {
19075
- mcpServers: {
19076
- papi: {
19077
- command: "npx",
19078
- args: ["-y", "@papi-ai/server"],
19079
- env: {
19080
- PAPI_PROJECT_DIR: projectRoot,
19081
- PAPI_ADAPTER: "pg",
19082
- DATABASE_URL: "<YOUR_DATABASE_URL>",
19083
- PAPI_PROJECT_ID: projectId
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
- await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
19089
- await ensureGitignoreEntry(projectRoot, ".mcp.json");
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 Initialised \u2014 ${projectName}`,
19563
+ `# PAPI \u2014 Account Required`,
19092
19564
  "",
19093
- `**Project ID:** \`${projectId}\``,
19094
- `**Config:** \`${mcpJsonPath}\``,
19565
+ `PAPI needs an account to store your project data.`,
19095
19566
  "",
19096
- "## Next Steps",
19567
+ "## Get Started in 3 Steps",
19097
19568
  "",
19098
- "1. **Set your DATABASE_URL** \u2014 replace `<YOUR_DATABASE_URL>` in `.mcp.json` with the connection string provided to you.",
19099
- "2. **Restart Claude Code** \u2014 it will detect the new MCP server on restart.",
19100
- "3. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md.",
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
- "> `.mcp.json` uses `npx @papi-ai/server` \u2014 no local paths to maintain. See `docs/install-guide.md` for multi-client configs."
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 existsSync4 } from "fs";
19110
- import { join as join7 } from "path";
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
- lines.push(buildInfo.currentCycle === 0 ? "No tasks found \u2014 run `setup` then `plan` to create your first cycle." : "Board is empty \u2014 run `plan` to create your next cycle.");
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 === "Active") || horizons[0];
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 === "Active") || activeStages[0];
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 = join7(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
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 = join7(projectRoot, "CLAUDE.md");
19407
- if (!existsSync4(claudeMdPath)) return "";
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 readdirSync3, existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
19895
- import { join as join8, relative } from "path";
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 (!existsSync5(dir)) return [];
20543
+ if (!existsSync6(dir)) return [];
20037
20544
  const files = [];
20038
20545
  try {
20039
- const entries = readdirSync3(dir, { withFileTypes: true });
20546
+ const entries = readdirSync4(dir, { withFileTypes: true });
20040
20547
  for (const entry of entries) {
20041
- const full = join8(dir, entry.name);
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 = join8(config2.projectRoot, "docs");
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 = join8(homedir2(), ".claude", "plans");
20076
- if (existsSync5(plansDir)) {
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(join8(plansDir, f.replace("plans/", "")))
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(join8(config2.projectRoot, f));
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 __dirname = dirname(__filename);
20256
- const skillsDir = join9(__dirname, "..", "skills");
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(join9(skillsDir, file), "utf-8");
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(join9(skillsDir, file), "utf-8");
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 \`${config2.projectId}\`. Run \`setup\` first to initialise your project.`
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 = await createAdapter({
20496
- adapterType: config.adapterType,
20497
- papiDir: config.papiDir,
20498
- papiEndpoint: config.papiEndpoint
20499
- });
20500
- var server = createServer(adapter, config);
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);