@papi-ai/server 0.7.52 → 0.7.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backfill-cycle-metrics.js +73 -32
- package/dist/index.js +591 -300
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(git_exports, {
|
|
|
26
26
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
27
27
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
28
28
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
29
|
+
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
29
30
|
getBranchDiff: () => getBranchDiff,
|
|
30
31
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
31
32
|
getCurrentBranch: () => getCurrentBranch,
|
|
@@ -719,11 +720,29 @@ function escapeRegexLiteral(s) {
|
|
|
719
720
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
720
721
|
}
|
|
721
722
|
function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
722
|
-
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
723
723
|
const inProgress = tasks.filter((t) => t.status === "In Progress");
|
|
724
724
|
if (inProgress.length === 0) return [];
|
|
725
|
+
const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
|
|
726
|
+
const hits = [];
|
|
727
|
+
for (const task of inProgress) {
|
|
728
|
+
const hit = landed.get(task.displayId);
|
|
729
|
+
if (!hit) continue;
|
|
730
|
+
hits.push({
|
|
731
|
+
displayId: task.displayId,
|
|
732
|
+
title: task.title,
|
|
733
|
+
commit: hit.commit,
|
|
734
|
+
subject: hit.subject,
|
|
735
|
+
pr: hit.pr
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return hits;
|
|
739
|
+
}
|
|
740
|
+
function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
|
|
741
|
+
const out = /* @__PURE__ */ new Map();
|
|
742
|
+
if (displayIds.length === 0) return out;
|
|
743
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return out;
|
|
725
744
|
const base = resolveDefaultBranch(cwd, preferredBase);
|
|
726
|
-
if (!branchExists(cwd, base)) return
|
|
745
|
+
if (!branchExists(cwd, base)) return out;
|
|
727
746
|
let raw;
|
|
728
747
|
try {
|
|
729
748
|
raw = execFileSync(
|
|
@@ -732,28 +751,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
|
732
751
|
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
733
752
|
);
|
|
734
753
|
} catch {
|
|
735
|
-
return
|
|
754
|
+
return out;
|
|
736
755
|
}
|
|
737
756
|
const commits = raw.split("\n").map((line) => {
|
|
738
757
|
const idx = line.indexOf("");
|
|
739
758
|
if (idx === -1) return null;
|
|
740
759
|
return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
|
|
741
760
|
}).filter((c) => c !== null && c.hash !== "");
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
|
|
761
|
+
for (const displayId of displayIds) {
|
|
762
|
+
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
|
|
745
763
|
const hit = commits.find((c) => re.test(c.subject));
|
|
746
764
|
if (!hit) continue;
|
|
747
765
|
const prMatch = hit.subject.match(/#(\d+)/);
|
|
748
|
-
|
|
749
|
-
displayId: task.displayId,
|
|
750
|
-
title: task.title,
|
|
766
|
+
out.set(displayId, {
|
|
751
767
|
commit: hit.hash,
|
|
752
768
|
subject: hit.subject,
|
|
753
769
|
pr: prMatch ? `#${prMatch[1]}` : null
|
|
754
770
|
});
|
|
755
771
|
}
|
|
756
|
-
return
|
|
772
|
+
return out;
|
|
757
773
|
}
|
|
758
774
|
function detectUnrecordedCommits(cwd, baseBranch) {
|
|
759
775
|
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
@@ -1281,21 +1297,27 @@ var init_proxy_adapter = __esm({
|
|
|
1281
1297
|
// getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
|
|
1282
1298
|
// are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
|
|
1283
1299
|
"createOwnerAction",
|
|
1284
|
-
"findPendingDocActionsForTask",
|
|
1285
1300
|
"getContributorRole",
|
|
1286
|
-
"getDecisionScorePatterns",
|
|
1287
|
-
"getModuleEstimationStats",
|
|
1288
|
-
"correctLatestBuildReportEffort",
|
|
1289
1301
|
"recordContributorReleasePr",
|
|
1290
1302
|
"setContributorReleasePrStatus",
|
|
1291
1303
|
"listContributorReleasePrs",
|
|
1292
|
-
"resolveLearningsForDoneTasks",
|
|
1293
|
-
"markCycleLearningResolved",
|
|
1294
|
-
"updateStageExitCriteria",
|
|
1295
|
-
"updateDocAction",
|
|
1296
1304
|
"claimReview",
|
|
1297
1305
|
"getSiblingAds",
|
|
1298
1306
|
"getSiblingRepoTasks"
|
|
1307
|
+
// task-2394 (C329) — Batch A wired: findPendingDocActionsForTask,
|
|
1308
|
+
// getModuleEstimationStats and getDecisionScorePatterns now have edge case handlers
|
|
1309
|
+
// (each backed by a SECURITY DEFINER RPC, migration 20260714140000) plus
|
|
1310
|
+
// ALLOWED_METHODS entries, so they forward. The two planner-context reads returned
|
|
1311
|
+
// EMPTY for every hosted user before this — the planner ran on worse context than
|
|
1312
|
+
// the owner's on the only install path external users have (AD-72).
|
|
1313
|
+
// task-2412 (C329) — listOwnerActionsForBlockerScan + linkOwnerActionToTask wired.
|
|
1314
|
+
// First USER-scoped ([C]) methods to forward: the edge binds both to the bearer's
|
|
1315
|
+
// user_id and discards the client-supplied one, so the typed-blocker scan (task-2343)
|
|
1316
|
+
// now works for hosted users without exposing one member's owner actions to another.
|
|
1317
|
+
// task-2393 (C329) — Batch B wired: markCycleLearningResolved (the P1 — hosted
|
|
1318
|
+
// discovered_issue_resolve hard-errored), correctLatestBuildReportEffort,
|
|
1319
|
+
// updateStageExitCriteria, updateDocAction, resolveLearningsForDoneTasks all have
|
|
1320
|
+
// edge case handlers + ALLOWED_METHODS/WRITE_METHODS entries now, so they forward.
|
|
1299
1321
|
// task-2489 (C320): recordProgressStep is now wired to the edge data-proxy
|
|
1300
1322
|
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
1301
1323
|
// hosted callers and persists a project-scoped cycle_progress_steps row. Removed
|
|
@@ -4211,9 +4233,9 @@ __export(doctor_exports, {
|
|
|
4211
4233
|
__testing: () => __testing,
|
|
4212
4234
|
runDoctor: () => runDoctor
|
|
4213
4235
|
});
|
|
4214
|
-
import { existsSync as
|
|
4236
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
4215
4237
|
import { homedir as homedir4 } from "os";
|
|
4216
|
-
import { join as
|
|
4238
|
+
import { join as join20 } from "path";
|
|
4217
4239
|
function redact(name, value) {
|
|
4218
4240
|
if (!value) return "(empty)";
|
|
4219
4241
|
if (SECRET_VARS.has(name)) {
|
|
@@ -4224,14 +4246,14 @@ function redact(name, value) {
|
|
|
4224
4246
|
}
|
|
4225
4247
|
function findMcpJson() {
|
|
4226
4248
|
const candidates = [
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4249
|
+
join20(process.cwd(), ".mcp.json"),
|
|
4250
|
+
join20(homedir4(), ".claude", ".mcp.json"),
|
|
4251
|
+
join20(homedir4(), ".mcp.json")
|
|
4230
4252
|
];
|
|
4231
4253
|
for (const path7 of candidates) {
|
|
4232
|
-
if (!
|
|
4254
|
+
if (!existsSync11(path7)) continue;
|
|
4233
4255
|
try {
|
|
4234
|
-
const raw =
|
|
4256
|
+
const raw = readFileSync12(path7, "utf-8");
|
|
4235
4257
|
const parsed = JSON.parse(raw);
|
|
4236
4258
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
4237
4259
|
if (!papiEntry) continue;
|
|
@@ -4505,17 +4527,17 @@ __export(reset_exports, {
|
|
|
4505
4527
|
removePapiEntry: () => removePapiEntry,
|
|
4506
4528
|
runReset: () => runReset
|
|
4507
4529
|
});
|
|
4508
|
-
import { existsSync as
|
|
4530
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
4509
4531
|
import { homedir as homedir5 } from "os";
|
|
4510
|
-
import { join as
|
|
4532
|
+
import { join as join21 } from "path";
|
|
4511
4533
|
import { createInterface } from "readline/promises";
|
|
4512
4534
|
function findResetTarget() {
|
|
4513
4535
|
for (const path7 of CANDIDATE_PATHS()) {
|
|
4514
|
-
if (!
|
|
4536
|
+
if (!existsSync12(path7)) continue;
|
|
4515
4537
|
let raw;
|
|
4516
4538
|
let parsed;
|
|
4517
4539
|
try {
|
|
4518
|
-
raw =
|
|
4540
|
+
raw = readFileSync13(path7, "utf-8");
|
|
4519
4541
|
parsed = JSON.parse(raw);
|
|
4520
4542
|
} catch {
|
|
4521
4543
|
continue;
|
|
@@ -4586,7 +4608,7 @@ async function runReset(args = []) {
|
|
|
4586
4608
|
}
|
|
4587
4609
|
}
|
|
4588
4610
|
try {
|
|
4589
|
-
|
|
4611
|
+
writeFileSync6(target.path, removePapiEntry(target), "utf-8");
|
|
4590
4612
|
process.stdout.write(`
|
|
4591
4613
|
\u2713 Removed papi entry from ${target.path}
|
|
4592
4614
|
`);
|
|
@@ -4603,9 +4625,9 @@ var init_reset = __esm({
|
|
|
4603
4625
|
"src/cli/reset.ts"() {
|
|
4604
4626
|
"use strict";
|
|
4605
4627
|
CANDIDATE_PATHS = () => [
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4628
|
+
join21(process.cwd(), ".mcp.json"),
|
|
4629
|
+
join21(homedir5(), ".claude", ".mcp.json"),
|
|
4630
|
+
join21(homedir5(), ".mcp.json")
|
|
4609
4631
|
];
|
|
4610
4632
|
}
|
|
4611
4633
|
});
|
|
@@ -4616,9 +4638,9 @@ __export(audit_exports, {
|
|
|
4616
4638
|
__testing: () => __testing2,
|
|
4617
4639
|
runAudit: () => runAudit
|
|
4618
4640
|
});
|
|
4619
|
-
import { existsSync as
|
|
4641
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14, readdirSync as readdirSync7 } from "fs";
|
|
4620
4642
|
import { homedir as homedir6 } from "os";
|
|
4621
|
-
import { join as
|
|
4643
|
+
import { join as join22 } from "path";
|
|
4622
4644
|
function safeListDirs(dir) {
|
|
4623
4645
|
try {
|
|
4624
4646
|
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
@@ -4634,10 +4656,10 @@ function safeListFiles(dir, ext) {
|
|
|
4634
4656
|
}
|
|
4635
4657
|
}
|
|
4636
4658
|
function readMcp(projectPath) {
|
|
4637
|
-
const path7 =
|
|
4638
|
-
if (!
|
|
4659
|
+
const path7 = join22(projectPath, ".mcp.json");
|
|
4660
|
+
if (!existsSync13(path7)) return { servers: [] };
|
|
4639
4661
|
try {
|
|
4640
|
-
const parsed = JSON.parse(
|
|
4662
|
+
const parsed = JSON.parse(readFileSync14(path7, "utf-8"));
|
|
4641
4663
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4642
4664
|
const servers = Object.keys(mcpServers);
|
|
4643
4665
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4669,18 +4691,18 @@ function auditProjectSync(projectPath, name) {
|
|
|
4669
4691
|
path: projectPath,
|
|
4670
4692
|
papiProjectId,
|
|
4671
4693
|
mcpServers: servers,
|
|
4672
|
-
skills: safeListDirs(
|
|
4673
|
-
agentSkills: safeListDirs(
|
|
4674
|
-
agents: safeListFiles(
|
|
4675
|
-
hooks: safeListFiles(
|
|
4694
|
+
skills: safeListDirs(join22(projectPath, ".claude", "skills")),
|
|
4695
|
+
agentSkills: safeListDirs(join22(projectPath, ".agents", "skills")),
|
|
4696
|
+
agents: safeListFiles(join22(projectPath, ".claude", "agents"), ".md"),
|
|
4697
|
+
hooks: safeListFiles(join22(projectPath, ".claude", "hooks"), ".sh")
|
|
4676
4698
|
};
|
|
4677
4699
|
}
|
|
4678
4700
|
function discoverProjects() {
|
|
4679
4701
|
const out = [];
|
|
4680
4702
|
for (const root of PROJECT_ROOTS) {
|
|
4681
4703
|
for (const name of safeListDirs(root)) {
|
|
4682
|
-
const path7 =
|
|
4683
|
-
if (
|
|
4704
|
+
const path7 = join22(root, name);
|
|
4705
|
+
if (existsSync13(join22(path7, ".mcp.json")) || existsSync13(join22(path7, ".claude"))) {
|
|
4684
4706
|
out.push({ name, path: path7 });
|
|
4685
4707
|
}
|
|
4686
4708
|
}
|
|
@@ -4691,9 +4713,9 @@ function readGlobalSkills() {
|
|
|
4691
4713
|
return safeListDirs(GLOBAL_SKILLS_DIR);
|
|
4692
4714
|
}
|
|
4693
4715
|
function readGlobalMcpServers() {
|
|
4694
|
-
if (!
|
|
4716
|
+
if (!existsSync13(GLOBAL_CLAUDE_JSON)) return [];
|
|
4695
4717
|
try {
|
|
4696
|
-
const parsed = JSON.parse(
|
|
4718
|
+
const parsed = JSON.parse(readFileSync14(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4697
4719
|
const servers = parsed.mcpServers ?? {};
|
|
4698
4720
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4699
4721
|
} catch {
|
|
@@ -4845,9 +4867,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
|
|
|
4845
4867
|
var init_audit = __esm({
|
|
4846
4868
|
"src/cli/audit.ts"() {
|
|
4847
4869
|
"use strict";
|
|
4848
|
-
PROJECT_ROOTS = [
|
|
4849
|
-
GLOBAL_SKILLS_DIR =
|
|
4850
|
-
GLOBAL_CLAUDE_JSON =
|
|
4870
|
+
PROJECT_ROOTS = [join22(homedir6(), "Ai-App-Projects"), join22(homedir6(), "android-projects")];
|
|
4871
|
+
GLOBAL_SKILLS_DIR = join22(homedir6(), ".claude", "skills");
|
|
4872
|
+
GLOBAL_CLAUDE_JSON = join22(homedir6(), ".claude.json");
|
|
4851
4873
|
IDLE_WINDOW_DAYS = 30;
|
|
4852
4874
|
GLOBALIZE_THRESHOLD = 3;
|
|
4853
4875
|
__testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
|
|
@@ -4859,8 +4881,8 @@ var setup_exports = {};
|
|
|
4859
4881
|
__export(setup_exports, {
|
|
4860
4882
|
runSetup: () => runSetup
|
|
4861
4883
|
});
|
|
4862
|
-
import { existsSync as
|
|
4863
|
-
import { join as
|
|
4884
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync7 } from "fs";
|
|
4885
|
+
import { join as join23 } from "path";
|
|
4864
4886
|
function baseUrl() {
|
|
4865
4887
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
4866
4888
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
@@ -4892,11 +4914,11 @@ function sleep(ms) {
|
|
|
4892
4914
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4893
4915
|
}
|
|
4894
4916
|
function writeMcpJson(opts) {
|
|
4895
|
-
const path7 =
|
|
4917
|
+
const path7 = join23(process.cwd(), ".mcp.json");
|
|
4896
4918
|
let parsed = {};
|
|
4897
|
-
if (
|
|
4919
|
+
if (existsSync14(path7)) {
|
|
4898
4920
|
try {
|
|
4899
|
-
parsed = JSON.parse(
|
|
4921
|
+
parsed = JSON.parse(readFileSync15(path7, "utf-8"));
|
|
4900
4922
|
} catch {
|
|
4901
4923
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
4902
4924
|
}
|
|
@@ -4917,9 +4939,9 @@ function writeMcpJson(opts) {
|
|
|
4917
4939
|
}
|
|
4918
4940
|
mcpServers.papi = papiEntry;
|
|
4919
4941
|
parsed.mcpServers = mcpServers;
|
|
4920
|
-
|
|
4942
|
+
writeFileSync7(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
4921
4943
|
try {
|
|
4922
|
-
const mode =
|
|
4944
|
+
const mode = statSync7(path7).mode & 511;
|
|
4923
4945
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
4924
4946
|
} catch {
|
|
4925
4947
|
}
|
|
@@ -5027,8 +5049,8 @@ var init_setup = __esm({
|
|
|
5027
5049
|
});
|
|
5028
5050
|
|
|
5029
5051
|
// src/index.ts
|
|
5030
|
-
import { readFileSync as
|
|
5031
|
-
import { dirname as dirname6, join as
|
|
5052
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
5053
|
+
import { dirname as dirname6, join as join24 } from "path";
|
|
5032
5054
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5033
5055
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5034
5056
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -8037,9 +8059,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8037
8059
|
}
|
|
8038
8060
|
|
|
8039
8061
|
// src/server.ts
|
|
8040
|
-
import { readFileSync as
|
|
8062
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
8041
8063
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8042
|
-
import { join as
|
|
8064
|
+
import { join as join19, dirname as dirname5 } from "path";
|
|
8043
8065
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8044
8066
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8045
8067
|
import {
|
|
@@ -8301,16 +8323,41 @@ ${deferredSection}` : body;
|
|
|
8301
8323
|
if (deferredSection) sections.push(deferredSection);
|
|
8302
8324
|
return sections.join("\n\n");
|
|
8303
8325
|
}
|
|
8304
|
-
|
|
8326
|
+
var PLAN_FULL_NOTES_BUDGET_BYTES = Number(process.env.PAPI_PLAN_NOTES_BUDGET) || 6e4;
|
|
8327
|
+
var priorityRank = (p) => p ? PRIORITY_RANK[p] ?? 4 : 4;
|
|
8328
|
+
function formatCandidateTaskFullNotes(tasks, budgetBytes = PLAN_FULL_NOTES_BUDGET_BYTES) {
|
|
8305
8329
|
const candidates = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status)).filter((t) => (t.notes?.length ?? 0) > PLAN_NOTES_MAX_LENGTH);
|
|
8306
8330
|
if (candidates.length === 0) return void 0;
|
|
8307
|
-
const
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8331
|
+
const ranked = candidates.map((t, i) => ({ t, i })).sort((a, b2) => priorityRank(a.t.priority) - priorityRank(b2.t.priority) || a.i - b2.i).map(({ t }) => t);
|
|
8332
|
+
const included = [];
|
|
8333
|
+
const elided = [];
|
|
8334
|
+
let spent = 0;
|
|
8335
|
+
for (const t of ranked) {
|
|
8336
|
+
const entry = `**${t.id}** \u2014 ${t.title}
|
|
8337
|
+
${t.notes}`;
|
|
8338
|
+
const cost = Buffer.byteLength(entry, "utf-8");
|
|
8339
|
+
if (included.length > 0 && spent + cost > budgetBytes) {
|
|
8340
|
+
elided.push(t);
|
|
8341
|
+
continue;
|
|
8342
|
+
}
|
|
8343
|
+
included.push(entry);
|
|
8344
|
+
spent += cost;
|
|
8345
|
+
}
|
|
8346
|
+
const header = `${candidates.length} candidate task(s) have notes longer than ${PLAN_NOTES_MAX_LENGTH} chars. Full untruncated notes below \u2014 reference these when generating BUILD HANDOFFs so submitter context, constraints, and reasoning are preserved. The Board section above uses truncated notes for concise task selection; this section supplies the missing detail for tasks you choose to schedule.`;
|
|
8347
|
+
const parts = [header, "", ...included];
|
|
8348
|
+
if (elided.length > 0) {
|
|
8349
|
+
parts.push(
|
|
8350
|
+
"",
|
|
8351
|
+
`### ELIDED \u2014 ${elided.length} lower-priority candidate(s) omitted to keep this payload under ${Math.round(budgetBytes / 1024)} KB`,
|
|
8352
|
+
"",
|
|
8353
|
+
"Full notes were included above for the highest-priority candidates only. The following tasks have long notes that are NOT shown here:",
|
|
8354
|
+
"",
|
|
8355
|
+
elided.map((t) => `- ${t.id} (${t.priority ?? "unranked"})`).join("\n"),
|
|
8356
|
+
"",
|
|
8357
|
+
`You still have each of these tasks in the Board section with its notes truncated to ${PLAN_NOTES_MAX_LENGTH} chars, which is enough to rank and select them. If you schedule one, say so in the cycle log and scope its handoff from the truncated notes. Do NOT invent or infer the elided detail \u2014 if a task genuinely needs its full notes to be scoped, prefer leaving it unscheduled over guessing at the submitter's intent.`
|
|
8358
|
+
);
|
|
8359
|
+
}
|
|
8360
|
+
return parts.join("\n\n");
|
|
8314
8361
|
}
|
|
8315
8362
|
function formatBoardForReview(tasks) {
|
|
8316
8363
|
if (tasks.length === 0) return "No tasks on the board.";
|
|
@@ -8341,6 +8388,29 @@ function effortWeight(size2) {
|
|
|
8341
8388
|
return 3;
|
|
8342
8389
|
}
|
|
8343
8390
|
}
|
|
8391
|
+
function computeCycleEffort(cycleTaskRows, cycleReports) {
|
|
8392
|
+
if (cycleTaskRows && cycleTaskRows.length > 0) {
|
|
8393
|
+
const done = cycleTaskRows.filter((t) => t.status === "Done");
|
|
8394
|
+
const reportByTask = /* @__PURE__ */ new Map();
|
|
8395
|
+
for (const r of cycleReports) if (r.taskId) reportByTask.set(r.taskId, r);
|
|
8396
|
+
return {
|
|
8397
|
+
completed: done.length,
|
|
8398
|
+
total: cycleTaskRows.length,
|
|
8399
|
+
plannedPoints: done.reduce((s, t) => s + effortWeight(t.complexity), 0),
|
|
8400
|
+
deliveredPoints: done.reduce((s, t) => {
|
|
8401
|
+
const actual = reportByTask.get(t.id)?.actualEffort;
|
|
8402
|
+
return s + effortWeight(actual || t.complexity);
|
|
8403
|
+
}, 0)
|
|
8404
|
+
};
|
|
8405
|
+
}
|
|
8406
|
+
const completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
8407
|
+
return {
|
|
8408
|
+
completed,
|
|
8409
|
+
total: cycleReports.length,
|
|
8410
|
+
plannedPoints: cycleReports.reduce((s, r) => s + effortWeight(r.estimatedEffort || r.actualEffort), 0),
|
|
8411
|
+
deliveredPoints: cycleReports.reduce((s, r) => s + effortWeight(r.actualEffort || r.estimatedEffort), 0)
|
|
8412
|
+
};
|
|
8413
|
+
}
|
|
8344
8414
|
function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
8345
8415
|
const reportsByCycle = /* @__PURE__ */ new Map();
|
|
8346
8416
|
for (const r of reports) {
|
|
@@ -8364,24 +8434,19 @@ function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
|
8364
8434
|
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
8365
8435
|
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
8366
8436
|
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
8367
|
-
|
|
8368
|
-
let total;
|
|
8369
|
-
let effortPoints;
|
|
8370
|
-
if (cycleTaskRows && cycleTaskRows.length > 0) {
|
|
8371
|
-
const done = cycleTaskRows.filter((t) => t.status === "Done");
|
|
8372
|
-
completed = done.length;
|
|
8373
|
-
total = cycleTaskRows.length;
|
|
8374
|
-
effortPoints = done.reduce((s, t) => s + effortWeight(t.complexity), 0);
|
|
8375
|
-
} else {
|
|
8376
|
-
completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
8377
|
-
total = cycleReports.length;
|
|
8378
|
-
effortPoints = cycleReports.reduce((s, r) => s + effortWeight(r.actualEffort), 0);
|
|
8379
|
-
}
|
|
8437
|
+
const { completed, total, plannedPoints, deliveredPoints } = computeCycleEffort(cycleTaskRows, cycleReports);
|
|
8380
8438
|
snapshots.push({
|
|
8381
8439
|
cycle: sn,
|
|
8382
8440
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8383
8441
|
accuracy: [{ cycle: sn, reports: cycleReports.length, matchRate, mae: 0, bias: 0 }],
|
|
8384
|
-
velocity: [{
|
|
8442
|
+
velocity: [{
|
|
8443
|
+
cycle: sn,
|
|
8444
|
+
completed,
|
|
8445
|
+
partial: 0,
|
|
8446
|
+
failed: Math.max(0, total - completed),
|
|
8447
|
+
effortPoints: plannedPoints,
|
|
8448
|
+
deliveredPoints
|
|
8449
|
+
}]
|
|
8385
8450
|
});
|
|
8386
8451
|
}
|
|
8387
8452
|
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
@@ -8393,13 +8458,25 @@ function formatCycleMetrics(snapshots) {
|
|
|
8393
8458
|
const allVelocities = snapshots.flatMap((s) => s.velocity).sort((a, b2) => a.cycle - b2.cycle);
|
|
8394
8459
|
const recentVelocities = allVelocities.slice(-5);
|
|
8395
8460
|
if (recentVelocities.length > 0) {
|
|
8396
|
-
const
|
|
8461
|
+
const avgPlanned = Math.round(
|
|
8397
8462
|
recentVelocities.reduce((sum, v) => sum + v.effortPoints, 0) / recentVelocities.length * 10
|
|
8398
8463
|
) / 10;
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
lines.push(
|
|
8402
|
-
lines.push(
|
|
8464
|
+
const withDelivered = recentVelocities.filter((v) => v.deliveredPoints !== void 0);
|
|
8465
|
+
const avgDelivered = withDelivered.length > 0 ? Math.round(withDelivered.reduce((sum, v) => sum + (v.deliveredPoints ?? 0), 0) / withDelivered.length * 10) / 10 : void 0;
|
|
8466
|
+
lines.push("**Cycle Sizing \u2014 planned vs delivered effort points**");
|
|
8467
|
+
lines.push(
|
|
8468
|
+
`- Last ${recentVelocities.length} cycles: ` + recentVelocities.map((v) => v.deliveredPoints !== void 0 ? `S${v.cycle}=${v.effortPoints} planned/${v.deliveredPoints} delivered` : `S${v.cycle}=${v.effortPoints} planned`).join(", ")
|
|
8469
|
+
);
|
|
8470
|
+
if (avgDelivered !== void 0) {
|
|
8471
|
+
const delta = Math.round((avgDelivered - avgPlanned) * 10) / 10;
|
|
8472
|
+
const sign = delta > 0 ? "+" : "";
|
|
8473
|
+
const read = delta === 0 ? "delivered matches planned" : delta > 0 ? "cycles cost MORE than scoped (under-scoping)" : "cycles cost LESS than scoped (over-scoping)";
|
|
8474
|
+
lines.push(`- Average: ${avgPlanned} planned / ${avgDelivered} delivered (XS=1, S=2, M=3, L=5, XL=8)`);
|
|
8475
|
+
lines.push(`- Scope accuracy: ${sign}${delta} pts/cycle \u2014 ${read}.`);
|
|
8476
|
+
} else {
|
|
8477
|
+
lines.push(`- Average: ${avgPlanned} planned effort points/cycle (XS=1, S=2, M=3, L=5, XL=8)`);
|
|
8478
|
+
}
|
|
8479
|
+
lines.push(`- Size cycles on what the selected tasks actually require \u2014 planned is a reference, not a target.`);
|
|
8403
8480
|
}
|
|
8404
8481
|
return lines.join("\n");
|
|
8405
8482
|
}
|
|
@@ -12474,6 +12551,9 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12474
12551
|
const prepareScope = await resolvePlanScope(adapter2, config2);
|
|
12475
12552
|
const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force, prepareScope.callerUserId);
|
|
12476
12553
|
const validateMs = t();
|
|
12554
|
+
const incomingCycle = cycleNumber + 1;
|
|
12555
|
+
tracker?.setStreamScope({ cycle: incomingCycle });
|
|
12556
|
+
await recordPlanPrepareStep(tracker, incomingCycle, "health-check");
|
|
12477
12557
|
if (handoffsOnly) {
|
|
12478
12558
|
tracker?.mark("handoffs_only_assemble");
|
|
12479
12559
|
t = startTimer();
|
|
@@ -12518,6 +12598,8 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12518
12598
|
t = startTimer();
|
|
12519
12599
|
const { context, contextHashes } = await assembleContext(adapter2, mode, config2, filters, focus);
|
|
12520
12600
|
const assembleMs = t();
|
|
12601
|
+
await recordPlanPrepareStep(tracker, incomingCycle, "inbox-triage");
|
|
12602
|
+
await recordPlanPrepareStep(tracker, incomingCycle, "board-integrity");
|
|
12521
12603
|
const TEMPLATE_MARKER2 = "*Describe your project's core value proposition here.*";
|
|
12522
12604
|
if (mode !== "bootstrap" && context.productBrief.includes(TEMPLATE_MARKER2)) {
|
|
12523
12605
|
throw new Error("TEMPLATE_BRIEF");
|
|
@@ -12535,6 +12617,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12535
12617
|
}
|
|
12536
12618
|
const scanMs = t();
|
|
12537
12619
|
console.error(`[plan-perf] codebaseScan: ${scanMs}ms`);
|
|
12620
|
+
await recordPlanPrepareStep(tracker, incomingCycle, "maturity-gate");
|
|
12538
12621
|
tracker?.mark("build_user_message");
|
|
12539
12622
|
t = startTimer();
|
|
12540
12623
|
const foundation = await buildProjectFoundation(adapter2);
|
|
@@ -12570,7 +12653,13 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12570
12653
|
contextHashes
|
|
12571
12654
|
};
|
|
12572
12655
|
}
|
|
12573
|
-
var
|
|
12656
|
+
var PLAN_PREPARE_STEPS = ["health-check", "inbox-triage", "board-integrity", "maturity-gate"];
|
|
12657
|
+
var PLAN_APPLY_STEPS = ["recommendation", "dependency-chain"];
|
|
12658
|
+
var PLAN_STAGE_STEPS = [...PLAN_PREPARE_STEPS, ...PLAN_APPLY_STEPS];
|
|
12659
|
+
async function recordPlanPrepareStep(tracker, incomingCycleNumber, step) {
|
|
12660
|
+
if (!tracker) return;
|
|
12661
|
+
await tracker.recordStep(step, { cycle: incomingCycleNumber, stage: "plan" });
|
|
12662
|
+
}
|
|
12574
12663
|
async function streamPlanStageSteps(tracker, newCycleNumber) {
|
|
12575
12664
|
tracker.setStreamScope({ cycle: newCycleNumber });
|
|
12576
12665
|
for (const step of PLAN_STAGE_STEPS) {
|
|
@@ -13091,6 +13180,49 @@ var PerCallerCache = class {
|
|
|
13091
13180
|
}
|
|
13092
13181
|
};
|
|
13093
13182
|
|
|
13183
|
+
// src/lib/plan-prepare-store.ts
|
|
13184
|
+
import { createHash as createHash2 } from "crypto";
|
|
13185
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, existsSync, statSync } from "fs";
|
|
13186
|
+
import { tmpdir } from "os";
|
|
13187
|
+
import { join as join3 } from "path";
|
|
13188
|
+
var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13189
|
+
var DEFAULT_CALLER_KEY3 = "__default__";
|
|
13190
|
+
function spillPath(projectId, callerKey) {
|
|
13191
|
+
const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
|
|
13192
|
+
return join3(tmpdir(), `papi-plan-prepare-${id}.json`);
|
|
13193
|
+
}
|
|
13194
|
+
function savePrepareSpill(projectId, callerKey, state) {
|
|
13195
|
+
try {
|
|
13196
|
+
writeFileSync2(
|
|
13197
|
+
spillPath(projectId, callerKey),
|
|
13198
|
+
JSON.stringify({ savedAt: Date.now(), state }),
|
|
13199
|
+
{ mode: 384 }
|
|
13200
|
+
);
|
|
13201
|
+
} catch {
|
|
13202
|
+
}
|
|
13203
|
+
}
|
|
13204
|
+
function loadPrepareSpill(projectId, callerKey) {
|
|
13205
|
+
const path7 = spillPath(projectId, callerKey);
|
|
13206
|
+
try {
|
|
13207
|
+
if (!existsSync(path7)) return void 0;
|
|
13208
|
+
if (Date.now() - statSync(path7).mtimeMs > SPILL_TTL_MS) {
|
|
13209
|
+
clearPrepareSpill(projectId, callerKey);
|
|
13210
|
+
return void 0;
|
|
13211
|
+
}
|
|
13212
|
+
const parsed = JSON.parse(readFileSync2(path7, "utf-8"));
|
|
13213
|
+
return parsed.state;
|
|
13214
|
+
} catch {
|
|
13215
|
+
return void 0;
|
|
13216
|
+
}
|
|
13217
|
+
}
|
|
13218
|
+
function clearPrepareSpill(projectId, callerKey) {
|
|
13219
|
+
try {
|
|
13220
|
+
const path7 = spillPath(projectId, callerKey);
|
|
13221
|
+
if (existsSync(path7)) unlinkSync(path7);
|
|
13222
|
+
} catch {
|
|
13223
|
+
}
|
|
13224
|
+
}
|
|
13225
|
+
|
|
13094
13226
|
// src/tools/plan.ts
|
|
13095
13227
|
var planPrepareCache = new PerCallerCache();
|
|
13096
13228
|
var planTool = {
|
|
@@ -13258,7 +13390,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13258
13390
|
const planMode = args.plan_mode || "full";
|
|
13259
13391
|
const rawCycleNumber = args.cycle_number != null ? Number(args.cycle_number) : NaN;
|
|
13260
13392
|
const strategyReviewWarning = args.strategy_review_warning || "";
|
|
13261
|
-
const prep = planPrepareCache.peek(callerKey);
|
|
13393
|
+
const prep = planPrepareCache.peek(callerKey) ?? loadPrepareSpill(adapter2.getProjectId?.(), callerKey);
|
|
13262
13394
|
const contextHashes = prep?.contextHashes;
|
|
13263
13395
|
const inputContext = prep?.userMessage;
|
|
13264
13396
|
const contextBytes = prep?.contextBytes;
|
|
@@ -13286,6 +13418,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13286
13418
|
}
|
|
13287
13419
|
const cycleNumber = newCycleNumber - 1;
|
|
13288
13420
|
planPrepareCache.clear(callerKey);
|
|
13421
|
+
clearPrepareSpill(adapter2.getProjectId?.(), callerKey);
|
|
13289
13422
|
let utilisation;
|
|
13290
13423
|
if (inputContext) {
|
|
13291
13424
|
try {
|
|
@@ -13316,13 +13449,15 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13316
13449
|
}
|
|
13317
13450
|
const skipHandoffs = args.skip_handoffs === true;
|
|
13318
13451
|
const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker);
|
|
13319
|
-
|
|
13452
|
+
const prepareState = {
|
|
13320
13453
|
contextHashes: result.contextHashes,
|
|
13321
13454
|
userMessage: result.userMessage,
|
|
13322
13455
|
contextBytes: result.contextBytes,
|
|
13323
13456
|
cycleNumber: result.cycleNumber,
|
|
13324
13457
|
skipHandoffs: skipHandoffs || void 0
|
|
13325
|
-
}
|
|
13458
|
+
};
|
|
13459
|
+
planPrepareCache.set(callerKey, prepareState);
|
|
13460
|
+
savePrepareSpill(adapter2.getProjectId?.(), callerKey, prepareState);
|
|
13326
13461
|
const autoDispatchEnabled = process.env.PAPI_AUTO_DISPATCH !== "false";
|
|
13327
13462
|
const autoDispatchThreshold = 50 * 1024;
|
|
13328
13463
|
let dispatch;
|
|
@@ -13406,10 +13541,10 @@ ${result.userMessage}
|
|
|
13406
13541
|
}
|
|
13407
13542
|
|
|
13408
13543
|
// src/services/strategy.ts
|
|
13409
|
-
import { randomUUID as randomUUID10, createHash as
|
|
13544
|
+
import { randomUUID as randomUUID10, createHash as createHash3 } from "crypto";
|
|
13410
13545
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13411
|
-
import { existsSync, readdirSync, statSync } from "fs";
|
|
13412
|
-
import { join as
|
|
13546
|
+
import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
13547
|
+
import { join as join4 } from "path";
|
|
13413
13548
|
import { homedir as homedir2 } from "os";
|
|
13414
13549
|
|
|
13415
13550
|
// src/lib/hosted-mode.ts
|
|
@@ -14355,7 +14490,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
14355
14490
|
try {
|
|
14356
14491
|
const fullCanvasText = formatDiscoveryCanvas(canvas);
|
|
14357
14492
|
if (fullCanvasText) {
|
|
14358
|
-
const canvasHash =
|
|
14493
|
+
const canvasHash = createHash3("md5").update(fullCanvasText).digest("hex");
|
|
14359
14494
|
const lastReview = previousStrategyReviews?.[0];
|
|
14360
14495
|
const prevHash = lastReview?.structuredData?.canvasHash;
|
|
14361
14496
|
if (prevHash && prevHash === canvasHash) {
|
|
@@ -14425,12 +14560,12 @@ ${lines.join("\n")}`;
|
|
|
14425
14560
|
}
|
|
14426
14561
|
let recentPlansText;
|
|
14427
14562
|
try {
|
|
14428
|
-
const plansDir =
|
|
14429
|
-
if (
|
|
14563
|
+
const plansDir = join4(homedir2(), ".claude", "plans");
|
|
14564
|
+
if (existsSync2(plansDir)) {
|
|
14430
14565
|
const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
|
|
14431
14566
|
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
14432
|
-
const fullPath =
|
|
14433
|
-
const stat4 =
|
|
14567
|
+
const fullPath = join4(plansDir, f);
|
|
14568
|
+
const stat4 = statSync2(fullPath);
|
|
14434
14569
|
return { name: f, modified: stat4.mtime, size: stat4.size };
|
|
14435
14570
|
}).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
|
|
14436
14571
|
if (planFiles.length > 0) {
|
|
@@ -14446,15 +14581,15 @@ ${lines.join("\n")}`;
|
|
|
14446
14581
|
}
|
|
14447
14582
|
let unregisteredDocsText;
|
|
14448
14583
|
try {
|
|
14449
|
-
const docsDir =
|
|
14450
|
-
if (hasLocalWorkspace() &&
|
|
14584
|
+
const docsDir = join4(projectRoot, "docs");
|
|
14585
|
+
if (hasLocalWorkspace() && existsSync2(docsDir)) {
|
|
14451
14586
|
const registeredPaths = new Set(
|
|
14452
14587
|
(registeredDocs ?? []).map((d) => d.path).filter(Boolean)
|
|
14453
14588
|
);
|
|
14454
14589
|
const allDocFiles = [];
|
|
14455
14590
|
const scanDir = (dir, prefix) => {
|
|
14456
14591
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
14457
|
-
if (entry.isDirectory()) scanDir(
|
|
14592
|
+
if (entry.isDirectory()) scanDir(join4(dir, entry.name), `${prefix}${entry.name}/`);
|
|
14458
14593
|
else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
|
|
14459
14594
|
}
|
|
14460
14595
|
};
|
|
@@ -14730,7 +14865,7 @@ ${cleanContent}`;
|
|
|
14730
14865
|
const currentCanvas = await adapter2.readDiscoveryCanvas();
|
|
14731
14866
|
const canvasText = formatDiscoveryCanvas(currentCanvas);
|
|
14732
14867
|
if (canvasText) {
|
|
14733
|
-
return { ...sd, canvasHash:
|
|
14868
|
+
return { ...sd, canvasHash: createHash3("md5").update(canvasText).digest("hex") };
|
|
14734
14869
|
}
|
|
14735
14870
|
} catch {
|
|
14736
14871
|
}
|
|
@@ -16592,15 +16727,15 @@ ${existing}` : entry;
|
|
|
16592
16727
|
|
|
16593
16728
|
// src/services/setup.ts
|
|
16594
16729
|
import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
|
|
16595
|
-
import { join as
|
|
16730
|
+
import { join as join9, basename, extname, dirname as dirname3 } from "path";
|
|
16596
16731
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
16597
16732
|
|
|
16598
16733
|
// src/lib/detect-codebase.ts
|
|
16599
|
-
import { existsSync as
|
|
16600
|
-
import { readdirSync as readdirSync2, statSync as
|
|
16601
|
-
import { join as
|
|
16734
|
+
import { existsSync as existsSync3 } from "fs";
|
|
16735
|
+
import { readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
16736
|
+
import { join as join5 } from "path";
|
|
16602
16737
|
function detectCodebaseType(projectRoot) {
|
|
16603
|
-
if (
|
|
16738
|
+
if (existsSync3(join5(projectRoot, ".git"))) {
|
|
16604
16739
|
return "existing_codebase";
|
|
16605
16740
|
}
|
|
16606
16741
|
const manifests = [
|
|
@@ -16614,7 +16749,7 @@ function detectCodebaseType(projectRoot) {
|
|
|
16614
16749
|
"CMakeLists.txt"
|
|
16615
16750
|
];
|
|
16616
16751
|
for (const manifest of manifests) {
|
|
16617
|
-
if (
|
|
16752
|
+
if (existsSync3(join5(projectRoot, manifest))) {
|
|
16618
16753
|
return "existing_codebase";
|
|
16619
16754
|
}
|
|
16620
16755
|
}
|
|
@@ -16622,7 +16757,7 @@ function detectCodebaseType(projectRoot) {
|
|
|
16622
16757
|
const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
|
|
16623
16758
|
const fileCount = entries.filter((f) => {
|
|
16624
16759
|
try {
|
|
16625
|
-
return
|
|
16760
|
+
return statSync3(join5(projectRoot, f)).isFile();
|
|
16626
16761
|
} catch {
|
|
16627
16762
|
return false;
|
|
16628
16763
|
}
|
|
@@ -16634,18 +16769,18 @@ function detectCodebaseType(projectRoot) {
|
|
|
16634
16769
|
}
|
|
16635
16770
|
|
|
16636
16771
|
// src/lib/agents-bundle.ts
|
|
16637
|
-
import { readFileSync as
|
|
16638
|
-
import { dirname, join as
|
|
16772
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
16773
|
+
import { dirname, join as join6, resolve } from "path";
|
|
16639
16774
|
import { fileURLToPath } from "url";
|
|
16640
|
-
var PROJECT_BUNDLE_REL =
|
|
16775
|
+
var PROJECT_BUNDLE_REL = join6(".agents", "skills", "papi-cycle");
|
|
16641
16776
|
function bundleDestRel(rel) {
|
|
16642
|
-
return rel === "AGENTS.md" ? "AGENTS.md" :
|
|
16777
|
+
return rel === "AGENTS.md" ? "AGENTS.md" : join6(PROJECT_BUNDLE_REL, rel);
|
|
16643
16778
|
}
|
|
16644
16779
|
function resolveBundleDir() {
|
|
16645
16780
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
16646
16781
|
for (let i = 0; i < 5; i++) {
|
|
16647
|
-
const candidate =
|
|
16648
|
-
if (
|
|
16782
|
+
const candidate = join6(dir, "skills", "papi-cycle");
|
|
16783
|
+
if (existsSync4(join6(candidate, "AGENTS.md"))) return candidate;
|
|
16649
16784
|
const parent = resolve(dir, "..");
|
|
16650
16785
|
if (parent === dir) break;
|
|
16651
16786
|
dir = parent;
|
|
@@ -16653,14 +16788,14 @@ function resolveBundleDir() {
|
|
|
16653
16788
|
return void 0;
|
|
16654
16789
|
}
|
|
16655
16790
|
function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
16656
|
-
if (!bundleDir || !
|
|
16791
|
+
if (!bundleDir || !existsSync4(bundleDir)) return [];
|
|
16657
16792
|
const files = [];
|
|
16658
16793
|
const walk = (abs, rel) => {
|
|
16659
16794
|
for (const entry of readdirSync3(abs, { withFileTypes: true })) {
|
|
16660
|
-
const childAbs =
|
|
16661
|
-
const childRel = rel ?
|
|
16795
|
+
const childAbs = join6(abs, entry.name);
|
|
16796
|
+
const childRel = rel ? join6(rel, entry.name) : entry.name;
|
|
16662
16797
|
if (entry.isDirectory()) walk(childAbs, childRel);
|
|
16663
|
-
else if (entry.isFile()) files.push({ rel: childRel, content:
|
|
16798
|
+
else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync3(childAbs, "utf8") });
|
|
16664
16799
|
}
|
|
16665
16800
|
};
|
|
16666
16801
|
walk(bundleDir, "");
|
|
@@ -16669,8 +16804,8 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
|
16669
16804
|
function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
16670
16805
|
const out = {};
|
|
16671
16806
|
for (const f of readBundleFiles()) {
|
|
16672
|
-
const dest =
|
|
16673
|
-
if (opts.skipExisting &&
|
|
16807
|
+
const dest = join6(projectRoot, bundleDestRel(f.rel));
|
|
16808
|
+
if (opts.skipExisting && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
16674
16809
|
const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
|
|
16675
16810
|
out[dest] = content;
|
|
16676
16811
|
}
|
|
@@ -16678,20 +16813,20 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
|
16678
16813
|
}
|
|
16679
16814
|
|
|
16680
16815
|
// src/lib/design-bundle.ts
|
|
16681
|
-
import { readFileSync as
|
|
16682
|
-
import { dirname as dirname2, join as
|
|
16816
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync5 } from "fs";
|
|
16817
|
+
import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
|
|
16683
16818
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16684
16819
|
var DESIGN_ASSETS = [
|
|
16685
|
-
{ srcRel:
|
|
16686
|
-
{ srcRel:
|
|
16687
|
-
{ srcRel:
|
|
16820
|
+
{ srcRel: join7("agents", "frontend-design-engineer.md"), destRel: join7(".claude", "agents", "frontend-design-engineer.md"), executable: false },
|
|
16821
|
+
{ srcRel: join7("skills", "design-critique", "SKILL.md"), destRel: join7(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
|
|
16822
|
+
{ srcRel: join7("hooks", "frontend-design-guard.sh"), destRel: join7(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
|
|
16688
16823
|
];
|
|
16689
16824
|
var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
|
|
16690
16825
|
function resolveDesignAssetsDir() {
|
|
16691
16826
|
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
16692
16827
|
for (let i = 0; i < 5; i++) {
|
|
16693
|
-
const candidate =
|
|
16694
|
-
if (
|
|
16828
|
+
const candidate = join7(dir, "design-assets");
|
|
16829
|
+
if (existsSync5(join7(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
|
|
16695
16830
|
const parent = resolve2(dir, "..");
|
|
16696
16831
|
if (parent === dir) break;
|
|
16697
16832
|
dir = parent;
|
|
@@ -16703,23 +16838,23 @@ function planDesignInstall(projectRoot, opts = {}) {
|
|
|
16703
16838
|
if (!assetsDir) return [];
|
|
16704
16839
|
const out = [];
|
|
16705
16840
|
for (const asset of DESIGN_ASSETS) {
|
|
16706
|
-
const srcAbs =
|
|
16707
|
-
if (!
|
|
16708
|
-
const dest = projectRoot ?
|
|
16709
|
-
if (opts.skipExisting && projectRoot &&
|
|
16710
|
-
out.push({ dest, content:
|
|
16841
|
+
const srcAbs = join7(assetsDir, asset.srcRel);
|
|
16842
|
+
if (!existsSync5(srcAbs)) continue;
|
|
16843
|
+
const dest = projectRoot ? join7(projectRoot, asset.destRel) : asset.destRel;
|
|
16844
|
+
if (opts.skipExisting && projectRoot && existsSync5(dest) && statSync5(dest).isFile()) continue;
|
|
16845
|
+
out.push({ dest, content: readFileSync4(srcAbs, "utf8"), executable: asset.executable });
|
|
16711
16846
|
}
|
|
16712
16847
|
return out;
|
|
16713
16848
|
}
|
|
16714
16849
|
|
|
16715
16850
|
// src/lib/skill-detection.ts
|
|
16716
|
-
import { existsSync as
|
|
16717
|
-
import { join as
|
|
16851
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync6 } from "fs";
|
|
16852
|
+
import { join as join8 } from "path";
|
|
16718
16853
|
function readPackageJson(projectRoot) {
|
|
16719
|
-
const path7 =
|
|
16720
|
-
if (!
|
|
16854
|
+
const path7 = join8(projectRoot, "package.json");
|
|
16855
|
+
if (!existsSync6(path7)) return null;
|
|
16721
16856
|
try {
|
|
16722
|
-
const raw =
|
|
16857
|
+
const raw = readFileSync5(path7, "utf-8");
|
|
16723
16858
|
return JSON.parse(raw);
|
|
16724
16859
|
} catch {
|
|
16725
16860
|
return null;
|
|
@@ -16740,8 +16875,8 @@ function detectsFrontendStack(projectRoot) {
|
|
|
16740
16875
|
return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
|
|
16741
16876
|
}
|
|
16742
16877
|
function hasGitHubWorkflows(projectRoot) {
|
|
16743
|
-
const dir =
|
|
16744
|
-
if (!
|
|
16878
|
+
const dir = join8(projectRoot, ".github", "workflows");
|
|
16879
|
+
if (!existsSync6(dir)) return false;
|
|
16745
16880
|
try {
|
|
16746
16881
|
const entries = readdirSync4(dir);
|
|
16747
16882
|
return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
@@ -16750,21 +16885,21 @@ function hasGitHubWorkflows(projectRoot) {
|
|
|
16750
16885
|
}
|
|
16751
16886
|
}
|
|
16752
16887
|
function envExampleMentionsStaging(projectRoot) {
|
|
16753
|
-
const path7 =
|
|
16754
|
-
if (!
|
|
16888
|
+
const path7 = join8(projectRoot, ".env.example");
|
|
16889
|
+
if (!existsSync6(path7)) return false;
|
|
16755
16890
|
try {
|
|
16756
|
-
const raw =
|
|
16891
|
+
const raw = readFileSync5(path7, "utf-8");
|
|
16757
16892
|
return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
|
|
16758
16893
|
} catch {
|
|
16759
16894
|
return false;
|
|
16760
16895
|
}
|
|
16761
16896
|
}
|
|
16762
16897
|
function hasVercelConfig(projectRoot) {
|
|
16763
|
-
if (
|
|
16764
|
-
const vercelDir =
|
|
16765
|
-
if (!
|
|
16898
|
+
if (existsSync6(join8(projectRoot, "vercel.json"))) return true;
|
|
16899
|
+
const vercelDir = join8(projectRoot, ".vercel");
|
|
16900
|
+
if (!existsSync6(vercelDir)) return false;
|
|
16766
16901
|
try {
|
|
16767
|
-
return
|
|
16902
|
+
return statSync6(vercelDir).isDirectory();
|
|
16768
16903
|
} catch {
|
|
16769
16904
|
return false;
|
|
16770
16905
|
}
|
|
@@ -17250,7 +17385,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17250
17385
|
await mkdir(config2.papiDir, { recursive: true });
|
|
17251
17386
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
17252
17387
|
const content = substitute(template, vars);
|
|
17253
|
-
await writeFile2(
|
|
17388
|
+
await writeFile2(join9(config2.papiDir, filename), content, "utf-8");
|
|
17254
17389
|
}
|
|
17255
17390
|
}
|
|
17256
17391
|
} else {
|
|
@@ -17268,13 +17403,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17268
17403
|
const useCollector = config2.adapterType === "proxy";
|
|
17269
17404
|
const docsRel = "docs";
|
|
17270
17405
|
const commandsRel = ".claude/commands";
|
|
17271
|
-
const commandsDir = useCollector ? commandsRel :
|
|
17272
|
-
const docsDir = useCollector ? docsRel :
|
|
17406
|
+
const commandsDir = useCollector ? commandsRel : join9(config2.projectRoot, ".claude", "commands");
|
|
17407
|
+
const docsDir = useCollector ? docsRel : join9(config2.projectRoot, "docs");
|
|
17273
17408
|
if (!useCollector) {
|
|
17274
17409
|
await mkdir(commandsDir, { recursive: true });
|
|
17275
17410
|
await mkdir(docsDir, { recursive: true });
|
|
17276
17411
|
}
|
|
17277
|
-
const claudeMdPath = useCollector ? "CLAUDE.md" :
|
|
17412
|
+
const claudeMdPath = useCollector ? "CLAUDE.md" : join9(config2.projectRoot, "CLAUDE.md");
|
|
17278
17413
|
let claudeMdExists = false;
|
|
17279
17414
|
if (!useCollector) {
|
|
17280
17415
|
try {
|
|
@@ -17283,7 +17418,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17283
17418
|
} catch {
|
|
17284
17419
|
}
|
|
17285
17420
|
}
|
|
17286
|
-
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` :
|
|
17421
|
+
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join9(docsDir, "INDEX.md");
|
|
17287
17422
|
let docsIndexExists = false;
|
|
17288
17423
|
if (!useCollector) {
|
|
17289
17424
|
try {
|
|
@@ -17293,9 +17428,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17293
17428
|
}
|
|
17294
17429
|
}
|
|
17295
17430
|
const scaffoldFiles = {
|
|
17296
|
-
[useCollector ? `${commandsRel}/papi-audit.md` :
|
|
17297
|
-
[useCollector ? `${commandsRel}/test.md` :
|
|
17298
|
-
[useCollector ? `${docsRel}/README.md` :
|
|
17431
|
+
[useCollector ? `${commandsRel}/papi-audit.md` : join9(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
|
|
17432
|
+
[useCollector ? `${commandsRel}/test.md` : join9(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
|
|
17433
|
+
[useCollector ? `${docsRel}/README.md` : join9(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
|
|
17299
17434
|
};
|
|
17300
17435
|
if (!docsIndexExists) {
|
|
17301
17436
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
@@ -17322,7 +17457,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17322
17457
|
if (useCollector) {
|
|
17323
17458
|
scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
|
|
17324
17459
|
} else {
|
|
17325
|
-
const cursorDir =
|
|
17460
|
+
const cursorDir = join9(config2.projectRoot, ".cursor");
|
|
17326
17461
|
let cursorDetected = false;
|
|
17327
17462
|
try {
|
|
17328
17463
|
await access2(cursorDir);
|
|
@@ -17330,8 +17465,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17330
17465
|
} catch {
|
|
17331
17466
|
}
|
|
17332
17467
|
if (cursorDetected) {
|
|
17333
|
-
const cursorRulesDir =
|
|
17334
|
-
const cursorRulesPath =
|
|
17468
|
+
const cursorRulesDir = join9(cursorDir, "rules");
|
|
17469
|
+
const cursorRulesPath = join9(cursorRulesDir, "papi.mdc");
|
|
17335
17470
|
await mkdir(cursorRulesDir, { recursive: true });
|
|
17336
17471
|
try {
|
|
17337
17472
|
await access2(cursorRulesPath);
|
|
@@ -17386,7 +17521,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
17386
17521
|
}
|
|
17387
17522
|
var PAPI_PERMISSION = "mcp__papi__*";
|
|
17388
17523
|
async function ensurePapiPermission(projectRoot) {
|
|
17389
|
-
const settingsPath =
|
|
17524
|
+
const settingsPath = join9(projectRoot, ".claude", "settings.json");
|
|
17390
17525
|
try {
|
|
17391
17526
|
let settings = {};
|
|
17392
17527
|
try {
|
|
@@ -17405,13 +17540,13 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
17405
17540
|
if (!allow.includes(PAPI_PERMISSION)) {
|
|
17406
17541
|
allow.push(PAPI_PERMISSION);
|
|
17407
17542
|
}
|
|
17408
|
-
await mkdir(
|
|
17543
|
+
await mkdir(join9(projectRoot, ".claude"), { recursive: true });
|
|
17409
17544
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
17410
17545
|
} catch {
|
|
17411
17546
|
}
|
|
17412
17547
|
}
|
|
17413
17548
|
async function ensureDesignHookRegistered(projectRoot) {
|
|
17414
|
-
const settingsPath =
|
|
17549
|
+
const settingsPath = join9(projectRoot, ".claude", "settings.json");
|
|
17415
17550
|
try {
|
|
17416
17551
|
let settings = {};
|
|
17417
17552
|
try {
|
|
@@ -17441,7 +17576,7 @@ async function ensureDesignHookRegistered(projectRoot) {
|
|
|
17441
17576
|
chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
|
|
17442
17577
|
}
|
|
17443
17578
|
}
|
|
17444
|
-
await mkdir(
|
|
17579
|
+
await mkdir(join9(projectRoot, ".claude"), { recursive: true });
|
|
17445
17580
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
17446
17581
|
} catch {
|
|
17447
17582
|
}
|
|
@@ -17545,7 +17680,7 @@ ${conventionsText.trim()}
|
|
|
17545
17680
|
);
|
|
17546
17681
|
} else {
|
|
17547
17682
|
try {
|
|
17548
|
-
const claudeMdPath =
|
|
17683
|
+
const claudeMdPath = join9(config2.projectRoot, "CLAUDE.md");
|
|
17549
17684
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
17550
17685
|
if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
|
|
17551
17686
|
warnings.push(
|
|
@@ -17632,13 +17767,13 @@ async function scanCodebase(projectRoot) {
|
|
|
17632
17767
|
}
|
|
17633
17768
|
let packageJson;
|
|
17634
17769
|
try {
|
|
17635
|
-
const content = await readFile4(
|
|
17770
|
+
const content = await readFile4(join9(projectRoot, "package.json"), "utf-8");
|
|
17636
17771
|
packageJson = JSON.parse(content);
|
|
17637
17772
|
} catch {
|
|
17638
17773
|
}
|
|
17639
17774
|
let readme;
|
|
17640
17775
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
17641
|
-
const content = await safeReadFile(
|
|
17776
|
+
const content = await safeReadFile(join9(projectRoot, name), 5e3);
|
|
17642
17777
|
if (content) {
|
|
17643
17778
|
readme = content;
|
|
17644
17779
|
break;
|
|
@@ -17648,7 +17783,7 @@ async function scanCodebase(projectRoot) {
|
|
|
17648
17783
|
let totalFiles = topLevelFiles.length;
|
|
17649
17784
|
for (const dir of topLevelDirs) {
|
|
17650
17785
|
try {
|
|
17651
|
-
const entries = await readdir(
|
|
17786
|
+
const entries = await readdir(join9(projectRoot, dir), { withFileTypes: true });
|
|
17652
17787
|
const files = entries.filter((e) => e.isFile());
|
|
17653
17788
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
17654
17789
|
totalFiles += files.length;
|
|
@@ -17995,7 +18130,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17995
18130
|
collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
|
|
17996
18131
|
} else {
|
|
17997
18132
|
try {
|
|
17998
|
-
const claudeMdPath =
|
|
18133
|
+
const claudeMdPath = join9(config2.projectRoot, "CLAUDE.md");
|
|
17999
18134
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
18000
18135
|
if (!existing.includes("Dogfood Logging")) {
|
|
18001
18136
|
await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
|
|
@@ -18040,7 +18175,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
18040
18175
|
cursorScaffolded = true;
|
|
18041
18176
|
} else {
|
|
18042
18177
|
try {
|
|
18043
|
-
await access2(
|
|
18178
|
+
await access2(join9(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
18044
18179
|
cursorScaffolded = true;
|
|
18045
18180
|
} catch {
|
|
18046
18181
|
}
|
|
@@ -18060,11 +18195,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
18060
18195
|
}
|
|
18061
18196
|
async function ensureMcpJsonGitignored(projectRoot) {
|
|
18062
18197
|
try {
|
|
18063
|
-
await access2(
|
|
18198
|
+
await access2(join9(projectRoot, ".git"));
|
|
18064
18199
|
} catch {
|
|
18065
18200
|
return void 0;
|
|
18066
18201
|
}
|
|
18067
|
-
const gitignorePath =
|
|
18202
|
+
const gitignorePath = join9(projectRoot, ".gitignore");
|
|
18068
18203
|
let existing = "";
|
|
18069
18204
|
try {
|
|
18070
18205
|
existing = await readFile4(gitignorePath, "utf-8");
|
|
@@ -18579,8 +18714,8 @@ PAPI never runs this command itself (AD-58) \u2014 you run it in your own enviro
|
|
|
18579
18714
|
|
|
18580
18715
|
// src/services/build.ts
|
|
18581
18716
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
18582
|
-
import { readdirSync as readdirSync5, existsSync as
|
|
18583
|
-
import { join as
|
|
18717
|
+
import { readdirSync as readdirSync5, existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
18718
|
+
import { join as join11 } from "path";
|
|
18584
18719
|
|
|
18585
18720
|
// src/lib/harness-capability.ts
|
|
18586
18721
|
var HARNESS_REGISTRY = {
|
|
@@ -18656,7 +18791,7 @@ init_git();
|
|
|
18656
18791
|
// src/services/release.ts
|
|
18657
18792
|
init_telemetry();
|
|
18658
18793
|
import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
18659
|
-
import { join as
|
|
18794
|
+
import { join as join10 } from "path";
|
|
18660
18795
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
18661
18796
|
init_git();
|
|
18662
18797
|
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
@@ -19087,20 +19222,40 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
19087
19222
|
if (adapter2 && resolvedCycleNum > 0) {
|
|
19088
19223
|
try {
|
|
19089
19224
|
const stampedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19225
|
+
const resolvedBase = resolveBaseBranch(config2.projectRoot, branch);
|
|
19090
19226
|
const doneCycleTasks = (await adapter2.queryBoard()).filter(
|
|
19091
19227
|
(t) => t.cycle === resolvedCycleNum && t.status === "Done"
|
|
19092
19228
|
);
|
|
19229
|
+
const canVerify = isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
19230
|
+
const landed = canVerify ? findTaskCommitsOnBase(
|
|
19231
|
+
config2.projectRoot,
|
|
19232
|
+
resolvedBase,
|
|
19233
|
+
doneCycleTasks.map((t) => t.displayId)
|
|
19234
|
+
) : /* @__PURE__ */ new Map();
|
|
19235
|
+
const unverified = [];
|
|
19093
19236
|
for (const t of doneCycleTasks) {
|
|
19237
|
+
const hit = landed.get(t.displayId);
|
|
19238
|
+
if (canVerify && !hit) {
|
|
19239
|
+
unverified.push(
|
|
19240
|
+
`${t.displayId}${t.branchName ? ` (branch \`${t.branchName}\`)` : " (no branch recorded)"}`
|
|
19241
|
+
);
|
|
19242
|
+
continue;
|
|
19243
|
+
}
|
|
19094
19244
|
try {
|
|
19095
19245
|
await adapter2.updateTask(t.id, { mergedAt: stampedAt });
|
|
19096
19246
|
} catch {
|
|
19097
19247
|
}
|
|
19098
19248
|
}
|
|
19249
|
+
if (unverified.length > 0) {
|
|
19250
|
+
warnings.push(
|
|
19251
|
+
`\u26A0\uFE0F DATA-INTEGRITY \u2014 ${unverified.length} task(s) are marked **Done** in cycle ${resolvedCycleNum} but their work is NOT on ${resolvedBase}: ${unverified.join("; ")}. They were NOT stamped as merged. Held ad-hoc work is the usual cause (\`ad_hoc\` + \`board_edit\` leaves the branch unmerged by design). Merge the branch(es), or move the task(s) to the next cycle \u2014 do not leave Done unbacked by git.`
|
|
19252
|
+
);
|
|
19253
|
+
}
|
|
19099
19254
|
} catch {
|
|
19100
19255
|
}
|
|
19101
19256
|
}
|
|
19102
19257
|
const latestTag = getLatestTag(config2.projectRoot);
|
|
19103
|
-
const changelogPath =
|
|
19258
|
+
const changelogPath = join10(config2.projectRoot, "CHANGELOG.md");
|
|
19104
19259
|
if (!latestTag) {
|
|
19105
19260
|
const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
19106
19261
|
if (config2.adapterType === "proxy") {
|
|
@@ -19155,6 +19310,44 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
19155
19310
|
};
|
|
19156
19311
|
}
|
|
19157
19312
|
|
|
19313
|
+
// src/services/release-bookkeeping.ts
|
|
19314
|
+
async function beginRelease(tracker, cycleNum) {
|
|
19315
|
+
tracker.setStreamScope({ cycle: cycleNum ?? null });
|
|
19316
|
+
await tracker.recordStep("release_starting");
|
|
19317
|
+
}
|
|
19318
|
+
async function recordReadinessVerified(tracker) {
|
|
19319
|
+
await tracker.recordStep("readiness_verified");
|
|
19320
|
+
}
|
|
19321
|
+
async function recordQualityGate(tracker, decision, caps) {
|
|
19322
|
+
await tracker.recordStep("quality_gate", {
|
|
19323
|
+
status: decision.stepStatus,
|
|
19324
|
+
capabilityKey: "releaseGate",
|
|
19325
|
+
capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
|
|
19326
|
+
metadata: decision.metadata
|
|
19327
|
+
});
|
|
19328
|
+
}
|
|
19329
|
+
async function completeRelease(tracker, opts) {
|
|
19330
|
+
tracker.setStreamScope({ cycle: opts.cycleClosed ?? null });
|
|
19331
|
+
await tracker.recordStep("cycle_complete", { metadata: { version: opts.version } });
|
|
19332
|
+
for (const m of opts.branchMerges ?? []) {
|
|
19333
|
+
await tracker.recordStep("branch_merged", {
|
|
19334
|
+
metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
|
|
19335
|
+
});
|
|
19336
|
+
}
|
|
19337
|
+
await tracker.recordStep("changelog", {
|
|
19338
|
+
capabilityKey: "changelog",
|
|
19339
|
+
capabilityEnabled: isCapabilityEnabled(opts.caps, "changelog"),
|
|
19340
|
+
status: opts.changelogEmitted ? "complete" : "active"
|
|
19341
|
+
});
|
|
19342
|
+
if (opts.deployHookEmitted) {
|
|
19343
|
+
await tracker.recordStep("deploy_hook", {
|
|
19344
|
+
capabilityKey: "deployHook",
|
|
19345
|
+
capabilityEnabled: isCapabilityEnabled(opts.caps, "deployHook")
|
|
19346
|
+
});
|
|
19347
|
+
}
|
|
19348
|
+
await tracker.recordStep("released", { metadata: { version: opts.version } });
|
|
19349
|
+
}
|
|
19350
|
+
|
|
19158
19351
|
// src/tools/release.ts
|
|
19159
19352
|
init_git();
|
|
19160
19353
|
|
|
@@ -19444,9 +19637,16 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19444
19637
|
}
|
|
19445
19638
|
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
|
|
19446
19639
|
try {
|
|
19447
|
-
await tracker.recordStep("release_starting");
|
|
19448
19640
|
tracker.mark("owner-identity-guard");
|
|
19449
19641
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
19642
|
+
let cycleToClose = null;
|
|
19643
|
+
try {
|
|
19644
|
+
const resolved = await resolveCycleToClose(adapter2, version, gate.callerUserId);
|
|
19645
|
+
cycleToClose = resolved > 0 ? resolved : null;
|
|
19646
|
+
} catch {
|
|
19647
|
+
cycleToClose = null;
|
|
19648
|
+
}
|
|
19649
|
+
await beginRelease(tracker, cycleToClose);
|
|
19450
19650
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19451
19651
|
if (branch === productionBaseBranch) {
|
|
19452
19652
|
if (gate.enforced && !gate.callerIsOwner) {
|
|
@@ -19511,6 +19711,13 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
19511
19711
|
}
|
|
19512
19712
|
}
|
|
19513
19713
|
}
|
|
19714
|
+
let caps = {};
|
|
19715
|
+
try {
|
|
19716
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
19717
|
+
caps = info?.capabilities ?? {};
|
|
19718
|
+
} catch {
|
|
19719
|
+
caps = {};
|
|
19720
|
+
}
|
|
19514
19721
|
if (isHostedTransport()) {
|
|
19515
19722
|
tracker.mark("hosted-close-cycle");
|
|
19516
19723
|
let closed;
|
|
@@ -19523,6 +19730,16 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
19523
19730
|
const message = err instanceof Error ? err.message : String(err);
|
|
19524
19731
|
return errorResponse(message);
|
|
19525
19732
|
}
|
|
19733
|
+
await recordReadinessVerified(tracker);
|
|
19734
|
+
const hostedGate = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
|
|
19735
|
+
await recordQualityGate(tracker, hostedGate, caps);
|
|
19736
|
+
await completeRelease(tracker, {
|
|
19737
|
+
cycleClosed: closed.resolvedCycleNum > 0 ? closed.resolvedCycleNum : null,
|
|
19738
|
+
version,
|
|
19739
|
+
caps,
|
|
19740
|
+
branchMerges: [],
|
|
19741
|
+
changelogEmitted: false
|
|
19742
|
+
});
|
|
19526
19743
|
const tagAnnotation = `Release ${version}`;
|
|
19527
19744
|
const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
|
|
19528
19745
|
const warningsBlock = closed.warnings.length > 0 ? `
|
|
@@ -19583,22 +19800,10 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19583
19800
|
console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
|
|
19584
19801
|
}
|
|
19585
19802
|
}
|
|
19586
|
-
await tracker
|
|
19587
|
-
let caps = {};
|
|
19588
|
-
try {
|
|
19589
|
-
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
19590
|
-
caps = info?.capabilities ?? {};
|
|
19591
|
-
} catch {
|
|
19592
|
-
caps = {};
|
|
19593
|
-
}
|
|
19803
|
+
await recordReadinessVerified(tracker);
|
|
19594
19804
|
tracker.mark("quality-gate");
|
|
19595
19805
|
const gateDecision = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
|
|
19596
|
-
await tracker
|
|
19597
|
-
status: gateDecision.stepStatus,
|
|
19598
|
-
capabilityKey: "releaseGate",
|
|
19599
|
-
capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
|
|
19600
|
-
metadata: gateDecision.metadata
|
|
19601
|
-
});
|
|
19806
|
+
await recordQualityGate(tracker, gateDecision, caps);
|
|
19602
19807
|
if (gateDecision.action === "directive") {
|
|
19603
19808
|
return textResponse(gateDecision.message);
|
|
19604
19809
|
}
|
|
@@ -19611,13 +19816,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19611
19816
|
skipVersion: skipVersion ?? false,
|
|
19612
19817
|
callerUserId: gate.callerUserId
|
|
19613
19818
|
});
|
|
19614
|
-
tracker.setStreamScope({ cycle: result.cycleClosed ?? null });
|
|
19615
|
-
await tracker.recordStep("cycle_complete", { metadata: { version: result.version } });
|
|
19616
|
-
for (const m of result.groupedBranchMerges ?? []) {
|
|
19617
|
-
await tracker.recordStep("branch_merged", {
|
|
19618
|
-
metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
|
|
19619
|
-
});
|
|
19620
|
-
}
|
|
19621
19819
|
const lines = [
|
|
19622
19820
|
`## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
|
|
19623
19821
|
"",
|
|
@@ -19672,21 +19870,16 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19672
19870
|
buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
|
|
19673
19871
|
);
|
|
19674
19872
|
if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
|
|
19675
|
-
await tracker.recordStep("changelog", {
|
|
19676
|
-
capabilityKey: "changelog",
|
|
19677
|
-
capabilityEnabled: isCapabilityEnabled(caps, "changelog"),
|
|
19678
|
-
status: cycleUpdateDirective ? "complete" : "active"
|
|
19679
|
-
});
|
|
19680
19873
|
const deployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
19681
|
-
if (deployDirective)
|
|
19682
|
-
|
|
19683
|
-
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
|
|
19689
|
-
}
|
|
19874
|
+
if (deployDirective) lines.push(deployDirective);
|
|
19875
|
+
await completeRelease(tracker, {
|
|
19876
|
+
cycleClosed: result.cycleClosed ?? null,
|
|
19877
|
+
version: result.version,
|
|
19878
|
+
caps,
|
|
19879
|
+
branchMerges: result.groupedBranchMerges ?? [],
|
|
19880
|
+
changelogEmitted: Boolean(cycleUpdateDirective),
|
|
19881
|
+
deployHookEmitted: Boolean(deployDirective)
|
|
19882
|
+
});
|
|
19690
19883
|
const publishEnabled = isCapabilityEnabled(caps, "publishDirective");
|
|
19691
19884
|
if (publishEnabled) {
|
|
19692
19885
|
try {
|
|
@@ -19714,7 +19907,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19714
19907
|
const verifyDirective = buildVerifyHealthCheckDirective(caps);
|
|
19715
19908
|
if (verifyDirective) lines.push(verifyDirective);
|
|
19716
19909
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`);
|
|
19717
|
-
await tracker.recordStep("released", { metadata: { version: result.version } });
|
|
19718
19910
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
19719
19911
|
return textResponse(lines.join("\n") + filesToWriteSection);
|
|
19720
19912
|
} catch (err) {
|
|
@@ -19874,32 +20066,94 @@ function capitalizeCompleted(value) {
|
|
|
19874
20066
|
};
|
|
19875
20067
|
return map[value] ?? "No";
|
|
19876
20068
|
}
|
|
20069
|
+
function stripAnnotations(text) {
|
|
20070
|
+
let out = "";
|
|
20071
|
+
let i = 0;
|
|
20072
|
+
while (i < text.length) {
|
|
20073
|
+
const ch = text[i];
|
|
20074
|
+
if (ch === ")") {
|
|
20075
|
+
i++;
|
|
20076
|
+
continue;
|
|
20077
|
+
}
|
|
20078
|
+
if (ch === "(") {
|
|
20079
|
+
let depth = 0;
|
|
20080
|
+
let j = i;
|
|
20081
|
+
for (; j < text.length; j++) {
|
|
20082
|
+
if (text[j] === "(") depth++;
|
|
20083
|
+
else if (text[j] === ")") {
|
|
20084
|
+
depth--;
|
|
20085
|
+
if (depth === 0) {
|
|
20086
|
+
j++;
|
|
20087
|
+
break;
|
|
20088
|
+
}
|
|
20089
|
+
}
|
|
20090
|
+
}
|
|
20091
|
+
if (depth !== 0) {
|
|
20092
|
+
break;
|
|
20093
|
+
}
|
|
20094
|
+
const inner = text.slice(i + 1, j - 1);
|
|
20095
|
+
const prev = i > 0 ? text[i - 1] : "/";
|
|
20096
|
+
const next = text[j] ?? "";
|
|
20097
|
+
const isRouteGroup = (prev === "/" || i === 0) && !/\s/.test(inner) && next === "/";
|
|
20098
|
+
if (isRouteGroup) {
|
|
20099
|
+
out += text.slice(i, j);
|
|
20100
|
+
i = j;
|
|
20101
|
+
continue;
|
|
20102
|
+
}
|
|
20103
|
+
i = j;
|
|
20104
|
+
continue;
|
|
20105
|
+
}
|
|
20106
|
+
out += ch;
|
|
20107
|
+
i++;
|
|
20108
|
+
}
|
|
20109
|
+
return out;
|
|
20110
|
+
}
|
|
19877
20111
|
function sanitisePredictedFiles(raw) {
|
|
19878
20112
|
const out = [];
|
|
19879
20113
|
for (const entry of raw) {
|
|
19880
20114
|
if (typeof entry !== "string") continue;
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
20115
|
+
let working = entry;
|
|
20116
|
+
const altMatches = working.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
|
|
20117
|
+
for (const alt of altMatches) {
|
|
20118
|
+
const inner = alt.match(/`([^`]+)`/);
|
|
20119
|
+
if (inner) out.push(inner[1].trim());
|
|
20120
|
+
working = working.replace(alt, " ");
|
|
20121
|
+
}
|
|
20122
|
+
const cleanedEntry = stripAnnotations(working.replace(/`/g, ""));
|
|
20123
|
+
for (const segment of cleanedEntry.split(/[;,]/)) {
|
|
20124
|
+
const cleaned = segment.trim();
|
|
20125
|
+
if (!cleaned) continue;
|
|
20126
|
+
if (/\s/.test(cleaned)) continue;
|
|
20127
|
+
if (!/[A-Za-z0-9]/.test(cleaned)) continue;
|
|
20128
|
+
out.push(cleaned);
|
|
19889
20129
|
}
|
|
19890
20130
|
}
|
|
19891
20131
|
return Array.from(new Set(out));
|
|
19892
20132
|
}
|
|
20133
|
+
function globToRegExp(entry) {
|
|
20134
|
+
const source = entry.split(/(\*\*|\*)/).map((part) => {
|
|
20135
|
+
if (part === "**") return ".*";
|
|
20136
|
+
if (part === "*") return "[^/]*";
|
|
20137
|
+
return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
20138
|
+
}).join("");
|
|
20139
|
+
return new RegExp(`^${source}$`, "i");
|
|
20140
|
+
}
|
|
19893
20141
|
function pathBasename(p) {
|
|
19894
20142
|
const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
19895
20143
|
return parts[parts.length - 1] ?? p;
|
|
19896
20144
|
}
|
|
19897
20145
|
function isPathInPredictedScope(changedPath, predicted) {
|
|
19898
|
-
const
|
|
20146
|
+
const changed = changedPath.replace(/\\/g, "/");
|
|
20147
|
+
const changedLower = changed.toLowerCase();
|
|
19899
20148
|
const changedBase = pathBasename(changedPath);
|
|
19900
20149
|
for (const raw of predicted) {
|
|
19901
20150
|
const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
|
|
19902
20151
|
if (!entry) continue;
|
|
20152
|
+
if (entry.includes("*")) {
|
|
20153
|
+
if (globToRegExp(entry).test(changed)) return true;
|
|
20154
|
+
if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return true;
|
|
20155
|
+
continue;
|
|
20156
|
+
}
|
|
19903
20157
|
if (pathBasename(entry) === changedBase) return true;
|
|
19904
20158
|
const entryLower = entry.toLowerCase();
|
|
19905
20159
|
if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
|
|
@@ -19943,23 +20197,27 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
19943
20197
|
};
|
|
19944
20198
|
const cleanedPredicted = sanitisePredictedFiles(predictedFiles);
|
|
19945
20199
|
const scoped = modified.filter((p) => isPathInPredictedScope(p, cleanedPredicted));
|
|
19946
|
-
|
|
20200
|
+
const untracked = getUntrackedFiles(cwd);
|
|
20201
|
+
const scopedSet = new Set(scoped);
|
|
20202
|
+
const untrackedInScope = untracked.filter(
|
|
20203
|
+
(p) => !scopedSet.has(p) && isPathInPredictedScope(p, cleanedPredicted)
|
|
20204
|
+
);
|
|
20205
|
+
if (scoped.length === 0 && untrackedInScope.length === 0) {
|
|
19947
20206
|
const modSample = modified.slice(0, 5).join(", ");
|
|
19948
20207
|
const predSample = cleanedPredicted.slice(0, 5).join(", ");
|
|
19949
20208
|
return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
19950
20209
|
}
|
|
19951
|
-
const untracked = getUntrackedFiles(cwd);
|
|
19952
20210
|
const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
|
|
19953
20211
|
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
19954
|
-
const
|
|
20212
|
+
const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
|
|
19955
20213
|
const adjacentUntracked = untracked.filter(
|
|
19956
|
-
(p) => !
|
|
20214
|
+
(p) => !inScopeSet.has(p) && isUnderScopedDir(p)
|
|
19957
20215
|
);
|
|
19958
|
-
const toStage = [...scoped, ...adjacentUntracked];
|
|
20216
|
+
const toStage = [...scoped, ...untrackedInScope, ...adjacentUntracked];
|
|
19959
20217
|
const toStageSet = new Set(toStage);
|
|
19960
20218
|
const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
|
|
19961
20219
|
const droppedModified = modified.filter((p) => !scopedSet.has(p));
|
|
19962
|
-
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
20220
|
+
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (untrackedInScope.length > 0 ? ` + ${untrackedInScope.length} new file(s) named in the handoff` : "") + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
19963
20221
|
if (droppedModified.length > 0) {
|
|
19964
20222
|
const sample = droppedModified.slice(0, 10).join(", ");
|
|
19965
20223
|
const more = droppedModified.length > 10 ? ` (+${droppedModified.length - 10} more)` : "";
|
|
@@ -20434,17 +20692,17 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
20434
20692
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
20435
20693
|
return;
|
|
20436
20694
|
}
|
|
20437
|
-
const papiDir =
|
|
20438
|
-
if (!
|
|
20695
|
+
const papiDir = join11(projectRoot, ".papi");
|
|
20696
|
+
if (!existsSync7(papiDir)) {
|
|
20439
20697
|
mkdirSync2(papiDir, { recursive: true });
|
|
20440
20698
|
}
|
|
20441
|
-
const scopePath =
|
|
20442
|
-
|
|
20699
|
+
const scopePath = join11(papiDir, "active-task-scope.txt");
|
|
20700
|
+
writeFileSync3(scopePath, content, "utf-8");
|
|
20443
20701
|
}
|
|
20444
20702
|
function clearActiveTaskScope(projectRoot) {
|
|
20445
|
-
const scopePath =
|
|
20446
|
-
if (
|
|
20447
|
-
|
|
20703
|
+
const scopePath = join11(projectRoot, ".papi", "active-task-scope.txt");
|
|
20704
|
+
if (existsSync7(scopePath)) {
|
|
20705
|
+
unlinkSync2(scopePath);
|
|
20448
20706
|
}
|
|
20449
20707
|
}
|
|
20450
20708
|
function sanitiseResponseExcerpt(raw) {
|
|
@@ -20463,7 +20721,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
20463
20721
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
20464
20722
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
20465
20723
|
try {
|
|
20466
|
-
const content =
|
|
20724
|
+
const content = readFileSync6(absolutePath, "utf-8").slice(0, 2e3);
|
|
20467
20725
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
20468
20726
|
if (fmMatch) {
|
|
20469
20727
|
const fm = fmMatch[1];
|
|
@@ -20842,14 +21100,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
20842
21100
|
let docWarning;
|
|
20843
21101
|
try {
|
|
20844
21102
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
20845
|
-
const docsDir =
|
|
20846
|
-
if (
|
|
21103
|
+
const docsDir = join11(config2.projectRoot, "docs");
|
|
21104
|
+
if (existsSync7(docsDir)) {
|
|
20847
21105
|
const scanDir = (dir, depth = 0) => {
|
|
20848
21106
|
if (depth > 8) return [];
|
|
20849
21107
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
20850
21108
|
const files = [];
|
|
20851
21109
|
for (const e of entries) {
|
|
20852
|
-
const full =
|
|
21110
|
+
const full = join11(dir, e.name);
|
|
20853
21111
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
20854
21112
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
20855
21113
|
}
|
|
@@ -20864,7 +21122,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
20864
21122
|
const failed = [];
|
|
20865
21123
|
for (const docPath of unregistered) {
|
|
20866
21124
|
try {
|
|
20867
|
-
const meta = extractDocMeta(
|
|
21125
|
+
const meta = extractDocMeta(join11(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
20868
21126
|
await adapter2.registerDoc({
|
|
20869
21127
|
title: meta.title,
|
|
20870
21128
|
type: meta.type,
|
|
@@ -21013,8 +21271,8 @@ ${instructions}`;
|
|
|
21013
21271
|
}
|
|
21014
21272
|
|
|
21015
21273
|
// src/tools/doc-registry.ts
|
|
21016
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
21017
|
-
import { join as
|
|
21274
|
+
import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
21275
|
+
import { join as join12, relative } from "path";
|
|
21018
21276
|
import { homedir as homedir3 } from "os";
|
|
21019
21277
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
21020
21278
|
var docRegisterTool = {
|
|
@@ -21211,7 +21469,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
21211
21469
|
const lines = docs.map((d) => {
|
|
21212
21470
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
21213
21471
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
21214
|
-
const missingNote = root && d.path && !
|
|
21472
|
+
const missingNote = root && d.path && !existsSync8(join12(root, d.path)) ? `
|
|
21215
21473
|
> \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there. Check \`git stash list\` for a papi-autostash entry, or re-create/deregister the doc.` : "";
|
|
21216
21474
|
return `### ${d.title}
|
|
21217
21475
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -21225,12 +21483,12 @@ ${d.summary}
|
|
|
21225
21483
|
${lines.join("\n---\n\n")}`);
|
|
21226
21484
|
}
|
|
21227
21485
|
function scanMdFiles(dir, rootDir) {
|
|
21228
|
-
if (!
|
|
21486
|
+
if (!existsSync8(dir)) return [];
|
|
21229
21487
|
const files = [];
|
|
21230
21488
|
try {
|
|
21231
21489
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
21232
21490
|
for (const entry of entries) {
|
|
21233
|
-
const full =
|
|
21491
|
+
const full = join12(dir, entry.name);
|
|
21234
21492
|
if (entry.isDirectory()) {
|
|
21235
21493
|
files.push(...scanMdFiles(full, rootDir));
|
|
21236
21494
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -21243,7 +21501,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
21243
21501
|
}
|
|
21244
21502
|
function extractTitle(filePath) {
|
|
21245
21503
|
try {
|
|
21246
|
-
const content =
|
|
21504
|
+
const content = readFileSync7(filePath, "utf-8").slice(0, 1e3);
|
|
21247
21505
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
21248
21506
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
21249
21507
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -21255,7 +21513,7 @@ function extractTitle(filePath) {
|
|
|
21255
21513
|
async function detectUnregisteredDocsNote(adapter2, config2) {
|
|
21256
21514
|
try {
|
|
21257
21515
|
if (!adapter2.searchDocs || !hasLocalWorkspace()) return "";
|
|
21258
|
-
const docsDir =
|
|
21516
|
+
const docsDir = join12(config2.projectRoot, "docs");
|
|
21259
21517
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21260
21518
|
if (docsFiles.length === 0) return "";
|
|
21261
21519
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -21281,17 +21539,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21281
21539
|
const includePlans = args.include_plans ?? false;
|
|
21282
21540
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21283
21541
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21284
|
-
const docsDir =
|
|
21542
|
+
const docsDir = join12(config2.projectRoot, "docs");
|
|
21285
21543
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21286
21544
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
21287
21545
|
let unregisteredPlans = [];
|
|
21288
21546
|
if (includePlans) {
|
|
21289
|
-
const plansDir =
|
|
21290
|
-
if (
|
|
21547
|
+
const plansDir = join12(homedir3(), ".claude", "plans");
|
|
21548
|
+
if (existsSync8(plansDir)) {
|
|
21291
21549
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
21292
21550
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
21293
21551
|
path: f,
|
|
21294
|
-
title: extractTitle(
|
|
21552
|
+
title: extractTitle(join12(plansDir, f.replace("plans/", "")))
|
|
21295
21553
|
}));
|
|
21296
21554
|
}
|
|
21297
21555
|
}
|
|
@@ -21302,7 +21560,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21302
21560
|
if (unregisteredDocs.length > 0) {
|
|
21303
21561
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
21304
21562
|
for (const f of unregisteredDocs) {
|
|
21305
|
-
const title = extractTitle(
|
|
21563
|
+
const title = extractTitle(join12(config2.projectRoot, f));
|
|
21306
21564
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
21307
21565
|
}
|
|
21308
21566
|
}
|
|
@@ -22732,13 +22990,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
22732
22990
|
init_git();
|
|
22733
22991
|
|
|
22734
22992
|
// src/services/reconcile.ts
|
|
22735
|
-
import { readFileSync as
|
|
22736
|
-
import { join as
|
|
22993
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
22994
|
+
import { join as join13 } from "path";
|
|
22737
22995
|
function loadDocsIndex(projectRoot) {
|
|
22738
22996
|
if (!hasLocalWorkspace()) return "";
|
|
22739
22997
|
try {
|
|
22740
|
-
const indexPath =
|
|
22741
|
-
const raw =
|
|
22998
|
+
const indexPath = join13(projectRoot, "docs", "INDEX.md");
|
|
22999
|
+
const raw = readFileSync8(indexPath, "utf8");
|
|
22742
23000
|
const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
|
|
22743
23001
|
if (rows.length === 0) return "";
|
|
22744
23002
|
const entries = rows.map((row) => {
|
|
@@ -23313,8 +23571,8 @@ Produce your analysis and structured output above. Present Part 1 to the user an
|
|
|
23313
23571
|
}
|
|
23314
23572
|
|
|
23315
23573
|
// src/tools/review.ts
|
|
23316
|
-
import { existsSync as
|
|
23317
|
-
import { join as
|
|
23574
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
23575
|
+
import { join as join14 } from "path";
|
|
23318
23576
|
init_git();
|
|
23319
23577
|
|
|
23320
23578
|
// src/services/review.ts
|
|
@@ -23536,11 +23794,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
|
23536
23794
|
${diff}
|
|
23537
23795
|
\`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
|
|
23538
23796
|
let projectContext = "";
|
|
23539
|
-
const ctxPath =
|
|
23540
|
-
if (
|
|
23797
|
+
const ctxPath = join14(config2.projectRoot, ".agents", "papi-context.md");
|
|
23798
|
+
if (existsSync9(ctxPath)) {
|
|
23541
23799
|
try {
|
|
23542
23800
|
projectContext = `### Project context (.agents/papi-context.md)
|
|
23543
|
-
${
|
|
23801
|
+
${readFileSync9(ctxPath, "utf-8")}
|
|
23544
23802
|
|
|
23545
23803
|
`;
|
|
23546
23804
|
} catch {
|
|
@@ -23695,8 +23953,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
23695
23953
|
};
|
|
23696
23954
|
}
|
|
23697
23955
|
const details = [];
|
|
23698
|
-
const papiDir =
|
|
23699
|
-
if (
|
|
23956
|
+
const papiDir = join14(config2.projectRoot, ".papi");
|
|
23957
|
+
if (existsSync9(papiDir)) {
|
|
23700
23958
|
try {
|
|
23701
23959
|
const commitResult = stageDirAndCommit(
|
|
23702
23960
|
config2.projectRoot,
|
|
@@ -24035,11 +24293,38 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
24035
24293
|
} catch {
|
|
24036
24294
|
}
|
|
24037
24295
|
const version = `v0.${result.currentCycle}.0`;
|
|
24038
|
-
const
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
24296
|
+
const autoGate = evaluateReleaseGate(caps, config2.gateCommand, void 0);
|
|
24297
|
+
if (autoGate.action !== "proceed") {
|
|
24298
|
+
autoReleaseNote = `
|
|
24299
|
+
|
|
24300
|
+
---
|
|
24301
|
+
|
|
24302
|
+
\u26A0\uFE0F **Auto-release skipped** \u2014 a release quality gate is configured (\`${config2.gateCommand}\`), and it cannot be run from inside \`review_submit\`.
|
|
24303
|
+
|
|
24304
|
+
Run \`release\` manually: PAPI will hand you the gate command, then release once you report it green.`;
|
|
24305
|
+
} else {
|
|
24306
|
+
const releaseTracker = new ProgressTracker("auto-release").bindStream(adapter2, { stage: "release" });
|
|
24307
|
+
await beginRelease(releaseTracker, result.currentCycle);
|
|
24308
|
+
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
24309
|
+
await recordReadinessVerified(releaseTracker);
|
|
24310
|
+
await recordQualityGate(releaseTracker, autoGate, caps);
|
|
24311
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
24312
|
+
const autoChangelogDirective = buildChangelogDirective(
|
|
24313
|
+
caps,
|
|
24314
|
+
buildCycleUpdateCurationDirective(releaseResult.version, releaseResult.cycleClosed ?? 0)
|
|
24315
|
+
);
|
|
24316
|
+
const autoDeployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
24317
|
+
await completeRelease(releaseTracker, {
|
|
24318
|
+
cycleClosed: releaseResult.cycleClosed ?? null,
|
|
24319
|
+
version: releaseResult.version,
|
|
24320
|
+
caps,
|
|
24321
|
+
branchMerges: releaseResult.groupedBranchMerges ?? [],
|
|
24322
|
+
changelogEmitted: Boolean(autoChangelogDirective),
|
|
24323
|
+
deployHookEmitted: Boolean(autoDeployDirective)
|
|
24324
|
+
});
|
|
24325
|
+
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
24326
|
+
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
24327
|
+
autoReleaseNote = `
|
|
24043
24328
|
|
|
24044
24329
|
---
|
|
24045
24330
|
|
|
@@ -24049,9 +24334,15 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
24049
24334
|
- ${releaseResult.commitNote}
|
|
24050
24335
|
- ${releaseResult.tagMessage}
|
|
24051
24336
|
- ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
|
|
24052
|
-
- Warnings: ${releaseResult.warnings.join(", ")}` : "") +
|
|
24337
|
+
- Warnings: ${releaseResult.warnings.join(", ")}` : "") + // task-2598 (C328): the auto path previously swallowed the curated
|
|
24338
|
+
// cycle-update directive that the manual path emits, so an auto-released
|
|
24339
|
+
// cycle never prompted the Discord post. Same directive, same gate.
|
|
24340
|
+
(autoChangelogDirective ? `
|
|
24341
|
+
${autoChangelogDirective}` : "") + (autoDeployDirective ? `
|
|
24342
|
+
${autoDeployDirective}` : "") + `
|
|
24053
24343
|
|
|
24054
24344
|
Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
|
|
24345
|
+
}
|
|
24055
24346
|
}
|
|
24056
24347
|
}
|
|
24057
24348
|
} catch (err) {
|
|
@@ -25207,7 +25498,7 @@ function formatDeferredGateSection(sweep) {
|
|
|
25207
25498
|
|
|
25208
25499
|
// src/tools/agent-list.ts
|
|
25209
25500
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
25210
|
-
import { join as
|
|
25501
|
+
import { join as join15 } from "path";
|
|
25211
25502
|
var NO_AGENTS_HINT = "No project sub-agents found in `.claude/agents/`. Add a `*.md` file with `name` + `description` frontmatter \u2014 see the 1926-Census marketing sub-agent for a reference implementation.";
|
|
25212
25503
|
function parseAgentFrontmatter(content) {
|
|
25213
25504
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -25219,7 +25510,7 @@ function parseAgentFrontmatter(content) {
|
|
|
25219
25510
|
return { name: nameMatch?.[1].trim(), description };
|
|
25220
25511
|
}
|
|
25221
25512
|
async function listAgents(projectRoot) {
|
|
25222
|
-
const agentsDir =
|
|
25513
|
+
const agentsDir = join15(projectRoot, ".claude", "agents");
|
|
25223
25514
|
let files;
|
|
25224
25515
|
try {
|
|
25225
25516
|
files = await readdir2(agentsDir);
|
|
@@ -25230,7 +25521,7 @@ async function listAgents(projectRoot) {
|
|
|
25230
25521
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
25231
25522
|
let content;
|
|
25232
25523
|
try {
|
|
25233
|
-
content = await readFile7(
|
|
25524
|
+
content = await readFile7(join15(agentsDir, file), "utf-8");
|
|
25234
25525
|
} catch {
|
|
25235
25526
|
continue;
|
|
25236
25527
|
}
|
|
@@ -25238,7 +25529,7 @@ async function listAgents(projectRoot) {
|
|
|
25238
25529
|
agents.push({
|
|
25239
25530
|
name: meta?.name ?? file.replace(/\.md$/, ""),
|
|
25240
25531
|
description: meta?.description ?? "",
|
|
25241
|
-
path:
|
|
25532
|
+
path: join15(".claude", "agents", file)
|
|
25242
25533
|
});
|
|
25243
25534
|
}
|
|
25244
25535
|
agents.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
@@ -25379,8 +25670,8 @@ async function verifyProject(adapter2) {
|
|
|
25379
25670
|
// src/tools/orient.ts
|
|
25380
25671
|
import { execFile as execFile2 } from "child_process";
|
|
25381
25672
|
import { promisify as promisify2 } from "util";
|
|
25382
|
-
import { readFileSync as
|
|
25383
|
-
import { join as
|
|
25673
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync10 } from "fs";
|
|
25674
|
+
import { join as join16 } from "path";
|
|
25384
25675
|
var execFileAsync2 = promisify2(execFile2);
|
|
25385
25676
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
25386
25677
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -25695,8 +25986,8 @@ async function getLatestGitTag(projectRoot) {
|
|
|
25695
25986
|
}
|
|
25696
25987
|
async function checkNpmVersionDrift() {
|
|
25697
25988
|
try {
|
|
25698
|
-
const pkgPath =
|
|
25699
|
-
const pkg = JSON.parse(
|
|
25989
|
+
const pkgPath = join16(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
25990
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
25700
25991
|
const localVersion = pkg.version;
|
|
25701
25992
|
const packageName = pkg.name;
|
|
25702
25993
|
const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
|
|
@@ -26366,9 +26657,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
26366
26657
|
|
|
26367
26658
|
\u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
26368
26659
|
}
|
|
26369
|
-
const claudeMdPath =
|
|
26370
|
-
if (!
|
|
26371
|
-
const content =
|
|
26660
|
+
const claudeMdPath = join16(projectRoot, "CLAUDE.md");
|
|
26661
|
+
if (!existsSync10(claudeMdPath)) return "";
|
|
26662
|
+
const content = readFileSync10(claudeMdPath, "utf-8");
|
|
26372
26663
|
const additions = [];
|
|
26373
26664
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
26374
26665
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
|
|
@@ -26377,7 +26668,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
26377
26668
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
|
|
26378
26669
|
}
|
|
26379
26670
|
if (additions.length === 0) return "";
|
|
26380
|
-
|
|
26671
|
+
writeFileSync4(claudeMdPath, content + additions.join(""), "utf-8");
|
|
26381
26672
|
const tierNames = [];
|
|
26382
26673
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
26383
26674
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
@@ -27100,8 +27391,8 @@ ${result.userMessage}
|
|
|
27100
27391
|
}
|
|
27101
27392
|
|
|
27102
27393
|
// src/services/scope-brief.ts
|
|
27103
|
-
import { writeFileSync as
|
|
27104
|
-
import { join as
|
|
27394
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
27395
|
+
import { join as join17, dirname as dirname4 } from "path";
|
|
27105
27396
|
import Anthropic from "@anthropic-ai/sdk";
|
|
27106
27397
|
var SCOPE_BRIEF_SYSTEM = `You are a technical scoping tool. You receive a brief-class task (too large to build directly) and decompose it into a structured scope document.
|
|
27107
27398
|
|
|
@@ -27156,14 +27447,14 @@ async function runScopeBrief(adapter2, input) {
|
|
|
27156
27447
|
}
|
|
27157
27448
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
27158
27449
|
const relPath = `docs/scopes/${slug}.md`;
|
|
27159
|
-
const absPath =
|
|
27450
|
+
const absPath = join17(input.projectRoot, relPath);
|
|
27160
27451
|
const docBody = addFrontmatter(docContent, task, input.cycleNumber);
|
|
27161
27452
|
const collector = new FileWriteCollector();
|
|
27162
27453
|
if (input.adapterType === "proxy") {
|
|
27163
27454
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
27164
27455
|
} else {
|
|
27165
27456
|
mkdirSync3(dirname4(absPath), { recursive: true });
|
|
27166
|
-
|
|
27457
|
+
writeFileSync5(absPath, docBody, "utf-8");
|
|
27167
27458
|
}
|
|
27168
27459
|
const taskCount = countSubTasks(docContent);
|
|
27169
27460
|
const summary = buildSummary(task, taskCount);
|
|
@@ -27987,19 +28278,19 @@ Its build reports, comments, and history moved with it; the cycle assignment was
|
|
|
27987
28278
|
|
|
27988
28279
|
// src/services/harness-inventory.ts
|
|
27989
28280
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
27990
|
-
import { join as
|
|
27991
|
-
import { createHash as
|
|
28281
|
+
import { join as join18 } from "path";
|
|
28282
|
+
import { createHash as createHash4 } from "crypto";
|
|
27992
28283
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
27993
28284
|
async function computeFingerprint(root) {
|
|
27994
28285
|
const parts = [];
|
|
27995
28286
|
for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
|
|
27996
|
-
const dir =
|
|
28287
|
+
const dir = join18(root, sub);
|
|
27997
28288
|
try {
|
|
27998
28289
|
const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
|
|
27999
28290
|
for (const name of names) {
|
|
28000
28291
|
let mtime = "";
|
|
28001
28292
|
try {
|
|
28002
|
-
mtime = String(Math.floor((await stat3(
|
|
28293
|
+
mtime = String(Math.floor((await stat3(join18(dir, name))).mtimeMs));
|
|
28003
28294
|
} catch {
|
|
28004
28295
|
}
|
|
28005
28296
|
parts.push(`${sub}/${name}:${mtime}`);
|
|
@@ -28014,11 +28305,11 @@ async function computeFingerprint(root) {
|
|
|
28014
28305
|
} catch {
|
|
28015
28306
|
parts.push("manifest:none");
|
|
28016
28307
|
}
|
|
28017
|
-
return
|
|
28308
|
+
return createHash4("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
28018
28309
|
}
|
|
28019
28310
|
async function readSkillDescription(skillDir) {
|
|
28020
28311
|
try {
|
|
28021
|
-
const content = await readFile8(
|
|
28312
|
+
const content = await readFile8(join18(skillDir, "SKILL.md"), "utf-8");
|
|
28022
28313
|
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
28023
28314
|
if (!fm) return void 0;
|
|
28024
28315
|
const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
|
|
@@ -28038,17 +28329,17 @@ async function scanInventory(root, toolDefs) {
|
|
|
28038
28329
|
version = loadManifest().packageVersion;
|
|
28039
28330
|
} catch {
|
|
28040
28331
|
}
|
|
28041
|
-
const skillsDir =
|
|
28332
|
+
const skillsDir = join18(root, ".claude", "skills");
|
|
28042
28333
|
try {
|
|
28043
28334
|
const dirents = await readdir3(skillsDir, { withFileTypes: true });
|
|
28044
28335
|
for (const d of dirents.filter((e) => e.isDirectory())) {
|
|
28045
28336
|
entries.push({
|
|
28046
28337
|
kind: "skill",
|
|
28047
28338
|
name: d.name,
|
|
28048
|
-
description: await readSkillDescription(
|
|
28339
|
+
description: await readSkillDescription(join18(skillsDir, d.name)),
|
|
28049
28340
|
version,
|
|
28050
28341
|
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
28051
|
-
path:
|
|
28342
|
+
path: join18(".claude", "skills", d.name)
|
|
28052
28343
|
});
|
|
28053
28344
|
}
|
|
28054
28345
|
} catch {
|
|
@@ -28064,9 +28355,9 @@ async function scanInventory(root, toolDefs) {
|
|
|
28064
28355
|
}
|
|
28065
28356
|
const present = /* @__PURE__ */ new Set();
|
|
28066
28357
|
try {
|
|
28067
|
-
for (const f of (await readdir3(
|
|
28358
|
+
for (const f of (await readdir3(join18(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
|
|
28068
28359
|
present.add(f);
|
|
28069
|
-
entries.push({ kind: "hook", name: f, status: "ok", path:
|
|
28360
|
+
entries.push({ kind: "hook", name: f, status: "ok", path: join18(".claude", "hooks", f) });
|
|
28070
28361
|
}
|
|
28071
28362
|
} catch {
|
|
28072
28363
|
}
|
|
@@ -28506,7 +28797,7 @@ function createServer(adapter2, config2) {
|
|
|
28506
28797
|
const __pkgDir = dirname5(__pkgFilename);
|
|
28507
28798
|
let serverVersion = "unknown";
|
|
28508
28799
|
try {
|
|
28509
|
-
const pkg = JSON.parse(
|
|
28800
|
+
const pkg = JSON.parse(readFileSync11(join19(__pkgDir, "..", "package.json"), "utf-8"));
|
|
28510
28801
|
serverVersion = pkg.version ?? "unknown";
|
|
28511
28802
|
} catch {
|
|
28512
28803
|
}
|
|
@@ -28524,7 +28815,7 @@ function createServer(adapter2, config2) {
|
|
|
28524
28815
|
}
|
|
28525
28816
|
const __filename = fileURLToPath3(import.meta.url);
|
|
28526
28817
|
const __dirname2 = dirname5(__filename);
|
|
28527
|
-
const skillsDir =
|
|
28818
|
+
const skillsDir = join19(__dirname2, "..", "skills");
|
|
28528
28819
|
function parseSkillFrontmatter(content) {
|
|
28529
28820
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
28530
28821
|
if (!match) return null;
|
|
@@ -28542,7 +28833,7 @@ function createServer(adapter2, config2) {
|
|
|
28542
28833
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
28543
28834
|
const prompts = [];
|
|
28544
28835
|
for (const file of mdFiles) {
|
|
28545
|
-
const content = await readFile9(
|
|
28836
|
+
const content = await readFile9(join19(skillsDir, file), "utf-8");
|
|
28546
28837
|
const meta = parseSkillFrontmatter(content);
|
|
28547
28838
|
if (meta) {
|
|
28548
28839
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -28558,7 +28849,7 @@ function createServer(adapter2, config2) {
|
|
|
28558
28849
|
try {
|
|
28559
28850
|
const files = await readdir4(skillsDir);
|
|
28560
28851
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
28561
|
-
const content = await readFile9(
|
|
28852
|
+
const content = await readFile9(join19(skillsDir, file), "utf-8");
|
|
28562
28853
|
const meta = parseSkillFrontmatter(content);
|
|
28563
28854
|
if (meta?.name === name) {
|
|
28564
28855
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -29228,7 +29519,7 @@ async function dispatchRequest(args) {
|
|
|
29228
29519
|
var __dirname = dirname6(fileURLToPath4(import.meta.url));
|
|
29229
29520
|
var pkgVersion = "unknown";
|
|
29230
29521
|
try {
|
|
29231
|
-
const pkg = JSON.parse(
|
|
29522
|
+
const pkg = JSON.parse(readFileSync16(join24(__dirname, "..", "package.json"), "utf-8"));
|
|
29232
29523
|
pkgVersion = pkg.version;
|
|
29233
29524
|
} catch {
|
|
29234
29525
|
}
|