@papi-ai/server 0.6.1-alpha.1 → 0.6.2

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
@@ -8067,7 +8067,48 @@ var init_proxy_adapter = __esm({
8067
8067
  constructor(config2) {
8068
8068
  this.endpoint = config2.endpoint.replace(/\/$/, "");
8069
8069
  this.apiKey = config2.apiKey;
8070
- this.projectId = config2.projectId;
8070
+ this.projectId = config2.projectId ?? "";
8071
+ }
8072
+ /** Resolved project ID — available after ensureProject() completes. */
8073
+ getProjectId() {
8074
+ return this.projectId;
8075
+ }
8076
+ /**
8077
+ * Ensure the authenticated user has a project. If none exists, auto-creates one.
8078
+ * Returns the project ID (existing or newly created). Idempotent and race-safe.
8079
+ *
8080
+ * Call this before any other adapter method when PAPI_PROJECT_ID is not configured.
8081
+ */
8082
+ async ensureProject(projectName, repoUrl) {
8083
+ const url = `${this.endpoint}/ensure-project`;
8084
+ const response = await fetch(url, {
8085
+ method: "POST",
8086
+ headers: {
8087
+ "Content-Type": "application/json",
8088
+ "Authorization": `Bearer ${this.apiKey}`
8089
+ },
8090
+ body: JSON.stringify({
8091
+ ...projectName ? { projectName } : {},
8092
+ ...repoUrl ? { repoUrl } : {}
8093
+ })
8094
+ });
8095
+ if (!response.ok) {
8096
+ const errorBody = await response.text();
8097
+ let message;
8098
+ try {
8099
+ const parsed = JSON.parse(errorBody);
8100
+ message = parsed.error ?? errorBody;
8101
+ } catch {
8102
+ message = errorBody;
8103
+ }
8104
+ throw new Error(`Auto-provision failed (${response.status}): ${message}`);
8105
+ }
8106
+ const body = await response.json();
8107
+ if (!body.ok && body.error) {
8108
+ throw new Error(`Auto-provision failed: ${body.error}`);
8109
+ }
8110
+ this.projectId = body.projectId;
8111
+ return { projectId: body.projectId, projectName: body.projectName, created: body.created };
8071
8112
  }
8072
8113
  /**
8073
8114
  * Send an adapter method call to the proxy Edge Function.
@@ -8378,1042 +8419,1163 @@ var init_proxy_adapter = __esm({
8378
8419
  }
8379
8420
  });
8380
8421
 
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
- );
8393
- }
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
- }
8416
-
8417
- // src/adapter-factory.ts
8418
- init_dist2();
8419
- import path2 from "path";
8420
- import { execSync } from "child_process";
8421
- function detectUserId() {
8422
+ // src/lib/git.ts
8423
+ var git_exports = {};
8424
+ __export(git_exports, {
8425
+ AUTO_WRITTEN_PATHS: () => AUTO_WRITTEN_PATHS,
8426
+ branchExists: () => branchExists,
8427
+ checkoutBranch: () => checkoutBranch,
8428
+ createAndCheckoutBranch: () => createAndCheckoutBranch,
8429
+ createPullRequest: () => createPullRequest,
8430
+ createTag: () => createTag,
8431
+ deleteLocalBranch: () => deleteLocalBranch,
8432
+ detectBoardMismatches: () => detectBoardMismatches,
8433
+ detectUnrecordedCommits: () => detectUnrecordedCommits,
8434
+ ensureLatestDevelop: () => ensureLatestDevelop,
8435
+ getCommitsSinceTag: () => getCommitsSinceTag,
8436
+ getCurrentBranch: () => getCurrentBranch,
8437
+ getFilesChangedFromBase: () => getFilesChangedFromBase,
8438
+ getHeadCommitSha: () => getHeadCommitSha,
8439
+ getLatestTag: () => getLatestTag,
8440
+ getOriginRepoSlug: () => getOriginRepoSlug,
8441
+ getUnmergedBranches: () => getUnmergedBranches,
8442
+ gitPull: () => gitPull,
8443
+ gitPush: () => gitPush,
8444
+ hasRemote: () => hasRemote,
8445
+ hasUncommittedChanges: () => hasUncommittedChanges,
8446
+ hasUnpushedCommits: () => hasUnpushedCommits,
8447
+ isGhAvailable: () => isGhAvailable,
8448
+ isGitAvailable: () => isGitAvailable,
8449
+ isGitRepo: () => isGitRepo,
8450
+ mergePullRequest: () => mergePullRequest,
8451
+ resolveBaseBranch: () => resolveBaseBranch,
8452
+ runAutoCommit: () => runAutoCommit,
8453
+ stageAllAndCommit: () => stageAllAndCommit,
8454
+ stageDirAndCommit: () => stageDirAndCommit,
8455
+ tagExists: () => tagExists,
8456
+ taskBranchName: () => taskBranchName,
8457
+ withBaseBranchSync: () => withBaseBranchSync
8458
+ });
8459
+ import { execFileSync } from "child_process";
8460
+ function isGitAvailable() {
8422
8461
  try {
8423
- const email = execSync("git config user.email", { encoding: "utf8", timeout: 5e3 }).trim();
8424
- if (email) return email;
8462
+ execFileSync("git", ["--version"], { stdio: "ignore" });
8463
+ return true;
8425
8464
  } catch {
8465
+ return false;
8426
8466
  }
8467
+ }
8468
+ function isGitRepo(cwd) {
8427
8469
  try {
8428
- const ghUser = execSync("gh api user --jq .email", { encoding: "utf8", timeout: 1e4 }).trim();
8429
- if (ghUser && ghUser !== "null") return ghUser;
8470
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
8471
+ cwd,
8472
+ stdio: "ignore"
8473
+ });
8474
+ return true;
8430
8475
  } catch {
8476
+ return false;
8431
8477
  }
8432
- return void 0;
8433
8478
  }
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
- );
8479
+ function stageDirAndCommit(cwd, dir, message) {
8480
+ try {
8481
+ execFileSync("git", ["check-ignore", "-q", dir], { cwd });
8482
+ return { committed: false, message: `Skipped commit \u2014 '${dir}' is gitignored.` };
8483
+ } catch {
8450
8484
  }
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
- );
8485
+ execFileSync("git", ["add", dir], { cwd });
8486
+ const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
8487
+ cwd,
8488
+ encoding: "utf-8"
8489
+ }).trim();
8490
+ if (!staged) {
8491
+ return { committed: false, message: "No changes to commit." };
8457
8492
  }
8493
+ execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
8494
+ return { committed: true, message };
8458
8495
  }
8459
- var _connectionStatus = "offline";
8460
- function getConnectionStatus() {
8461
- return _connectionStatus;
8496
+ function stageAllAndCommit(cwd, message) {
8497
+ execFileSync("git", ["add", "."], { cwd });
8498
+ const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
8499
+ cwd,
8500
+ encoding: "utf-8"
8501
+ }).trim();
8502
+ if (!staged) {
8503
+ return { committed: false, message: "No changes to commit." };
8504
+ }
8505
+ execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
8506
+ return { committed: true, message };
8462
8507
  }
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
- );
8582
- }
8508
+ function getCurrentBranch(cwd) {
8509
+ try {
8510
+ return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
8511
+ cwd,
8512
+ encoding: "utf-8"
8513
+ }).trim();
8514
+ } catch {
8515
+ return null;
8583
8516
  }
8584
8517
  }
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;
8518
+ function hasUncommittedChanges(cwd, ignore) {
8519
+ try {
8520
+ const args = ["status", "--porcelain"];
8521
+ if (ignore?.length) {
8522
+ args.push("--", ".", ...ignore.map((p) => `:!${p}`));
8523
+ }
8524
+ const status = execFileSync("git", args, {
8525
+ cwd,
8526
+ encoding: "utf-8"
8527
+ }).trim();
8528
+ return status.length > 0;
8529
+ } catch {
8530
+ return false;
8605
8531
  }
8606
- return result;
8607
8532
  }
8608
- function errorResponse(message) {
8609
- return { content: [{ type: "text", text: `Error: ${message}` }] };
8533
+ function branchExists(cwd, branch) {
8534
+ try {
8535
+ execFileSync("git", ["rev-parse", "--verify", branch], {
8536
+ cwd,
8537
+ stdio: "ignore"
8538
+ });
8539
+ return true;
8540
+ } catch {
8541
+ return false;
8542
+ }
8610
8543
  }
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");
8544
+ function checkoutBranch(cwd, branch) {
8545
+ try {
8546
+ execFileSync("git", ["checkout", branch], { cwd, encoding: "utf-8" });
8547
+ return { success: true, message: `Checked out branch '${branch}'.` };
8548
+ } catch {
8549
+ return { success: false, message: `Failed to checkout branch '${branch}'.` };
8550
+ }
8624
8551
  }
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");
8552
+ function createAndCheckoutBranch(cwd, branch) {
8553
+ try {
8554
+ execFileSync("git", ["checkout", "-b", branch], { cwd, encoding: "utf-8" });
8555
+ return { success: true, message: `Created and checked out branch '${branch}'.` };
8556
+ } catch {
8557
+ return { success: false, message: `Failed to create branch '${branch}'.` };
8558
+ }
8637
8559
  }
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;
8560
+ function hasRemote(cwd, remote = "origin") {
8561
+ try {
8562
+ const remotes = execFileSync("git", ["remote"], {
8563
+ cwd,
8564
+ encoding: "utf-8"
8565
+ }).trim();
8566
+ return remotes.split("\n").includes(remote);
8567
+ } catch {
8568
+ return false;
8646
8569
  }
8647
- return "";
8648
8570
  }
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;
8571
+ function gitPull(cwd) {
8572
+ try {
8573
+ execFileSync("git", ["pull"], { cwd, encoding: "utf-8", timeout: GIT_NETWORK_TIMEOUT_MS });
8574
+ return { success: true, message: "Pulled latest changes." };
8575
+ } catch (err) {
8576
+ const msg = err instanceof Error ? err.message : String(err);
8577
+ const isTimeout = msg.includes("ETIMEDOUT") || msg.includes("killed");
8578
+ return {
8579
+ success: false,
8580
+ message: isTimeout ? "Pull timed out after 30s." : `Pull failed: ${msg}`
8581
+ };
8656
8582
  }
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`);
8583
+ }
8584
+ function gitPush(cwd, branch) {
8585
+ try {
8586
+ execFileSync("git", ["push", "-u", "origin", branch], {
8587
+ cwd,
8588
+ encoding: "utf-8",
8589
+ timeout: GIT_NETWORK_TIMEOUT_MS
8590
+ });
8591
+ return { success: true, message: `Pushed branch '${branch}' to origin.` };
8592
+ } catch (err) {
8593
+ const msg = err instanceof Error ? err.message : String(err);
8594
+ const isTimeout = msg.includes("ETIMEDOUT") || msg.includes("killed");
8595
+ return {
8596
+ success: false,
8597
+ message: isTimeout ? `Push timed out after 30s.` : `Push failed: ${msg}`
8598
+ };
8661
8599
  }
8662
- return parts.join(", ");
8663
8600
  }
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");
8601
+ function isGhAvailable() {
8602
+ try {
8603
+ execFileSync("gh", ["--version"], { stdio: "ignore" });
8604
+ return true;
8605
+ } catch {
8606
+ return false;
8607
+ }
8678
8608
  }
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)` : ""}`);
8690
- }
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");
8609
+ function getOriginRepoSlug(cwd) {
8610
+ try {
8611
+ const url = execFileSync("git", ["remote", "get-url", "origin"], {
8612
+ cwd,
8613
+ encoding: "utf-8"
8614
+ }).trim();
8615
+ const sshMatch = url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
8616
+ if (sshMatch) return sshMatch[1];
8617
+ const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
8618
+ if (httpsMatch) return httpsMatch[1];
8619
+ return null;
8620
+ } catch {
8621
+ return null;
8622
+ }
8700
8623
  }
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");
8624
+ function createPullRequest(cwd, branch, baseBranch, title, body) {
8625
+ try {
8626
+ const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
8627
+ const repo = getOriginRepoSlug(cwd);
8628
+ if (repo) {
8629
+ args.push("--repo", repo);
8630
+ }
8631
+ const output = execFileSync("gh", args, { cwd, encoding: "utf-8" }).trim();
8632
+ return { success: true, message: output };
8633
+ } catch (err) {
8634
+ return {
8635
+ success: false,
8636
+ message: `PR creation failed: ${err instanceof Error ? err.message : String(err)}`
8637
+ };
8638
+ }
8712
8639
  }
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) + "...";
8640
+ function sleepSync(ms) {
8641
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
8719
8642
  }
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;
8643
+ function mergePullRequest(cwd, branch) {
8644
+ const repo = getOriginRepoSlug(cwd);
8645
+ const baseArgs = ["pr", "merge", branch, "--merge", "--delete-branch"];
8646
+ if (repo) {
8647
+ baseArgs.push("--repo", repo);
8648
+ }
8649
+ for (let attempt = 1; attempt <= MERGE_MAX_RETRIES; attempt++) {
8650
+ try {
8651
+ execFileSync("gh", baseArgs, { cwd, encoding: "utf-8" });
8652
+ return { success: true, message: `Merged PR for '${branch}' and deleted branch.` };
8653
+ } catch (err) {
8654
+ const msg = err instanceof Error ? err.message : String(err);
8655
+ const isNotMergeable = msg.includes("not mergeable");
8656
+ if (isNotMergeable && attempt < MERGE_MAX_RETRIES) {
8657
+ sleepSync(MERGE_RETRY_DELAY_MS);
8658
+ continue;
8659
+ }
8660
+ return { success: false, message: `PR merge failed: ${msg}` };
8661
+ }
8662
+ }
8663
+ return { success: false, message: "PR merge failed: max retries exceeded" };
8728
8664
  }
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}]`;
8665
+ function deleteLocalBranch(cwd, branch) {
8666
+ try {
8667
+ execFileSync("git", ["branch", "-d", branch], { cwd, encoding: "utf-8" });
8668
+ return { success: true, message: `Deleted local branch '${branch}'.` };
8669
+ } catch (err) {
8670
+ return {
8671
+ success: false,
8672
+ message: `Failed to delete local branch '${branch}': ${err instanceof Error ? err.message : String(err)}`
8673
+ };
8674
+ }
8733
8675
  }
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}` : ""}`;
8676
+ function tagExists(cwd, tag) {
8677
+ try {
8678
+ execFileSync("git", ["rev-parse", "--verify", `refs/tags/${tag}`], {
8679
+ cwd,
8680
+ stdio: "ignore"
8681
+ });
8682
+ return true;
8683
+ } catch {
8684
+ return false;
8685
+ }
8743
8686
  }
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);
8687
+ function createTag(cwd, tag, message) {
8688
+ try {
8689
+ execFileSync("git", ["tag", "-a", tag, "-m", message], { cwd, encoding: "utf-8" });
8690
+ return { success: true, message: `Created tag '${tag}'.` };
8691
+ } catch (err) {
8692
+ return {
8693
+ success: false,
8694
+ message: `Failed to create tag: ${err instanceof Error ? err.message : String(err)}`
8695
+ };
8753
8696
  }
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"}).`;
8697
+ }
8698
+ function getLatestTag(cwd) {
8699
+ try {
8700
+ return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
8701
+ cwd,
8702
+ encoding: "utf-8"
8703
+ }).trim() || null;
8704
+ } catch {
8705
+ return null;
8761
8706
  }
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}`;
8707
+ }
8708
+ function getCommitsSinceTag(cwd, tag) {
8709
+ try {
8710
+ const output = execFileSync(
8711
+ "git",
8712
+ ["log", `${tag}..HEAD`, "--oneline"],
8713
+ { cwd, encoding: "utf-8" }
8714
+ ).trim();
8715
+ return output ? output.split("\n") : [];
8716
+ } catch {
8717
+ return [];
8772
8718
  }
8773
- const recent = [];
8774
- const stable = [];
8775
- for (const t of active) {
8776
- if (isRecentTask(t, currentCycle)) {
8777
- recent.push(t);
8719
+ }
8720
+ async function withBaseBranchSync(config2, fn) {
8721
+ const warnings = [];
8722
+ if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
8723
+ return { result: await fn(), warnings };
8724
+ }
8725
+ const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
8726
+ if (baseBranch !== config2.baseBranch) {
8727
+ warnings.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
8728
+ }
8729
+ const currentBranch = getCurrentBranch(config2.projectRoot);
8730
+ const needsBranchSwitch = currentBranch !== null && currentBranch !== baseBranch;
8731
+ let previousBranch = null;
8732
+ if (needsBranchSwitch && hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
8733
+ warnings.push("Skipping pull \u2014 uncommitted changes detected. Board data may be stale.");
8734
+ } else if (needsBranchSwitch) {
8735
+ const checkout = checkoutBranch(config2.projectRoot, baseBranch);
8736
+ if (!checkout.success) {
8737
+ warnings.push(`Could not switch to ${baseBranch}: ${checkout.message} Board data may be stale.`);
8778
8738
  } else {
8779
- stable.push(t);
8739
+ previousBranch = currentBranch;
8740
+ if (hasRemote(config2.projectRoot)) {
8741
+ const pull = gitPull(config2.projectRoot);
8742
+ if (!pull.success && config2.abortOnConflict && /conflict/i.test(pull.message)) {
8743
+ checkoutBranch(config2.projectRoot, previousBranch);
8744
+ return { result: void 0, warnings, abort: pull.message };
8745
+ }
8746
+ warnings.push(pull.success ? `Synced from ${baseBranch}.` : `Pull failed: ${pull.message}`);
8747
+ }
8780
8748
  }
8749
+ } else if (hasRemote(config2.projectRoot)) {
8750
+ const pull = gitPull(config2.projectRoot);
8751
+ if (!pull.success && config2.abortOnConflict && /conflict/i.test(pull.message)) {
8752
+ return { result: void 0, warnings, abort: pull.message };
8753
+ }
8754
+ warnings.push(pull.success ? `Synced from ${baseBranch}.` : `Pull failed: ${pull.message}`);
8781
8755
  }
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
- );
8756
+ const result = await fn();
8757
+ if (previousBranch) {
8758
+ checkoutBranch(config2.projectRoot, previousBranch);
8791
8759
  }
8792
- return sections.join("\n\n");
8793
- }
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");
8760
+ return { result, warnings };
8803
8761
  }
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";
8762
+ function hasUnpushedCommits(cwd) {
8763
+ try {
8764
+ const output = execFileSync(
8765
+ "git",
8766
+ ["rev-list", "@{u}..HEAD", "--count"],
8767
+ { cwd, encoding: "utf-8" }
8768
+ ).trim();
8769
+ return parseInt(output, 10) > 0;
8770
+ } catch {
8771
+ return false;
8772
+ }
8809
8773
  }
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);
8774
+ function ensureLatestDevelop(cwd, baseBranch) {
8775
+ if (!isGitAvailable() || !isGitRepo(cwd)) {
8776
+ return { pulled: false };
8818
8777
  }
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
- });
8778
+ const current = getCurrentBranch(cwd);
8779
+ if (!current || current !== baseBranch) {
8780
+ return { pulled: false, warning: `Skipping pull \u2014 not on ${baseBranch} (on ${current ?? "unknown"}).` };
8836
8781
  }
8837
- snapshots.sort((a, b2) => a.cycle - b2.cycle);
8838
- return snapshots;
8839
- }
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}`);
8851
- }
8782
+ if (hasUncommittedChanges(cwd, AUTO_WRITTEN_PATHS)) {
8783
+ return { pulled: false, warning: "Skipping pull \u2014 uncommitted changes detected." };
8852
8784
  }
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.`);
8785
+ if (hasUnpushedCommits(cwd)) {
8786
+ return { pulled: false, warning: "Skipping pull \u2014 unpushed commits on current branch." };
8864
8787
  }
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)}`);
8788
+ if (!hasRemote(cwd)) {
8789
+ return { pulled: false };
8873
8790
  }
8874
- return lines.join("\n");
8791
+ const result = gitPull(cwd);
8792
+ if (!result.success) {
8793
+ return { pulled: false, warning: `Pull failed \u2014 using local data. ${result.message}` };
8794
+ }
8795
+ return { pulled: true };
8875
8796
  }
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
- }
8797
+ function getUnmergedBranches(cwd, baseBranch) {
8798
+ try {
8799
+ const output = execFileSync(
8800
+ "git",
8801
+ ["branch", "--no-merged", baseBranch],
8802
+ { cwd, encoding: "utf-8" }
8803
+ ).trim();
8804
+ if (!output) return [];
8805
+ return output.split("\n").map((b2) => b2.replace(/^\*?\s+/, "").trim()).filter(Boolean);
8806
+ } catch {
8807
+ return [];
8888
8808
  }
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}`);
8809
+ }
8810
+ function resolveBaseBranch(cwd, preferred) {
8811
+ if (branchExists(cwd, preferred)) return preferred;
8812
+ if (preferred !== "main" && branchExists(cwd, "main")) return "main";
8813
+ if (preferred !== "master" && branchExists(cwd, "master")) return "master";
8814
+ return preferred;
8815
+ }
8816
+ function detectBoardMismatches(cwd, tasks) {
8817
+ const empty = { codeAhead: [], staleInProgress: [] };
8818
+ if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
8819
+ try {
8820
+ const mergedOutput = execFileSync("git", ["branch", "--merged", "HEAD"], {
8821
+ cwd,
8822
+ encoding: "utf-8",
8823
+ stdio: ["ignore", "pipe", "ignore"]
8824
+ });
8825
+ const allOutput = execFileSync("git", ["branch"], {
8826
+ cwd,
8827
+ encoding: "utf-8",
8828
+ stdio: ["ignore", "pipe", "ignore"]
8829
+ });
8830
+ const parseBranches = (raw) => new Set(raw.split("\n").map((l) => l.trim().replace(/^\* /, "")).filter(Boolean));
8831
+ const mergedBranches = parseBranches(mergedOutput);
8832
+ const allBranches = parseBranches(allOutput);
8833
+ const codeAhead = [];
8834
+ const staleInProgress = [];
8835
+ for (const task of tasks) {
8836
+ const branch = `feat/${task.displayId}`;
8837
+ if (task.status === "Backlog" && mergedBranches.has(branch)) {
8838
+ codeAhead.push({ displayId: task.displayId, title: task.title, branch });
8839
+ }
8840
+ if (task.status === "In Progress" && !allBranches.has(branch)) {
8841
+ staleInProgress.push({ displayId: task.displayId, title: task.title });
8842
+ }
8913
8843
  }
8844
+ return { codeAhead, staleInProgress };
8845
+ } catch {
8846
+ return empty;
8914
8847
  }
8915
- return lines.join("\n");
8916
8848
  }
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.`);
8849
+ function detectUnrecordedCommits(cwd, baseBranch) {
8850
+ if (!isGitAvailable() || !isGitRepo(cwd)) return [];
8851
+ const latestTag = getLatestTag(cwd);
8852
+ if (!latestTag) return [];
8853
+ try {
8854
+ const output = execFileSync(
8855
+ "git",
8856
+ ["log", `${latestTag}..${baseBranch}`, "--oneline", "--max-count=20"],
8857
+ { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
8858
+ ).trim();
8859
+ if (!output) return [];
8860
+ const CYCLE_PATTERNS = [
8861
+ /feat\(task-\d+\):/,
8862
+ // build_execute commits: feat(task-NNN): title
8863
+ /^[a-f0-9]+ release:/,
8864
+ // release commits
8865
+ /^[a-f0-9]+ Merge /,
8866
+ // merge commits from PRs
8867
+ /chore\(task-/
8868
+ // task-related housekeeping
8869
+ ];
8870
+ return output.split("\n").filter((line) => line.trim() && !CYCLE_PATTERNS.some((p) => p.test(line))).map((line) => {
8871
+ const spaceIdx = line.indexOf(" ");
8872
+ return {
8873
+ hash: line.slice(0, spaceIdx),
8874
+ message: line.slice(spaceIdx + 1)
8875
+ };
8876
+ });
8877
+ } catch {
8878
+ return [];
8928
8879
  }
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}`);
8880
+ }
8881
+ function taskBranchName(taskId) {
8882
+ return `feat/${taskId}`;
8883
+ }
8884
+ function getHeadCommitSha(cwd) {
8885
+ try {
8886
+ return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
8887
+ } catch {
8888
+ return null;
8934
8889
  }
8935
- if (patterns.untriagedIssues.length > 0) {
8936
- const items = patterns.untriagedIssues.map((issue) => `- ${issue}`).join("\n");
8937
- sections.push(`**Discovered Issues (untriaged)**
8938
- ${items}`);
8890
+ }
8891
+ function runAutoCommit(projectRoot, commitFn) {
8892
+ if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
8893
+ if (!isGitRepo(projectRoot)) return "Auto-commit: skipped (not a git repository).";
8894
+ try {
8895
+ const result = commitFn();
8896
+ return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
8897
+ } catch (err) {
8898
+ return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
8939
8899
  }
8940
- return sections.join("\n\n");
8941
8900
  }
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}`);
8901
+ function getFilesChangedFromBase(cwd, baseBranch) {
8902
+ try {
8903
+ const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
8904
+ const output = execFileSync("git", ["diff", "--name-only", mergeBase, "HEAD"], { cwd, encoding: "utf-8" }).trim();
8905
+ return output ? output.split("\n").filter(Boolean) : [];
8906
+ } catch {
8907
+ return [];
8948
8908
  }
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}`);
8909
+ }
8910
+ var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES;
8911
+ var init_git = __esm({
8912
+ "src/lib/git.ts"() {
8913
+ "use strict";
8914
+ AUTO_WRITTEN_PATHS = [".papi/*"];
8915
+ GIT_NETWORK_TIMEOUT_MS = 6e4;
8916
+ MERGE_RETRY_DELAY_MS = 2e3;
8917
+ MERGE_MAX_RETRIES = 3;
8954
8918
  }
8955
- if (patterns.requestChangesRate >= 50) {
8956
- sections.push(`**High Rework Rate**
8957
- ${patterns.requestChangesRate}% of recent reviews requested changes.`);
8919
+ });
8920
+
8921
+ // src/index.ts
8922
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8923
+
8924
+ // src/config.ts
8925
+ import path from "path";
8926
+ function loadConfig() {
8927
+ const projectArgIdx = process.argv.indexOf("--project");
8928
+ const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
8929
+ const projectRoot = configuredRoot ?? process.cwd();
8930
+ if (!configuredRoot) {
8931
+ process.stderr.write(
8932
+ '\nPAPI is running but no project is configured.\nSay "run setup" to get started.\n\n'
8933
+ );
8958
8934
  }
8959
- return sections.join("\n\n");
8935
+ const anthropicApiKey = process.env.PAPI_API_KEY ?? "";
8936
+ const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
8937
+ const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
8938
+ const autoPR = process.env.PAPI_AUTO_PR !== "false";
8939
+ const lightMode = process.env.PAPI_LIGHT_MODE === "true";
8940
+ const papiEndpoint = process.env.PAPI_ENDPOINT;
8941
+ const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
8942
+ const databaseUrl = process.env.DATABASE_URL;
8943
+ const explicitAdapter = process.env.PAPI_ADAPTER;
8944
+ const adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
8945
+ return {
8946
+ projectRoot,
8947
+ papiDir: path.join(projectRoot, ".papi"),
8948
+ anthropicApiKey,
8949
+ autoCommit: autoCommit2,
8950
+ baseBranch,
8951
+ autoPR,
8952
+ adapterType,
8953
+ papiEndpoint,
8954
+ lightMode
8955
+ };
8960
8956
  }
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
8957
 
8966
- - **Verdict:** ${r.verdict}
8967
- - **Reviewer:** ${r.reviewer}
8968
- - **Comments:** ${r.comments}`
8969
- ).join("\n\n---\n\n");
8958
+ // src/adapter-factory.ts
8959
+ init_dist2();
8960
+ import path2 from "path";
8961
+ import { execSync } from "child_process";
8962
+ function detectUserId() {
8963
+ try {
8964
+ const email = execSync("git config user.email", { encoding: "utf8", timeout: 5e3 }).trim();
8965
+ if (email) return email;
8966
+ } catch {
8967
+ }
8968
+ try {
8969
+ const ghUser = execSync("gh api user --jq .email", { encoding: "utf8", timeout: 1e4 }).trim();
8970
+ if (ghUser && ghUser !== "null") return ghUser;
8971
+ } catch {
8972
+ }
8973
+ return void 0;
8970
8974
  }
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);
8975
+ var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
8976
+ var PLACEHOLDER_PATTERNS = [
8977
+ "<YOUR_DATABASE_URL>",
8978
+ "your-database-url",
8979
+ "your_database_url",
8980
+ "placeholder",
8981
+ "example.com",
8982
+ "localhost:5432/dbname",
8983
+ "user:password@host"
8984
+ ];
8985
+ function validateDatabaseUrl(connectionString) {
8986
+ const lower = connectionString.toLowerCase().trim();
8987
+ if (PLACEHOLDER_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
8988
+ throw new Error(
8989
+ "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."
8990
+ );
8979
8991
  }
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
- }
8992
+ if (!lower.startsWith("postgres://") && !lower.startsWith("postgresql://")) {
8993
+ throw new Error(
8994
+ `DATABASE_URL must be a PostgreSQL connection string (postgres:// or postgresql://).
8995
+ Got: "${connectionString.slice(0, 30)}..."
8996
+ Check your .mcp.json configuration.`
8997
+ );
8987
8998
  }
8988
- return lines.join("\n");
8989
8999
  }
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}`);
9000
+ var _connectionStatus = "offline";
9001
+ function getConnectionStatus() {
9002
+ return _connectionStatus;
9003
+ }
9004
+ async function createAdapter(optionsOrType, maybePapiDir) {
9005
+ const options = typeof optionsOrType === "string" ? { adapterType: optionsOrType, papiDir: maybePapiDir } : optionsOrType;
9006
+ const { adapterType, papiDir, papiEndpoint } = options;
9007
+ switch (adapterType) {
9008
+ case "md":
9009
+ _connectionStatus = "offline";
9010
+ return new MdFileAdapter(papiDir);
9011
+ case "pg": {
9012
+ const { PgAdapter: PgAdapter2, PgPapiAdapter: PgPapiAdapter2, configFromEnv: configFromEnv2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
9013
+ const projectId = process.env["PAPI_PROJECT_ID"];
9014
+ if (!projectId) {
9015
+ throw new Error(
9016
+ "PAPI_PROJECT_ID is required when using PostgreSQL storage. Set it to the UUID of your project in the database."
9017
+ );
9018
+ }
9019
+ const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv2();
9020
+ validateDatabaseUrl(config2.connectionString);
9021
+ try {
9022
+ const { ensureSchema: ensureSchema2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
9023
+ await ensureSchema2(config2);
9024
+ } catch (err) {
9025
+ const msg = err instanceof Error ? err.message : String(err);
9026
+ console.error(`[papi] \u2717 Schema creation failed: ${msg}`);
9027
+ console.error("[papi] Check DATABASE_URL and Supabase access.");
9028
+ }
9029
+ try {
9030
+ const pgAdapter = new PgAdapter2(config2);
9031
+ const existing = await pgAdapter.getProject(projectId);
9032
+ if (!existing) {
9033
+ const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? "";
9034
+ const slug = path2.basename(projectRoot) || "unnamed";
9035
+ let userId = process.env["PAPI_USER_ID"] ?? void 0;
9036
+ if (!userId) {
9037
+ userId = detectUserId();
9038
+ if (userId) {
9039
+ console.error(`[papi] Auto-detected user identity: ${userId}`);
9040
+ console.error("[papi] Set PAPI_USER_ID in .mcp.json to make this explicit.");
9041
+ } else {
9042
+ console.error("[papi] \u26A0 No PAPI_USER_ID set and auto-detection failed.");
9043
+ console.error("[papi] Project will have no user scope \u2014 it may be visible to all dashboard users.");
9044
+ console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
9045
+ }
9046
+ }
9047
+ await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
9048
+ }
9049
+ await pgAdapter.close();
9050
+ } catch {
9051
+ }
9052
+ const adapter2 = new PgPapiAdapter2(config2, projectId);
9053
+ try {
9054
+ await adapter2.initRls();
9055
+ } catch {
9056
+ }
9057
+ const connected = await adapter2.probeConnection();
9058
+ if (connected) {
9059
+ _connectionStatus = "connected";
9060
+ console.error("[papi] \u2713 Supabase connected");
9061
+ } else {
9062
+ _connectionStatus = "degraded";
9063
+ console.error("[papi] \u2717 Supabase unreachable \u2014 running in degraded mode, data may be stale");
9064
+ console.error("[papi] Check your DATABASE_URL in .mcp.json \u2014 is the connection string correct?");
9065
+ }
9066
+ return adapter2;
8998
9067
  }
8999
- }
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}`);
9068
+ case "proxy": {
9069
+ const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
9070
+ const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app";
9071
+ const projectId = process.env["PAPI_PROJECT_ID"];
9072
+ const dataApiKey = process.env["PAPI_DATA_API_KEY"];
9073
+ if (!dataApiKey) {
9074
+ throw new Error(
9075
+ `PAPI needs an account to store your project data.
9076
+
9077
+ Get started in 3 steps:
9078
+ 1. Sign up at ${dashboardUrl}/login
9079
+ 2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
9080
+ 3. Download the config, place it in your project root, and restart Claude Code
9081
+
9082
+ Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json env config.`
9083
+ );
9084
+ }
9085
+ const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
9086
+ const adapter2 = new ProxyPapiAdapter2({
9087
+ endpoint: dataEndpoint,
9088
+ apiKey: dataApiKey,
9089
+ projectId: projectId || void 0
9090
+ });
9091
+ const connected = await adapter2.probeConnection();
9092
+ if (connected) {
9093
+ _connectionStatus = "connected";
9094
+ console.error("[papi] \u2713 Data proxy connected");
9095
+ } else {
9096
+ _connectionStatus = "degraded";
9097
+ console.error("[papi] \u2717 Data proxy unreachable \u2014 running in degraded mode");
9098
+ console.error("[papi] Check your PAPI_DATA_ENDPOINT configuration.");
9099
+ }
9100
+ if (!projectId && connected) {
9101
+ try {
9102
+ const { getOriginRepoSlug: getOriginRepoSlug2 } = await Promise.resolve().then(() => (init_git(), git_exports));
9103
+ const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
9104
+ const repoSlug = getOriginRepoSlug2(projectRoot);
9105
+ const projectName = repoSlug ? repoSlug.split("/").pop() || path2.basename(projectRoot) || "My Project" : path2.basename(projectRoot) || "My Project";
9106
+ let repoUrl;
9107
+ try {
9108
+ repoUrl = execSync("git remote get-url origin", { cwd: projectRoot, encoding: "utf8", timeout: 3e3 }).trim() || void 0;
9109
+ } catch {
9110
+ }
9111
+ const result = await adapter2.ensureProject(projectName, repoUrl);
9112
+ if (result.created) {
9113
+ console.error(`[papi] \u2713 Project "${result.projectName}" auto-provisioned (${result.projectId})`);
9114
+ console.error("[papi] Tip: add PAPI_PROJECT_ID to .mcp.json to skip this check on future starts.");
9115
+ } else {
9116
+ console.error(`[papi] \u2713 Using project "${result.projectName}" (${result.projectId})`);
9117
+ }
9118
+ } catch (err) {
9119
+ const msg = err instanceof Error ? err.message : String(err);
9120
+ console.error(`[papi] \u26A0 Auto-provision failed: ${msg}`);
9121
+ console.error("[papi] Set PAPI_PROJECT_ID in .mcp.json to connect to an existing project.");
9122
+ }
9123
+ }
9124
+ return adapter2;
9005
9125
  }
9006
- }
9007
- if (canvas.mvpBoundary) {
9008
- sections.push("**MVP Boundary:**", canvas.mvpBoundary);
9009
- }
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}`);
9126
+ default: {
9127
+ const _exhaustive = adapterType;
9128
+ throw new Error(
9129
+ `Unknown PAPI_ADAPTER value: "${_exhaustive}". Valid options: "md", "pg", "proxy".`
9130
+ );
9015
9131
  }
9016
9132
  }
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
- }
9133
+ }
9134
+
9135
+ // src/server.ts
9136
+ import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
9137
+ import { join as join10, dirname } from "path";
9138
+ import { fileURLToPath } from "url";
9139
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9140
+ import {
9141
+ CallToolRequestSchema,
9142
+ ListToolsRequestSchema,
9143
+ ListPromptsRequestSchema,
9144
+ GetPromptRequestSchema
9145
+ } from "@modelcontextprotocol/sdk/types.js";
9146
+
9147
+ // src/lib/response.ts
9148
+ function textResponse(text, usage) {
9149
+ const result = {
9150
+ content: [{ type: "text", text }]
9151
+ };
9152
+ if (usage) {
9153
+ result._usage = usage;
9023
9154
  }
9024
- return sections.length > 0 ? sections.join("\n") : void 0;
9155
+ return result;
9156
+ }
9157
+ function errorResponse(message) {
9158
+ return { content: [{ type: "text", text: `Error: ${message}` }] };
9025
9159
  }
9026
9160
 
9027
- // src/lib/git.ts
9028
- import { execFileSync } from "child_process";
9029
- var AUTO_WRITTEN_PATHS = [".papi/*"];
9030
- function isGitAvailable() {
9031
- try {
9032
- execFileSync("git", ["--version"], { stdio: "ignore" });
9033
- return true;
9034
- } catch {
9035
- return false;
9036
- }
9161
+ // src/services/plan.ts
9162
+ init_dist2();
9163
+ import { createHash, randomUUID as randomUUID7 } from "crypto";
9164
+ import { readFile as readFile2 } from "fs/promises";
9165
+ import path3 from "path";
9166
+
9167
+ // src/lib/formatters.ts
9168
+ function formatActiveDecisionsForPlan(decisions) {
9169
+ if (decisions.length === 0) return "No active decisions.";
9170
+ return decisions.filter((d) => !d.superseded).map((d) => `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
9171
+
9172
+ ${d.body}`).join("\n\n");
9037
9173
  }
9038
- function isGitRepo(cwd) {
9039
- try {
9040
- execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
9041
- cwd,
9042
- stdio: "ignore"
9043
- });
9044
- return true;
9045
- } catch {
9046
- return false;
9047
- }
9174
+ function formatActiveDecisionsForReview(decisions) {
9175
+ if (decisions.length === 0) return "No active decisions.";
9176
+ return decisions.map((d) => {
9177
+ const lifecycle = d.outcome && d.outcome !== "pending" ? ` | Outcome: ${d.outcome}` + (d.revisionCount ? ` | ${d.revisionCount} revision(s)` : "") : "";
9178
+ const summary = extractDecisionSummary(d.body);
9179
+ const summaryLine = summary ? `
9180
+ Summary: ${summary}` : "";
9181
+ if (d.superseded) {
9182
+ return `- ${d.id}: ${d.title} [SUPERSEDED by ${d.supersededBy}${lifecycle}]${summaryLine}`;
9183
+ }
9184
+ return `- ${d.id}: ${d.title} [${d.confidence}${lifecycle}]${summaryLine}`;
9185
+ }).join("\n");
9048
9186
  }
9049
- function stageDirAndCommit(cwd, dir, message) {
9050
- try {
9051
- execFileSync("git", ["check-ignore", "-q", dir], { cwd });
9052
- return { committed: false, message: `Skipped commit \u2014 '${dir}' is gitignored.` };
9053
- } catch {
9054
- }
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." };
9187
+ function extractDecisionSummary(body) {
9188
+ if (!body) return "";
9189
+ const lines = body.split("\n");
9190
+ for (const line of lines) {
9191
+ const trimmed = line.trim();
9192
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
9193
+ const clean = trimmed.replace(/\*\*/g, "").replace(/\*/g, "");
9194
+ return clean.length > 150 ? clean.slice(0, 147) + "..." : clean;
9062
9195
  }
