@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/README.md +26 -130
- package/dist/index.js +1312 -985
- package/dist/prompts.js +9 -1
- package/package.json +1 -1
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/
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
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
|
-
|
|
8424
|
-
|
|
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
|
-
|
|
8429
|
-
|
|
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
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
"placeholder",
|
|
8440
|
-
"example.com",
|
|
8441
|
-
"localhost:5432/dbname",
|
|
8442
|
-
"user:password@host"
|
|
8443
|
-
];
|
|
8444
|
-
function validateDatabaseUrl(connectionString) {
|
|
8445
|
-
const lower = connectionString.toLowerCase().trim();
|
|
8446
|
-
if (PLACEHOLDER_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
|
|
8447
|
-
throw new Error(
|
|
8448
|
-
"DATABASE_URL contains a placeholder value and is not configured.\nReplace it with your actual Supabase connection string in .mcp.json.\nIf you don't have one yet, contact the PAPI admin for access."
|
|
8449
|
-
);
|
|
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
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
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
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
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
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
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
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
}
|
|
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
|
|
8609
|
-
|
|
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
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
// src/lib/formatters.ts
|
|
8619
|
-
function formatActiveDecisionsForPlan(decisions) {
|
|
8620
|
-
if (decisions.length === 0) return "No active decisions.";
|
|
8621
|
-
return decisions.filter((d) => !d.superseded).map((d) => `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
|
|
8622
|
-
|
|
8623
|
-
${d.body}`).join("\n\n");
|
|
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
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
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
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
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
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
const
|
|
8655
|
-
|
|
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
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
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
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
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
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
if (
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
return
|
|
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
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
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
|
-
|
|
8714
|
-
|
|
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
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
if (
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
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
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
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
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
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
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
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
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
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
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
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
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
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
|
-
|
|
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
|
|
8783
|
-
if (
|
|
8784
|
-
|
|
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
|
|
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
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
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
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
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
|
|
8820
|
-
|
|
8821
|
-
|
|
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
|
-
|
|
8838
|
-
|
|
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
|
-
|
|
8854
|
-
|
|
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 (
|
|
8866
|
-
|
|
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
|
-
|
|
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
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
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
|
-
|
|
8890
|
-
|
|
8891
|
-
);
|
|
8892
|
-
if (
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
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
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
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
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
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
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
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
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
const
|
|
8946
|
-
|
|
8947
|
-
|
|
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
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
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
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
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
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
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
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
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
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
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
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
const
|
|
9004
|
-
|
|
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
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
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
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
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
|
|
9155
|
+
return result;
|
|
9156
|
+
}
|
|
9157
|
+
function errorResponse(message) {
|
|
9158
|
+
return { content: [{ type: "text", text: `Error: ${message}` }] };
|
|
9025
9159
|
}
|
|
9026
9160
|
|
|
9027
|
-
// src/
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
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
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
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
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
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
|
-
|
|
9064
|
-
return { committed: true, message };
|
|
9196
|
+
return "";
|
|
9065
9197
|
}
|
|
9066
|
-
function
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
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
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
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
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
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
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
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
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
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
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
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
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
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
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
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
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
}
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
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
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
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
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
return
|
|
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
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
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
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
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
|
|
9212
|
-
|
|
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
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
const
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
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
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
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
|
-
|
|
9386
|
+
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
9387
|
+
return snapshots;
|
|
9237
9388
|
}
|
|
9238
|
-
function
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
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
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
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
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
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
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
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
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
).
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
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
|
-
|
|
9294
|
-
const
|
|
9295
|
-
if (
|
|
9296
|
-
|
|
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
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
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
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
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
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
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
|
|
9489
|
+
return sections.join("\n\n");
|
|
9334
9490
|
}
|
|
9335
|
-
function
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
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
|
|
9349
|
-
if (
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
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
|
|
9355
|
-
const
|
|
9356
|
-
if (
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
const mergedBranches = parseBranches(mergedOutput);
|
|
9370
|
-
const allBranches = parseBranches(allOutput);
|
|
9371
|
-
const codeAhead = [];
|
|
9372
|
-
const staleInProgress = [];
|
|
9373
|
-
for (const task of tasks) {
|
|
9374
|
-
const branch = `feat/${task.displayId}`;
|
|
9375
|
-
if (task.status === "Backlog" && mergedBranches.has(branch)) {
|
|
9376
|
-
codeAhead.push({ displayId: task.displayId, title: task.title, branch });
|
|
9377
|
-
}
|
|
9378
|
-
if (task.status === "In Progress" && !allBranches.has(branch)) {
|
|
9379
|
-
staleInProgress.push({ displayId: task.displayId, title: task.title });
|
|
9380
|
-
}
|
|
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
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
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
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
|
|
9403
|
-
} catch (err) {
|
|
9404
|
-
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
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
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
15098
|
-
const docsDir =
|
|
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 =
|
|
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 =
|
|
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
|
-
[
|
|
15117
|
-
[
|
|
15118
|
-
[
|
|
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 =
|
|
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 =
|
|
15144
|
-
const cursorRulesPath =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
15822
|
-
import { join as
|
|
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 =
|
|
16224
|
-
if (
|
|
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 =
|
|
16457
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
16228
16458
|
const files = [];
|
|
16229
16459
|
for (const e of entries) {
|
|
16230
|
-
const full =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
18570
|
-
import { join as
|
|
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 =
|
|
18809
|
-
if (
|
|
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
|
|
19110
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
19407
|
-
if (!
|
|
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
|
|
19895
|
-
import { join as
|
|
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 (!
|
|
20363
|
+
if (!existsSync6(dir)) return [];
|
|
20037
20364
|
const files = [];
|
|
20038
20365
|
try {
|
|
20039
|
-
const entries =
|
|
20366
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20040
20367
|
for (const entry of entries) {
|
|
20041
|
-
const full =
|
|
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 =
|
|
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 =
|
|
20076
|
-
if (
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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*/, "");
|