@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/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 hits = [];
743
- for (const task of inProgress) {
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
- hits.push({
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 hits;
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 existsSync10, readFileSync as readFileSync11 } from "fs";
4236
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
4215
4237
  import { homedir as homedir4 } from "os";
4216
- import { join as join19 } from "path";
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
- join19(process.cwd(), ".mcp.json"),
4228
- join19(homedir4(), ".claude", ".mcp.json"),
4229
- join19(homedir4(), ".mcp.json")
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 (!existsSync10(path7)) continue;
4254
+ if (!existsSync11(path7)) continue;
4233
4255
  try {
4234
- const raw = readFileSync11(path7, "utf-8");
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 existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
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 join20 } from "path";
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 (!existsSync11(path7)) continue;
4536
+ if (!existsSync12(path7)) continue;
4515
4537
  let raw;
4516
4538
  let parsed;
4517
4539
  try {
4518
- raw = readFileSync12(path7, "utf-8");
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
- writeFileSync5(target.path, removePapiEntry(target), "utf-8");
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
- join20(process.cwd(), ".mcp.json"),
4607
- join20(homedir5(), ".claude", ".mcp.json"),
4608
- join20(homedir5(), ".mcp.json")
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 existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
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 join21 } from "path";
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 = join21(projectPath, ".mcp.json");
4638
- if (!existsSync12(path7)) return { servers: [] };
4659
+ const path7 = join22(projectPath, ".mcp.json");
4660
+ if (!existsSync13(path7)) return { servers: [] };
4639
4661
  try {
4640
- const parsed = JSON.parse(readFileSync13(path7, "utf-8"));
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(join21(projectPath, ".claude", "skills")),
4673
- agentSkills: safeListDirs(join21(projectPath, ".agents", "skills")),
4674
- agents: safeListFiles(join21(projectPath, ".claude", "agents"), ".md"),
4675
- hooks: safeListFiles(join21(projectPath, ".claude", "hooks"), ".sh")
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 = join21(root, name);
4683
- if (existsSync12(join21(path7, ".mcp.json")) || existsSync12(join21(path7, ".claude"))) {
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 (!existsSync12(GLOBAL_CLAUDE_JSON)) return [];
4716
+ if (!existsSync13(GLOBAL_CLAUDE_JSON)) return [];
4695
4717
  try {
4696
- const parsed = JSON.parse(readFileSync13(GLOBAL_CLAUDE_JSON, "utf-8"));
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 = [join21(homedir6(), "Ai-App-Projects"), join21(homedir6(), "android-projects")];
4849
- GLOBAL_SKILLS_DIR = join21(homedir6(), ".claude", "skills");
4850
- GLOBAL_CLAUDE_JSON = join21(homedir6(), ".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 existsSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync6, chmodSync as chmodSync2, statSync as statSync6 } from "fs";
4863
- import { join as join22 } from "path";
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 = join22(process.cwd(), ".mcp.json");
4917
+ const path7 = join23(process.cwd(), ".mcp.json");
4896
4918
  let parsed = {};
4897
- if (existsSync13(path7)) {
4919
+ if (existsSync14(path7)) {
4898
4920
  try {
4899
- parsed = JSON.parse(readFileSync14(path7, "utf-8"));
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
- writeFileSync6(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
4942
+ writeFileSync7(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
4921
4943
  try {
4922
- const mode = statSync6(path7).mode & 511;
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 readFileSync15 } from "fs";
5031
- import { dirname as dirname6, join as join23 } from "path";
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 readFileSync10 } from "fs";
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 join18, dirname as dirname5 } from "path";
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
- function formatCandidateTaskFullNotes(tasks) {
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 lines = candidates.map((t) => `**${t.id}** \u2014 ${t.title}
8308
- ${t.notes}`);
8309
- return [
8310
- `${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.`,
8311
- "",
8312
- ...lines
8313
- ].join("\n\n");
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
- let completed;
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: [{ cycle: sn, completed, partial: 0, failed: Math.max(0, total - completed), effortPoints }]
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 avgEffort = Math.round(
8461
+ const avgPlanned = Math.round(
8397
8462
  recentVelocities.reduce((sum, v) => sum + v.effortPoints, 0) / recentVelocities.length * 10
8398
8463
  ) / 10;
8399
- lines.push("**Cycle Sizing (effort points \u2014 primary signal)**");
8400
- lines.push(`- Last ${recentVelocities.length} cycles: ${recentVelocities.map((v) => `S${v.cycle}=${v.effortPoints}pts`).join(", ")}`);
8401
- lines.push(`- Average: ${avgEffort} effort points/cycle (XS=1, S=2, M=3, L=5, XL=8)`);
8402
- lines.push(`- Use average as a reference, not a target \u2014 size cycles based on what the selected tasks actually require.`);
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 PLAN_STAGE_STEPS = ["health-check", "inbox-triage", "board-integrity", "maturity-gate", "recommendation", "dependency-chain"];
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
- planPrepareCache.set(callerKey, {
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 createHash2 } from "crypto";
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 join3 } from "path";
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 = createHash2("md5").update(fullCanvasText).digest("hex");
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 = join3(homedir2(), ".claude", "plans");
14429
- if (existsSync(plansDir)) {
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 = join3(plansDir, f);
14433
- const stat4 = statSync(fullPath);
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 = join3(projectRoot, "docs");
14450
- if (hasLocalWorkspace() && existsSync(docsDir)) {
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(join3(dir, entry.name), `${prefix}${entry.name}/`);
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: createHash2("md5").update(canvasText).digest("hex") };
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 join8, basename, extname, dirname as dirname3 } from "path";
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 existsSync2 } from "fs";
16600
- import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
16601
- import { join as join4 } from "path";
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 (existsSync2(join4(projectRoot, ".git"))) {
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 (existsSync2(join4(projectRoot, manifest))) {
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 statSync2(join4(projectRoot, f)).isFile();
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 readFileSync2, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
16638
- import { dirname, join as join5, resolve } from "path";
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 = join5(".agents", "skills", "papi-cycle");
16775
+ var PROJECT_BUNDLE_REL = join6(".agents", "skills", "papi-cycle");
16641
16776
  function bundleDestRel(rel) {
16642
- return rel === "AGENTS.md" ? "AGENTS.md" : join5(PROJECT_BUNDLE_REL, rel);
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 = join5(dir, "skills", "papi-cycle");
16648
- if (existsSync3(join5(candidate, "AGENTS.md"))) return candidate;
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 || !existsSync3(bundleDir)) return [];
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 = join5(abs, entry.name);
16661
- const childRel = rel ? join5(rel, entry.name) : entry.name;
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: readFileSync2(childAbs, "utf8") });
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 = join5(projectRoot, bundleDestRel(f.rel));
16673
- if (opts.skipExisting && existsSync3(dest) && statSync3(dest).isFile()) continue;
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 readFileSync3, existsSync as existsSync4, statSync as statSync4 } from "fs";
16682
- import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
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: join6("agents", "frontend-design-engineer.md"), destRel: join6(".claude", "agents", "frontend-design-engineer.md"), executable: false },
16686
- { srcRel: join6("skills", "design-critique", "SKILL.md"), destRel: join6(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
16687
- { srcRel: join6("hooks", "frontend-design-guard.sh"), destRel: join6(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
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 = join6(dir, "design-assets");
16694
- if (existsSync4(join6(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
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 = join6(assetsDir, asset.srcRel);
16707
- if (!existsSync4(srcAbs)) continue;
16708
- const dest = projectRoot ? join6(projectRoot, asset.destRel) : asset.destRel;
16709
- if (opts.skipExisting && projectRoot && existsSync4(dest) && statSync4(dest).isFile()) continue;
16710
- out.push({ dest, content: readFileSync3(srcAbs, "utf8"), executable: asset.executable });
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 existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync5 } from "fs";
16717
- import { join as join7 } from "path";
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 = join7(projectRoot, "package.json");
16720
- if (!existsSync5(path7)) return null;
16854
+ const path7 = join8(projectRoot, "package.json");
16855
+ if (!existsSync6(path7)) return null;
16721
16856
  try {
16722
- const raw = readFileSync4(path7, "utf-8");
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 = join7(projectRoot, ".github", "workflows");
16744
- if (!existsSync5(dir)) return false;
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 = join7(projectRoot, ".env.example");
16754
- if (!existsSync5(path7)) return false;
16888
+ const path7 = join8(projectRoot, ".env.example");
16889
+ if (!existsSync6(path7)) return false;
16755
16890
  try {
16756
- const raw = readFileSync4(path7, "utf-8");
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 (existsSync5(join7(projectRoot, "vercel.json"))) return true;
16764
- const vercelDir = join7(projectRoot, ".vercel");
16765
- if (!existsSync5(vercelDir)) return false;
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 statSync5(vercelDir).isDirectory();
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(join8(config2.papiDir, filename), content, "utf-8");
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 : join8(config2.projectRoot, ".claude", "commands");
17272
- const docsDir = useCollector ? docsRel : join8(config2.projectRoot, "docs");
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" : join8(config2.projectRoot, "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` : join8(docsDir, "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` : join8(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
17297
- [useCollector ? `${commandsRel}/test.md` : join8(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
17298
- [useCollector ? `${docsRel}/README.md` : join8(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
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 = join8(config2.projectRoot, ".cursor");
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 = join8(cursorDir, "rules");
17334
- const cursorRulesPath = join8(cursorRulesDir, "papi.mdc");
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 = join8(projectRoot, ".claude", "settings.json");
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(join8(projectRoot, ".claude"), { recursive: true });
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 = join8(projectRoot, ".claude", "settings.json");
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(join8(projectRoot, ".claude"), { recursive: true });
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 = join8(config2.projectRoot, "CLAUDE.md");
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(join8(projectRoot, "package.json"), "utf-8");
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(join8(projectRoot, name), 5e3);
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(join8(projectRoot, dir), { withFileTypes: true });
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 = join8(config2.projectRoot, "CLAUDE.md");
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(join8(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
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(join8(projectRoot, ".git"));
18198
+ await access2(join9(projectRoot, ".git"));
18064
18199
  } catch {
18065
18200
  return void 0;
18066
18201
  }
18067
- const gitignorePath = join8(projectRoot, ".gitignore");
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 existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
18583
- import { join as join10 } from "path";
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 join9 } from "path";
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 = join9(config2.projectRoot, "CHANGELOG.md");
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.recordStep("readiness_verified");
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.recordStep("quality_gate", {
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
- lines.push(deployDirective);
19683
- await tracker.recordStep("deploy_hook", {
19684
- capabilityKey: "deployHook",
19685
- capabilityEnabled: isCapabilityEnabled(caps, "deployHook"),
19686
- status: "complete",
19687
- metadata: { configured: true }
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
- for (const segment of entry.split(";")) {
19882
- const altMatches = segment.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
19883
- for (const alt of altMatches) {
19884
- const inner = alt.match(/`([^`]+)`/);
19885
- if (inner) out.push(inner[1].trim());
19886
- }
19887
- const cleaned = segment.replace(/`/g, "").replace(/\s*\(.*$/, "").trim();
19888
- if (cleaned) out.push(cleaned);
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 changedLower = changedPath.replace(/\\/g, "/").toLowerCase();
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
- if (scoped.length === 0) {
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 scopedSet = new Set(scoped);
20212
+ const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
19955
20213
  const adjacentUntracked = untracked.filter(
19956
- (p) => !scopedSet.has(p) && isUnderScopedDir(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 = join10(projectRoot, ".papi");
20438
- if (!existsSync6(papiDir)) {
20695
+ const papiDir = join11(projectRoot, ".papi");
20696
+ if (!existsSync7(papiDir)) {
20439
20697
  mkdirSync2(papiDir, { recursive: true });
20440
20698
  }
20441
- const scopePath = join10(papiDir, "active-task-scope.txt");
20442
- writeFileSync2(scopePath, content, "utf-8");
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 = join10(projectRoot, ".papi", "active-task-scope.txt");
20446
- if (existsSync6(scopePath)) {
20447
- unlinkSync(scopePath);
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 = readFileSync5(absolutePath, "utf-8").slice(0, 2e3);
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 = join10(config2.projectRoot, "docs");
20846
- if (existsSync6(docsDir)) {
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 = join10(dir, e.name);
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(join10(config2.projectRoot, docPath), docPath, cycleNumber);
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 existsSync7, readFileSync as readFileSync6 } from "fs";
21017
- import { join as join11, relative } from "path";
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 && !existsSync7(join11(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 (!existsSync7(dir)) return [];
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 = join11(dir, entry.name);
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 = readFileSync6(filePath, "utf-8").slice(0, 1e3);
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 = join11(config2.projectRoot, "docs");
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 = join11(config2.projectRoot, "docs");
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 = join11(homedir3(), ".claude", "plans");
21290
- if (existsSync7(plansDir)) {
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(join11(plansDir, f.replace("plans/", "")))
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(join11(config2.projectRoot, f));
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 readFileSync7 } from "fs";
22736
- import { join as join12 } from "path";
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 = join12(projectRoot, "docs", "INDEX.md");
22741
- const raw = readFileSync7(indexPath, "utf8");
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 existsSync8, readFileSync as readFileSync8 } from "fs";
23317
- import { join as join13 } from "path";
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 = join13(config2.projectRoot, ".agents", "papi-context.md");
23540
- if (existsSync8(ctxPath)) {
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
- ${readFileSync8(ctxPath, "utf-8")}
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 = join13(config2.projectRoot, ".papi");
23699
- if (existsSync8(papiDir)) {
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 releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
24039
- await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
24040
- const pushInfo = releaseResult.pushNotes.join(" ");
24041
- const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
24042
- autoReleaseNote = `
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 join14 } from "path";
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 = join14(projectRoot, ".claude", "agents");
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(join14(agentsDir, file), "utf-8");
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: join14(".claude", "agents", file)
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 readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync9 } from "fs";
25383
- import { join as join15 } from "path";
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 = join15(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
25699
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
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 = join15(projectRoot, "CLAUDE.md");
26370
- if (!existsSync9(claudeMdPath)) return "";
26371
- const content = readFileSync9(claudeMdPath, "utf-8");
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
- writeFileSync3(claudeMdPath, content + additions.join(""), "utf-8");
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 writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
27104
- import { join as join16, dirname as dirname4 } from "path";
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 = join16(input.projectRoot, relPath);
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
- writeFileSync4(absPath, docBody, "utf-8");
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 join17 } from "path";
27991
- import { createHash as createHash3 } from "crypto";
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 = join17(root, sub);
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(join17(dir, name))).mtimeMs));
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 createHash3("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
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(join17(skillDir, "SKILL.md"), "utf-8");
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 = join17(root, ".claude", "skills");
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(join17(skillsDir, d.name)),
28339
+ description: await readSkillDescription(join18(skillsDir, d.name)),
28049
28340
  version,
28050
28341
  status: stale.has(d.name) ? "stale_fork" : "ok",
28051
- path: join17(".claude", "skills", d.name)
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(join17(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
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: join17(".claude", "hooks", f) });
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(readFileSync10(join18(__pkgDir, "..", "package.json"), "utf-8"));
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 = join18(__dirname2, "..", "skills");
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(join18(skillsDir, file), "utf-8");
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(join18(skillsDir, file), "utf-8");
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(readFileSync15(join23(__dirname, "..", "package.json"), "utf-8"));
29522
+ const pkg = JSON.parse(readFileSync16(join24(__dirname, "..", "package.json"), "utf-8"));
29232
29523
  pkgVersion = pkg.version;
29233
29524
  } catch {
29234
29525
  }