9063
- execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
9064
- return { committed: true, message };
9196
+ return "";
9065
9197
  }
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." };
9198
+ function formatDecisionLifecycleSummary(decisions) {
9199
+ const active = decisions.filter((d) => !d.superseded);
9200
+ if (active.length === 0) return void 0;
9201
+ const counts = {};
9202
+ for (const d of active) {
9203
+ const outcome = d.outcome ?? "pending";
9204
+ counts[outcome] = (counts[outcome] ?? 0) + 1;
9074
9205
  }
9075
- execFileSync("git", ["commit", "-m", message], { cwd, encoding: "utf-8" });
9076
- return { committed: true, message };
9077
- }
9078
- function getCurrentBranch(cwd) {
9079
- try {
9080
- return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9081
- cwd,
9082
- encoding: "utf-8"
9083
- }).trim();
9084
- } catch {
9085
- return null;
9206
+ const parts = Object.entries(counts).map(([k, v]) => `${v} ${k}`);
9207
+ const revised = active.filter((d) => (d.revisionCount ?? 0) > 0);
9208
+ if (revised.length > 0) {
9209
+ parts.push(`${revised.length} with revisions`);
9086
9210
  }
9211
+ return parts.join(", ");
9087
9212
  }
9088
- function hasUncommittedChanges(cwd, ignore) {
9089
- try {
9090
- const args = ["status", "--porcelain"];
9091
- if (ignore?.length) {
9092
- args.push("--", ".", ...ignore.map((p) => `:!${p}`));
9213
+ function formatBuildReports(reports) {
9214
+ if (reports.length === 0) return "No build reports yet.";
9215
+ return reports.map(
9216
+ (r) => `### ${r.taskName} \u2014 ${r.date} \u2014 Cycle ${r.cycle}
9217
+
9218
+ - **Completed:** ${r.completed}
9219
+ - **Actual Effort:** ${r.actualEffort} vs estimated ${r.estimatedEffort}
9220
+ ` + (r.correctionsCount ? `- **Corrections:** ${r.correctionsCount}
9221
+ ` : "") + (Array.isArray(r.briefImplications) && r.briefImplications.length ? `- **Brief Implications:** ${r.briefImplications.map((bi) => `[${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("; ")}
9222
+ ` : "") + `- **Surprises:** ${r.surprises}
9223
+ - **Discovered Issues:** ${r.discoveredIssues}
9224
+ - **Architecture Notes:** ${r.architectureNotes}` + (r.deadEnds ? `
9225
+ - **Dead Ends:** ${r.deadEnds}` : "")
9226
+ ).join("\n\n---\n\n");
9227
+ }
9228
+ function formatRecentlyShippedCapabilities(reports) {
9229
+ const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
9230
+ if (completed.length === 0) return void 0;
9231
+ const lines = completed.map((r) => {
9232
+ const parts = [`- **${r.taskId}:** ${r.taskName}`];
9233
+ if (r.architectureNotes && r.architectureNotes !== "None") {
9234
+ const trimmed = r.architectureNotes.length > 150 ? r.architectureNotes.slice(0, 150) + "..." : r.architectureNotes;
9235
+ parts.push(` _Delivered:_ ${trimmed}`);
9093
9236
  }
9094
- const status = execFileSync("git", args, {
9095
- cwd,
9096
- encoding: "utf-8"
9097
- }).trim();
9098
- return status.length > 0;
9099
- } catch {
9100
- return false;
9101
- }
9237
+ if (r.filesChanged && r.filesChanged.length > 0) {
9238
+ parts.push(` _Files:_ ${r.filesChanged.slice(0, 5).join(", ")}${r.filesChanged.length > 5 ? ` (+${r.filesChanged.length - 5} more)` : ""}`);
9239
+ }
9240
+ return parts.join("\n");
9241
+ });
9242
+ return [
9243
+ `${completed.length} task(s) completed in recent cycles:`,
9244
+ "",
9245
+ ...lines,
9246
+ "",
9247
+ "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."
9248
+ ].join("\n");
9102
9249
  }
9103
- function branchExists(cwd, branch) {
9104
- try {
9105
- execFileSync("git", ["rev-parse", "--verify", branch], {
9106
- cwd,
9107
- stdio: "ignore"
9108
- });
9109
- return true;
9110
- } catch {
9111
- return false;
9112
- }
9250
+ function formatCycleLog(entries) {
9251
+ if (entries.length === 0) return "No cycle log entries yet.";
9252
+ return entries.map(
9253
+ (e) => `### Cycle ${e.cycleNumber} \u2014 ${e.title}
9254
+
9255
+ ${e.content}` + (e.carryForward ? `
9256
+
9257
+ **Carry Forward:** ${e.carryForward}` : "") + (e.notes ? `
9258
+
9259
+ **Cycle Notes:** ${e.notes}` : "")
9260
+ ).join("\n\n---\n\n");
9113
9261
  }
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}'.` };
9120
- }
9262
+ var PLAN_EXCLUDED_STATUSES = /* @__PURE__ */ new Set(["Done", "Cancelled", "Archived", "Deferred"]);
9263
+ var PLAN_NOTES_MAX_LENGTH = 300;
9264
+ function truncateNotes(notes, maxLen) {
9265
+ if (!notes) return "";
9266
+ if (notes.length <= maxLen) return notes;
9267
+ return notes.slice(0, maxLen) + "...";
9121
9268
  }
9122
- function createAndCheckoutBranch(cwd, branch) {
9123
- try {
9124
- execFileSync("git", ["checkout", "-b", branch], { cwd, encoding: "utf-8" });
9125
- return { success: true, message: `Created and checked out branch '${branch}'.` };
9126
- } catch {
9127
- return { success: false, message: `Failed to create branch '${branch}'.` };
9128
- }
9269
+ var ACTIVE_WORK_STATUSES = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
9270
+ function isRecentTask(task, currentCycle) {
9271
+ if (!task.reviewed) return true;
9272
+ if (ACTIVE_WORK_STATUSES.has(task.status)) return true;
9273
+ if (task.createdCycle === void 0) return true;
9274
+ if (task.createdCycle >= currentCycle - 1) return true;
9275
+ if (task.cycle !== void 0 && task.cycle >= currentCycle) return true;
9276
+ return false;
9129
9277
  }
9130
- function hasRemote(cwd, remote = "origin") {
9131
- try {
9132
- const remotes = execFileSync("git", ["remote"], {
9133
- cwd,
9134
- encoding: "utf-8"
9135
- }).trim();
9136
- return remotes.split("\n").includes(remote);
9137
- } catch {
9138
- return false;
9139
- }
9278
+ function formatCompactTask(task) {
9279
+ const handoffTag = task.hasHandoff ? " | \u2713 handoff" : "";
9280
+ const typeTag = task.taskType && task.taskType !== "task" ? ` | ${task.taskType}` : "";
9281
+ return `- ${task.id}: ${task.title} [${task.status} | ${task.priority} | ${task.complexity}${typeTag}${handoffTag}]`;
9140
9282
  }
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
- };
9153
- }
9283
+ function formatDetailedTask(t) {
9284
+ const notes = truncateNotes(t.notes, PLAN_NOTES_MAX_LENGTH);
9285
+ const hasHandoff = t.hasHandoff;
9286
+ const typeTag = t.taskType && t.taskType !== "task" ? ` | Type: ${t.taskType}` : "";
9287
+ return `- **${t.id}:** ${t.title}
9288
+ Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
9289
+ Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
9290
+ Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
9291
+ Notes: ${notes}` : ""}`;
9154
9292
  }
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
- };
9293
+ function formatBoardForPlan(tasks, filters, currentCycle) {
9294
+ if (tasks.length === 0) return "No tasks on the board.";
9295
+ let active = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status));
9296
+ const excludedCount = tasks.length - active.length;
9297
+ if (filters) {
9298
+ if (filters.phase) active = active.filter((t) => t.phase === filters.phase);
9299
+ if (filters.module) active = active.filter((t) => t.module === filters.module);
9300
+ if (filters.epic) active = active.filter((t) => t.epic === filters.epic);
9301
+ if (filters.priority) active = active.filter((t) => t.priority === filters.priority);
9170
9302
  }
9171
- }
9172
- function isGhAvailable() {
9173
- try {
9174
- execFileSync("gh", ["--version"], { stdio: "ignore" });
9175
- return true;
9176
- } catch {
9177
- return false;
9303
+ const userFilteredCount = tasks.length - excludedCount - active.length;
9304
+ const filterParts = [];
9305
+ if (excludedCount > 0) filterParts.push(`${excludedCount} completed filtered`);
9306
+ if (userFilteredCount > 0) filterParts.push(`${userFilteredCount} excluded by filters`);
9307
+ const filterSuffix = filterParts.length > 0 ? filterParts.join(", ") : "";
9308
+ if (active.length === 0) {
9309
+ return `No active tasks on the board (${filterSuffix || "all filtered"}).`;
9178
9310
  }
9179
- }
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;
9311
+ const byCounts = (statuses) => statuses.map((s) => {
9312
+ const n = active.filter((t) => t.status === s).length;
9313
+ return n > 0 ? `${n} ${s.toLowerCase()}` : null;
9314
+ }).filter(Boolean).join(", ");
9315
+ const summary = `Board: ${active.length} active tasks (${byCounts(["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"])})` + (filterSuffix ? ` \u2014 ${filterSuffix}` : "");
9316
+ if (currentCycle === void 0) {
9317
+ const formatted = active.map(formatDetailedTask).join("\n\n");
9318
+ return `${summary}
9319
+
9320
+ ${formatted}`;
9193
9321
  }
9194
- }
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);
9322
+ const recent = [];
9323
+ const stable = [];
9324
+ for (const t of active) {
9325
+ if (isRecentTask(t, currentCycle)) {
9326
+ recent.push(t);
9327
+ } else {
9328
+ stable.push(t);
9201
9329
  }
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
9330
  }
9331
+ const sections = [summary];
9332
+ if (recent.length > 0) {
9333
+ sections.push(recent.map(formatDetailedTask).join("\n\n"));
9334
+ }
9335
+ if (stable.length > 0) {
9336
+ sections.push(
9337
+ `**Stable backlog (${stable.length} tasks \u2014 compact):**
9338
+ ` + stable.map(formatCompactTask).join("\n")
9339
+ );
9340
+ }
9341
+ return sections.join("\n\n");
9210
9342
  }
9211
- function sleepSync(ms) {
9212
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
9343
+ function formatBoardForReview(tasks) {
9344
+ if (tasks.length === 0) return "No tasks on the board.";
9345
+ return tasks.map(
9346
+ (t) => `- **${t.id}:** ${t.title}
9347
+ Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}
9348
+ Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
9349
+ Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${t.notes ? `
9350
+ Notes: ${t.notes}` : ""}`
9351
+ ).join("\n\n");
9213
9352
  }
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);
9353
+ function trendArrow(current, previous, higherIsBetter) {
9354
+ if (previous === void 0) return "";
9355
+ if (current === previous) return " \u2192";
9356
+ const improving = higherIsBetter ? current > previous : current < previous;
9357
+ return improving ? " \u2191" : " \u2193";
9358
+ }
9359
+ var EFFORT_MAP2 = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
9360
+ function computeSnapshotsFromBuildReports(reports) {
9361
+ if (reports.length === 0) return [];
9362
+ const byCycleMap = /* @__PURE__ */ new Map();
9363
+ for (const r of reports) {
9364
+ const existing = byCycleMap.get(r.cycle) ?? [];
9365
+ existing.push(r);
9366
+ byCycleMap.set(r.cycle, existing);
9221
9367
  }
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}` };
9368
+ const snapshots = [];
9369
+ for (const [sn, cycleReports] of byCycleMap) {
9370
+ const completed = cycleReports.filter((r) => r.completed === "Yes").length;
9371
+ const total = cycleReports.length;
9372
+ const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
9373
+ const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
9374
+ const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
9375
+ let effortPoints = 0;
9376
+ for (const r of cycleReports) {
9377
+ effortPoints += EFFORT_MAP2[r.actualEffort] ?? 3;
9234
9378
  }
9379
+ snapshots.push({
9380
+ cycle: sn,
9381
+ date: (/* @__PURE__ */ new Date()).toISOString(),
9382
+ accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
9383
+ velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
9384
+ });
9235
9385
  }
9236
- return { success: false, message: "PR merge failed: max retries exceeded" };
9386
+ snapshots.sort((a, b2) => a.cycle - b2.cycle);
9387
+ return snapshots;
9237
9388
  }
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
- };
9389
+ function formatCycleMetrics(snapshots) {
9390
+ if (snapshots.length === 0) return "No methodology metrics yet.";
9391
+ const latest = snapshots[snapshots.length - 1];
9392
+ const previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] : void 0;
9393
+ const lines = [];
9394
+ if (latest.velocity.length > 0) {
9395
+ const latestV = latest.velocity[latest.velocity.length - 1];
9396
+ lines.push("**Cycle Sizing**");
9397
+ lines.push(`- Last cycle: ${latestV.completed} tasks, ${latestV.effortPoints} effort points`);
9398
+ if (latestV.partial > 0 || latestV.failed > 0) {
9399
+ lines.push(`- Partial: ${latestV.partial} | Failed: ${latestV.failed}`);
9400
+ }
9247
9401
  }
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;
9402
+ const allVelocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
9403
+ const recentVelocities = allVelocities.slice(-5);
9404
+ if (recentVelocities.length > 0) {
9405
+ const avgEffort = Math.round(
9406
+ recentVelocities.reduce((sum, v) => sum + v.effortPoints, 0) / recentVelocities.length * 10
9407
+ ) / 10;
9408
+ lines.push("");
9409
+ lines.push("**Cycle Sizing (effort points \u2014 primary signal)**");
9410
+ lines.push(`- Last ${recentVelocities.length} cycles: ${recentVelocities.map((v) => `S${v.cycle}=${v.effortPoints}pts`).join(", ")}`);
9411
+ lines.push(`- Average: ${avgEffort} effort points/cycle (XS=1, S=2, M=3, L=5, XL=8)`);
9412
+ lines.push(`- Use average as a reference, not a target \u2014 size cycles based on what the selected tasks actually require.`);
9258
9413
  }
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
- };
9414
+ if (latest.accuracy.length > 0) {
9415
+ const latestA = latest.accuracy[latest.accuracy.length - 1];
9416
+ const prevA = previous?.accuracy[previous.accuracy.length - 1];
9417
+ lines.push("");
9418
+ lines.push("**Estimation Accuracy**");
9419
+ lines.push(`- Match rate: ${latestA.matchRate}%${trendArrow(latestA.matchRate, prevA?.matchRate, true)}`);
9420
+ lines.push(`- MAE: ${latestA.mae}${trendArrow(latestA.mae, prevA?.mae, false)}`);
9421
+ lines.push(`- Bias: ${latestA.bias >= 0 ? "+" : ""}${latestA.bias}${trendArrow(Math.abs(latestA.bias), prevA ? Math.abs(prevA.bias) : void 0, false)}`);
9269
9422
  }
