@papi-ai/server 0.7.40 → 0.7.41

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
@@ -22,6 +22,7 @@ __export(git_exports, {
22
22
  cycleBranchName: () => cycleBranchName,
23
23
  deleteLocalBranch: () => deleteLocalBranch,
24
24
  detectBoardMismatches: () => detectBoardMismatches,
25
+ detectMergedInProgress: () => detectMergedInProgress,
25
26
  detectUnrecordedCommits: () => detectUnrecordedCommits,
26
27
  ensureLatestDevelop: () => ensureLatestDevelop,
27
28
  ensureTagAtHead: () => ensureTagAtHead,
@@ -63,6 +64,7 @@ __export(git_exports, {
63
64
  normalizeGitUrl: () => normalizeGitUrl,
64
65
  pickModuleCycleBranch: () => pickModuleCycleBranch,
65
66
  resolveBaseBranch: () => resolveBaseBranch,
67
+ resolveDefaultBranch: () => resolveDefaultBranch,
66
68
  runAutoCommit: () => runAutoCommit,
67
69
  squashMergePullRequest: () => squashMergePullRequest,
68
70
  stageAllAndCommit: () => stageAllAndCommit,
@@ -660,6 +662,22 @@ function resolveBaseBranch(cwd, preferred) {
660
662
  if (preferred !== "master" && branchExists(cwd, "master")) return "master";
661
663
  return preferred;
662
664
  }
665
+ function resolveDefaultBranch(cwd, preferred) {
666
+ try {
667
+ const ref = execFileSync(
668
+ "git",
669
+ ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
670
+ { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
671
+ ).trim();
672
+ const name = ref.replace(/^origin\//, "");
673
+ if (name && branchExists(cwd, name)) return name;
674
+ } catch {
675
+ }
676
+ for (const candidate of [preferred, "main", "master", "trunk"]) {
677
+ if (candidate && branchExists(cwd, candidate)) return candidate;
678
+ }
679
+ return preferred;
680
+ }
663
681
  function detectBoardMismatches(cwd, tasks) {
664
682
  const empty = { codeAhead: [], staleInProgress: [] };
665
683
  if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
@@ -693,6 +711,46 @@ function detectBoardMismatches(cwd, tasks) {
693
711
  return empty;
694
712
  }
695
713
  }
714
+ function escapeRegexLiteral(s) {
715
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
716
+ }
717
+ function detectMergedInProgress(cwd, preferredBase, tasks) {
718
+ if (!isGitAvailable() || !isGitRepo(cwd)) return [];
719
+ const inProgress = tasks.filter((t) => t.status === "In Progress");
720
+ if (inProgress.length === 0) return [];
721
+ const base = resolveDefaultBranch(cwd, preferredBase);
722
+ if (!branchExists(cwd, base)) return [];
723
+ let raw;
724
+ try {
725
+ raw = execFileSync(
726
+ "git",
727
+ ["log", base, "--format=%h%x01%s", "-n", "1000"],
728
+ { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
729
+ );
730
+ } catch {
731
+ return [];
732
+ }
733
+ const commits = raw.split("\n").map((line) => {
734
+ const idx = line.indexOf("");
735
+ if (idx === -1) return null;
736
+ return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
737
+ }).filter((c) => c !== null && c.hash !== "");
738
+ const hits = [];
739
+ for (const task of inProgress) {
740
+ const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
741
+ const hit = commits.find((c) => re.test(c.subject));
742
+ if (!hit) continue;
743
+ const prMatch = hit.subject.match(/#(\d+)/);
744
+ hits.push({
745
+ displayId: task.displayId,
746
+ title: task.title,
747
+ commit: hit.hash,
748
+ subject: hit.subject,
749
+ pr: prMatch ? `#${prMatch[1]}` : null
750
+ });
751
+ }
752
+ return hits;
753
+ }
696
754
  function detectUnrecordedCommits(cwd, baseBranch) {
697
755
  if (!isGitAvailable() || !isGitRepo(cwd)) return [];
698
756
  const latestTag = getLatestTag(cwd);
@@ -1256,6 +1314,13 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1256
1314
  updateTaskStatus(id, status) {
1257
1315
  return this.invoke("updateTaskStatus", [id, status]);
1258
1316
  }
1317
+ // task-2292: cross-project move. The bound projectId (set on this adapter) is
1318
+ // the SOURCE; the target is passed in args. Ownership of BOTH projects is
1319
+ // verified server-side in the edge function from the bearer — callerUserId is
1320
+ // ignored here (cannot be spoofed). The edge resolves a target slug → UUID.
1321
+ moveTask(taskId, targetProject, _callerUserId) {
1322
+ return this.invoke("moveTask", [taskId, targetProject]);
1323
+ }
1259
1324
  // task-2155 (MU-3 task B): hosted/proxy claim write-path. The data-proxy gates
1260
1325
  // these via WRITE_METHODS (active editor may write, viewer cannot) and binds the
1261
1326
  // assignee to the bearer-derived caller server-side — the assigneeId passed here
@@ -1712,9 +1777,9 @@ var init_query = __esm({
1712
1777
  CLOSE = {};
1713
1778
  Query = class extends Promise {
1714
1779
  constructor(strings, args, handler, canceller, options = {}) {
1715
- let resolve2, reject;
1780
+ let resolve3, reject;
1716
1781
  super((a, b2) => {
1717
- resolve2 = a;
1782
+ resolve3 = a;
1718
1783
  reject = b2;
1719
1784
  });
1720
1785
  this.tagged = Array.isArray(strings.raw);
@@ -1725,7 +1790,7 @@ var init_query = __esm({
1725
1790
  this.options = options;
1726
1791
  this.state = null;
1727
1792
  this.statement = null;
1728
- this.resolve = (x) => (this.active = false, resolve2(x));
1793
+ this.resolve = (x) => (this.active = false, resolve3(x));
1729
1794
  this.reject = (x) => (this.active = false, reject(x));
1730
1795
  this.active = false;
1731
1796
  this.cancelled = null;
@@ -1773,12 +1838,12 @@ var init_query = __esm({
1773
1838
  if (this.executed && !this.active)
1774
1839
  return { done: true };
1775
1840
  prev && prev();
1776
- const promise = new Promise((resolve2, reject) => {
1841
+ const promise = new Promise((resolve3, reject) => {
1777
1842
  this.cursorFn = (value) => {
1778
- resolve2({ value, done: false });
1843
+ resolve3({ value, done: false });
1779
1844
  return new Promise((r) => prev = r);
1780
1845
  };
1781
- this.resolve = () => (this.active = false, resolve2({ done: true }));
1846
+ this.resolve = () => (this.active = false, resolve3({ done: true }));
1782
1847
  this.reject = (x) => (this.active = false, reject(x));
1783
1848
  });
1784
1849
  this.execute();
@@ -2376,12 +2441,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
2376
2441
  x.on("drain", drain);
2377
2442
  return x;
2378
2443
  }
2379
- async function cancel({ pid, secret }, resolve2, reject) {
2444
+ async function cancel({ pid, secret }, resolve3, reject) {
2380
2445
  try {
2381
2446
  cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
2382
2447
  await connect();
2383
2448
  socket.once("error", reject);
2384
- socket.once("close", resolve2);
2449
+ socket.once("close", resolve3);
2385
2450
  } catch (error2) {
2386
2451
  reject(error2);
2387
2452
  }
@@ -3398,7 +3463,7 @@ var init_subscribe = __esm({
3398
3463
  // ../../node_modules/postgres/src/large.js
3399
3464
  import Stream2 from "stream";
3400
3465
  function largeObject(sql, oid, mode = 131072 | 262144) {
3401
- return new Promise(async (resolve2, reject) => {
3466
+ return new Promise(async (resolve3, reject) => {
3402
3467
  await sql.begin(async (sql2) => {
3403
3468
  let finish;
3404
3469
  !oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
@@ -3424,7 +3489,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
3424
3489
  ) seek
3425
3490
  `
3426
3491
  };
3427
- resolve2(lo);
3492
+ resolve3(lo);
3428
3493
  return new Promise(async (r) => finish = r);
3429
3494
  async function readable({
3430
3495
  highWaterMark = 2048 * 8,
@@ -3589,8 +3654,8 @@ function Postgres(a, b2) {
3589
3654
  }
3590
3655
  async function reserve() {
3591
3656
  const queue = queue_default();
3592
- const c = open.length ? open.shift() : await new Promise((resolve2, reject) => {
3593
- const query = { reserve: resolve2, reject };
3657
+ const c = open.length ? open.shift() : await new Promise((resolve3, reject) => {
3658
+ const query = { reserve: resolve3, reject };
3594
3659
  queries.push(query);
3595
3660
  closed.length && connect(closed.shift(), query);
3596
3661
  });
@@ -3627,9 +3692,9 @@ function Postgres(a, b2) {
3627
3692
  let uncaughtError, result;
3628
3693
  name && await sql2`savepoint ${sql2(name)}`;
3629
3694
  try {
3630
- result = await new Promise((resolve2, reject) => {
3695
+ result = await new Promise((resolve3, reject) => {
3631
3696
  const x = fn2(sql2);
3632
- Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve2, reject);
3697
+ Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve3, reject);
3633
3698
  });
3634
3699
  if (uncaughtError)
3635
3700
  throw uncaughtError;
@@ -3686,8 +3751,8 @@ function Postgres(a, b2) {
3686
3751
  return c.execute(query) ? move(c, busy) : move(c, full);
3687
3752
  }
3688
3753
  function cancel(query) {
3689
- return new Promise((resolve2, reject) => {
3690
- query.state ? query.active ? connection_default(options).cancel(query.state, resolve2, reject) : query.cancelled = { resolve: resolve2, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve2());
3754
+ return new Promise((resolve3, reject) => {
3755
+ query.state ? query.active ? connection_default(options).cancel(query.state, resolve3, reject) : query.cancelled = { resolve: resolve3, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve3());
3691
3756
  });
3692
3757
  }
3693
3758
  async function end({ timeout = null } = {}) {
@@ -3706,11 +3771,11 @@ function Postgres(a, b2) {
3706
3771
  async function close() {
3707
3772
  await Promise.all(connections.map((c) => c.end()));
3708
3773
  }
3709
- async function destroy(resolve2) {
3774
+ async function destroy(resolve3) {
3710
3775
  await Promise.all(connections.map((c) => c.terminate()));
3711
3776
  while (queries.length)
3712
3777
  queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
3713
- resolve2();
3778
+ resolve3();
3714
3779
  }
3715
3780
  function connect(c, query) {
3716
3781
  move(c, connecting);
@@ -3894,9 +3959,9 @@ __export(doctor_exports, {
3894
3959
  __testing: () => __testing,
3895
3960
  runDoctor: () => runDoctor
3896
3961
  });
3897
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
3962
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
3898
3963
  import { homedir as homedir4 } from "os";
3899
- import { join as join18 } from "path";
3964
+ import { join as join19 } from "path";
3900
3965
  function redact(name, value) {
3901
3966
  if (!value) return "(empty)";
3902
3967
  if (SECRET_VARS.has(name)) {
@@ -3907,14 +3972,14 @@ function redact(name, value) {
3907
3972
  }
3908
3973
  function findMcpJson() {
3909
3974
  const candidates = [
3910
- join18(process.cwd(), ".mcp.json"),
3911
- join18(homedir4(), ".claude", ".mcp.json"),
3912
- join18(homedir4(), ".mcp.json")
3975
+ join19(process.cwd(), ".mcp.json"),
3976
+ join19(homedir4(), ".claude", ".mcp.json"),
3977
+ join19(homedir4(), ".mcp.json")
3913
3978
  ];
3914
3979
  for (const path7 of candidates) {
3915
- if (!existsSync9(path7)) continue;
3980
+ if (!existsSync10(path7)) continue;
3916
3981
  try {
3917
- const raw = readFileSync10(path7, "utf-8");
3982
+ const raw = readFileSync11(path7, "utf-8");
3918
3983
  const parsed = JSON.parse(raw);
3919
3984
  const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
3920
3985
  if (!papiEntry) continue;
@@ -4188,17 +4253,17 @@ __export(reset_exports, {
4188
4253
  removePapiEntry: () => removePapiEntry,
4189
4254
  runReset: () => runReset
4190
4255
  });
4191
- import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
4256
+ import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
4192
4257
  import { homedir as homedir5 } from "os";
4193
- import { join as join19 } from "path";
4258
+ import { join as join20 } from "path";
4194
4259
  import { createInterface } from "readline/promises";
4195
4260
  function findResetTarget() {
4196
4261
  for (const path7 of CANDIDATE_PATHS()) {
4197
- if (!existsSync10(path7)) continue;
4262
+ if (!existsSync11(path7)) continue;
4198
4263
  let raw;
4199
4264
  let parsed;
4200
4265
  try {
4201
- raw = readFileSync11(path7, "utf-8");
4266
+ raw = readFileSync12(path7, "utf-8");
4202
4267
  parsed = JSON.parse(raw);
4203
4268
  } catch {
4204
4269
  continue;
@@ -4286,9 +4351,9 @@ var init_reset = __esm({
4286
4351
  "src/cli/reset.ts"() {
4287
4352
  "use strict";
4288
4353
  CANDIDATE_PATHS = () => [
4289
- join19(process.cwd(), ".mcp.json"),
4290
- join19(homedir5(), ".claude", ".mcp.json"),
4291
- join19(homedir5(), ".mcp.json")
4354
+ join20(process.cwd(), ".mcp.json"),
4355
+ join20(homedir5(), ".claude", ".mcp.json"),
4356
+ join20(homedir5(), ".mcp.json")
4292
4357
  ];
4293
4358
  }
4294
4359
  });
@@ -4299,9 +4364,9 @@ __export(audit_exports, {
4299
4364
  __testing: () => __testing2,
4300
4365
  runAudit: () => runAudit
4301
4366
  });
4302
- import { existsSync as existsSync11, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
4367
+ import { existsSync as existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
4303
4368
  import { homedir as homedir6 } from "os";
4304
- import { join as join20 } from "path";
4369
+ import { join as join21 } from "path";
4305
4370
  function safeListDirs(dir) {
4306
4371
  try {
4307
4372
  return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
@@ -4317,10 +4382,10 @@ function safeListFiles(dir, ext) {
4317
4382
  }
4318
4383
  }
4319
4384
  function readMcp(projectPath) {
4320
- const path7 = join20(projectPath, ".mcp.json");
4321
- if (!existsSync11(path7)) return { servers: [] };
4385
+ const path7 = join21(projectPath, ".mcp.json");
4386
+ if (!existsSync12(path7)) return { servers: [] };
4322
4387
  try {
4323
- const parsed = JSON.parse(readFileSync12(path7, "utf-8"));
4388
+ const parsed = JSON.parse(readFileSync13(path7, "utf-8"));
4324
4389
  const mcpServers = parsed.mcpServers ?? {};
4325
4390
  const servers = Object.keys(mcpServers);
4326
4391
  if (parsed.papi && !servers.includes("papi")) servers.push("papi");
@@ -4352,18 +4417,18 @@ function auditProjectSync(projectPath, name) {
4352
4417
  path: projectPath,
4353
4418
  papiProjectId,
4354
4419
  mcpServers: servers,
4355
- skills: safeListDirs(join20(projectPath, ".claude", "skills")),
4356
- agentSkills: safeListDirs(join20(projectPath, ".agents", "skills")),
4357
- agents: safeListFiles(join20(projectPath, ".claude", "agents"), ".md"),
4358
- hooks: safeListFiles(join20(projectPath, ".claude", "hooks"), ".sh")
4420
+ skills: safeListDirs(join21(projectPath, ".claude", "skills")),
4421
+ agentSkills: safeListDirs(join21(projectPath, ".agents", "skills")),
4422
+ agents: safeListFiles(join21(projectPath, ".claude", "agents"), ".md"),
4423
+ hooks: safeListFiles(join21(projectPath, ".claude", "hooks"), ".sh")
4359
4424
  };
4360
4425
  }
4361
4426
  function discoverProjects() {
4362
4427
  const out = [];
4363
4428
  for (const root of PROJECT_ROOTS) {
4364
4429
  for (const name of safeListDirs(root)) {
4365
- const path7 = join20(root, name);
4366
- if (existsSync11(join20(path7, ".mcp.json")) || existsSync11(join20(path7, ".claude"))) {
4430
+ const path7 = join21(root, name);
4431
+ if (existsSync12(join21(path7, ".mcp.json")) || existsSync12(join21(path7, ".claude"))) {
4367
4432
  out.push({ name, path: path7 });
4368
4433
  }
4369
4434
  }
@@ -4374,9 +4439,9 @@ function readGlobalSkills() {
4374
4439
  return safeListDirs(GLOBAL_SKILLS_DIR);
4375
4440
  }
4376
4441
  function readGlobalMcpServers() {
4377
- if (!existsSync11(GLOBAL_CLAUDE_JSON)) return [];
4442
+ if (!existsSync12(GLOBAL_CLAUDE_JSON)) return [];
4378
4443
  try {
4379
- const parsed = JSON.parse(readFileSync12(GLOBAL_CLAUDE_JSON, "utf-8"));
4444
+ const parsed = JSON.parse(readFileSync13(GLOBAL_CLAUDE_JSON, "utf-8"));
4380
4445
  const servers = parsed.mcpServers ?? {};
4381
4446
  return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
4382
4447
  } catch {
@@ -4528,9 +4593,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
4528
4593
  var init_audit = __esm({
4529
4594
  "src/cli/audit.ts"() {
4530
4595
  "use strict";
4531
- PROJECT_ROOTS = [join20(homedir6(), "Ai-App-Projects"), join20(homedir6(), "android-projects")];
4532
- GLOBAL_SKILLS_DIR = join20(homedir6(), ".claude", "skills");
4533
- GLOBAL_CLAUDE_JSON = join20(homedir6(), ".claude.json");
4596
+ PROJECT_ROOTS = [join21(homedir6(), "Ai-App-Projects"), join21(homedir6(), "android-projects")];
4597
+ GLOBAL_SKILLS_DIR = join21(homedir6(), ".claude", "skills");
4598
+ GLOBAL_CLAUDE_JSON = join21(homedir6(), ".claude.json");
4534
4599
  IDLE_WINDOW_DAYS = 30;
4535
4600
  GLOBALIZE_THRESHOLD = 3;
4536
4601
  __testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
@@ -4542,8 +4607,8 @@ var setup_exports = {};
4542
4607
  __export(setup_exports, {
4543
4608
  runSetup: () => runSetup
4544
4609
  });
4545
- import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync6, chmodSync as chmodSync2, statSync as statSync5 } from "fs";
4546
- import { join as join21 } from "path";
4610
+ import { existsSync as existsSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync6, chmodSync as chmodSync2, statSync as statSync6 } from "fs";
4611
+ import { join as join22 } from "path";
4547
4612
  function baseUrl() {
4548
4613
  const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
4549
4614
  if (fromEnv) return fromEnv.replace(/\/$/, "");
@@ -4572,14 +4637,14 @@ async function postJson(url, body) {
4572
4637
  return { status: res.status, data };
4573
4638
  }
4574
4639
  function sleep(ms) {
4575
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4640
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4576
4641
  }
4577
4642
  function writeMcpJson(opts) {
4578
- const path7 = join21(process.cwd(), ".mcp.json");
4643
+ const path7 = join22(process.cwd(), ".mcp.json");
4579
4644
  let parsed = {};
4580
- if (existsSync12(path7)) {
4645
+ if (existsSync13(path7)) {
4581
4646
  try {
4582
- parsed = JSON.parse(readFileSync13(path7, "utf-8"));
4647
+ parsed = JSON.parse(readFileSync14(path7, "utf-8"));
4583
4648
  } catch {
4584
4649
  throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
4585
4650
  }
@@ -4602,7 +4667,7 @@ function writeMcpJson(opts) {
4602
4667
  parsed.mcpServers = mcpServers;
4603
4668
  writeFileSync6(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
4604
4669
  try {
4605
- const mode = statSync5(path7).mode & 511;
4670
+ const mode = statSync6(path7).mode & 511;
4606
4671
  if (mode !== 384) chmodSync2(path7, 384);
4607
4672
  } catch {
4608
4673
  }
@@ -4710,9 +4775,9 @@ var init_setup = __esm({
4710
4775
  });
4711
4776
 
4712
4777
  // src/index.ts
4713
- import { readFileSync as readFileSync14 } from "fs";
4714
- import { dirname as dirname5, join as join22 } from "path";
4715
- import { fileURLToPath as fileURLToPath3 } from "url";
4778
+ import { readFileSync as readFileSync15 } from "fs";
4779
+ import { dirname as dirname6, join as join23 } from "path";
4780
+ import { fileURLToPath as fileURLToPath4 } from "url";
4716
4781
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4717
4782
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
4718
4783
  import {
@@ -4744,6 +4809,8 @@ var HELP_FOOTER_MD = `
4744
4809
  `;
4745
4810
 
4746
4811
  // src/config.ts
4812
+ var STRATEGY_REVIEW_OFFER_GAP = 5;
4813
+ var STRATEGY_REVIEW_BLOCK_GAP = 7;
4747
4814
  function loadConfig() {
4748
4815
  const projectArgIdx = process.argv.indexOf("--project");
4749
4816
  const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
@@ -7709,18 +7776,36 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
7709
7776
  }
7710
7777
 
7711
7778
  // src/server.ts
7712
- import { readFileSync as readFileSync9 } from "fs";
7779
+ import { readFileSync as readFileSync10 } from "fs";
7713
7780
  import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
7714
- import { join as join17, dirname as dirname4 } from "path";
7715
- import { fileURLToPath as fileURLToPath2 } from "url";
7781
+ import { join as join18, dirname as dirname5 } from "path";
7782
+ import { fileURLToPath as fileURLToPath3 } from "url";
7716
7783
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7717
7784
  import {
7718
7785
  CallToolRequestSchema,
7719
7786
  ListToolsRequestSchema,
7720
7787
  ListPromptsRequestSchema,
7721
- GetPromptRequestSchema
7788
+ GetPromptRequestSchema,
7789
+ ListResourcesRequestSchema,
7790
+ ListResourceTemplatesRequestSchema,
7791
+ ReadResourceRequestSchema
7722
7792
  } from "@modelcontextprotocol/sdk/types.js";
7723
7793
 
7794
+ // src/universal-frame.ts
7795
+ var UNIVERSAL_FRAME = `PAPI gives this project a structured plan \u2192 build \u2192 review cycle, persisted across sessions. Follow it:
7796
+
7797
+ 1. ORIENT FIRST. At the start of every session, call \`orient\` (or \`papi\`) before anything else \u2014 it returns the current cycle, what's in flight, and the recommended next action. Re-run it after any context compression.
7798
+
7799
+ 2. THE CYCLE, IN ORDER: \`plan\` (once per cycle) \u2192 \`build_list\` (pick a task) \u2192 \`build_execute <task>\` to start \u2192 implement the task from its BUILD HANDOFF \u2192 \`build_execute\` again to complete with a build report \u2192 \`review_submit\` \u2192 \`release\` when every cycle task is done.
7800
+
7801
+ 3. STATUS DISCIPLINE. Check a task's status before acting. "In Review" = already built \u2014 never re-build it; submit a review instead. "In Progress" = a build started \u2014 check the existing branch before writing new code. "Backlog" = not started, but check for an existing feature branch first.
7802
+
7803
+ 4. STAY IN SCOPE. Build exactly what the BUILD HANDOFF specifies. Mechanical steps (branch creation, commits, status updates) are automatic \u2014 only "what to build" needs the user's confirmation.
7804
+
7805
+ 5. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
7806
+
7807
+ PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.`;
7808
+
7724
7809
  // src/lib/response.ts
7725
7810
  function textResponse(text, usage) {
7726
7811
  const result = {
@@ -11041,7 +11126,7 @@ ${lines.join("\n")}`;
11041
11126
  console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
11042
11127
  const gap = health.cyclesSinceLastStrategyReview;
11043
11128
  const lastReviewCycle = health.totalCycles - gap;
11044
- const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Due now.`;
11129
+ const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < STRATEGY_REVIEW_OFFER_GAP ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + STRATEGY_REVIEW_OFFER_GAP}.` : gap < STRATEGY_REVIEW_BLOCK_GAP ? `\u25CB Strategy review available \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Offered now (optional); planning hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Planning is blocked until it runs (or \`force: true\`).`;
11045
11130
  let ctx2 = {
11046
11131
  mode,
11047
11132
  cycleNumber: health.totalCycles,
@@ -11231,7 +11316,7 @@ ${logLines}`);
11231
11316
  const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
11232
11317
  const gapFull = health.cyclesSinceLastStrategyReview;
11233
11318
  const lastReviewCycleFull = health.totalCycles - gapFull;
11234
- const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Due now.`;
11319
+ const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < STRATEGY_REVIEW_OFFER_GAP ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + STRATEGY_REVIEW_OFFER_GAP}.` : gapFull < STRATEGY_REVIEW_BLOCK_GAP ? `\u25CB Strategy review available \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Offered now (optional); planning hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Planning is blocked until it runs (or \`force: true\`).`;
11235
11320
  let ctx = {
11236
11321
  mode,
11237
11322
  cycleNumber: health.totalCycles,
@@ -11835,15 +11920,15 @@ Run \`review_submit\` to clear them, or pass \`force: true\` to bypass this bloc
11835
11920
  console.error(`[plan] ${note}`);
11836
11921
  }
11837
11922
  const gap = health.cyclesSinceLastStrategyReview;
11838
- if (gap >= 5 && !force) {
11923
+ if (gap >= STRATEGY_REVIEW_BLOCK_GAP && !force) {
11839
11924
  const lastReviewCycle = cycleNumber - gap;
11840
11925
  throw new Error(
11841
- `Strategy Review gate \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). A strategy review is required every 5 cycles before planning can continue.
11926
+ `Strategy Review gate \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). A strategy review is required at least every ${STRATEGY_REVIEW_BLOCK_GAP} cycles before planning can continue.
11842
11927
 
11843
11928
  Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
11844
11929
  );
11845
11930
  }
11846
- if (gap >= 5 && force) {
11931
+ if (gap >= STRATEGY_REVIEW_BLOCK_GAP && force) {
11847
11932
  const lastReviewCycle = cycleNumber - gap;
11848
11933
  strategyReviewWarning = `> \u26A0\uFE0F **Strategy Review gate bypassed** (force: true) \u2014 last review was Cycle ${lastReviewCycle}, current is Cycle ${cycleNumber} (${gap} cycles ago). Run \`strategy_review\` after this cycle.
11849
11934
 
@@ -12416,8 +12501,6 @@ function getState(callerKey) {
12416
12501
  function callerKeyFromConfig(config2) {
12417
12502
  return config2.projectId ?? config2.userId ?? void 0;
12418
12503
  }
12419
- var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
12420
- var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
12421
12504
  var REVIEW_LIST_GUARD_WINDOW_MS = 15 * 60 * 1e3;
12422
12505
  var FAILURE_WINDOW_MS = 30 * 60 * 1e3;
12423
12506
  var FAILURE_RATE_THRESHOLD = 8;
@@ -12457,37 +12540,20 @@ function getProjectConnectionBanner(projectName, projectSlug) {
12457
12540
  function detectContextDegradation(now = Date.now(), callerKey) {
12458
12541
  const state = getState(callerKey);
12459
12542
  if (state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD) {
12460
- return `Context degradation (clash): ${state.consecutiveFailures} tool calls failed in a row \u2014 the session may be stuck on a contradiction. Consider a fresh window after this task.`;
12543
+ return `${state.consecutiveFailures} tool calls failed in a row \u2014 the session may be stuck on a contradiction. Re-read the last error and re-check your assumptions before continuing.`;
12461
12544
  }
12462
12545
  const cutoff = now - FAILURE_WINDOW_MS;
12463
12546
  const recentFailures = state.failureTimestamps.filter((t) => t >= cutoff).length;
12464
12547
  if (recentFailures >= FAILURE_RATE_THRESHOLD) {
12465
12548
  const mins = Math.round(FAILURE_WINDOW_MS / 6e4);
12466
- return `Context degradation (distraction/confusion): ${recentFailures} tool failures in the last ${mins}min. A fresh window often clears this faster than pushing on.`;
12549
+ return `${recentFailures} tool failures in the last ${mins}min \u2014 something's off. Re-read the last error and re-check your assumptions before continuing.`;
12467
12550
  }
12468
12551
  return null;
12469
12552
  }
12470
12553
  async function buildSessionGuidance(callerKey) {
12471
- const state = getState(callerKey);
12472
12554
  const signals = [];
12473
12555
  const degradation = detectContextDegradation(Date.now(), callerKey);
12474
12556
  if (degradation) signals.push(degradation);
12475
- if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
12476
- signals.push(
12477
- `${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
12478
- );
12479
- }
12480
- if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
12481
- const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
12482
- signals.push(
12483
- `${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
12484
- );
12485
- }
12486
- if (state.releaseSinceLastOrient) {
12487
- signals.push(
12488
- "Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
12489
- );
12490
- }
12491
12557
  return signals.slice(0, 3);
12492
12558
  }
12493
12559
 
@@ -15906,8 +15972,8 @@ ${existing}` : entry;
15906
15972
  }
15907
15973
 
15908
15974
  // src/services/setup.ts
15909
- import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2 } from "fs/promises";
15910
- import { join as join5, basename, extname, dirname as dirname2 } from "path";
15975
+ import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
15976
+ import { join as join7, basename, extname, dirname as dirname3 } from "path";
15911
15977
  import { execFileSync as execFileSync3 } from "child_process";
15912
15978
 
15913
15979
  // src/lib/detect-codebase.ts
@@ -15992,6 +16058,159 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
15992
16058
  return out;
15993
16059
  }
15994
16060
 
16061
+ // src/lib/design-bundle.ts
16062
+ import { readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync4 } from "fs";
16063
+ import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
16064
+ import { fileURLToPath as fileURLToPath2 } from "url";
16065
+ var DESIGN_ASSETS = [
16066
+ { srcRel: join5("agents", "frontend-design-engineer.md"), destRel: join5(".claude", "agents", "frontend-design-engineer.md"), executable: false },
16067
+ { srcRel: join5("skills", "design-critique", "SKILL.md"), destRel: join5(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
16068
+ { srcRel: join5("hooks", "frontend-design-guard.sh"), destRel: join5(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
16069
+ ];
16070
+ var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
16071
+ function resolveDesignAssetsDir() {
16072
+ let dir = dirname2(fileURLToPath2(import.meta.url));
16073
+ for (let i = 0; i < 5; i++) {
16074
+ const candidate = join5(dir, "design-assets");
16075
+ if (existsSync4(join5(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
16076
+ const parent = resolve2(dir, "..");
16077
+ if (parent === dir) break;
16078
+ dir = parent;
16079
+ }
16080
+ return void 0;
16081
+ }
16082
+ function planDesignInstall(projectRoot, opts = {}) {
16083
+ const assetsDir = resolveDesignAssetsDir();
16084
+ if (!assetsDir) return [];
16085
+ const out = [];
16086
+ for (const asset of DESIGN_ASSETS) {
16087
+ const srcAbs = join5(assetsDir, asset.srcRel);
16088
+ if (!existsSync4(srcAbs)) continue;
16089
+ const dest = projectRoot ? join5(projectRoot, asset.destRel) : asset.destRel;
16090
+ if (opts.skipExisting && projectRoot && existsSync4(dest) && statSync4(dest).isFile()) continue;
16091
+ out.push({ dest, content: readFileSync2(srcAbs, "utf8"), executable: asset.executable });
16092
+ }
16093
+ return out;
16094
+ }
16095
+
16096
+ // src/lib/skill-detection.ts
16097
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync3, statSync as statSync5 } from "fs";
16098
+ import { join as join6 } from "path";
16099
+ function readPackageJson(projectRoot) {
16100
+ const path7 = join6(projectRoot, "package.json");
16101
+ if (!existsSync5(path7)) return null;
16102
+ try {
16103
+ const raw = readFileSync3(path7, "utf-8");
16104
+ return JSON.parse(raw);
16105
+ } catch {
16106
+ return null;
16107
+ }
16108
+ }
16109
+ function allDeps(pkg) {
16110
+ if (!pkg) return {};
16111
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
16112
+ }
16113
+ function hasDependencyMatching(deps, pattern) {
16114
+ for (const name of Object.keys(deps)) {
16115
+ if (pattern.test(name)) return true;
16116
+ }
16117
+ return false;
16118
+ }
16119
+ var FRONTEND_DEP_PATTERN = /^(react|react-dom|next|vue|svelte|preact|solid-js|astro|nuxt|tailwindcss)$|^@(sveltejs|angular|remix-run)\//;
16120
+ function detectsFrontendStack(projectRoot) {
16121
+ return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
16122
+ }
16123
+ function hasGitHubWorkflows(projectRoot) {
16124
+ const dir = join6(projectRoot, ".github", "workflows");
16125
+ if (!existsSync5(dir)) return false;
16126
+ try {
16127
+ const entries = readdirSync4(dir);
16128
+ return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
16129
+ } catch {
16130
+ return false;
16131
+ }
16132
+ }
16133
+ function envExampleMentionsStaging(projectRoot) {
16134
+ const path7 = join6(projectRoot, ".env.example");
16135
+ if (!existsSync5(path7)) return false;
16136
+ try {
16137
+ const raw = readFileSync3(path7, "utf-8");
16138
+ return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
16139
+ } catch {
16140
+ return false;
16141
+ }
16142
+ }
16143
+ function hasVercelConfig(projectRoot) {
16144
+ if (existsSync5(join6(projectRoot, "vercel.json"))) return true;
16145
+ const vercelDir = join6(projectRoot, ".vercel");
16146
+ if (!existsSync5(vercelDir)) return false;
16147
+ try {
16148
+ return statSync5(vercelDir).isDirectory();
16149
+ } catch {
16150
+ return false;
16151
+ }
16152
+ }
16153
+ function scanForSkillSignals(projectRoot) {
16154
+ const proposals = [];
16155
+ const pkg = readPackageJson(projectRoot);
16156
+ const deps = allDeps(pkg);
16157
+ if (hasGitHubWorkflows(projectRoot)) {
16158
+ proposals.push({
16159
+ id: "gh-actions-debug",
16160
+ name: "GitHub Actions debugger",
16161
+ rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
16162
+ hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
16163
+ });
16164
+ }
16165
+ if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
16166
+ proposals.push({
16167
+ id: "error-tracking",
16168
+ name: "Error tracking helper",
16169
+ rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
16170
+ hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
16171
+ });
16172
+ }
16173
+ if (hasVercelConfig(projectRoot)) {
16174
+ proposals.push({
16175
+ id: "read-vercel-logs",
16176
+ name: "Vercel deploy logs",
16177
+ rationale: "Detected vercel.json or .vercel/ \u2014 a skill for pulling deploy + runtime logs from Vercel directly into Claude Code helps when builds fail or runtime errors land.",
16178
+ hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
16179
+ });
16180
+ }
16181
+ if (envExampleMentionsStaging(projectRoot)) {
16182
+ proposals.push({
16183
+ id: "staging-environment",
16184
+ name: "Staging-environment helper",
16185
+ rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
16186
+ hint: 'Search Claude Code skill registry for "staging" or define your own.'
16187
+ });
16188
+ }
16189
+ if (detectsFrontendStack(projectRoot)) {
16190
+ proposals.push({
16191
+ id: "frontend-design-engineer",
16192
+ name: "Frontend design engineer (agent + critique)",
16193
+ rationale: "Detected a frontend stack (React/Next/Vue/Svelte/Tailwind) \u2014 PAPI ships a frontend-design-engineer sub-agent + a design-critique skill that isolate the visual layer, run a falsifiable anti-slop critique before and after building, and verify in-browser. They read your own DESIGN.md/PRODUCT.md for brand, so the output is yours, not generic.",
16194
+ hint: "Re-run `setup` to install them into .claude/, or copy from the PAPI server package under design-assets/. A build-time guard hook is included (opt-in)."
16195
+ });
16196
+ }
16197
+ return proposals;
16198
+ }
16199
+ function formatSkillProposals(proposals) {
16200
+ if (proposals.length === 0) return "";
16201
+ const lines = ["", "## Skill proposals", "_PAPI scanned your project once and noticed tooling that often pairs with a Claude Code skill. Each proposal is one-shot \u2014 they won't resurface._"];
16202
+ for (const p of proposals) {
16203
+ lines.push("");
16204
+ lines.push(`### ${p.name}`);
16205
+ lines.push(p.rationale);
16206
+ if (p.hint) {
16207
+ lines.push(`_${p.hint}_`);
16208
+ }
16209
+ lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
16210
+ }
16211
+ return "\n" + lines.join("\n");
16212
+ }
16213
+
15995
16214
  // src/templates.ts
15996
16215
  var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
15997
16216
 
@@ -16412,7 +16631,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16412
16631
  await mkdir(config2.papiDir, { recursive: true });
16413
16632
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
16414
16633
  const content = substitute(template, vars);
16415
- await writeFile2(join5(config2.papiDir, filename), content, "utf-8");
16634
+ await writeFile2(join7(config2.papiDir, filename), content, "utf-8");
16416
16635
  }
16417
16636
  }
16418
16637
  } else {
@@ -16430,13 +16649,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16430
16649
  const useCollector = config2.adapterType === "proxy";
16431
16650
  const docsRel = "docs";
16432
16651
  const commandsRel = ".claude/commands";
16433
- const commandsDir = useCollector ? commandsRel : join5(config2.projectRoot, ".claude", "commands");
16434
- const docsDir = useCollector ? docsRel : join5(config2.projectRoot, "docs");
16652
+ const commandsDir = useCollector ? commandsRel : join7(config2.projectRoot, ".claude", "commands");
16653
+ const docsDir = useCollector ? docsRel : join7(config2.projectRoot, "docs");
16435
16654
  if (!useCollector) {
16436
16655
  await mkdir(commandsDir, { recursive: true });
16437
16656
  await mkdir(docsDir, { recursive: true });
16438
16657
  }
16439
- const claudeMdPath = useCollector ? "CLAUDE.md" : join5(config2.projectRoot, "CLAUDE.md");
16658
+ const claudeMdPath = useCollector ? "CLAUDE.md" : join7(config2.projectRoot, "CLAUDE.md");
16440
16659
  let claudeMdExists = false;
16441
16660
  if (!useCollector) {
16442
16661
  try {
@@ -16445,7 +16664,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16445
16664
  } catch {
16446
16665
  }
16447
16666
  }
16448
- const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join5(docsDir, "INDEX.md");
16667
+ const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join7(docsDir, "INDEX.md");
16449
16668
  let docsIndexExists = false;
16450
16669
  if (!useCollector) {
16451
16670
  try {
@@ -16455,9 +16674,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16455
16674
  }
16456
16675
  }
16457
16676
  const scaffoldFiles = {
16458
- [useCollector ? `${commandsRel}/papi-audit.md` : join5(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
16459
- [useCollector ? `${commandsRel}/test.md` : join5(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
16460
- [useCollector ? `${docsRel}/README.md` : join5(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
16677
+ [useCollector ? `${commandsRel}/papi-audit.md` : join7(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
16678
+ [useCollector ? `${commandsRel}/test.md` : join7(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
16679
+ [useCollector ? `${docsRel}/README.md` : join7(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
16461
16680
  };
16462
16681
  if (!docsIndexExists) {
16463
16682
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -16477,14 +16696,14 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16477
16696
  const bundleRoot = useCollector ? "" : config2.projectRoot;
16478
16697
  for (const [dest, content] of Object.entries(planBundleInstall(bundleRoot, input.projectName, { skipExisting: true }))) {
16479
16698
  if (!useCollector) {
16480
- await mkdir(dirname2(dest), { recursive: true });
16699
+ await mkdir(dirname3(dest), { recursive: true });
16481
16700
  }
16482
16701
  scaffoldFiles[dest] = content;
16483
16702
  }
16484
16703
  if (useCollector) {
16485
16704
  scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
16486
16705
  } else {
16487
- const cursorDir = join5(config2.projectRoot, ".cursor");
16706
+ const cursorDir = join7(config2.projectRoot, ".cursor");
16488
16707
  let cursorDetected = false;
16489
16708
  try {
16490
16709
  await access2(cursorDir);
@@ -16492,8 +16711,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16492
16711
  } catch {
16493
16712
  }
16494
16713
  if (cursorDetected) {
16495
- const cursorRulesDir = join5(cursorDir, "rules");
16496
- const cursorRulesPath = join5(cursorRulesDir, "papi.mdc");
16714
+ const cursorRulesDir = join7(cursorDir, "rules");
16715
+ const cursorRulesPath = join7(cursorRulesDir, "papi.mdc");
16497
16716
  await mkdir(cursorRulesDir, { recursive: true });
16498
16717
  try {
16499
16718
  await access2(cursorRulesPath);
@@ -16511,6 +16730,19 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16511
16730
  await writeFile2(filepath, content, "utf-8");
16512
16731
  }
16513
16732
  }
16733
+ if (!useCollector && detectsFrontendStack(config2.projectRoot)) {
16734
+ for (const entry of planDesignInstall(config2.projectRoot, { skipExisting: true })) {
16735
+ await mkdir(dirname3(entry.dest), { recursive: true });
16736
+ await writeFile2(entry.dest, entry.content, "utf-8");
16737
+ if (entry.executable) {
16738
+ try {
16739
+ await chmod(entry.dest, 493);
16740
+ } catch {
16741
+ }
16742
+ }
16743
+ }
16744
+ await ensureDesignHookRegistered(config2.projectRoot);
16745
+ }
16514
16746
  if (!isPg) {
16515
16747
  await adapter2.writePhases([{
16516
16748
  id: "phase-0",
@@ -16535,7 +16767,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
16535
16767
  }
16536
16768
  var PAPI_PERMISSION = "mcp__papi__*";
16537
16769
  async function ensurePapiPermission(projectRoot) {
16538
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
16770
+ const settingsPath = join7(projectRoot, ".claude", "settings.json");
16539
16771
  try {
16540
16772
  let settings = {};
16541
16773
  try {
@@ -16554,7 +16786,43 @@ async function ensurePapiPermission(projectRoot) {
16554
16786
  if (!allow.includes(PAPI_PERMISSION)) {
16555
16787
  allow.push(PAPI_PERMISSION);
16556
16788
  }
16557
- await mkdir(join5(projectRoot, ".claude"), { recursive: true });
16789
+ await mkdir(join7(projectRoot, ".claude"), { recursive: true });
16790
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
16791
+ } catch {
16792
+ }
16793
+ }
16794
+ async function ensureDesignHookRegistered(projectRoot) {
16795
+ const settingsPath = join7(projectRoot, ".claude", "settings.json");
16796
+ try {
16797
+ let settings = {};
16798
+ try {
16799
+ settings = JSON.parse(await readFile4(settingsPath, "utf-8"));
16800
+ } catch {
16801
+ }
16802
+ if (!settings.hooks || typeof settings.hooks !== "object") {
16803
+ settings.hooks = {};
16804
+ }
16805
+ const hooks = settings.hooks;
16806
+ if (!Array.isArray(hooks.PreToolUse)) {
16807
+ hooks.PreToolUse = [];
16808
+ }
16809
+ const pre = hooks.PreToolUse;
16810
+ for (const matcher of ["Edit", "Write"]) {
16811
+ let entry = pre.find((e) => e && e["matcher"] === matcher);
16812
+ if (!entry) {
16813
+ entry = { matcher, hooks: [] };
16814
+ pre.push(entry);
16815
+ }
16816
+ if (!Array.isArray(entry["hooks"])) {
16817
+ entry["hooks"] = [];
16818
+ }
16819
+ const chain = entry["hooks"];
16820
+ const already = chain.some((h) => h && h["command"] === DESIGN_HOOK_COMMAND);
16821
+ if (!already) {
16822
+ chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
16823
+ }
16824
+ }
16825
+ await mkdir(join7(projectRoot, ".claude"), { recursive: true });
16558
16826
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
16559
16827
  } catch {
16560
16828
  }
@@ -16658,7 +16926,7 @@ ${conventionsText.trim()}
16658
16926
  );
16659
16927
  } else {
16660
16928
  try {
16661
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
16929
+ const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
16662
16930
  const existing = await readFile4(claudeMdPath, "utf-8");
16663
16931
  if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
16664
16932
  warnings.push(
@@ -16745,13 +17013,13 @@ async function scanCodebase(projectRoot) {
16745
17013
  }
16746
17014
  let packageJson;
16747
17015
  try {
16748
- const content = await readFile4(join5(projectRoot, "package.json"), "utf-8");
17016
+ const content = await readFile4(join7(projectRoot, "package.json"), "utf-8");
16749
17017
  packageJson = JSON.parse(content);
16750
17018
  } catch {
16751
17019
  }
16752
17020
  let readme;
16753
17021
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
16754
- const content = await safeReadFile(join5(projectRoot, name), 5e3);
17022
+ const content = await safeReadFile(join7(projectRoot, name), 5e3);
16755
17023
  if (content) {
16756
17024
  readme = content;
16757
17025
  break;
@@ -16761,7 +17029,7 @@ async function scanCodebase(projectRoot) {
16761
17029
  let totalFiles = topLevelFiles.length;
16762
17030
  for (const dir of topLevelDirs) {
16763
17031
  try {
16764
- const entries = await readdir(join5(projectRoot, dir), { withFileTypes: true });
17032
+ const entries = await readdir(join7(projectRoot, dir), { withFileTypes: true });
16765
17033
  const files = entries.filter((e) => e.isFile());
16766
17034
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
16767
17035
  totalFiles += files.length;
@@ -17108,7 +17376,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17108
17376
  collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
17109
17377
  } else {
17110
17378
  try {
17111
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
17379
+ const claudeMdPath = join7(config2.projectRoot, "CLAUDE.md");
17112
17380
  const existing = await readFile4(claudeMdPath, "utf-8");
17113
17381
  if (!existing.includes("Dogfood Logging")) {
17114
17382
  await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
@@ -17153,7 +17421,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17153
17421
  cursorScaffolded = true;
17154
17422
  } else {
17155
17423
  try {
17156
- await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
17424
+ await access2(join7(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
17157
17425
  cursorScaffolded = true;
17158
17426
  } catch {
17159
17427
  }
@@ -17173,11 +17441,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
17173
17441
  }
17174
17442
  async function ensureMcpJsonGitignored(projectRoot) {
17175
17443
  try {
17176
- await access2(join5(projectRoot, ".git"));
17444
+ await access2(join7(projectRoot, ".git"));
17177
17445
  } catch {
17178
17446
  return void 0;
17179
17447
  }
17180
- const gitignorePath = join5(projectRoot, ".gitignore");
17448
+ const gitignorePath = join7(projectRoot, ".gitignore");
17181
17449
  let existing = "";
17182
17450
  try {
17183
17451
  existing = await readFile4(gitignorePath, "utf-8");
@@ -17560,10 +17828,127 @@ ${result.initialTasksPrompt.user}
17560
17828
  }
17561
17829
  }
17562
17830
 
17831
+ // src/lib/model-routing.ts
17832
+ var TIER_SPECS = {
17833
+ cheap: {
17834
+ label: "Cheap / fast",
17835
+ rationale: "XS\u2013S work \u2014 single-file fixes, UI polish, mechanical edits. A small model is faster and cheaper with no quality loss here.",
17836
+ examples: "Anthropic Claude Haiku \xB7 OpenAI GPT-5 mini \xB7 Google Gemini Flash \xB7 Mistral/Groq small"
17837
+ },
17838
+ capable: {
17839
+ label: "Capable",
17840
+ rationale: "M\u2013L work \u2014 multi-file features, data/plumbing, ambiguous handoffs. Needs a strong general-purpose model.",
17841
+ examples: "Anthropic Claude Sonnet \xB7 OpenAI GPT-5 \xB7 Google Gemini Pro \xB7 Mistral Large"
17842
+ },
17843
+ frontier: {
17844
+ label: "Frontier",
17845
+ rationale: "XL work \u2014 architecture, cross-cutting refactors, the hardest reasoning. Reach for your most capable model.",
17846
+ examples: "Anthropic Claude Opus \xB7 OpenAI GPT-5 Pro / o-series \xB7 Google Gemini Ultra"
17847
+ }
17848
+ };
17849
+ function tierForComplexity(complexity) {
17850
+ switch ((complexity ?? "").trim()) {
17851
+ case "XS":
17852
+ case "S":
17853
+ case "Small":
17854
+ return "cheap";
17855
+ case "XL":
17856
+ return "frontier";
17857
+ case "M":
17858
+ case "Medium":
17859
+ case "L":
17860
+ case "Large":
17861
+ return "capable";
17862
+ default:
17863
+ return "capable";
17864
+ }
17865
+ }
17866
+ function modelTierTag(complexity) {
17867
+ return `\u{1F916} ${TIER_SPECS[tierForComplexity(complexity)].label}`;
17868
+ }
17869
+ function formatModelRecommendation(complexity) {
17870
+ const spec = TIER_SPECS[tierForComplexity(complexity)];
17871
+ return `
17872
+
17873
+ ---
17874
+
17875
+ **\u{1F916} Suggested model tier: ${spec.label}**
17876
+ ${spec.rationale}
17877
+ Examples (use whichever provider you have): ${spec.examples}.
17878
+ _Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
17879
+ }
17880
+
17563
17881
  // src/services/build.ts
17564
17882
  import { randomUUID as randomUUID11 } from "crypto";
17565
- import { readdirSync as readdirSync4, existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
17566
- import { join as join8 } from "path";
17883
+ import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
17884
+ import { join as join10 } from "path";
17885
+
17886
+ // src/lib/harness-capability.ts
17887
+ var HARNESS_REGISTRY = {
17888
+ // Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
17889
+ "claude-code": { build: true, label: "Claude Code" },
17890
+ "opencode": { build: true, label: "opencode" },
17891
+ "zcode": { build: true, label: "zcode" },
17892
+ "codex": { build: true, label: "Codex" },
17893
+ "cursor": { build: true, label: "Cursor" },
17894
+ // Cloud harnesses with code sandboxes — their agent builds in-sandbox over HTTP+OAuth.
17895
+ "lovable": { build: true, label: "Lovable" },
17896
+ "bolt": { build: true, label: "Bolt" },
17897
+ "replit": { build: true, label: "Replit" },
17898
+ // Chat-only surfaces, no sandbox — planning only.
17899
+ "chatgpt": { build: false, label: "ChatGPT" },
17900
+ "claude.ai": { build: false, label: "Claude.ai" },
17901
+ "claude-desktop": { build: false, label: "Claude Desktop" }
17902
+ };
17903
+ var UNKNOWN_DEFAULT = { build: false, label: "your tool" };
17904
+ function detectHarness(clientName) {
17905
+ const raw = clientName?.trim() || null;
17906
+ if (!raw) {
17907
+ return { ...UNKNOWN_DEFAULT, raw: null, key: null, known: false };
17908
+ }
17909
+ const norm = raw.toLowerCase();
17910
+ const key = HARNESS_REGISTRY[norm] ? norm : Object.keys(HARNESS_REGISTRY).find((k) => norm.includes(k)) ?? null;
17911
+ if (!key) {
17912
+ return { ...UNKNOWN_DEFAULT, label: raw, raw, key: null, known: false };
17913
+ }
17914
+ return { ...HARNESS_REGISTRY[key], raw, key, known: true };
17915
+ }
17916
+
17917
+ // src/lib/harness-build-steps.ts
17918
+ init_git();
17919
+ function startBuildSteps(branch, base, taskId) {
17920
+ return [
17921
+ `PAPI is connected over HTTP and can't reach your files \u2014 your harness runs git in its own sandbox. To start ${taskId}:`,
17922
+ ` git fetch origin`,
17923
+ ` git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${base} 2>/dev/null || git checkout -b ${branch}`,
17924
+ `Then implement the task on branch \`${branch}\`.`
17925
+ ];
17926
+ }
17927
+ function completeBuildSteps(branch, base, taskId, title) {
17928
+ const message = `${taskId}: ${title}`.replace(/["`\\]/g, "'");
17929
+ return [
17930
+ `To ship ${taskId} from your sandbox:`,
17931
+ ` git add -A`,
17932
+ ` git commit -m "${message}"`,
17933
+ ` git push -u origin ${branch}`,
17934
+ `Then open a PR from \`${branch}\` into \`${base}\`.`
17935
+ ];
17936
+ }
17937
+ function hostedBranchName(taskId, module, cycleNumber) {
17938
+ return module && cycleNumber > 0 ? cycleBranchName(cycleNumber, module) : taskBranchName(taskId);
17939
+ }
17940
+ function hostedStartSteps(clientName, taskId, module, cycleNumber, base) {
17941
+ if (!detectHarness(clientName).build) return null;
17942
+ const branch = hostedBranchName(taskId, module, cycleNumber);
17943
+ return { branch, steps: startBuildSteps(branch, base, taskId) };
17944
+ }
17945
+ function hostedCompleteSteps(clientName, taskId, module, cycleNumber, base, title) {
17946
+ if (!detectHarness(clientName).build) return null;
17947
+ const branch = hostedBranchName(taskId, module, cycleNumber);
17948
+ return completeBuildSteps(branch, base, taskId, title);
17949
+ }
17950
+
17951
+ // src/services/build.ts
17567
17952
  init_git();
17568
17953
 
17569
17954
  // src/lib/owns-local-workspace.ts
@@ -17571,16 +17956,16 @@ init_git();
17571
17956
 
17572
17957
  // src/services/release.ts
17573
17958
  import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
17574
- import { join as join7 } from "path";
17959
+ import { join as join9 } from "path";
17575
17960
  import { execFileSync as execFileSync4 } from "child_process";
17576
17961
 
17577
17962
  // src/lib/install-id.ts
17578
17963
  import { randomUUID as randomUUID10 } from "crypto";
17579
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync, chmodSync } from "fs";
17964
+ import { mkdirSync, readFileSync as readFileSync4, writeFileSync, chmodSync } from "fs";
17580
17965
  import { homedir as homedir2 } from "os";
17581
- import { join as join6 } from "path";
17582
- var PAPI_HOME_DIR = join6(homedir2(), ".papi");
17583
- var INSTALL_ID_FILE = join6(PAPI_HOME_DIR, "install-id.json");
17966
+ import { join as join8 } from "path";
17967
+ var PAPI_HOME_DIR = join8(homedir2(), ".papi");
17968
+ var INSTALL_ID_FILE = join8(PAPI_HOME_DIR, "install-id.json");
17584
17969
  var cachedInstallId = null;
17585
17970
  function isValidUuid(s) {
17586
17971
  return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
@@ -17588,7 +17973,7 @@ function isValidUuid(s) {
17588
17973
  function getInstallId() {
17589
17974
  if (cachedInstallId) return cachedInstallId;
17590
17975
  try {
17591
- const raw = readFileSync2(INSTALL_ID_FILE, "utf-8");
17976
+ const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
17592
17977
  const parsed = JSON.parse(raw);
17593
17978
  if (isValidUuid(parsed.install_id)) {
17594
17979
  cachedInstallId = parsed.install_id;
@@ -18132,7 +18517,7 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
18132
18517
  }
18133
18518
  }
18134
18519
  const latestTag = getLatestTag(config2.projectRoot);
18135
- const changelogPath = join7(config2.projectRoot, "CHANGELOG.md");
18520
+ const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
18136
18521
  if (!latestTag) {
18137
18522
  const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
18138
18523
  if (config2.adapterType === "proxy") {
@@ -18827,7 +19212,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
18827
19212
  if (modified.length === 0) {
18828
19213
  return "Auto-commit: skipped (no working-tree changes).";
18829
19214
  }
18830
- const dirname6 = (p) => {
19215
+ const dirname7 = (p) => {
18831
19216
  const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
18832
19217
  return idx > 0 ? p.slice(0, idx) : "";
18833
19218
  };
@@ -18839,7 +19224,7 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
18839
19224
  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.`;
18840
19225
  }
18841
19226
  const untracked = getUntrackedFiles(cwd);
18842
- const scopedDirs = [...new Set(scoped.map(dirname6).filter((d) => d.length > 0))];
19227
+ const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
18843
19228
  const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
18844
19229
  const scopedSet = new Set(scoped);
18845
19230
  const adjacentUntracked = untracked.filter(
@@ -18864,8 +19249,13 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
18864
19249
  }
18865
19250
  return safeRun(() => stageAllAndCommit(cwd, message));
18866
19251
  }
18867
- function pushAndCreatePR(config2, taskId, taskTitle) {
19252
+ function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
18868
19253
  const lines = [];
19254
+ if (!hasLocalWorkspace()) {
19255
+ const steps = hostedCompleteSteps(clientName, taskId, module, cycleNumber ?? 0, config2.baseBranch, taskTitle);
19256
+ if (steps) lines.push(...steps);
19257
+ return lines;
19258
+ }
18869
19259
  if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
18870
19260
  return lines;
18871
19261
  }
@@ -19050,7 +19440,7 @@ async function describeTask(adapter2, taskId) {
19050
19440
  }
19051
19441
  return { task };
19052
19442
  }
19053
- async function startBuild(adapter2, config2, taskId, options = {}) {
19443
+ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
19054
19444
  const task = await adapter2.getTask(taskId);
19055
19445
  if (!task) {
19056
19446
  throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
@@ -19092,6 +19482,12 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
19092
19482
  const branchLines = [];
19093
19483
  if (options.light) {
19094
19484
  branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
19485
+ } else if (!hasLocalWorkspace()) {
19486
+ const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
19487
+ const hosted = hostedStartSteps(clientName, taskId, task.module, cycleHealth?.totalCycles ?? 0, config2.baseBranch);
19488
+ if (hosted) {
19489
+ branchLines.push(...hosted.steps);
19490
+ }
19095
19491
  } else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
19096
19492
  const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
19097
19493
  const cycleNumber = cycleHealth?.totalCycles ?? 0;
@@ -19295,16 +19691,16 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
19295
19691
  collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
19296
19692
  return;
19297
19693
  }
19298
- const papiDir = join8(projectRoot, ".papi");
19299
- if (!existsSync4(papiDir)) {
19694
+ const papiDir = join10(projectRoot, ".papi");
19695
+ if (!existsSync6(papiDir)) {
19300
19696
  mkdirSync2(papiDir, { recursive: true });
19301
19697
  }
19302
- const scopePath = join8(papiDir, "active-task-scope.txt");
19698
+ const scopePath = join10(papiDir, "active-task-scope.txt");
19303
19699
  writeFileSync2(scopePath, content, "utf-8");
19304
19700
  }
19305
19701
  function clearActiveTaskScope(projectRoot) {
19306
- const scopePath = join8(projectRoot, ".papi", "active-task-scope.txt");
19307
- if (existsSync4(scopePath)) {
19702
+ const scopePath = join10(projectRoot, ".papi", "active-task-scope.txt");
19703
+ if (existsSync6(scopePath)) {
19308
19704
  unlinkSync(scopePath);
19309
19705
  }
19310
19706
  }
@@ -19324,7 +19720,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
19324
19720
  else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
19325
19721
  else if (relativePath.startsWith("docs/audits/")) type = "audit";
19326
19722
  try {
19327
- const content = readFileSync3(absolutePath, "utf-8").slice(0, 2e3);
19723
+ const content = readFileSync5(absolutePath, "utf-8").slice(0, 2e3);
19328
19724
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
19329
19725
  if (fmMatch) {
19330
19726
  const fm = fmMatch[1];
@@ -19371,7 +19767,7 @@ Fix the deploy first, then re-run build_execute complete with a 2xx verification
19371
19767
  );
19372
19768
  }
19373
19769
  }
19374
- async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19770
+ async function completeBuild(adapter2, config2, taskId, input, options = {}, clientName) {
19375
19771
  const task = await adapter2.getTask(taskId);
19376
19772
  if (!task) {
19377
19773
  throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
@@ -19649,7 +20045,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19649
20045
  if (options.light) {
19650
20046
  prLines.push("Light mode: skipping push and PR creation.");
19651
20047
  } else if (config2.autoCommit && input.completed === "yes") {
19652
- prLines = pushAndCreatePR(config2, taskId, task.title);
20048
+ prLines = pushAndCreatePR(config2, taskId, task.title, clientName, task.module, cycleNumber);
19653
20049
  }
19654
20050
  const allTasks = await adapter2.queryBoard();
19655
20051
  warnIfEmpty("queryBoard (cycle progress)", allTasks);
@@ -19669,14 +20065,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19669
20065
  let docWarning;
19670
20066
  try {
19671
20067
  if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
19672
- const docsDir = join8(config2.projectRoot, "docs");
19673
- if (existsSync4(docsDir)) {
20068
+ const docsDir = join10(config2.projectRoot, "docs");
20069
+ if (existsSync6(docsDir)) {
19674
20070
  const scanDir = (dir, depth = 0) => {
19675
20071
  if (depth > 8) return [];
19676
- const entries = readdirSync4(dir, { withFileTypes: true });
20072
+ const entries = readdirSync5(dir, { withFileTypes: true });
19677
20073
  const files = [];
19678
20074
  for (const e of entries) {
19679
- const full = join8(dir, e.name);
20075
+ const full = join10(dir, e.name);
19680
20076
  if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
19681
20077
  else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
19682
20078
  }
@@ -19691,7 +20087,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
19691
20087
  const failed = [];
19692
20088
  for (const docPath of unregistered) {
19693
20089
  try {
19694
- const meta = extractDocMeta(join8(config2.projectRoot, docPath), docPath, cycleNumber);
20090
+ const meta = extractDocMeta(join10(config2.projectRoot, docPath), docPath, cycleNumber);
19695
20091
  await adapter2.registerDoc({
19696
20092
  title: meta.title,
19697
20093
  type: meta.type,
@@ -19768,6 +20164,9 @@ var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
19768
20164
  var MODULE_INSTRUCTIONS = {
19769
20165
  Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
19770
20166
 
20167
+ **STEP 0 \u2014 Dispatch the design agent for the visual layer (do NOT hand-write UI in the main build context).**
20168
+ Any \`.tsx\` that renders visible UI must be built by the \`frontend-design-engineer\` subagent (dispatch via the Task tool), not inline here. It works in an isolated window loaded only with brand canon (DESIGN.md / PRODUCT.md / brand-book), runs design-critique before and after, builds via the \`frontend-design\` / \`impeccable\` skills, self-checks the falsifiable anti-slop blocklist, and verifies in-browser at populated / empty / mobile states. You remain responsible for data, routes, types, and tests \u2014 hand the visual layer to the agent and integrate the report it returns. Components hand-written in this context produce generic output that fails review. (If \`.claude/agents/frontend-design-engineer.md\` is absent \u2014 e.g. a fresh external install \u2014 run STEPs 1-4 below yourself.)
20169
+
19771
20170
  **STEP 1 \u2014 BEFORE writing any code:**
19772
20171
  If you use the \`impeccable\` skill (recommended for dashboard work), it reads two root files for design context: **PRODUCT.md** (strategic \u2014 brand, users, product purpose, design principles) and **DESIGN.md** (visual tokens \u2014 palette, typography, elevation, components). Run \`impeccable init\` to create them if they don't exist yet. Every visual decision must align with these files; if your output contradicts them, it is wrong. (The legacy \`.impeccable.md\` is no longer read by the skill.)
19773
20172
 
@@ -20007,7 +20406,7 @@ function isResearchOrSpike(task) {
20007
20406
  }
20008
20407
  function formatListItem(task) {
20009
20408
  const base = `- **${task.id}:** ${task.title}
20010
- Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity}`;
20409
+ Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity} | ${modelTierTag(task.complexity)}`;
20011
20410
  if (isResearchOrSpike(task)) {
20012
20411
  return base + "\n _Research/spike \u2014 still needed?_ **(a)** build_execute | **(b)** fast-close (answered) | **(c)** deprioritise | **(d)** discuss";
20013
20412
  }
@@ -20129,7 +20528,7 @@ async function handleBuildDescribe(adapter2, args) {
20129
20528
  return errorResponse(err instanceof Error ? err.message : String(err));
20130
20529
  }
20131
20530
  }
20132
- async function handleBuildExecute(adapter2, config2, args) {
20531
+ async function handleBuildExecute(adapter2, config2, args, clientName) {
20133
20532
  const taskId = args.task_id;
20134
20533
  if (!taskId) {
20135
20534
  return errorResponse("task_id is required.");
@@ -20153,11 +20552,11 @@ async function handleBuildExecute(adapter2, config2, args) {
20153
20552
  }
20154
20553
  }
20155
20554
  if (hasReportFields(args)) {
20156
- return handleExecuteComplete(adapter2, config2, taskId, args, light);
20555
+ return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
20157
20556
  }
20158
20557
  const tracker = new ProgressTracker("start_build");
20159
20558
  try {
20160
- const result = await startBuild(adapter2, config2, taskId, { light });
20559
+ const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
20161
20560
  tracker.mark("start_decorate_handoff");
20162
20561
  const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
20163
20562
  const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
@@ -20209,7 +20608,8 @@ ${entries}`;
20209
20608
  const moduleInstructions = getModuleInstructions(result.task.module);
20210
20609
  const moduleContext = await getModuleContext(adapter2, result.task);
20211
20610
  const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
20212
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
20611
+ const modelNote = formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity);
20612
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
20213
20613
  } catch (err) {
20214
20614
  if (isNoHandoffError(err)) {
20215
20615
  const lines = [
@@ -20245,7 +20645,7 @@ ${entries}`;
20245
20645
  }));
20246
20646
  }
20247
20647
  }
20248
- async function handleExecuteComplete(adapter2, config2, taskId, args, light = false) {
20648
+ async function handleExecuteComplete(adapter2, config2, taskId, args, light = false, clientName) {
20249
20649
  const completed = args.completed;
20250
20650
  const effort = args.effort;
20251
20651
  const estimatedEffort = args.estimated_effort;
@@ -20323,7 +20723,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
20323
20723
  })),
20324
20724
  productionVerification,
20325
20725
  preview
20326
- }, { light });
20726
+ }, { light }, clientName);
20327
20727
  tracker.mark("complete_format");
20328
20728
  if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
20329
20729
  for (const learningId of resolvesLearnings) {
@@ -20454,14 +20854,12 @@ function formatCompleteResult(result) {
20454
20854
  } else {
20455
20855
  lines.push("", `Next: run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, then \`build_execute <task_id>\` to start the next one.`);
20456
20856
  }
20457
- lines.push("", "*Tip: Start a new chat for your next build to keep context fresh and avoid hitting context window limits.*");
20458
20857
  } else {
20459
20858
  if (hasDiscoveredIssues) {
20460
20859
  lines.push("", "All cycle tasks complete. Discovered issues logged \u2014 run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
20461
20860
  } else {
20462
20861
  lines.push("", "All cycle tasks complete. Run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
20463
20862
  }
20464
- lines.push("", "*Tip: Start a new chat for your next session to keep context fresh.*");
20465
20863
  }
20466
20864
  return lines.join("\n");
20467
20865
  }
@@ -21050,13 +21448,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
21050
21448
  init_git();
21051
21449
 
21052
21450
  // src/services/reconcile.ts
21053
- import { readFileSync as readFileSync4 } from "fs";
21054
- import { join as join9 } from "path";
21451
+ import { readFileSync as readFileSync6 } from "fs";
21452
+ import { join as join11 } from "path";
21055
21453
  function loadDocsIndex(projectRoot) {
21056
21454
  if (!hasLocalWorkspace()) return "";
21057
21455
  try {
21058
- const indexPath = join9(projectRoot, "docs", "INDEX.md");
21059
- const raw = readFileSync4(indexPath, "utf8");
21456
+ const indexPath = join11(projectRoot, "docs", "INDEX.md");
21457
+ const raw = readFileSync6(indexPath, "utf8");
21060
21458
  const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
21061
21459
  if (rows.length === 0) return "";
21062
21460
  const entries = rows.map((row) => {
@@ -21631,8 +22029,8 @@ Produce your analysis and structured output above. Present Part 1 to the user an
21631
22029
  }
21632
22030
 
21633
22031
  // src/tools/review.ts
21634
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
21635
- import { join as join10 } from "path";
22032
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
22033
+ import { join as join12 } from "path";
21636
22034
  init_git();
21637
22035
 
21638
22036
  // src/services/review.ts
@@ -21844,11 +22242,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
21844
22242
  ${diff}
21845
22243
  \`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
21846
22244
  let projectContext = "";
21847
- const ctxPath = join10(config2.projectRoot, ".agents", "papi-context.md");
21848
- if (existsSync5(ctxPath)) {
22245
+ const ctxPath = join12(config2.projectRoot, ".agents", "papi-context.md");
22246
+ if (existsSync7(ctxPath)) {
21849
22247
  try {
21850
22248
  projectContext = `### Project context (.agents/papi-context.md)
21851
- ${readFileSync5(ctxPath, "utf-8")}
22249
+ ${readFileSync7(ctxPath, "utf-8")}
21852
22250
 
21853
22251
  `;
21854
22252
  } catch {
@@ -22003,8 +22401,8 @@ function mergeAfterAccept(config2, taskId) {
22003
22401
  };
22004
22402
  }
22005
22403
  const details = [];
22006
- const papiDir = join10(config2.projectRoot, ".papi");
22007
- if (existsSync5(papiDir)) {
22404
+ const papiDir = join12(config2.projectRoot, ".papi");
22405
+ if (existsSync7(papiDir)) {
22008
22406
  try {
22009
22407
  const commitResult = stageDirAndCommit(
22010
22408
  config2.projectRoot,
@@ -23034,8 +23432,9 @@ async function getHealthSummary(adapter2) {
23034
23432
  const cycleNumber = health.totalCycles;
23035
23433
  const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
23036
23434
  const reviewDue = health.strategyReviewDue;
23037
- const reviewGateBlocking = cyclesSinceReview >= 5;
23038
- const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
23435
+ const reviewGateBlocking = cyclesSinceReview >= STRATEGY_REVIEW_BLOCK_GAP;
23436
+ const reviewAvailable = cyclesSinceReview >= STRATEGY_REVIEW_OFFER_GAP && !reviewGateBlocking;
23437
+ const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : reviewAvailable ? `\u25CB Strategy review available \u2014 ${cyclesSinceReview} cycles since last review. Offered now (optional); \`plan\` hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
23039
23438
  const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
23040
23439
  const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
23041
23440
  const statusCounts = countByStatus(nonDeferredTasks);
@@ -23314,115 +23713,9 @@ function formatUnblockSection(candidates) {
23314
23713
  return lines.join("\n");
23315
23714
  }
23316
23715
 
23317
- // src/lib/skill-detection.ts
23318
- import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
23319
- import { join as join11 } from "path";
23320
- function readPackageJson(projectRoot) {
23321
- const path7 = join11(projectRoot, "package.json");
23322
- if (!existsSync6(path7)) return null;
23323
- try {
23324
- const raw = readFileSync6(path7, "utf-8");
23325
- return JSON.parse(raw);
23326
- } catch {
23327
- return null;
23328
- }
23329
- }
23330
- function allDeps(pkg) {
23331
- if (!pkg) return {};
23332
- return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
23333
- }
23334
- function hasDependencyMatching(deps, pattern) {
23335
- for (const name of Object.keys(deps)) {
23336
- if (pattern.test(name)) return true;
23337
- }
23338
- return false;
23339
- }
23340
- function hasGitHubWorkflows(projectRoot) {
23341
- const dir = join11(projectRoot, ".github", "workflows");
23342
- if (!existsSync6(dir)) return false;
23343
- try {
23344
- const entries = readdirSync5(dir);
23345
- return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
23346
- } catch {
23347
- return false;
23348
- }
23349
- }
23350
- function envExampleMentionsStaging(projectRoot) {
23351
- const path7 = join11(projectRoot, ".env.example");
23352
- if (!existsSync6(path7)) return false;
23353
- try {
23354
- const raw = readFileSync6(path7, "utf-8");
23355
- return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
23356
- } catch {
23357
- return false;
23358
- }
23359
- }
23360
- function hasVercelConfig(projectRoot) {
23361
- if (existsSync6(join11(projectRoot, "vercel.json"))) return true;
23362
- const vercelDir = join11(projectRoot, ".vercel");
23363
- if (!existsSync6(vercelDir)) return false;
23364
- try {
23365
- return statSync4(vercelDir).isDirectory();
23366
- } catch {
23367
- return false;
23368
- }
23369
- }
23370
- function scanForSkillSignals(projectRoot) {
23371
- const proposals = [];
23372
- const pkg = readPackageJson(projectRoot);
23373
- const deps = allDeps(pkg);
23374
- if (hasGitHubWorkflows(projectRoot)) {
23375
- proposals.push({
23376
- id: "gh-actions-debug",
23377
- name: "GitHub Actions debugger",
23378
- rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
23379
- hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
23380
- });
23381
- }
23382
- if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
23383
- proposals.push({
23384
- id: "error-tracking",
23385
- name: "Error tracking helper",
23386
- rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
23387
- hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
23388
- });
23389
- }
23390
- if (hasVercelConfig(projectRoot)) {
23391
- proposals.push({
23392
- id: "read-vercel-logs",
23393
- name: "Vercel deploy logs",
23394
- rationale: "Detected vercel.json or .vercel/ \u2014 a skill for pulling deploy + runtime logs from Vercel directly into Claude Code helps when builds fail or runtime errors land.",
23395
- hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
23396
- });
23397
- }
23398
- if (envExampleMentionsStaging(projectRoot)) {
23399
- proposals.push({
23400
- id: "staging-environment",
23401
- name: "Staging-environment helper",
23402
- rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
23403
- hint: 'Search Claude Code skill registry for "staging" or define your own.'
23404
- });
23405
- }
23406
- return proposals;
23407
- }
23408
- function formatSkillProposals(proposals) {
23409
- if (proposals.length === 0) return "";
23410
- const lines = ["", "## Skill proposals", "_PAPI scanned your project once and noticed tooling that often pairs with a Claude Code skill. Each proposal is one-shot \u2014 they won't resurface._"];
23411
- for (const p of proposals) {
23412
- lines.push("");
23413
- lines.push(`### ${p.name}`);
23414
- lines.push(p.rationale);
23415
- if (p.hint) {
23416
- lines.push(`_${p.hint}_`);
23417
- }
23418
- lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
23419
- }
23420
- return "\n" + lines.join("\n");
23421
- }
23422
-
23423
23716
  // src/tools/agent-list.ts
23424
23717
  import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
23425
- import { join as join12 } from "path";
23718
+ import { join as join13 } from "path";
23426
23719
  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.";
23427
23720
  function parseAgentFrontmatter(content) {
23428
23721
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -23434,7 +23727,7 @@ function parseAgentFrontmatter(content) {
23434
23727
  return { name: nameMatch?.[1].trim(), description };
23435
23728
  }
23436
23729
  async function listAgents(projectRoot) {
23437
- const agentsDir = join12(projectRoot, ".claude", "agents");
23730
+ const agentsDir = join13(projectRoot, ".claude", "agents");
23438
23731
  let files;
23439
23732
  try {
23440
23733
  files = await readdir2(agentsDir);
@@ -23445,7 +23738,7 @@ async function listAgents(projectRoot) {
23445
23738
  for (const file of files.filter((f) => f.endsWith(".md"))) {
23446
23739
  let content;
23447
23740
  try {
23448
- content = await readFile7(join12(agentsDir, file), "utf-8");
23741
+ content = await readFile7(join13(agentsDir, file), "utf-8");
23449
23742
  } catch {
23450
23743
  continue;
23451
23744
  }
@@ -23453,7 +23746,7 @@ async function listAgents(projectRoot) {
23453
23746
  agents.push({
23454
23747
  name: meta?.name ?? file.replace(/\.md$/, ""),
23455
23748
  description: meta?.description ?? "",
23456
- path: join12(".claude", "agents", file)
23749
+ path: join13(".claude", "agents", file)
23457
23750
  });
23458
23751
  }
23459
23752
  agents.sort((a, b2) => a.name.localeCompare(b2.name));
@@ -23494,8 +23787,8 @@ ${formatted}`);
23494
23787
  }
23495
23788
 
23496
23789
  // src/tools/doc-registry.ts
23497
- import { readdirSync as readdirSync6, existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
23498
- import { join as join13, relative } from "path";
23790
+ import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
23791
+ import { join as join14, relative } from "path";
23499
23792
  import { homedir as homedir3 } from "os";
23500
23793
  import { randomUUID as randomUUID16 } from "crypto";
23501
23794
  var docRegisterTool = {
@@ -23692,7 +23985,7 @@ async function handleDocSearch(adapter2, args, config2) {
23692
23985
  const lines = docs.map((d) => {
23693
23986
  const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
23694
23987
  const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
23695
- const missingNote = root && d.path && !existsSync7(join13(root, d.path)) ? `
23988
+ const missingNote = root && d.path && !existsSync8(join14(root, d.path)) ? `
23696
23989
  > \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.` : "";
23697
23990
  return `### ${d.title}
23698
23991
  **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
@@ -23706,12 +23999,12 @@ ${d.summary}
23706
23999
  ${lines.join("\n---\n\n")}`);
23707
24000
  }
23708
24001
  function scanMdFiles(dir, rootDir) {
23709
- if (!existsSync7(dir)) return [];
24002
+ if (!existsSync8(dir)) return [];
23710
24003
  const files = [];
23711
24004
  try {
23712
24005
  const entries = readdirSync6(dir, { withFileTypes: true });
23713
24006
  for (const entry of entries) {
23714
- const full = join13(dir, entry.name);
24007
+ const full = join14(dir, entry.name);
23715
24008
  if (entry.isDirectory()) {
23716
24009
  files.push(...scanMdFiles(full, rootDir));
23717
24010
  } else if (entry.name.endsWith(".md")) {
@@ -23724,7 +24017,7 @@ function scanMdFiles(dir, rootDir) {
23724
24017
  }
23725
24018
  function extractTitle(filePath) {
23726
24019
  try {
23727
- const content = readFileSync7(filePath, "utf-8").slice(0, 1e3);
24020
+ const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
23728
24021
  const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
23729
24022
  if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
23730
24023
  const headingMatch = content.match(/^#+\s+(.+)$/m);
@@ -23745,17 +24038,17 @@ async function handleDocScan(adapter2, config2, args) {
23745
24038
  const includePlans = args.include_plans ?? false;
23746
24039
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
23747
24040
  const registeredPaths = new Set(registered.map((d) => d.path));
23748
- const docsDir = join13(config2.projectRoot, "docs");
24041
+ const docsDir = join14(config2.projectRoot, "docs");
23749
24042
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
23750
24043
  const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
23751
24044
  let unregisteredPlans = [];
23752
24045
  if (includePlans) {
23753
- const plansDir = join13(homedir3(), ".claude", "plans");
23754
- if (existsSync7(plansDir)) {
24046
+ const plansDir = join14(homedir3(), ".claude", "plans");
24047
+ if (existsSync8(plansDir)) {
23755
24048
  const planFiles = scanMdFiles(plansDir, plansDir);
23756
24049
  unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
23757
24050
  path: f,
23758
- title: extractTitle(join13(plansDir, f.replace("plans/", "")))
24051
+ title: extractTitle(join14(plansDir, f.replace("plans/", "")))
23759
24052
  }));
23760
24053
  }
23761
24054
  }
@@ -23766,7 +24059,7 @@ async function handleDocScan(adapter2, config2, args) {
23766
24059
  if (unregisteredDocs.length > 0) {
23767
24060
  lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
23768
24061
  for (const f of unregisteredDocs) {
23769
- const title = extractTitle(join13(config2.projectRoot, f));
24062
+ const title = extractTitle(join14(config2.projectRoot, f));
23770
24063
  lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
23771
24064
  }
23772
24065
  }
@@ -23876,6 +24169,22 @@ ${userNotes}` : referenceLine;
23876
24169
  );
23877
24170
  }
23878
24171
 
24172
+ // src/lib/harness-guidance.ts
24173
+ function buildLoopRunsHere(i) {
24174
+ if (i.forceLocal) return true;
24175
+ if (i.harness.known) return i.harness.build;
24176
+ if (i.envHintsNoGit) return false;
24177
+ return i.localWorkspace;
24178
+ }
24179
+ function nextActionQualifier(harness) {
24180
+ const where = harness.known ? harness.label : "your tool";
24181
+ return `_Note: \`build_execute\`, \`review_submit\`, and \`release\` run where your project files live. From ${where} you can plan, log ideas, and steer the board \u2014 the build / review / release loop runs in the environment that holds your code._`;
24182
+ }
24183
+ function connectOnlyRoleNote(harness) {
24184
+ if (harness.build || !harness.known) return null;
24185
+ return `**You're connected as a planning surface.** ${harness.label} is a first-class place to plan, capture decisions, and steer the board. The code gets built wherever your build harness lives (a CLI like Codex, or a sandbox like Lovable or Bolt) and your plan + decisions travel with it. This is a full half of the loop, not a limited mode.`;
24186
+ }
24187
+
23879
24188
  // src/lib/enrichment.ts
23880
24189
  function headingSlug(line) {
23881
24190
  const match = line.match(/^##\s+(.+?)(?:\s*\([^)]*\))?\s*$/);
@@ -23961,8 +24270,8 @@ async function verifyProject(adapter2) {
23961
24270
  // src/tools/orient.ts
23962
24271
  import { execFile as execFile2 } from "child_process";
23963
24272
  import { promisify as promisify2 } from "util";
23964
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, existsSync as existsSync8 } from "fs";
23965
- import { join as join14 } from "path";
24273
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync9 } from "fs";
24274
+ import { join as join15 } from "path";
23966
24275
  var execFileAsync2 = promisify2(execFile2);
23967
24276
  var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
23968
24277
  var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
@@ -24007,7 +24316,7 @@ async function runWithLimit(concurrency, tasks) {
24007
24316
  }
24008
24317
  var orientTool = {
24009
24318
  name: "orient",
24010
- description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. Pass `environment` to qualify git-dependent recommendations (build_execute, release, review_submit) for non-Claude-Code callers.",
24319
+ description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. PAPI detects build capability from the connecting harness (clientInfo); pass `environment` only to override that detection for git-dependent recommendations (build_execute, release, review_submit).",
24011
24320
  annotations: { readOnlyHint: true, destructiveHint: false },
24012
24321
  inputSchema: {
24013
24322
  type: "object",
@@ -24015,7 +24324,7 @@ var orientTool = {
24015
24324
  environment: {
24016
24325
  type: "string",
24017
24326
  enum: ["local-cli", "hosted", "api", "unknown"],
24018
- description: 'Caller environment. "local-cli" (local CLI with git \u2014 Claude Code, Codex, or any local MCP client) sees all recommendations unchanged. "hosted" or "api" (no local git) suppresses/qualifies build_execute, release, and review_submit recommendations because those require a local session with git access. Default "unknown" = show everything (safe default). Legacy values "claude-code" and "cowork" are accepted as aliases (deprecated).'
24327
+ description: `Caller environment OVERRIDE. By default PAPI branches git-dependent recommendations on the connecting harness's detected capability \u2014 a build-capable harness (local CLI like Codex/opencode, or a sandboxed cloud harness like Lovable/Bolt/Replit) sees the full loop; a connect-only chat surface sees planning-only framing. Pass this only to override: "local-cli" forces the full loop, "hosted"/"api" hints no local git for an UNRECOGNISED harness. Default "unknown" = trust detection. Legacy "claude-code"/"cowork" accepted as aliases (deprecated).`
24019
24328
  },
24020
24329
  deep_housekeeping: {
24021
24330
  type: "boolean",
@@ -24059,7 +24368,7 @@ function formatSynthesisParagraph(opts) {
24059
24368
  parts.push(`Next: ${cleanMode}`);
24060
24369
  return parts.join(" ");
24061
24370
  }
24062
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = [], projectName, teamSummary) {
24371
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = [], projectName, teamSummary, clientName) {
24063
24372
  const lines = [];
24064
24373
  const cycleIsComplete = health.latestCycleStatus === "complete";
24065
24374
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -24081,6 +24390,12 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
24081
24390
  lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
24082
24391
  lines.push("");
24083
24392
  }
24393
+ const harness = detectHarness(clientName);
24394
+ const roleNote = connectOnlyRoleNote(harness);
24395
+ if (roleNote) {
24396
+ lines.push(`> ${roleNote}`);
24397
+ lines.push("");
24398
+ }
24084
24399
  if (buildInfo.warnings.length > 0) {
24085
24400
  for (const w of buildInfo.warnings) {
24086
24401
  lines.push(`> ${w}`);
@@ -24092,11 +24407,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
24092
24407
  lines.push("");
24093
24408
  }
24094
24409
  const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
24095
- if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
24096
- lines.push(`> **Next action:** ${health.recommendedMode}`);
24097
- lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local session in your AI coding tool with git access. From \`${environment}\` you can view state, log ideas, and edit the board \u2014 but the build/review/release loop must run through your local tool._`);
24098
- } else {
24099
- lines.push(`> **Next action:** ${health.recommendedMode}`);
24410
+ const loopRunsHere = buildLoopRunsHere({
24411
+ harness,
24412
+ localWorkspace: hasLocalWorkspace(),
24413
+ forceLocal: environment === "local-cli",
24414
+ envHintsNoGit: GIT_DEPENDENT_ENVS.has(environment)
24415
+ });
24416
+ lines.push(`> **Next action:** ${health.recommendedMode}`);
24417
+ if (isGitDependentRec && !loopRunsHere) {
24418
+ lines.push(`> ${nextActionQualifier(harness)}`);
24100
24419
  }
24101
24420
  lines.push("");
24102
24421
  lines.push(`**Strategy Review:** ${health.reviewWarning}`);
@@ -24262,8 +24581,8 @@ async function getLatestGitTag(projectRoot) {
24262
24581
  }
24263
24582
  async function checkNpmVersionDrift() {
24264
24583
  try {
24265
- const pkgPath = join14(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
24266
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
24584
+ const pkgPath = join15(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
24585
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
24267
24586
  const localVersion = pkg.version;
24268
24587
  const packageName = pkg.name;
24269
24588
  const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
@@ -24396,7 +24715,7 @@ function formatResolvedFeedback(items) {
24396
24715
  lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
24397
24716
  return lines.join("\n");
24398
24717
  }
24399
- async function handleOrient(adapter2, config2, args = {}) {
24718
+ async function handleOrient(adapter2, config2, args = {}, clientName) {
24400
24719
  const environment = normaliseEnvironment(args.environment);
24401
24720
  const deepHousekeeping = args.deep_housekeeping === true;
24402
24721
  const fullEnrichment = args.full === true || deepHousekeeping;
@@ -24720,7 +25039,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24720
25039
  // task-1652: deep housekeeping — opt-in sweep (board, unrecorded commits, unregistered docs)
24721
25040
  // task-1865: also detects stale skill forks vs the @papi-ai/skills registry.
24722
25041
  tracked("deep-housekeeping", async () => {
24723
- if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
25042
+ if (!deepHousekeeping) return { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
24724
25043
  let reconciliationNote2 = "";
24725
25044
  try {
24726
25045
  const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
@@ -24732,6 +25051,22 @@ async function handleOrient(adapter2, config2, args = {}) {
24732
25051
  }
24733
25052
  } catch {
24734
25053
  }
25054
+ let mergedInProgressNote2 = "";
25055
+ try {
25056
+ if (hasLocalWorkspace()) {
25057
+ const merged = detectMergedInProgress(config2.projectRoot, config2.baseBranch, allTasks);
25058
+ if (merged.length > 0) {
25059
+ const lines = ["\n\n## Merged but In Progress"];
25060
+ lines.push(`${merged.length} In-Progress task(s) already merged to the default branch \u2014 close them out with \`build_execute\` complete (or \`board_edit\` to mark Done):`);
25061
+ for (const m of merged) {
25062
+ const ref = m.pr ? `${m.pr} (\`${m.commit}\`)` : `\`${m.commit}\``;
25063
+ lines.push(`- \u26A0\uFE0F **${m.displayId}** \u2014 merged via ${ref}: ${m.subject}`);
25064
+ }
25065
+ mergedInProgressNote2 = lines.join("\n");
25066
+ }
25067
+ }
25068
+ } catch {
25069
+ }
24735
25070
  let unrecordedNote2 = "";
24736
25071
  try {
24737
25072
  const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
@@ -24756,7 +25091,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24756
25091
  let unregisteredDocsNote2 = "";
24757
25092
  try {
24758
25093
  if (adapter2.searchDocs && hasLocalWorkspace()) {
24759
- const docsDir = join14(config2.projectRoot, "docs");
25094
+ const docsDir = join15(config2.projectRoot, "docs");
24760
25095
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
24761
25096
  if (docsFiles.length > 0) {
24762
25097
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -24783,7 +25118,7 @@ async function handleOrient(adapter2, config2, args = {}) {
24783
25118
  }
24784
25119
  } catch {
24785
25120
  }
24786
- return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
25121
+ return { reconciliationNote: reconciliationNote2, mergedInProgressNote: mergedInProgressNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
24787
25122
  })
24788
25123
  ]);
24789
25124
  const proxyWarning = proxyVersionOutcome.status === "fulfilled" ? proxyVersionOutcome.value : void 0;
@@ -24825,7 +25160,7 @@ ${versionDrift}` : "";
24825
25160
  void adapter2.markFeedbackNotified(resolvedFeedback.map((f) => f.id), config2.userId).catch(() => {
24826
25161
  });
24827
25162
  }
24828
- const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
25163
+ const { reconciliationNote, mergedInProgressNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", mergedInProgressNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
24829
25164
  let enrichmentNote = "";
24830
25165
  const enrichmentCollector = new FileWriteCollector();
24831
25166
  try {
@@ -24872,8 +25207,8 @@ ${section}`;
24872
25207
  tracked("release-history", () => computeReleaseHistory(adapter2))().catch(() => void 0)
24873
25208
  ]);
24874
25209
  const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
24875
- const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
24876
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
25210
+ const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
25211
+ return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
24877
25212
  } catch (err) {
24878
25213
  const message = err instanceof Error ? err.message : String(err);
24879
25214
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -24904,9 +25239,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector) {
24904
25239
 
24905
25240
  \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
24906
25241
  }
24907
- const claudeMdPath = join14(projectRoot, "CLAUDE.md");
24908
- if (!existsSync8(claudeMdPath)) return "";
24909
- const content = readFileSync8(claudeMdPath, "utf-8");
25242
+ const claudeMdPath = join15(projectRoot, "CLAUDE.md");
25243
+ if (!existsSync9(claudeMdPath)) return "";
25244
+ const content = readFileSync9(claudeMdPath, "utf-8");
24910
25245
  const additions = [];
24911
25246
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
24912
25247
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
@@ -25612,7 +25947,7 @@ ${result.userMessage}
25612
25947
 
25613
25948
  // src/services/scope-brief.ts
25614
25949
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
25615
- import { join as join15, dirname as dirname3 } from "path";
25950
+ import { join as join16, dirname as dirname4 } from "path";
25616
25951
  import Anthropic from "@anthropic-ai/sdk";
25617
25952
  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.
25618
25953
 
@@ -25667,13 +26002,13 @@ async function runScopeBrief(adapter2, input) {
25667
26002
  }
25668
26003
  const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
25669
26004
  const relPath = `docs/scopes/${slug}.md`;
25670
- const absPath = join15(input.projectRoot, relPath);
26005
+ const absPath = join16(input.projectRoot, relPath);
25671
26006
  const docBody = addFrontmatter(docContent, task, input.cycleNumber);
25672
26007
  const collector = new FileWriteCollector();
25673
26008
  if (input.adapterType === "proxy") {
25674
26009
  collector.add({ path: relPath, content: docBody, mode: "overwrite" });
25675
26010
  } else {
25676
- mkdirSync3(dirname3(absPath), { recursive: true });
26011
+ mkdirSync3(dirname4(absPath), { recursive: true });
25677
26012
  writeFileSync4(absPath, docBody, "utf-8");
25678
26013
  }
25679
26014
  const taskCount = countSubTasks(docContent);
@@ -26411,21 +26746,90 @@ async function handleTaskUnclaim(adapter2, config2, args) {
26411
26746
  return textResponse(`\u2705 Released **${result.id}** (${result.title}) back to the shared Pool.`);
26412
26747
  }
26413
26748
 
26749
+ // src/tools/task-move.ts
26750
+ var taskMoveTool = {
26751
+ name: "task_move",
26752
+ description: "Move a task from the current project to another project you own. Reassigns the task a fresh id in the target (collision-free) and carries its build reports, comments, and history with it; the cycle assignment is cleared so it lands in the target project's backlog. You must own (or have write access to) BOTH projects. Destructive-ish and cross-project, so it requires confirm=true \u2014 without it you get a preview only. Does not call the Anthropic API.",
26753
+ annotations: { readOnlyHint: false, destructiveHint: true },
26754
+ inputSchema: {
26755
+ type: "object",
26756
+ properties: {
26757
+ task_id: { type: "string", description: 'The task to move, e.g. "task-2292".' },
26758
+ target_project: {
26759
+ type: "string",
26760
+ description: 'The destination project \u2014 its slug (e.g. "papi-ui") or UUID. Must be a project you own.'
26761
+ },
26762
+ confirm: {
26763
+ type: "boolean",
26764
+ description: "Set true to perform the move. Omit (or false) to get a preview of what would happen."
26765
+ }
26766
+ },
26767
+ required: ["task_id", "target_project"]
26768
+ }
26769
+ };
26770
+ function requireStr(value) {
26771
+ const s = typeof value === "string" ? value.trim() : "";
26772
+ return s.length > 0 ? s : null;
26773
+ }
26774
+ async function handleTaskMove(adapter2, config2, args) {
26775
+ const taskId = requireStr(args.task_id);
26776
+ if (!taskId) return errorResponse('A task_id is required. Example: task_move task_id="task-42" target_project="other-project" confirm=true');
26777
+ const targetProject = requireStr(args.target_project);
26778
+ if (!targetProject) return errorResponse("A target_project (slug or UUID) is required \u2014 the project you want to move the task into.");
26779
+ if (typeof adapter2.moveTask !== "function") {
26780
+ return errorResponse("Cross-project move is not available on this adapter.");
26781
+ }
26782
+ const gate = await resolveOwnerGate(adapter2, config2);
26783
+ const callerUserId = gate.callerUserId;
26784
+ if (!callerUserId) {
26785
+ const note = gate.resolutionError ? ` (${gate.resolutionError})` : "";
26786
+ return errorResponse(
26787
+ "Moving a task requires a resolvable user identity, but none was found" + note + ". Set PAPI_USER_ID to your account UUID (local) \u2014 hosted sessions derive it from your bearer token."
26788
+ );
26789
+ }
26790
+ const task = await adapter2.getTask(taskId);
26791
+ if (!task) return errorResponse(`Task "${taskId}" was not found on this project's board. task_move moves a task FROM the project you're connected to \u2014 switch to the source project first if needed.`);
26792
+ if (args.confirm !== true) {
26793
+ return textResponse(
26794
+ `\u26A0\uFE0F **Preview \u2014 no changes made.**
26795
+
26796
+ This will move **${taskId}** (${task.title}) into project \`${targetProject}\`:
26797
+ - it gets a NEW task id in the target (the current "${taskId}" is freed in this project)
26798
+ - its build reports, comments, and history move with it
26799
+ - its cycle assignment is cleared (it lands in the target's backlog)
26800
+ - you must own both this project and \`${targetProject}\`
26801
+
26802
+ This is a cross-project write and cannot be auto-undone. To proceed:
26803
+ \`task_move task_id="${taskId}" target_project="${targetProject}" confirm=true\``
26804
+ );
26805
+ }
26806
+ try {
26807
+ const res = await adapter2.moveTask(taskId, targetProject, callerUserId);
26808
+ return textResponse(
26809
+ `\u2705 Moved **${taskId}** from ${res.sourceName} to ${res.targetName} as **${res.newDisplayId}**.
26810
+
26811
+ Its build reports, comments, and history moved with it; the cycle assignment was cleared, so it sits in ${res.targetName}'s backlog. Switch to ${res.targetName} to plan or build it.`
26812
+ );
26813
+ } catch (err) {
26814
+ return errorResponse(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
26815
+ }
26816
+ }
26817
+
26414
26818
  // src/services/harness-inventory.ts
26415
26819
  import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
26416
- import { join as join16 } from "path";
26820
+ import { join as join17 } from "path";
26417
26821
  import { createHash as createHash3 } from "crypto";
26418
26822
  var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
26419
26823
  async function computeFingerprint(root) {
26420
26824
  const parts = [];
26421
26825
  for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
26422
- const dir = join16(root, sub);
26826
+ const dir = join17(root, sub);
26423
26827
  try {
26424
26828
  const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
26425
26829
  for (const name of names) {
26426
26830
  let mtime = "";
26427
26831
  try {
26428
- mtime = String(Math.floor((await stat3(join16(dir, name))).mtimeMs));
26832
+ mtime = String(Math.floor((await stat3(join17(dir, name))).mtimeMs));
26429
26833
  } catch {
26430
26834
  }
26431
26835
  parts.push(`${sub}/${name}:${mtime}`);
@@ -26444,7 +26848,7 @@ async function computeFingerprint(root) {
26444
26848
  }
26445
26849
  async function readSkillDescription(skillDir) {
26446
26850
  try {
26447
- const content = await readFile8(join16(skillDir, "SKILL.md"), "utf-8");
26851
+ const content = await readFile8(join17(skillDir, "SKILL.md"), "utf-8");
26448
26852
  const fm = content.match(/^---\n([\s\S]*?)\n---/);
26449
26853
  if (!fm) return void 0;
26450
26854
  const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
@@ -26464,17 +26868,17 @@ async function scanInventory(root, toolDefs) {
26464
26868
  version = loadManifest().packageVersion;
26465
26869
  } catch {
26466
26870
  }
26467
- const skillsDir = join16(root, ".claude", "skills");
26871
+ const skillsDir = join17(root, ".claude", "skills");
26468
26872
  try {
26469
26873
  const dirents = await readdir3(skillsDir, { withFileTypes: true });
26470
26874
  for (const d of dirents.filter((e) => e.isDirectory())) {
26471
26875
  entries.push({
26472
26876
  kind: "skill",
26473
26877
  name: d.name,
26474
- description: await readSkillDescription(join16(skillsDir, d.name)),
26878
+ description: await readSkillDescription(join17(skillsDir, d.name)),
26475
26879
  version,
26476
26880
  status: stale.has(d.name) ? "stale_fork" : "ok",
26477
- path: join16(".claude", "skills", d.name)
26881
+ path: join17(".claude", "skills", d.name)
26478
26882
  });
26479
26883
  }
26480
26884
  } catch {
@@ -26490,9 +26894,9 @@ async function scanInventory(root, toolDefs) {
26490
26894
  }
26491
26895
  const present = /* @__PURE__ */ new Set();
26492
26896
  try {
26493
- for (const f of (await readdir3(join16(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
26897
+ for (const f of (await readdir3(join17(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
26494
26898
  present.add(f);
26495
- entries.push({ kind: "hook", name: f, status: "ok", path: join16(".claude", "hooks", f) });
26899
+ entries.push({ kind: "hook", name: f, status: "ok", path: join17(".claude", "hooks", f) });
26496
26900
  }
26497
26901
  } catch {
26498
26902
  }
@@ -26571,6 +26975,130 @@ async function handleInventorySync(adapter2, config2, args, toolDefs) {
26571
26975
  return textResponse(lines.join("\n"));
26572
26976
  }
26573
26977
 
26978
+ // src/resources.ts
26979
+ var SCHEME = "mcp://papi";
26980
+ var RESOURCE_TEMPLATES = [
26981
+ {
26982
+ uriTemplate: `${SCHEME}/decisions/{adId}`,
26983
+ name: "Active Decision",
26984
+ description: "A single Active Decision (e.g. AD-12) \u2014 title, confidence, body, and supersession status.",
26985
+ mimeType: "text/markdown"
26986
+ },
26987
+ {
26988
+ uriTemplate: `${SCHEME}/build-reports/cycle-{cycle}`,
26989
+ name: "Cycle build reports",
26990
+ description: "Every build report recorded for a given cycle number (e.g. cycle-310).",
26991
+ mimeType: "text/markdown"
26992
+ },
26993
+ {
26994
+ uriTemplate: `${SCHEME}/cycles/{cycle}`,
26995
+ name: "Cycle summary",
26996
+ description: "Summary, task count, effort, and carry-forward for a given cycle number.",
26997
+ mimeType: "text/markdown"
26998
+ },
26999
+ {
27000
+ uriTemplate: `${SCHEME}/handoffs/{taskId}`,
27001
+ name: "Build handoff",
27002
+ description: "The read-only BUILD HANDOFF for a task (e.g. task-1801).",
27003
+ mimeType: "text/markdown"
27004
+ }
27005
+ ];
27006
+ async function listPapiResources(adapter2) {
27007
+ const out = [];
27008
+ try {
27009
+ const ads = await adapter2.getActiveDecisions();
27010
+ for (const d of ads.filter((d2) => !d2.superseded).slice(0, 50)) {
27011
+ out.push({
27012
+ uri: `${SCHEME}/decisions/${d.id}`,
27013
+ name: `${d.id}: ${d.title}`,
27014
+ description: `Active Decision [${d.confidence}]`,
27015
+ mimeType: "text/markdown"
27016
+ });
27017
+ }
27018
+ } catch {
27019
+ }
27020
+ try {
27021
+ const log2 = await adapter2.getCycleLog(10);
27022
+ for (const e of log2) {
27023
+ out.push({
27024
+ uri: `${SCHEME}/cycles/${e.cycleNumber}`,
27025
+ name: `Cycle ${e.cycleNumber} summary`,
27026
+ description: e.title,
27027
+ mimeType: "text/markdown"
27028
+ });
27029
+ out.push({
27030
+ uri: `${SCHEME}/build-reports/cycle-${e.cycleNumber}`,
27031
+ name: `Cycle ${e.cycleNumber} build reports`,
27032
+ mimeType: "text/markdown"
27033
+ });
27034
+ }
27035
+ } catch {
27036
+ }
27037
+ return out;
27038
+ }
27039
+ function parseCycleNumber(raw) {
27040
+ const m = raw.match(/(\d+)\s*$/);
27041
+ return m ? parseInt(m[1], 10) : NaN;
27042
+ }
27043
+ async function readPapiResource(adapter2, uri) {
27044
+ if (!uri.startsWith(`${SCHEME}/`)) {
27045
+ throw new Error(`Unknown resource URI: ${uri}. Expected ${SCHEME}/...`);
27046
+ }
27047
+ const path7 = uri.slice(`${SCHEME}/`.length);
27048
+ const slash = path7.indexOf("/");
27049
+ const kind = slash === -1 ? path7 : path7.slice(0, slash);
27050
+ const rest = slash === -1 ? "" : path7.slice(slash + 1);
27051
+ const md = (text) => ({ uri, mimeType: "text/markdown", text });
27052
+ switch (kind) {
27053
+ case "decisions": {
27054
+ const adId = rest.trim();
27055
+ if (!adId) throw new Error(`Resource URI missing an AD id: ${uri}`);
27056
+ const ads = await adapter2.getActiveDecisions({ includeRetired: true });
27057
+ const target = ads.find((d) => d.id === adId);
27058
+ if (!target) throw new Error(`Active Decision not found in this project: ${adId}`);
27059
+ const supersededNote = target.superseded ? ` [SUPERSEDED by ${target.supersededBy ?? "unknown"}]` : "";
27060
+ const supersedes = ads.filter((d) => d.supersededBy === target.id).map((d) => d.id);
27061
+ const chain = supersedes.length > 0 ? `
27062
+
27063
+ _Supersedes: ${supersedes.join(", ")}_` : "";
27064
+ return md(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
27065
+
27066
+ ${target.body}${chain}`);
27067
+ }
27068
+ case "build-reports": {
27069
+ const cycle = parseCycleNumber(rest);
27070
+ if (Number.isNaN(cycle)) throw new Error(`Resource URI missing a cycle number: ${uri}`);
27071
+ const reports = (await adapter2.getBuildReportsSince(cycle)).filter((r) => r.cycle === cycle);
27072
+ return md(`# Build Reports \u2014 Cycle ${cycle} (${reports.length})
27073
+
27074
+ ${formatBuildReports(reports)}`);
27075
+ }
27076
+ case "cycles": {
27077
+ const cycle = parseCycleNumber(rest);
27078
+ if (Number.isNaN(cycle)) throw new Error(`Resource URI missing a cycle number: ${uri}`);
27079
+ const entry = (await adapter2.getCycleLogSince(cycle)).find((e) => e.cycleNumber === cycle);
27080
+ if (!entry) throw new Error(`No cycle log entry found in this project for cycle ${cycle}`);
27081
+ return md(formatCycleLog([entry]));
27082
+ }
27083
+ case "handoffs": {
27084
+ const taskId = rest.trim();
27085
+ if (!taskId) throw new Error(`Resource URI missing a task id: ${uri}`);
27086
+ const task = await adapter2.getTask(taskId);
27087
+ if (!task) throw new Error(`Task not found in this project: ${taskId}`);
27088
+ if (!task.buildHandoff) {
27089
+ return md(`# ${taskId}: ${task.title}
27090
+
27091
+ _No BUILD HANDOFF recorded for this task._`);
27092
+ }
27093
+ return md(`# BUILD HANDOFF \u2014 ${taskId}: ${task.title}
27094
+
27095
+ ${serializeBuildHandoff(task.buildHandoff)}`);
27096
+ }
27097
+ default:
27098
+ throw new Error(`Unknown resource family "${kind}" in ${uri}. Known: decisions, build-reports, cycles, handoffs.`);
27099
+ }
27100
+ }
27101
+
26574
27102
  // src/services/metering.ts
26575
27103
  var TIER_ALLOWANCES = {
26576
27104
  free: 1e3,
@@ -26791,32 +27319,36 @@ var PAPI_TOOLS = [
26791
27319
  contributorListTool,
26792
27320
  taskClaimTool,
26793
27321
  taskUnclaimTool,
27322
+ taskMoveTool,
26794
27323
  inventorySyncTool
26795
27324
  ];
26796
27325
  function getToolMetadata() {
26797
27326
  return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
26798
27327
  }
26799
27328
  function createServer(adapter2, config2) {
26800
- const __pkgFilename = fileURLToPath2(import.meta.url);
26801
- const __pkgDir = dirname4(__pkgFilename);
27329
+ const __pkgFilename = fileURLToPath3(import.meta.url);
27330
+ const __pkgDir = dirname5(__pkgFilename);
26802
27331
  let serverVersion = "unknown";
26803
27332
  try {
26804
- const pkg = JSON.parse(readFileSync9(join17(__pkgDir, "..", "package.json"), "utf-8"));
27333
+ const pkg = JSON.parse(readFileSync10(join18(__pkgDir, "..", "package.json"), "utf-8"));
26805
27334
  serverVersion = pkg.version ?? "unknown";
26806
27335
  } catch {
26807
27336
  }
26808
27337
  const server2 = new Server(
26809
27338
  { name: "papi", version: serverVersion },
26810
- { capabilities: { tools: {}, prompts: {} } }
27339
+ // task-2305: `instructions` is surfaced in the MCP initialize response, so every
27340
+ // connecting host LLM gets PAPI's cycle frame on connect — zero config, any harness.
27341
+ // task-1801: `resources` capability for the PAPI read surface exposed as MCP resources.
27342
+ { capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: UNIVERSAL_FRAME }
26811
27343
  );
26812
27344
  if (config2.adapterType === "md") {
26813
27345
  process.stderr.write(
26814
27346
  "\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
26815
27347
  );
26816
27348
  }
26817
- const __filename = fileURLToPath2(import.meta.url);
26818
- const __dirname2 = dirname4(__filename);
26819
- const skillsDir = join17(__dirname2, "..", "skills");
27349
+ const __filename = fileURLToPath3(import.meta.url);
27350
+ const __dirname2 = dirname5(__filename);
27351
+ const skillsDir = join18(__dirname2, "..", "skills");
26820
27352
  function parseSkillFrontmatter(content) {
26821
27353
  const match = content.match(/^---\n([\s\S]*?)\n---/);
26822
27354
  if (!match) return null;
@@ -26834,7 +27366,7 @@ function createServer(adapter2, config2) {
26834
27366
  const mdFiles = files.filter((f) => f.endsWith(".md"));
26835
27367
  const prompts = [];
26836
27368
  for (const file of mdFiles) {
26837
- const content = await readFile9(join17(skillsDir, file), "utf-8");
27369
+ const content = await readFile9(join18(skillsDir, file), "utf-8");
26838
27370
  const meta = parseSkillFrontmatter(content);
26839
27371
  if (meta) {
26840
27372
  prompts.push({ name: meta.name, description: meta.description });
@@ -26850,7 +27382,7 @@ function createServer(adapter2, config2) {
26850
27382
  try {
26851
27383
  const files = await readdir4(skillsDir);
26852
27384
  for (const file of files.filter((f) => f.endsWith(".md"))) {
26853
- const content = await readFile9(join17(skillsDir, file), "utf-8");
27385
+ const content = await readFile9(join18(skillsDir, file), "utf-8");
26854
27386
  const meta = parseSkillFrontmatter(content);
26855
27387
  if (meta?.name === name) {
26856
27388
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -26864,6 +27396,15 @@ function createServer(adapter2, config2) {
26864
27396
  }
26865
27397
  throw new Error(`Prompt not found: ${name}`);
26866
27398
  });
27399
+ server2.setRequestHandler(ListResourcesRequestSchema, async () => ({
27400
+ resources: await listPapiResources(adapter2)
27401
+ }));
27402
+ server2.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
27403
+ resourceTemplates: RESOURCE_TEMPLATES
27404
+ }));
27405
+ server2.setRequestHandler(ReadResourceRequestSchema, async (request) => ({
27406
+ contents: [await readPapiResource(adapter2, request.params.uri)]
27407
+ }));
26867
27408
  server2.setRequestHandler(ListToolsRequestSchema, async () => ({
26868
27409
  tools: [...PAPI_TOOLS]
26869
27410
  }));
@@ -26921,7 +27462,7 @@ function createServer(adapter2, config2) {
26921
27462
  case "build_describe":
26922
27463
  return handleBuildDescribe(adapter2, safeArgs);
26923
27464
  case "build_execute":
26924
- return handleBuildExecute(adapter2, config2, safeArgs);
27465
+ return handleBuildExecute(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26925
27466
  case "build_cancel":
26926
27467
  return handleBuildCancel(adapter2, safeArgs);
26927
27468
  case "idea":
@@ -26943,9 +27484,9 @@ function createServer(adapter2, config2) {
26943
27484
  case "init":
26944
27485
  return handleInit(config2, safeArgs);
26945
27486
  case "orient":
26946
- return handleOrient(adapter2, config2, safeArgs);
27487
+ return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26947
27488
  case "papi":
26948
- return handleOrient(adapter2, config2, safeArgs);
27489
+ return handleOrient(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
26949
27490
  // alias (task-2223)
26950
27491
  case "hierarchy_update":
26951
27492
  return handleHierarchyUpdate(adapter2, safeArgs);
@@ -26987,6 +27528,8 @@ function createServer(adapter2, config2) {
26987
27528
  return handleContributorList(adapter2, config2, safeArgs);
26988
27529
  case "task_claim":
26989
27530
  return handleTaskClaim(adapter2, config2, safeArgs);
27531
+ case "task_move":
27532
+ return handleTaskMove(adapter2, config2, safeArgs);
26990
27533
  case "task_unclaim":
26991
27534
  return handleTaskUnclaim(adapter2, config2, safeArgs);
26992
27535
  case "inventory_sync":
@@ -27506,10 +28049,10 @@ async function dispatchRequest(args) {
27506
28049
  }
27507
28050
 
27508
28051
  // src/index.ts
27509
- var __dirname = dirname5(fileURLToPath3(import.meta.url));
28052
+ var __dirname = dirname6(fileURLToPath4(import.meta.url));
27510
28053
  var pkgVersion = "unknown";
27511
28054
  try {
27512
- const pkg = JSON.parse(readFileSync14(join22(__dirname, "..", "package.json"), "utf-8"));
28055
+ const pkg = JSON.parse(readFileSync15(join23(__dirname, "..", "package.json"), "utf-8"));
27513
28056
  pkgVersion = pkg.version;
27514
28057
  } catch {
27515
28058
  }