9423
+ return lines.join("\n");
9270
9424
  }
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;
9425
+ function formatDerivedMetrics(snapshots, backlogTasks) {
9426
+ const lines = [];
9427
+ if (snapshots.length > 0) {
9428
+ const velocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
9429
+ if (velocities.length >= 2) {
9430
+ const recent = velocities.slice(-5);
9431
+ const avg = recent.reduce((sum, v) => sum + v.completed, 0) / recent.length;
9432
+ const latest = recent[recent.length - 1];
9433
+ lines.push("**Cycle History (5-cycle avg)**");
9434
+ lines.push(`- Average: ${avg.toFixed(1)} tasks/cycle`);
9435
+ lines.push(`- Latest (Cycle ${latest.cycle}): ${latest.completed} tasks, ${latest.effortPoints} effort points`);
9436
+ }
9279
9437
  }
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 [];
9438
+ const activeTasks = backlogTasks.filter(
9439
+ (t) => t.status === "Backlog" || t.status === "In Cycle" || t.status === "Ready"
9440
+ );
9441
+ if (activeTasks.length > 0) {
9442
+ const highPriority = activeTasks.filter(
9443
+ (t) => t.priority === "P1 High" || t.priority === "P0 Critical"
9444
+ ).length;
9445
+ const ratio = (highPriority / activeTasks.length * 100).toFixed(0);
9446
+ const health = highPriority <= 3 ? "healthy" : highPriority <= 6 ? "moderate" : "top-heavy";
9447
+ if (lines.length > 0) lines.push("");
9448
+ lines.push("**Backlog Health**");
9449
+ lines.push(`- ${activeTasks.length} items, ${highPriority} high-priority (${ratio}%) \u2014 ${health}`);
9450
+ }
9451
+ if (snapshots.length > 0) {
9452
+ const accuracies = snapshots.flatMap((s) => s.accuracy).sort((a, b2) => a.cycle - b2.cycle);
9453
+ if (accuracies.length >= 3) {
9454
+ const recent = accuracies.slice(-5);
9455
+ const avgBias = recent.reduce((sum, a) => sum + a.bias, 0) / recent.length;
9456
+ const biasDir = avgBias > 0.3 ? "over-estimates" : avgBias < -0.3 ? "under-estimates" : "balanced";
9457
+ const avgMatch = recent.reduce((sum, a) => sum + a.matchRate, 0) / recent.length;
9458
+ if (lines.length > 0) lines.push("");
9459
+ lines.push("**Estimation Trend (5-cycle)**");
9460
+ lines.push(`- Avg match rate: ${avgMatch.toFixed(0)}%`);
9461
+ lines.push(`- Avg bias: ${avgBias >= 0 ? "+" : ""}${avgBias.toFixed(1)} \u2014 ${biasDir}`);
9462
+ }
9291
9463
  }
9464
+ return lines.join("\n");
9292
9465
  }
9293
- async function withBaseBranchSync(config2, fn) {
9294
- const warnings = [];
9295
- if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
9296
- return { result: await fn(), warnings };
9466
+ function formatBuildPatterns(patterns) {
9467
+ const sections = [];
9468
+ if (patterns.recurringSurprises.length > 0) {
9469
+ const items = patterns.recurringSurprises.map((s) => `- "${s.text}" \u2014 ${s.count} occurrences (cycles ${s.cycles.join(", ")})`).join("\n");
9470
+ sections.push(`**Recurring Surprises**
9471
+ ${items}`);
9297
9472
  }
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}'.`);
9473
+ if (patterns.estimationBias !== "none") {
9474
+ const direction = patterns.estimationBias === "under" ? "under-estimated (actual effort exceeded estimates)" : "over-estimated (actual effort was less than estimates)";
9475
+ sections.push(`**Estimation Bias**
9476
+ Tasks are consistently ${direction} \u2014 ${patterns.estimationBiasRate}% of recent builds.`);
9301
9477
  }
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}`);
9478
+ const scopeKeys = Object.keys(patterns.scopeAccuracyBreakdown);
9479
+ if (scopeKeys.length > 0) {
9480
+ const items = scopeKeys.map((k) => `- ${k}: ${patterns.scopeAccuracyBreakdown[k]}`).join("\n");
9481
+ sections.push(`**Scope Accuracy**
9482
+ ${items}`);
9328
9483
  }
9329
- const result = await fn();
9330
- if (previousBranch) {
9331
- checkoutBranch(config2.projectRoot, previousBranch);
9484
+ if (patterns.untriagedIssues.length > 0) {
9485
+ const items = patterns.untriagedIssues.map((issue) => `- ${issue}`).join("\n");
9486
+ sections.push(`**Discovered Issues (untriaged)**
9487
+ ${items}`);
9332
9488
  }
9333
- return { result, warnings };
9489
+ return sections.join("\n\n");
9334
9490
  }
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 [];
9491
+ function formatReviewPatterns(patterns) {
9492
+ const sections = [];
9493
+ if (patterns.recurringFeedback.length > 0) {
9494
+ const items = patterns.recurringFeedback.map((f) => `- "${f.text}" \u2014 ${f.count} occurrences (${f.taskIds.join(", ")})`).join("\n");
9495
+ sections.push(`**Recurring Review Feedback**
9496
+ ${items}`);
9497
+ }
9498
+ const verdictKeys = Object.keys(patterns.verdictBreakdown);
9499
+ if (verdictKeys.length > 0) {
9500
+ const items = verdictKeys.map((k) => `- ${k}: ${patterns.verdictBreakdown[k]}`).join("\n");
9501
+ sections.push(`**Verdict Breakdown**
9502
+ ${items}`);
9503
+ }
9504
+ if (patterns.requestChangesRate >= 50) {
9505
+ sections.push(`**High Rework Rate**
9506
+ ${patterns.requestChangesRate}% of recent reviews requested changes.`);
9346
9507
  }
9508
+ return sections.join("\n\n");
9347
9509
  }
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;
9510
+ function formatReviews(reviews) {
9511
+ if (reviews.length === 0) return "";
9512
+ return reviews.map(
9513
+ (r) => `### ${r.taskId} \u2014 ${r.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance"} \u2014 ${r.date}
9514
+
9515
+ - **Verdict:** ${r.verdict}
9516
+ - **Reviewer:** ${r.reviewer}
9517
+ - **Comments:** ${r.comments}`
9518
+ ).join("\n\n---\n\n");
9353
9519
  }
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
- }
9520
+ function formatTaskComments(comments, taskIds, heading = "## Task Comments") {
9521
+ const relevant = comments.filter((c) => taskIds.has(c.taskId));
9522
+ if (relevant.length === 0) return "";
9523
+ const byTask = /* @__PURE__ */ new Map();
9524
+ for (const c of relevant) {
9525
+ const list = byTask.get(c.taskId) ?? [];
9526
+ list.push(c);
9527
+ byTask.set(c.taskId, list);
9528
+ }
9529
+ const lines = ["", heading];
9530
+ for (const [taskId, taskComments] of byTask) {
9531
+ for (const c of taskComments.slice(0, 3)) {
9532
+ const date = c.createdAt.split("T")[0];
9533
+ const text = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
9534
+ lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${text}"`);
9381
9535
  }
9382
- return { codeAhead, staleInProgress };
9383
- } catch {
9384
- return empty;
9385
9536
  }
9537
+ return lines.join("\n");
9386
9538
  }
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;
9539
+ function formatDiscoveryCanvas(canvas) {
9540
+ const sections = [];
9541
+ if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
9542
+ sections.push("**Landscape & References:**");
9543
+ for (const ref of canvas.landscapeReferences) {
9544
+ const url = ref.url ? ` (${ref.url})` : "";
9545
+ const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
9546
+ sections.push(`- ${ref.name}${url}${notes}`);
9547
+ }
9395
9548
  }
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)}`;
9549
+ if (canvas.userJourneys && canvas.userJourneys.length > 0) {
9550
+ sections.push("**User Journeys:**");
9551
+ for (const j of canvas.userJourneys) {
9552
+ const priority = j.priority ? ` [${j.priority}]` : "";
9553
+ sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
9554
+ }
9405
9555
  }
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 [];
9556
+ if (canvas.mvpBoundary) {
9557
+ sections.push("**MVP Boundary:**", canvas.mvpBoundary);
9558
+ }
9559
+ if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
9560
+ sections.push("**Assumptions & Open Questions:**");
9561
+ for (const a of canvas.assumptionsOpenQuestions) {
9562
+ const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
9563
+ sections.push(`- [${a.status}] ${a.text}${evidence}`);
9564
+ }
9565
+ }
9566
+ if (canvas.successSignals && canvas.successSignals.length > 0) {
9567
+ sections.push("**Success Signals:**");
9568
+ for (const s of canvas.successSignals) {
9569
+ const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
9570
+ sections.push(`- ${s.signal}${metric}`);
9571
+ }
9414
9572
  }
9573
+ return sections.length > 0 ? sections.join("\n") : void 0;
9415
9574
  }
9416
9575
 
9576
+ // src/services/plan.ts
9577
+ init_git();
9578
+
9417
9579
  // src/lib/horizon.ts
9418
9580
  function buildHorizonContext(phases, tasks) {
9419
9581
  if (phases.length === 0) return null;
@@ -9604,6 +9766,9 @@ ACCEPTANCE CRITERIA
9604
9766
  [ ] [criterion 1]
9605
9767
  [ ] [criterion 2]
9606
9768
 
9769
+ PRE-MORTEM
9770
+ [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.]
9771
+
9607
9772
  SECURITY CONSIDERATIONS
9608
9773
  [data exposure, secrets handling, auth/access control, dependency risks \u2014 or "None \u2014 no security-relevant changes"]
9609
9774
 
@@ -9798,6 +9963,7 @@ Standard planning cycle with full board review.
9798
9963
  **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
9964
  **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
9965
  **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.
9966
+ **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
9967
  **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
9968
 
9803
9969
  RESEARCH OUTPUT
@@ -9969,7 +10135,8 @@ Standard planning cycle with full board review.
9969
10135
  **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
10136
  **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
10137
  **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.`);
10138
+ **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.
10139
+ **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
10140
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
9974
10141
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
9975
10142
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
@@ -10632,6 +10799,9 @@ ACCEPTANCE CRITERIA
10632
10799
  [ ] [criterion 1]
10633
10800
  [ ] [criterion 2]
10634
10801
 
10802
+ PRE-MORTEM
10803
+ [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.]
10804
+
10635
10805
  SECURITY CONSIDERATIONS
10636
10806
  [or "None \u2014 no security-relevant changes"]
10637
10807
 
@@ -14486,7 +14656,45 @@ async function handleBoardEdit(adapter2, args) {
14486
14656
  // src/services/setup.ts
14487
14657
  init_dist2();
14488
14658
  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";
14659
+ import { join as join4, basename, extname } from "path";
14660
+
14661
+ // src/lib/detect-codebase.ts
14662
+ import { existsSync as existsSync2 } from "fs";
14663
+ import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
14664
+ import { join as join3 } from "path";
14665
+ function detectCodebaseType(projectRoot) {
14666
+ if (existsSync2(join3(projectRoot, ".git"))) {
14667
+ return "existing_codebase";
14668
+ }
14669
+ const manifests = [
14670
+ "package.json",
14671
+ "Cargo.toml",
14672
+ "pyproject.toml",
14673
+ "go.mod",
14674
+ "Gemfile",
14675
+ "pom.xml",
14676
+ "build.gradle",
14677
+ "CMakeLists.txt"
14678
+ ];
14679
+ for (const manifest of manifests) {
14680
+ if (existsSync2(join3(projectRoot, manifest))) {
14681
+ return "existing_codebase";
14682
+ }
14683
+ }
14684
+ try {
14685
+ const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
14686
+ const fileCount = entries.filter((f) => {
14687
+ try {
14688
+ return statSync2(join3(projectRoot, f)).isFile();
14689
+ } catch {
14690
+ return false;
14691
+ }
14692
+ }).length;
14693
+ if (fileCount > 5) return "existing_codebase";
14694
+ } catch {
14695
+ }
14696
+ return "new_project";
14697
+ }
14490
14698
 
14491
14699
  // src/templates.ts
14492
14700
  var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
@@ -15079,7 +15287,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
15079
15287
  await mkdir(config2.papiDir, { recursive: true });
15080
15288
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
15081
15289
  const content = substitute(template, vars);
15082
- await writeFile2(join3(config2.papiDir, filename), content, "utf-8");
15290
+ await writeFile2(join4(config2.papiDir, filename), content, "utf-8");
15083
15291
  }
15084
15292
  }
15085
15293
  } else {
@@ -15094,18 +15302,18 @@ async function scaffoldPapiDir(adapter2, config2, input) {
15094
15302
  } catch {
15095
15303
  }
15096
15304
  }
15097
- const commandsDir = join3(config2.projectRoot, ".claude", "commands");
15098
- const docsDir = join3(config2.projectRoot, "docs");
15305
+ const commandsDir = join4(config2.projectRoot, ".claude", "commands");
15306
+ const docsDir = join4(config2.projectRoot, "docs");
15099
15307
  await mkdir(commandsDir, { recursive: true });
15100
15308
  await mkdir(docsDir, { recursive: true });
15101
- const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
15309
+ const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
15102
15310
  let claudeMdExists = false;
15103
15311
  try {
15104
15312
  await access2(claudeMdPath);
15105
15313
  claudeMdExists = true;
15106
15314
  } catch {
15107
15315
  }
15108
- const docsIndexPath = join3(docsDir, "INDEX.md");
15316
+ const docsIndexPath = join4(docsDir, "INDEX.md");
15109
15317
  let docsIndexExists = false;
15110
15318
  try {
15111
15319
  await access2(docsIndexPath);
@@ -15113,9 +15321,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
15113
15321
  } catch {
15114
15322
  }
15115
15323
  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)
15324
+ [join4(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
15325
+ [join4(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
15326
+ [join4(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
15119
15327
  };
15120
15328
  if (!docsIndexExists) {
15121
15329
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -15132,7 +15340,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
15132
15340
  } catch {
15133
15341
  }
15134
15342
  }
15135
- const cursorDir = join3(config2.projectRoot, ".cursor");
15343
+ const cursorDir = join4(config2.projectRoot, ".cursor");
15136
15344
  let cursorDetected = false;
15137
15345
  try {
15138
15346
  await access2(cursorDir);
@@ -15140,8 +15348,8 @@ async function scaffoldPapiDir(adapter2, config2, input) {
15140
15348
  } catch {
15141
15349
  }
15142
15350
  if (cursorDetected) {
15143
- const cursorRulesDir = join3(cursorDir, "rules");
15144
- const cursorRulesPath = join3(cursorRulesDir, "papi.mdc");
15351
+ const cursorRulesDir = join4(cursorDir, "rules");
15352
+ const cursorRulesPath = join4(cursorRulesDir, "papi.mdc");
15145
15353
  await mkdir(cursorRulesDir, { recursive: true });
15146
15354
  try {
15147
15355
  await access2(cursorRulesPath);
@@ -15167,7 +15375,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
15167
15375
  }
15168
15376
  var PAPI_PERMISSION = "mcp__papi__*";
15169
15377
  async function ensurePapiPermission(projectRoot) {
15170
- const settingsPath = join3(projectRoot, ".claude", "settings.json");
15378
+ const settingsPath = join4(projectRoot, ".claude", "settings.json");
15171
15379
  try {
15172
15380
  let settings = {};
15173
15381
  try {
@@ -15186,14 +15394,14 @@ async function ensurePapiPermission(projectRoot) {
15186
15394
  if (!allow.includes(PAPI_PERMISSION)) {
15187
15395
  allow.push(PAPI_PERMISSION);
15188
15396
  }
15189
- await mkdir(join3(projectRoot, ".claude"), { recursive: true });
15397
+ await mkdir(join4(projectRoot, ".claude"), { recursive: true });
15190
15398
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
15191
15399
  } catch {
15192
15400
  }
15193
15401
  }
15194
15402
  async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
15195
15403
  if (config2.adapterType !== "pg") {
15196
- await writeFile2(join3(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
15404
+ await writeFile2(join4(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
15197
15405
  }
15198
15406
  await adapter2.updateProductBrief(briefText);
15199
15407
  const briefPhases = parsePhases(briefText);
@@ -15227,7 +15435,12 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15227
15435
  if (Array.isArray(ads)) {
15228
15436
  for (const ad of ads) {
15229
15437
  if (ad.id && ad.body) {
15230
- await adapter2.updateActiveDecision(ad.id, ad.body, 0);
15438
+ if (adapter2.upsertActiveDecision) {
15439
+ const title = ad.title || ad.body.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
15440
+ await adapter2.upsertActiveDecision(ad.id, ad.body, title, ad.confidence || "MEDIUM", 0);
15441
+ } else {
15442
+ await adapter2.updateActiveDecision(ad.id, ad.body, 0);
15443
+ }
15231
15444
  seededAds++;
15232
15445
  }
15233
15446
  }
@@ -15237,7 +15450,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
15237
15450
  }
15238
15451
  if (conventionsText?.trim()) {
15239
15452
  try {
15240
- const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
15453
+ const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
15241
15454
  const existing = await readFile3(claudeMdPath, "utf-8");
15242
15455
  await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
15243
15456
  } catch {
@@ -15305,13 +15518,13 @@ async function scanCodebase(projectRoot) {
15305
15518
  }
15306
15519
  let packageJson;
15307
15520
  try {
15308
- const content = await readFile3(join3(projectRoot, "package.json"), "utf-8");
15521
+ const content = await readFile3(join4(projectRoot, "package.json"), "utf-8");
15309
15522
  packageJson = JSON.parse(content);
15310
15523
  } catch {
15311
15524
  }
15312
15525
  let readme;
15313
15526
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
15314
- const content = await safeReadFile(join3(projectRoot, name), 5e3);
15527
+ const content = await safeReadFile(join4(projectRoot, name), 5e3);
15315
15528
  if (content) {
15316
15529
  readme = content;
15317
15530
  break;
@@ -15321,7 +15534,7 @@ async function scanCodebase(projectRoot) {
15321
15534
  let totalFiles = topLevelFiles.length;
15322
15535
  for (const dir of topLevelDirs) {
15323
15536
  try {
15324
- const entries = await readdir(join3(projectRoot, dir), { withFileTypes: true });
15537
+ const entries = await readdir(join4(projectRoot, dir), { withFileTypes: true });
15325
15538
  const files = entries.filter((e) => e.isFile());
15326
15539
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
15327
15540
  totalFiles += files.length;
@@ -15400,16 +15613,19 @@ async function prepareSetup(adapter2, config2, input) {
15400
15613
  existingBrief = await adapter2.readProductBrief();
15401
15614
  } catch {
15402
15615
  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."
15616
+ 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
15617
  );
15405
15618
  }
15406
15619
  const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
15407
15620
  if (existingBrief.trim() && !existingBrief.includes(TEMPLATE_MARKER) && !input.force) {
15408
15621
  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
15622
  }
15623
+ const detectedCodebaseType = detectCodebaseType(config2.projectRoot);
15624
+ const autoDetected = input.existingProject === void 0 || input.existingProject === false;
15625
+ const isExistingProject = input.existingProject === true || autoDetected && detectedCodebaseType === "existing_codebase";
15410
15626
  let codebaseSummary;
15411
15627
  let sourceContents;
15412
- if (input.existingProject) {
15628
+ if (isExistingProject) {
15413
15629
  const scan = await scanCodebase(config2.projectRoot);
15414
15630
  if (input.sources) {
15415
15631
  sourceContents = await readSourceFiles(input.sources);
@@ -15452,7 +15668,7 @@ async function prepareSetup(adapter2, config2, input) {
15452
15668
  constraints: input.constraints
15453
15669
  })
15454
15670
  } : void 0;
15455
- const initialTasksPrompt = input.existingProject && codebaseSummary ? {
15671
+ const initialTasksPrompt = isExistingProject && codebaseSummary ? {
15456
15672
  system: INITIAL_TASKS_SYSTEM,
15457
15673
  user: buildInitialTasksPrompt({
15458
15674
  projectName: input.projectName,
@@ -15468,7 +15684,9 @@ async function prepareSetup(adapter2, config2, input) {
15468
15684
  adSeedPrompt,
15469
15685
  conventionsPrompt,
15470
15686
  initialTasksPrompt,
15471
- codebaseSummary
15687
+ codebaseSummary,
15688
+ detectedCodebaseType,
15689
+ autoDetected: autoDetected && detectedCodebaseType !== "new_project"
15472
15690
  };
15473
15691
  }
15474
15692
  async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
@@ -15507,7 +15725,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
15507
15725
  }
15508
15726
  }
15509
15727
  try {
15510
- const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
15728
+ const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
15511
15729
  const existing = await readFile3(claudeMdPath, "utf-8");
15512
15730
  if (!existing.includes("Dogfood Logging")) {
15513
15731
  const dogfoodSection = [
@@ -15551,7 +15769,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
15551
15769
  }
15552
15770
  let cursorScaffolded = false;
15553
15771
  try {
15554
- await access2(join3(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
15772
+ await access2(join4(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
15555
15773
  cursorScaffolded = true;
15556
15774
  } catch {
15557
15775
  }
@@ -15712,14 +15930,25 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
15712
15930
  if (!args.project_type) inferredDefaults.push(`- **Project type:** defaulted to "${input.projectType}"`);
15713
15931
  if (!args.team_size) inferredDefaults.push(`- **Team size:** defaulted to "${input.teamSize}"`);
15714
15932
  if (!args.deployment_target) inferredDefaults.push(`- **Deployment:** defaulted to "${input.deploymentTarget}"`);
15715
- const isExisting = input.existingProject;
15933
+ const isExisting = result.detectedCodebaseType === "existing_codebase";
15716
15934
  const sections = [
15717
- isExisting ? `## PAPI Setup \u2014 Adopt Existing Project (Prepare Phase)` : `## PAPI Setup \u2014 Prepare Phase`,
15935
+ isExisting ? `## PAPI Setup \u2014 Adopt Existing Project (Prepare Phase)` : `## PAPI Setup \u2014 New Project (Prepare Phase)`,
15718
15936
  "",
15719
15937
  result.createdProject ? config2.adapterType === "md" ? `Project "${result.projectName}" scaffolded \u2014 .papi/ directory created.
15720
15938
  ` : `Project "${result.projectName}" scaffolded \u2014 database tables created.
15721
15939
  ` : ""
15722
15940
  ];
15941
+ if (result.autoDetected) {
15942
+ sections.push(
15943
+ `**Codebase detected:** Existing codebase found \u2014 running in adoption mode. If this is wrong, re-run setup with \`existing_project: false\`.`,
15944
+ ""
15945
+ );
15946
+ } else if (result.detectedCodebaseType === "new_project" && input.existingProject === void 0) {
15947
+ sections.push(
15948
+ `**Codebase detected:** Fresh project \u2014 running in new project mode. If you have existing code, re-run setup with \`existing_project: true\`.`,
15949
+ ""
15950
+ );
15951
+ }
15723
15952
  if (inferredDefaults.length > 0) {
15724
15953
  sections.push(
15725
15954
  `**Defaults applied** (override by re-running setup with these fields):`,
@@ -15817,9 +16046,10 @@ ${result.initialTasksPrompt.user}
15817
16046
  init_dist2();
15818
16047
 
15819
16048
  // src/services/build.ts
16049
+ init_git();
15820
16050
  import { randomUUID as randomUUID9 } from "crypto";
15821
- import { readdirSync as readdirSync2, existsSync as existsSync2, readFileSync } from "fs";
15822
- import { join as join4 } from "path";
16051
+ import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
16052
+ import { join as join5 } from "path";
15823
16053
  function capitalizeCompleted(value) {
15824
16054
  const map = {
15825
16055
  yes: "Yes",
@@ -16220,14 +16450,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16220
16450
  let docWarning;
16221
16451
  try {
16222
16452
  if (adapter2.searchDocs) {
16223
- const docsDir = join4(config2.projectRoot, "docs");
16224
- if (existsSync2(docsDir)) {
16453
+ const docsDir = join5(config2.projectRoot, "docs");
16454
+ if (existsSync3(docsDir)) {
16225
16455
  const scanDir = (dir, depth = 0) => {
16226
16456
  if (depth > 8) return [];
16227
- const entries = readdirSync2(dir, { withFileTypes: true });
16457
+ const entries = readdirSync3(dir, { withFileTypes: true });
16228
16458
  const files = [];
16229
16459
  for (const e of entries) {
16230
- const full = join4(dir, e.name);
16460
+ const full = join5(dir, e.name);
16231
16461
  if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
16232
16462
  else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
16233
16463
  }
@@ -16242,7 +16472,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
16242
16472
  const failed = [];
16243
16473
  for (const docPath of unregistered) {
16244
16474
  try {
16245
- const meta = extractDocMeta(join4(config2.projectRoot, docPath), docPath, cycleNumber);
16475
+ const meta = extractDocMeta(join5(config2.projectRoot, docPath), docPath, cycleNumber);
16246
16476
  await adapter2.registerDoc({
16247
16477
  title: meta.title,
16248
16478
  type: meta.type,
@@ -16821,6 +17051,9 @@ Reason: ${result.reason}`);
16821
17051
  }
16822
17052
  }
16823
17053
 
17054
+ // src/tools/idea.ts
17055
+ init_git();
17056
+
16824
17057
  // src/services/idea.ts
16825
17058
  import { randomUUID as randomUUID10 } from "crypto";
16826
17059
  var ROUTING_PATTERNS = [
@@ -17273,6 +17506,9 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
17273
17506
  return textResponse(result.message);
17274
17507
  }
17275
17508
 
17509
+ // src/tools/bug.ts
17510
+ init_git();
17511
+
17276
17512
  // src/services/bug.ts
17277
17513
  import { randomUUID as randomUUID11 } from "crypto";
17278
17514
  function resolveCurrentPhase2(phases) {
@@ -17395,6 +17631,9 @@ async function handleBug(adapter2, config2, args) {
17395
17631
  return textResponse(`\u{1F41B} ${task.id}: "${task.title}" \u2014 ${severityLabel} bug added to backlog${branchNote}. Will be picked up by next plan.${truncateWarning}`);
17396
17632
  }
17397
17633
 
17634
+ // src/tools/ad-hoc.ts
17635
+ init_git();
17636
+
17398
17637
  // src/services/ad-hoc.ts
17399
17638
  import { randomUUID as randomUUID12 } from "crypto";
17400
17639
  async function recordAdHoc(adapter2, input) {
@@ -17531,6 +17770,9 @@ async function handleAdHoc(adapter2, config2, args) {
17531
17770
  );
17532
17771
  }
17533
17772
 
17773
+ // src/tools/board-reconcile.ts
17774
+ init_git();
17775
+
17534
17776
  // src/services/reconcile.ts
17535
17777
  async function prepareReconcile(adapter2) {
17536
17778
  const health = await adapter2.getCycleHealth();
@@ -18098,6 +18340,53 @@ Assess each task above and produce your retriage output. Then call \`board_recon
18098
18340
  }
18099
18341
 
18100
18342
  // src/services/health.ts
18343
+ function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
18344
+ if (cycleNumber < 3) return null;
18345
+ const scores = [];
18346
+ const recentSnaps = snapshots.slice(-3);
18347
+ const baselineSnaps = snapshots.slice(-10);
18348
+ if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
18349
+ const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
18350
+ const recentAvg = avg(recentSnaps);
18351
+ const baselineAvg = avg(baselineSnaps);
18352
+ const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
18353
+ scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
18354
+ } else {
18355
+ scores.push({ name: "Velocity", score: 50, weight: 0.25 });
18356
+ }
18357
+ if (recentSnaps.length > 0) {
18358
+ const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
18359
+ scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
18360
+ } else {
18361
+ scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
18362
+ }
18363
+ const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
18364
+ const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
18365
+ scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
18366
+ const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
18367
+ if (backlogTasks.length > 0) {
18368
+ const criticalCount = backlogTasks.filter(
18369
+ (t) => t.priority === "P0 Critical" || t.priority === "P1 High"
18370
+ ).length;
18371
+ const criticalRatio = criticalCount / backlogTasks.length;
18372
+ const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
18373
+ scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
18374
+ } else {
18375
+ scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
18376
+ }
18377
+ if (decisionUsage.length > 0) {
18378
+ const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
18379
+ const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
18380
+ scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
18381
+ } else {
18382
+ scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
18383
+ }
18384
+ const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
18385
+ const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
18386
+ const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
18387
+ const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
18388
+ return { score: totalScore, status, reason };
18389
+ }
18101
18390
  function countByStatus(tasks) {
18102
18391
  const counts = /* @__PURE__ */ new Map();
18103
18392
  for (const task of tasks) {
@@ -18175,8 +18464,8 @@ async function getHealthSummary(adapter2) {
18175
18464
  }
18176
18465
  let metricsSection;
18177
18466
  let derivedMetricsSection = "";
18467
+ let snapshots = [];
18178
18468
  try {
18179
- let snapshots = [];
18180
18469
  try {
18181
18470
  const reports = await adapter2.getRecentBuildReports(50);
18182
18471
  snapshots = computeSnapshotsFromBuildReports(reports);
@@ -18208,8 +18497,10 @@ async function getHealthSummary(adapter2) {
18208
18497
  }
18209
18498
  const costSection = "Disabled \u2014 local MCP, no API costs.";
18210
18499
  let decisionUsageSection = "";
18500
+ let decisionUsageEntries = [];
18211
18501
  try {
18212
18502
  const usage = await adapter2.getDecisionUsage(cycleNumber);
18503
+ decisionUsageEntries = usage;
18213
18504
  if (usage.length > 0) {
18214
18505
  const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
18215
18506
  if (stale.length > 0) {
@@ -18264,6 +18555,7 @@ ${lines.join("\n")}`;
18264
18555
  }
18265
18556
  } catch {
18266
18557
  }
18558
+ const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
18267
18559
  return {
18268
18560
  cycleNumber,
18269
18561
  latestCycleStatus: health.latestCycleStatus,
@@ -18281,7 +18573,10 @@ ${lines.join("\n")}`;
18281
18573
  decisionLifecycleSection,
18282
18574
  decisionScoresSection,
18283
18575
  contextUtilisationSection,
18284
- northStarSection
18576
+ northStarSection,
18577
+ healthScore: healthResult?.score ?? null,
18578
+ healthStatus: healthResult?.status ?? null,
18579
+ healthReason: healthResult?.reason ?? null
18285
18580
  };
18286
18581
  }
18287
18582
 
@@ -18378,8 +18673,9 @@ async function handleHealth(adapter2) {
18378
18673
  }
18379
18674
 
18380
18675
  // src/services/release.ts
18676
+ init_git();
18381
18677
  import { writeFile as writeFile3 } from "fs/promises";
18382
- import { join as join5 } from "path";
18678
+ import { join as join6 } from "path";
18383
18679
  var INITIAL_RELEASE_NOTES = `# Changelog
18384
18680
 
18385
18681
  ## v0.1.0-alpha \u2014 Initial Release
@@ -18470,7 +18766,7 @@ async function createRelease(config2, branch, version, adapter2) {
18470
18766
  const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
18471
18767
  changelogContent = generateChangelog(version, commits);
18472
18768
  }
18473
- const changelogPath = join5(config2.projectRoot, "CHANGELOG.md");
18769
+ const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
18474
18770
  await writeFile3(changelogPath, changelogContent, "utf-8");
18475
18771
  const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
18476
18772
  const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
@@ -18566,8 +18862,9 @@ async function handleRelease(adapter2, config2, args) {
18566
18862
  }
18567
18863
 
18568
18864
  // src/tools/review.ts
18569
- import { existsSync as existsSync3 } from "fs";
18570
- import { join as join6 } from "path";
18865
+ import { existsSync as existsSync4 } from "fs";
18866
+ import { join as join7 } from "path";
18867
+ init_git();
18571
18868
 
18572
18869
  // src/services/review.ts
18573
18870
  init_dist2();
@@ -18805,8 +19102,8 @@ function mergeAfterAccept(config2, taskId) {
18805
19102
  }
18806
19103
  const featureBranch = taskBranchName(taskId);
18807
19104
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
18808
- const papiDir = join6(config2.projectRoot, ".papi");
18809
- if (existsSync3(papiDir)) {
19105
+ const papiDir = join7(config2.projectRoot, ".papi");
19106
+ if (existsSync4(papiDir)) {
18810
19107
  try {
18811
19108
  const commitResult = stageDirAndCommit(
18812
19109
  config2.projectRoot,
@@ -19044,6 +19341,7 @@ async function handleInit(config2, args) {
19044
19341
  const mcpJsonPath = path4.join(projectRoot, ".mcp.json");
19045
19342
  const force = args.force === true;
19046
19343
  const projectName = args.project_name?.trim() || path4.basename(projectRoot);
19344
+ const usingCwdDefault = !process.env.PAPI_PROJECT_DIR && process.argv.indexOf("--project") === -1;
19047
19345
  let existingConfig = null;
19048
19346
  try {
19049
19347
  await access3(mcpJsonPath);
@@ -19091,6 +19389,7 @@ Path: ${mcpJsonPath}`
19091
19389
  `# PAPI Initialised \u2014 ${projectName}`,
19092
19390
  "",
19093
19391
  `**Project ID:** \`${projectId}\``,
19392
+ `**Project directory:** \`${projectRoot}\`${usingCwdDefault ? " *(detected from current directory)*" : ""}`,
19094
19393
  `**Config:** \`${mcpJsonPath}\``,
19095
19394
  "",
19096
19395
  "## Next Steps",
@@ -19105,9 +19404,10 @@ Path: ${mcpJsonPath}`
19105
19404
  }
19106
19405
 
19107
19406
  // src/tools/orient.ts
19407
+ init_git();
19108
19408
  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";
19409
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync5 } from "fs";
19410
+ import { join as join8 } from "path";
19111
19411
  var orientTool = {
19112
19412
  name: "orient",
19113
19413
  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 +19417,7 @@ var orientTool = {
19117
19417
  required: []
19118
19418
  }
19119
19419
  };
19120
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
19420
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot) {
19121
19421
  const lines = [];
19122
19422
  const cycleIsComplete = health.latestCycleStatus === "complete";
19123
19423
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -19156,6 +19456,11 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
19156
19456
  lines.push(`**North Star:** ${health.northStarSection}`);
19157
19457
  lines.push("");
19158
19458
  }
19459
+ if (health.healthScore !== null && health.healthStatus !== null) {
19460
+ const icon = health.healthStatus === "GREEN" ? "\u{1F7E2}" : health.healthStatus === "AMBER" ? "\u{1F7E1}" : "\u{1F534}";
19461
+ lines.push(`**Health:** ${icon} ${health.healthStatus} (${health.healthScore}/100) \u2014 ${health.healthReason}`);
19462
+ lines.push("");
19463
+ }
19159
19464
  lines.push("## Board");
19160
19465
  lines.push(health.boardSummary);
19161
19466
  lines.push("");
@@ -19184,7 +19489,16 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
19184
19489
  }
19185
19490
  if (buildInfo.isEmpty) {
19186
19491
  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.");
19492
+ if (buildInfo.currentCycle === 0) {
19493
+ const codebaseType = projectRoot ? detectCodebaseType(projectRoot) : "new_project";
19494
+ if (codebaseType === "existing_codebase") {
19495
+ 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.");
19496
+ } else {
19497
+ lines.push("No tasks found. Fresh project \u2014 run `setup` to define your Product Brief, then `plan` to create your first cycle.");
19498
+ }
19499
+ } else {
19500
+ lines.push("Board is empty \u2014 run `plan` to create your next cycle.");
19501
+ }
19188
19502
  lines.push("");
19189
19503
  } else if (buildInfo.noHandoffs) {
19190
19504
  lines.push("## Tasks");
@@ -19271,7 +19585,7 @@ function getLatestGitTag(projectRoot) {
19271
19585
  }
19272
19586
  function checkNpmVersionDrift() {
19273
19587
  try {
19274
- const pkgPath = join7(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
19588
+ const pkgPath = join8(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
19275
19589
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
19276
19590
  const localVersion = pkg.version;
19277
19591
  const packageName = pkg.name;
@@ -19363,6 +19677,19 @@ ${versionDrift}` : "";
19363
19677
  }
19364
19678
  } catch {
19365
19679
  }
19680
+ let unrecordedNote = "";
19681
+ try {
19682
+ const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
19683
+ if (unrecorded.length > 0) {
19684
+ const lines = ["\n\n## Unrecorded Work"];
19685
+ lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
19686
+ for (const c of unrecorded) {
19687
+ lines.push(`- \`${c.hash}\` ${c.message}`);
19688
+ }
19689
+ unrecordedNote = lines.join("\n");
19690
+ }
19691
+ } catch {
19692
+ }
19366
19693
  let recsNote = "";
19367
19694
  try {
19368
19695
  const pendingRecs = await adapter2.getPendingRecommendations();
@@ -19396,15 +19723,15 @@ ${versionDrift}` : "";
19396
19723
  }
19397
19724
  } catch {
19398
19725
  }
19399
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag) + ttfvNote + reconciliationNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
19726
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
19400
19727
  } catch (err) {
19401
19728
  const message = err instanceof Error ? err.message : String(err);
19402
19729
  return errorResponse(`Orient failed: ${message}`);
19403
19730
  }
19404
19731
  }
19405
19732
  function enrichClaudeMd(projectRoot, cycleNumber) {
19406
- const claudeMdPath = join7(projectRoot, "CLAUDE.md");
19407
- if (!existsSync4(claudeMdPath)) return "";
19733
+ const claudeMdPath = join8(projectRoot, "CLAUDE.md");
19734
+ if (!existsSync5(claudeMdPath)) return "";
19408
19735
  const content = readFileSync2(claudeMdPath, "utf-8");
19409
19736
  const additions = [];
19410
19737
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
@@ -19891,8 +20218,8 @@ ${result.userMessage}
19891
20218
  }
19892
20219
 
19893
20220
  // 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";
20221
+ import { readdirSync as readdirSync4, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
20222
+ import { join as join9, relative } from "path";
19896
20223
  import { homedir as homedir2 } from "os";
19897
20224
  var docRegisterTool = {
19898
20225
  name: "doc_register",
@@ -20033,12 +20360,12 @@ ${d.summary}
20033
20360
  ${lines.join("\n---\n\n")}`);
20034
20361
  }
20035
20362
  function scanMdFiles(dir, rootDir) {
20036
- if (!existsSync5(dir)) return [];
20363
+ if (!existsSync6(dir)) return [];
20037
20364
  const files = [];
20038
20365
  try {
20039
- const entries = readdirSync3(dir, { withFileTypes: true });
20366
+ const entries = readdirSync4(dir, { withFileTypes: true });
20040
20367
  for (const entry of entries) {
20041
- const full = join8(dir, entry.name);
20368
+ const full = join9(dir, entry.name);
20042
20369
  if (entry.isDirectory()) {
20043
20370
  files.push(...scanMdFiles(full, rootDir));
20044
20371
  } else if (entry.name.endsWith(".md")) {
@@ -20067,17 +20394,17 @@ async function handleDocScan(adapter2, config2, args) {
20067
20394
  const includePlans = args.include_plans ?? false;
20068
20395
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
20069
20396
  const registeredPaths = new Set(registered.map((d) => d.path));
20070
- const docsDir = join8(config2.projectRoot, "docs");
20397
+ const docsDir = join9(config2.projectRoot, "docs");
20071
20398
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
20072
20399
  const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
20073
20400
  let unregisteredPlans = [];
20074
20401
  if (includePlans) {
20075
- const plansDir = join8(homedir2(), ".claude", "plans");
20076
- if (existsSync5(plansDir)) {
20402
+ const plansDir = join9(homedir2(), ".claude", "plans");
20403
+ if (existsSync6(plansDir)) {
20077
20404
  const planFiles = scanMdFiles(plansDir, plansDir);
20078
20405
  unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
20079
20406
  path: f,
20080
- title: extractTitle(join8(plansDir, f.replace("plans/", "")))
20407
+ title: extractTitle(join9(plansDir, f.replace("plans/", "")))
20081
20408
  }));
20082
20409
  }
20083
20410
  }
@@ -20088,7 +20415,7 @@ async function handleDocScan(adapter2, config2, args) {
20088
20415
  if (unregisteredDocs.length > 0) {
20089
20416
  lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
20090
20417
  for (const f of unregisteredDocs) {
20091
- const title = extractTitle(join8(config2.projectRoot, f));
20418
+ const title = extractTitle(join9(config2.projectRoot, f));
20092
20419
  lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
20093
20420
  }
20094
20421
  }
@@ -20253,7 +20580,7 @@ function createServer(adapter2, config2) {
20253
20580
  );
20254
20581
  const __filename = fileURLToPath(import.meta.url);
20255
20582
  const __dirname = dirname(__filename);
20256
- const skillsDir = join9(__dirname, "..", "skills");
20583
+ const skillsDir = join10(__dirname, "..", "skills");
20257
20584
  function parseSkillFrontmatter(content) {
20258
20585
  const match = content.match(/^---\n([\s\S]*?)\n---/);
20259
20586
  if (!match) return null;
@@ -20271,7 +20598,7 @@ function createServer(adapter2, config2) {
20271
20598
  const mdFiles = files.filter((f) => f.endsWith(".md"));
20272
20599
  const prompts = [];
20273
20600
  for (const file of mdFiles) {
20274
- const content = await readFile5(join9(skillsDir, file), "utf-8");
20601
+ const content = await readFile5(join10(skillsDir, file), "utf-8");
20275
20602
  const meta = parseSkillFrontmatter(content);
20276
20603
  if (meta) {
20277
20604
  prompts.push({ name: meta.name, description: meta.description });
@@ -20287,7 +20614,7 @@ function createServer(adapter2, config2) {
20287
20614
  try {
20288
20615
  const files = await readdir2(skillsDir);
20289
20616
  for (const file of files.filter((f) => f.endsWith(".md"))) {
20290
- const content = await readFile5(join9(skillsDir, file), "utf-8");
20617
+ const content = await readFile5(join10(skillsDir, file), "utf-8");
20291
20618
  const meta = parseSkillFrontmatter(content);
20292
20619
  if (meta?.name === name) {
20293
20620
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");