@papi-ai/server 0.7.19 → 0.7.21

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
@@ -2364,9 +2364,9 @@ var init_query = __esm({
2364
2364
  CLOSE = {};
2365
2365
  Query = class extends Promise {
2366
2366
  constructor(strings, args, handler, canceller, options = {}) {
2367
- let resolve, reject;
2367
+ let resolve2, reject;
2368
2368
  super((a, b2) => {
2369
- resolve = a;
2369
+ resolve2 = a;
2370
2370
  reject = b2;
2371
2371
  });
2372
2372
  this.tagged = Array.isArray(strings.raw);
@@ -2377,7 +2377,7 @@ var init_query = __esm({
2377
2377
  this.options = options;
2378
2378
  this.state = null;
2379
2379
  this.statement = null;
2380
- this.resolve = (x) => (this.active = false, resolve(x));
2380
+ this.resolve = (x) => (this.active = false, resolve2(x));
2381
2381
  this.reject = (x) => (this.active = false, reject(x));
2382
2382
  this.active = false;
2383
2383
  this.cancelled = null;
@@ -2425,12 +2425,12 @@ var init_query = __esm({
2425
2425
  if (this.executed && !this.active)
2426
2426
  return { done: true };
2427
2427
  prev && prev();
2428
- const promise = new Promise((resolve, reject) => {
2428
+ const promise = new Promise((resolve2, reject) => {
2429
2429
  this.cursorFn = (value) => {
2430
- resolve({ value, done: false });
2430
+ resolve2({ value, done: false });
2431
2431
  return new Promise((r) => prev = r);
2432
2432
  };
2433
- this.resolve = () => (this.active = false, resolve({ done: true }));
2433
+ this.resolve = () => (this.active = false, resolve2({ done: true }));
2434
2434
  this.reject = (x) => (this.active = false, reject(x));
2435
2435
  });
2436
2436
  this.execute();
@@ -3028,12 +3028,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
3028
3028
  x.on("drain", drain);
3029
3029
  return x;
3030
3030
  }
3031
- async function cancel({ pid, secret }, resolve, reject) {
3031
+ async function cancel({ pid, secret }, resolve2, reject) {
3032
3032
  try {
3033
3033
  cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
3034
3034
  await connect();
3035
3035
  socket.once("error", reject);
3036
- socket.once("close", resolve);
3036
+ socket.once("close", resolve2);
3037
3037
  } catch (error2) {
3038
3038
  reject(error2);
3039
3039
  }
@@ -4050,7 +4050,7 @@ var init_subscribe = __esm({
4050
4050
  // ../../node_modules/postgres/src/large.js
4051
4051
  import Stream2 from "stream";
4052
4052
  function largeObject(sql, oid, mode = 131072 | 262144) {
4053
- return new Promise(async (resolve, reject) => {
4053
+ return new Promise(async (resolve2, reject) => {
4054
4054
  await sql.begin(async (sql2) => {
4055
4055
  let finish;
4056
4056
  !oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
@@ -4076,7 +4076,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
4076
4076
  ) seek
4077
4077
  `
4078
4078
  };
4079
- resolve(lo);
4079
+ resolve2(lo);
4080
4080
  return new Promise(async (r) => finish = r);
4081
4081
  async function readable({
4082
4082
  highWaterMark = 2048 * 8,
@@ -4237,8 +4237,8 @@ function Postgres(a, b2) {
4237
4237
  }
4238
4238
  async function reserve() {
4239
4239
  const queue = queue_default();
4240
- const c = open.length ? open.shift() : await new Promise((resolve, reject) => {
4241
- const query = { reserve: resolve, reject };
4240
+ const c = open.length ? open.shift() : await new Promise((resolve2, reject) => {
4241
+ const query = { reserve: resolve2, reject };
4242
4242
  queries.push(query);
4243
4243
  closed.length && connect(closed.shift(), query);
4244
4244
  });
@@ -4275,9 +4275,9 @@ function Postgres(a, b2) {
4275
4275
  let uncaughtError, result;
4276
4276
  name && await sql2`savepoint ${sql2(name)}`;
4277
4277
  try {
4278
- result = await new Promise((resolve, reject) => {
4278
+ result = await new Promise((resolve2, reject) => {
4279
4279
  const x = fn2(sql2);
4280
- Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve, reject);
4280
+ Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve2, reject);
4281
4281
  });
4282
4282
  if (uncaughtError)
4283
4283
  throw uncaughtError;
@@ -4334,8 +4334,8 @@ function Postgres(a, b2) {
4334
4334
  return c.execute(query) ? move(c, busy) : move(c, full);
4335
4335
  }
4336
4336
  function cancel(query) {
4337
- return new Promise((resolve, reject) => {
4338
- query.state ? query.active ? connection_default(options).cancel(query.state, resolve, reject) : query.cancelled = { resolve, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve());
4337
+ return new Promise((resolve2, reject) => {
4338
+ 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());
4339
4339
  });
4340
4340
  }
4341
4341
  async function end({ timeout = null } = {}) {
@@ -4354,11 +4354,11 @@ function Postgres(a, b2) {
4354
4354
  async function close() {
4355
4355
  await Promise.all(connections.map((c) => c.end()));
4356
4356
  }
4357
- async function destroy(resolve) {
4357
+ async function destroy(resolve2) {
4358
4358
  await Promise.all(connections.map((c) => c.terminate()));
4359
4359
  while (queries.length)
4360
4360
  queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
4361
- resolve();
4361
+ resolve2();
4362
4362
  }
4363
4363
  function connect(c, query) {
4364
4364
  move(c, connecting);
@@ -5757,6 +5757,38 @@ CREATE TABLE IF NOT EXISTS cost_snapshots (
5757
5757
  UNIQUE (project_id, cycle)
5758
5758
  );
5759
5759
 
5760
+ -- task-1896: per-project harness inventory (skills / sub-agents / hooks / MCP tools).
5761
+ -- Scanned from the local filesystem by the MCP server and persisted so the
5762
+ -- dashboard (no filesystem access) can surface what harness a project runs.
5763
+ CREATE TABLE IF NOT EXISTS project_harness_inventory (
5764
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5765
+ project_id UUID NOT NULL REFERENCES projects(id),
5766
+ user_id UUID,
5767
+ kind TEXT NOT NULL CHECK (kind IN ('skill', 'agent', 'hook', 'mcp_tool')),
5768
+ name TEXT NOT NULL,
5769
+ description TEXT,
5770
+ version TEXT,
5771
+ checksum TEXT,
5772
+ status TEXT DEFAULT 'ok'::text NOT NULL CHECK (status IN ('ok', 'stale_fork', 'missing')),
5773
+ path TEXT,
5774
+ synced_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5775
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5776
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5777
+ PRIMARY KEY (id),
5778
+ UNIQUE (project_id, kind, name)
5779
+ );
5780
+ CREATE INDEX IF NOT EXISTS idx_project_harness_inventory_project_kind
5781
+ ON project_harness_inventory (project_id, kind);
5782
+
5783
+ -- task-1896: cheap change-detection marker. The sync writer only does the full
5784
+ -- scan + DB write when this fingerprint changes, so DB writes stay rare.
5785
+ CREATE TABLE IF NOT EXISTS project_harness_state (
5786
+ project_id UUID NOT NULL REFERENCES projects(id),
5787
+ fingerprint TEXT,
5788
+ synced_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5789
+ PRIMARY KEY (project_id)
5790
+ );
5791
+
5760
5792
  CREATE TABLE IF NOT EXISTS cycle_metrics_snapshots (
5761
5793
  id UUID DEFAULT gen_random_uuid() NOT NULL,
5762
5794
  project_id UUID NOT NULL REFERENCES projects(id),
@@ -6846,6 +6878,63 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6846
6878
  createdAt: r.created_at
6847
6879
  }));
6848
6880
  }
6881
+ // --- Harness inventory (task-1896) ---
6882
+ async getHarnessInventory() {
6883
+ const rows = await this.sql`
6884
+ SELECT id, project_id, kind, name, description, version, checksum, status, path, synced_at
6885
+ FROM project_harness_inventory
6886
+ WHERE project_id = ${this.projectId}
6887
+ ORDER BY kind, name
6888
+ `;
6889
+ return rows.map((r) => ({
6890
+ id: r.id,
6891
+ projectId: r.project_id,
6892
+ kind: r.kind,
6893
+ name: r.name,
6894
+ description: r.description ?? void 0,
6895
+ version: r.version ?? void 0,
6896
+ checksum: r.checksum ?? void 0,
6897
+ status: r.status,
6898
+ path: r.path ?? void 0,
6899
+ syncedAt: r.synced_at
6900
+ }));
6901
+ }
6902
+ async replaceHarnessInventory(entries) {
6903
+ const userId = await this.ownerUserId();
6904
+ await this.sql.begin(async (_tx) => {
6905
+ const sql = _tx;
6906
+ await sql`DELETE FROM project_harness_inventory WHERE project_id = ${this.projectId}`;
6907
+ if (entries.length === 0) return;
6908
+ const values2 = entries.map((e) => ({
6909
+ project_id: this.projectId,
6910
+ user_id: userId,
6911
+ kind: e.kind,
6912
+ name: e.name,
6913
+ description: e.description ?? null,
6914
+ version: e.version ?? null,
6915
+ checksum: e.checksum ?? null,
6916
+ status: e.status,
6917
+ path: e.path ?? null
6918
+ }));
6919
+ await sql`INSERT INTO project_harness_inventory ${sql(values2)}`;
6920
+ });
6921
+ }
6922
+ async getHarnessState() {
6923
+ const rows = await this.sql`
6924
+ SELECT fingerprint, synced_at FROM project_harness_state
6925
+ WHERE project_id = ${this.projectId}
6926
+ LIMIT 1
6927
+ `;
6928
+ if (rows.length === 0 || rows[0].fingerprint === null) return null;
6929
+ return { fingerprint: rows[0].fingerprint, syncedAt: rows[0].synced_at };
6930
+ }
6931
+ async setHarnessState(fingerprint) {
6932
+ await this.sql`
6933
+ INSERT INTO project_harness_state (project_id, fingerprint, synced_at)
6934
+ VALUES (${this.projectId}, ${fingerprint}, now())
6935
+ ON CONFLICT (project_id) DO UPDATE SET fingerprint = ${fingerprint}, synced_at = now()
6936
+ `;
6937
+ }
6849
6938
  async getUnactionedDogfoodEntries(limit = 20) {
6850
6939
  const rows = await this.sql`
6851
6940
  SELECT id, project_id, cycle_number, category, content, source_tool, source_ref, created_at
@@ -9068,6 +9157,19 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
9068
9157
  updateDogfoodEntryStatus(id, status, linkedTaskId) {
9069
9158
  return this.invoke("updateDogfoodEntryStatus", [id, status, linkedTaskId]);
9070
9159
  }
9160
+ // --- Harness inventory (task-1896) ---
9161
+ getHarnessInventory() {
9162
+ return this.invoke("getHarnessInventory");
9163
+ }
9164
+ replaceHarnessInventory(entries) {
9165
+ return this.invoke("replaceHarnessInventory", [entries]);
9166
+ }
9167
+ getHarnessState() {
9168
+ return this.invoke("getHarnessState");
9169
+ }
9170
+ setHarnessState(fingerprint) {
9171
+ return this.invoke("setHarnessState", [fingerprint]);
9172
+ }
9071
9173
  // --- North Star ---
9072
9174
  getCurrentNorthStar() {
9073
9175
  return this.invoke("getCurrentNorthStar");
@@ -9227,6 +9329,7 @@ __export(git_exports, {
9227
9329
  detectBoardMismatches: () => detectBoardMismatches,
9228
9330
  detectUnrecordedCommits: () => detectUnrecordedCommits,
9229
9331
  ensureLatestDevelop: () => ensureLatestDevelop,
9332
+ getBranchDiff: () => getBranchDiff,
9230
9333
  getCommitsSinceTag: () => getCommitsSinceTag,
9231
9334
  getCurrentBranch: () => getCurrentBranch,
9232
9335
  getDocPathsTouchedOnBranch: () => getDocPathsTouchedOnBranch,
@@ -9370,6 +9473,26 @@ function getModifiedFiles(cwd) {
9370
9473
  return [];
9371
9474
  }
9372
9475
  }
9476
+ function getBranchDiff(cwd, base = "origin/main", maxBytes = 2e5) {
9477
+ const refs = [`${base}...HEAD`, "main...HEAD"];
9478
+ for (const ref of refs) {
9479
+ try {
9480
+ const out = execFileSync("git", ["diff", ref], {
9481
+ cwd,
9482
+ encoding: "utf-8",
9483
+ maxBuffer: 32 * 1024 * 1024
9484
+ });
9485
+ if (out) {
9486
+ return out.length > maxBytes ? `${out.slice(0, maxBytes)}
9487
+
9488
+ ... [diff truncated at ${Math.round(maxBytes / 1024)} KB]` : out;
9489
+ }
9490
+ return "";
9491
+ } catch {
9492
+ }
9493
+ }
9494
+ return "";
9495
+ }
9373
9496
  function getHeadCommitSubject(cwd) {
9374
9497
  try {
9375
9498
  const out = execFileSync("git", ["log", "-1", "--format=%s"], {
@@ -10002,9 +10125,9 @@ __export(doctor_exports, {
10002
10125
  __testing: () => __testing,
10003
10126
  runDoctor: () => runDoctor
10004
10127
  });
10005
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
10128
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
10006
10129
  import { homedir as homedir4 } from "os";
10007
- import { join as join15 } from "path";
10130
+ import { join as join18 } from "path";
10008
10131
  function redact(name, value) {
10009
10132
  if (!value) return "(empty)";
10010
10133
  if (SECRET_VARS.has(name)) {
@@ -10015,14 +10138,14 @@ function redact(name, value) {
10015
10138
  }
10016
10139
  function findMcpJson() {
10017
10140
  const candidates = [
10018
- join15(process.cwd(), ".mcp.json"),
10019
- join15(homedir4(), ".claude", ".mcp.json"),
10020
- join15(homedir4(), ".mcp.json")
10141
+ join18(process.cwd(), ".mcp.json"),
10142
+ join18(homedir4(), ".claude", ".mcp.json"),
10143
+ join18(homedir4(), ".mcp.json")
10021
10144
  ];
10022
10145
  for (const path7 of candidates) {
10023
- if (!existsSync8(path7)) continue;
10146
+ if (!existsSync9(path7)) continue;
10024
10147
  try {
10025
- const raw = readFileSync8(path7, "utf-8");
10148
+ const raw = readFileSync10(path7, "utf-8");
10026
10149
  const parsed = JSON.parse(raw);
10027
10150
  const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
10028
10151
  if (!papiEntry) continue;
@@ -10180,17 +10303,17 @@ __export(reset_exports, {
10180
10303
  removePapiEntry: () => removePapiEntry,
10181
10304
  runReset: () => runReset
10182
10305
  });
10183
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
10306
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
10184
10307
  import { homedir as homedir5 } from "os";
10185
- import { join as join16 } from "path";
10308
+ import { join as join19 } from "path";
10186
10309
  import { createInterface } from "readline/promises";
10187
10310
  function findResetTarget() {
10188
10311
  for (const path7 of CANDIDATE_PATHS()) {
10189
- if (!existsSync9(path7)) continue;
10312
+ if (!existsSync10(path7)) continue;
10190
10313
  let raw;
10191
10314
  let parsed;
10192
10315
  try {
10193
- raw = readFileSync9(path7, "utf-8");
10316
+ raw = readFileSync11(path7, "utf-8");
10194
10317
  parsed = JSON.parse(raw);
10195
10318
  } catch {
10196
10319
  continue;
@@ -10278,17 +10401,17 @@ var init_reset = __esm({
10278
10401
  "src/cli/reset.ts"() {
10279
10402
  "use strict";
10280
10403
  CANDIDATE_PATHS = () => [
10281
- join16(process.cwd(), ".mcp.json"),
10282
- join16(homedir5(), ".claude", ".mcp.json"),
10283
- join16(homedir5(), ".mcp.json")
10404
+ join19(process.cwd(), ".mcp.json"),
10405
+ join19(homedir5(), ".claude", ".mcp.json"),
10406
+ join19(homedir5(), ".mcp.json")
10284
10407
  ];
10285
10408
  }
10286
10409
  });
10287
10410
 
10288
10411
  // src/index.ts
10289
- import { readFileSync as readFileSync10 } from "fs";
10290
- import { dirname as dirname3, join as join17 } from "path";
10291
- import { fileURLToPath as fileURLToPath2 } from "url";
10412
+ import { readFileSync as readFileSync12 } from "fs";
10413
+ import { dirname as dirname5, join as join20 } from "path";
10414
+ import { fileURLToPath as fileURLToPath3 } from "url";
10292
10415
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10293
10416
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
10294
10417
  import {
@@ -10758,10 +10881,10 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
10758
10881
  }
10759
10882
 
10760
10883
  // src/server.ts
10761
- import { readFileSync as readFileSync7 } from "fs";
10762
- import { access as access4, readdir as readdir2, readFile as readFile7 } from "fs/promises";
10763
- import { join as join14, dirname as dirname2 } from "path";
10764
- import { fileURLToPath } from "url";
10884
+ import { readFileSync as readFileSync9 } from "fs";
10885
+ import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
10886
+ import { join as join17, dirname as dirname4 } from "path";
10887
+ import { fileURLToPath as fileURLToPath2 } from "url";
10765
10888
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10766
10889
  import {
10767
10890
  CallToolRequestSchema,
@@ -15083,34 +15206,60 @@ function buildSubagentDispatchPrompt(input) {
15083
15206
  const {
15084
15207
  tool,
15085
15208
  applyMode,
15086
- cycleNumber,
15209
+ cycleNumber = 0,
15087
15210
  strategyReviewWarning = "",
15211
+ taskId,
15088
15212
  systemPrompt,
15089
15213
  userMessage,
15090
15214
  contextBytes
15091
15215
  } = input;
15092
15216
  const sizeNote = contextBytes !== void 0 ? ` (~${Math.round(contextBytes / 1024)} KB context)` : "";
15093
- const applyNote = tool === "plan" ? `\`plan\` again with mode="apply", llm_response=<sub-agent output>, plan_mode="${applyMode ?? "full"}", cycle_number=${cycleNumber + 1}, strategy_review_warning=${JSON.stringify(strategyReviewWarning)}` : `\`strategy_review\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber}`;
15094
- const subagentInstructions = `
15095
- You are a one-shot sub-agent dispatched by Claude Code to execute a PAPI ${tool} call.
15096
-
15097
- CONTRACT:
15098
- - Read the system prompt and context exactly as the main agent would.
15217
+ const isReview = tool === "review_submit";
15218
+ let applyNote;
15219
+ if (tool === "plan") {
15220
+ applyNote = `\`plan\` again with mode="apply", llm_response=<sub-agent output>, plan_mode="${applyMode ?? "full"}", cycle_number=${cycleNumber + 1}, strategy_review_warning=${JSON.stringify(strategyReviewWarning)}`;
15221
+ } else if (tool === "strategy_review") {
15222
+ applyNote = `\`strategy_review\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber}`;
15223
+ } else {
15224
+ applyNote = `\`review_submit\` with task_id="${taskId ?? "<task-id>"}", stage="build-acceptance", verdict=<your decision: accept / request-changes / reject>, comments=<your reasoning>, and auto_review set to the sub-agent's JSON object verbatim. The sub-agent's verdict is a RECOMMENDATION \u2014 you make the final call`;
15225
+ }
15226
+ const contractBody = isReview ? `- Review the completed build below. The build report says what was intended; the diff is what actually changed. Check correctness, scope adherence (did it match the handoff?), security, and quality.
15227
+ - Return ONLY a single JSON object \u2014 no preamble, no commentary, no fences \u2014 in exactly this shape:
15228
+ {"verdict":"pass|warn|fail","summary":"<one-line overall assessment>","findings":[{"severity":"error|warning|info","file":"<path>","line":<number>,"message":"<specific issue>"}]}
15229
+ - "pass" = no blocking issues; "warn" = minor/non-blocking nits; "fail" = blocking issues that should send the build back. \`file\`/\`line\` are optional per finding. Empty \`findings\` is valid when clean.` : `- Read the system prompt and context exactly as the main agent would.
15099
15230
  - Produce the full structured output the system prompt requires (Part 1 markdown + Part 2 JSON after \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, or whatever the system prompt specifies).
15100
- - Return ONLY that output \u2014 no preamble, no commentary, no closing summary. The dispatching agent will pass your reply verbatim to the apply call.
15231
+ - Return ONLY that output \u2014 no preamble, no commentary, no closing summary. The dispatching agent will pass your reply verbatim to the apply call.`;
15232
+ const contextBlock = isReview ? `<review_rubric>
15233
+ ${systemPrompt}
15234
+ </review_rubric>
15101
15235
 
15102
- <system_prompt>
15236
+ <build_under_review>
15237
+ ${userMessage}
15238
+ </build_under_review>` : `<system_prompt>
15103
15239
  ${systemPrompt}
15104
15240
  </system_prompt>
15105
15241
 
15106
15242
  <context>
15107
15243
  ${userMessage}
15108
- </context>
15244
+ </context>`;
15245
+ const purpose = isReview ? "perform a PAPI build-acceptance code review" : `execute a PAPI ${tool} call`;
15246
+ const subagentInstructions = `
15247
+ You are a one-shot sub-agent dispatched by Claude Code to ${purpose}.
15248
+
15249
+ CONTRACT:
15250
+ ${contractBody}
15251
+
15252
+ ${contextBlock}
15109
15253
  `.trim();
15254
+ const headerLabel = isReview ? `## PAPI review_submit \u2014 Sub-Agent Dispatch (${taskId ?? "build review"}${sizeNote})` : `## PAPI ${tool} \u2014 Sub-Agent Dispatch (Cycle ${cycleNumber + (tool === "plan" ? 1 : 0)}${sizeNote})`;
15255
+ const intro = isReview ? "This build review carries a large diff/report \u2014 enough to crowd the main session context. Dispatch the analysis via `Task()` to a fresh sub-agent, then act on its structured findings." : `The reasoning for this ${tool} call is heavy enough to crowd the main session context. Dispatch it via \`Task()\` to a fresh sub-agent and pass its reply through to the apply call.`;
15256
+ const step2Note = isReview ? `When the sub-agent returns its JSON, surface the findings, decide your verdict, then call ${applyNote}.` : `When the sub-agent returns, call ${applyNote}.
15257
+
15258
+ Do NOT post-process the sub-agent reply. The apply path expects the full structured output exactly as the sub-agent emits it.`;
15110
15259
  return [
15111
- `## PAPI ${tool} \u2014 Sub-Agent Dispatch (Cycle ${cycleNumber + (tool === "plan" ? 1 : 0)}${sizeNote})`,
15260
+ headerLabel,
15112
15261
  "",
15113
- `The reasoning for this ${tool} call is heavy enough to crowd the main session context. Dispatch it via \`Task()\` to a fresh sub-agent and pass its reply through to the apply call.`,
15262
+ intro,
15114
15263
  "",
15115
15264
  "---",
15116
15265
  "",
@@ -15118,18 +15267,16 @@ ${userMessage}
15118
15267
  "",
15119
15268
  "Use the `Task` tool with:",
15120
15269
  `- \`subagent_type\`: \`general-purpose\``,
15121
- `- \`description\`: \`Run PAPI ${tool}\``,
15270
+ `- \`description\`: \`${isReview ? "Review PAPI build" : `Run PAPI ${tool}`}\``,
15122
15271
  `- \`prompt\`: the full prompt block below (everything between the BEGIN/END markers, trimmed of the markers themselves).`,
15123
15272
  "",
15124
15273
  "<<<BEGIN_SUBAGENT_PROMPT>>>",
15125
15274
  subagentInstructions,
15126
15275
  "<<<END_SUBAGENT_PROMPT>>>",
15127
15276
  "",
15128
- "### Step 2 \u2014 Pass the sub-agent reply to apply",
15129
- "",
15130
- `When the sub-agent returns, call ${applyNote}.`,
15277
+ `### Step 2 \u2014 ${isReview ? "Act on the review" : "Pass the sub-agent reply to apply"}`,
15131
15278
  "",
15132
- "Do NOT post-process the sub-agent reply. The apply path expects the full structured output exactly as the sub-agent emits it."
15279
+ step2Note
15133
15280
  ].join("\n");
15134
15281
  }
15135
15282
 
@@ -15975,8 +16122,8 @@ ${lines.join("\n")}`;
15975
16122
  const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
15976
16123
  const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
15977
16124
  const fullPath = join2(plansDir, f);
15978
- const stat3 = statSync(fullPath);
15979
- return { name: f, modified: stat3.mtime, size: stat3.size };
16125
+ const stat4 = statSync(fullPath);
16126
+ return { name: f, modified: stat4.mtime, size: stat4.size };
15980
16127
  }).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
15981
16128
  if (planFiles.length > 0) {
15982
16129
  const lines = planFiles.map((f) => {
@@ -17933,7 +18080,7 @@ ${existing}` : entry;
17933
18080
  // src/services/setup.ts
17934
18081
  init_dist2();
17935
18082
  import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2 } from "fs/promises";
17936
- import { join as join4, basename, extname } from "path";
18083
+ import { join as join5, basename, extname, dirname as dirname2 } from "path";
17937
18084
  import { execFileSync as execFileSync3 } from "child_process";
17938
18085
 
17939
18086
  // src/lib/detect-codebase.ts
@@ -17974,6 +18121,50 @@ function detectCodebaseType(projectRoot) {
17974
18121
  return "new_project";
17975
18122
  }
17976
18123
 
18124
+ // src/lib/agents-bundle.ts
18125
+ import { readFileSync, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
18126
+ import { dirname, join as join4, resolve } from "path";
18127
+ import { fileURLToPath } from "url";
18128
+ var PROJECT_BUNDLE_REL = join4(".agents", "skills", "papi-cycle");
18129
+ function bundleDestRel(rel) {
18130
+ return rel === "AGENTS.md" ? "AGENTS.md" : join4(PROJECT_BUNDLE_REL, rel);
18131
+ }
18132
+ function resolveBundleDir() {
18133
+ let dir = dirname(fileURLToPath(import.meta.url));
18134
+ for (let i = 0; i < 5; i++) {
18135
+ const candidate = join4(dir, "skills", "papi-cycle");
18136
+ if (existsSync3(join4(candidate, "AGENTS.md"))) return candidate;
18137
+ const parent = resolve(dir, "..");
18138
+ if (parent === dir) break;
18139
+ dir = parent;
18140
+ }
18141
+ return void 0;
18142
+ }
18143
+ function readBundleFiles(bundleDir = resolveBundleDir()) {
18144
+ if (!bundleDir || !existsSync3(bundleDir)) return [];
18145
+ const files = [];
18146
+ const walk = (abs, rel) => {
18147
+ for (const entry of readdirSync3(abs, { withFileTypes: true })) {
18148
+ const childAbs = join4(abs, entry.name);
18149
+ const childRel = rel ? join4(rel, entry.name) : entry.name;
18150
+ if (entry.isDirectory()) walk(childAbs, childRel);
18151
+ else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync(childAbs, "utf8") });
18152
+ }
18153
+ };
18154
+ walk(bundleDir, "");
18155
+ return files;
18156
+ }
18157
+ function planBundleInstall(projectRoot, projectName, opts = {}) {
18158
+ const out = {};
18159
+ for (const f of readBundleFiles()) {
18160
+ const dest = join4(projectRoot, bundleDestRel(f.rel));
18161
+ if (opts.skipExisting && existsSync3(dest) && statSync3(dest).isFile()) continue;
18162
+ const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
18163
+ out[dest] = content;
18164
+ }
18165
+ return out;
18166
+ }
18167
+
17977
18168
  // src/templates.ts
17978
18169
  var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
17979
18170
 
@@ -18100,283 +18291,17 @@ var CYCLES_TEMPLATE = `# Cycles
18100
18291
  cycles: []
18101
18292
  <!-- PAPI-YAML-END -->
18102
18293
  `;
18103
- var CLAUDE_MD_TEMPLATE = `# {{project_name}}
18104
-
18105
- ## Project Identity \u2014 Verify Before Editing
18106
-
18107
- On the first \`orient\` of any session, surface the connected project name to the user (e.g. "Connected to: <project_name>") and confirm it matches what they expect before making any code changes. PAPI projects are scoped by ID; if the wrong PAPI_PROJECT_ID is configured, edits land in the wrong project's history \u2014 a hard-to-undo class of mistake.
18108
-
18109
- If the user doesn't recognise the project, stop. To fix it from this chat \u2014 no file editing \u2014 use the project tools: \`project_list\` to see their projects, \`project_create\` to make an empty one for this folder, or \`project_switch\` to point at the right one. Or pass \`project=<id>\` on a single call to override for that call only.
18110
-
18111
- ## Documentation Maintenance
18112
-
18113
- Before creating a new doc, check \`docs/INDEX.md\` \u2014 it may already exist. When creating or archiving docs, update the index.
18114
-
18115
- After implementing any code change, check if the change affects any documentation in \`docs/\`. If a doc describes behaviour, architecture, or file interactions that your change modified, update the doc to stay accurate.
18116
-
18117
- When updating a doc, add or update a review header immediately below the title:
18118
-
18119
- \`\`\`
18120
- # Document Title
18121
- > Last reviewed: task-NNN \u2014 DD-MM-YYYY
18122
- \`\`\`
18123
-
18124
- Replace \`task-NNN\` with the task ID that triggered the update, and \`DD-MM-YYYY\` with today's date.
18125
-
18126
- ## Session Start
18127
-
18128
- When a conversation starts \u2014 fresh window, new session, or after context compression \u2014 orient before doing anything else:
18129
-
18130
- 1. **Run \`orient\`** \u2014 single call that returns cycle number, task counts, in-progress/in-review tasks, strategy review cadence, trends, and recommended next action.
18131
- 2. **Fix orphaned tasks silently** \u2014 check for feat/task-XXX branches that don't match board status. Fix and report after.
18132
- 3. **Summarise:** "You're on Cycle N. X tasks to build, Y builds pending review." or "Cycle N is complete \u2014 ready for the next plan."
18133
- 4. **Run \`build_list\` when picking a task** \u2014 \`orient\` shows counts only. \`build_list\` shows the full task list with handoffs.
18134
-
18135
- **CRITICAL: Check task statuses before acting.**
18136
- - **In Review** = already built. Suggest \`review_list\` \u2192 \`review_submit\`. **NEVER re-build an In Review task.**
18137
- - **In Progress** = build started but not completed. Check the branch and existing changes before writing new code.
18138
- - **Backlog** = not started. But first check if a \`feat/task-XXX\` branch already exists with commits \u2014 fix it, don't rebuild.
18139
- - If all cycle tasks are Done, suggest \`release\` or next \`plan\`.
18140
-
18141
- ## Workflow Sequences
18142
-
18143
- PAPI tools follow structured flows. The agent manages the cycle workflow automatically \u2014 the user should never need to type tool names or remember the flow. Handle the plumbing, surface the summaries.
18144
-
18145
- ### Cycle Workflow (auto-managed)
18146
-
18147
- - **Run tools automatically** \u2014 don't ask the user to invoke MCP tools manually
18148
- - Before implementing: silently run \`build_execute <task_id>\` (start phase)
18149
- - After implementing: run \`build_execute <task_id>\` (complete phase) with report fields
18150
- - After build_execute completes: audit the branch changes for bugs, convention violations, and doc drift (see Post-Build Audit below)
18151
- - After audit with findings: *MUST* automatically run \`review_submit\` with verdict \`request-changes\` and a concise summary of the audit findings as the changes requested \u2014 the builder fixes these before the task goes to human review
18152
- - After audit clean: present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
18153
- - User approves/requests changes \u2192 run \`review_submit\` behind the scenes
18154
-
18155
- ### The Cycle (main flow)
18156
-
18157
- \`\`\`
18158
- plan \u2192 build_list \u2192 build_execute \u2192 audit \u2192 review_list \u2192 review_submit \u2192 build_list
18159
- \`\`\`
18160
-
18161
- 1. **plan** \u2014 Run at the start of each cycle to generate the cycle plan and populate the board.
18162
- Next: \`build_list\` to see prioritised tasks.
18163
- 2. **build_list** \u2014 View tasks ready for execution, ordered by priority.
18164
- Next: \`build_execute <task_id>\` to start a task.
18165
- 3. **build_execute** (start) \u2014 Creates a feature branch and marks the task In Progress. Returns the build handoff.
18166
- Next: Implement the task, then \`build_execute <task_id>\` again with report fields to complete.
18167
- 4. **build_execute** (complete) \u2014 Submits the build report, commits, and marks the task In Review.
18168
- Next: Run the post-build audit automatically.
18169
- 5. **Post-build audit** \u2014 Review branch changes for bugs, convention violations, and doc drift (see Post-Build Audit section below).
18170
- Next: If findings exist, run \`review_submit\` with \`request-changes\` and the audit findings. If clean, proceed to \`review_list\`.
18171
- 6. **review_list** \u2014 Shows tasks pending human review (handoff-review or build-acceptance).
18172
- Next: \`review_submit\` to approve, accept, or request changes.
18173
- 7. **review_submit** \u2014 Records the review verdict and updates task status.
18174
- Next: \`build_list\` to view next build
18175
-
18176
- **DO NOT** use \`review_submit\` as a substitute for \`review_list\`. If you need to see what is pending review, always call \`review_list\` first. If \`review_list\` is unavailable in your tool set (e.g. your MCP client filters parameterless tools), STOP and tell the human their MCP integration is incomplete \u2014 never guess at the next pending task. To submit an accept verdict on a build-acceptance review, either pass \`reviewer_confirmed: true\` or ensure \`review_list\` has run in the same session within the last 15 minutes. (SUP-2026-010.)
18177
-
18178
- ### Strategy Review
18179
-
18180
- \`\`\`
18181
- strategy_review \u2192 strategy_change
18182
- \`\`\`
18183
-
18184
- - **strategy_review** \u2014 Analyses project health, velocity, and estimation accuracy.
18185
- Next: \`strategy_change\` if the review recommends adjustments.
18186
- - **strategy_change** \u2014 Updates active decisions, north star, or project direction based on review findings.
18187
-
18188
- ### Detect Strategic Decisions in Conversation
18189
-
18190
- Watch for: direction changes, architecture shifts, deprioritisation with reasoning, new principles, competitive positioning decisions.
18191
-
18192
- When detected:
18193
- 1. Flag it: "That sounds like a strategic direction change \u2014 should I run \`strategy_change\`?"
18194
- 2. If confirmed, run \`strategy_change\` immediately.
18195
- 3. If mid-build, finish the current task first.
18196
-
18197
- ### Idea Capture
18198
-
18199
- \`\`\`
18200
- idea \u2192 (picked up by next plan)
18201
- \`\`\`
18202
-
18203
- - **idea** \u2014 Captures a new task idea and writes it to the backlog.
18204
- Next: The next \`plan\` run will prioritise and schedule it.
18205
-
18206
- ### Project Bootstrap
18207
-
18208
- \`\`\`
18209
- setup \u2192 plan
18210
- \`\`\`
18211
-
18212
- - **setup** \u2014 {{setup_description}}
18213
- Next: \`plan\` to run the first cycle planning session.
18214
-
18215
- ### Board Management
18216
-
18217
- - **board_view** \u2014 Read-only view of all tasks on the board.
18218
- - **board_archive** \u2014 Removes completed/cancelled tasks from the board to an archive.
18219
- - **board_deprioritise** \u2014 Moves a task to a later phase.
18220
-
18221
- ### Quick Reference: Tool \u2192 Next Step
18222
-
18223
- | Tool | Next Step |
18224
- |------|-----------|
18225
- | \`setup\` | \`plan\` |
18226
- | \`plan\` | \`build_list\` |
18227
- | \`build_list\` | \`build_execute <task_id>\` |
18228
- | \`build_execute\` (start) | Implement, then \`build_execute\` (complete) |
18229
- | \`build_execute\` (complete) | Post-build audit (automatic) |
18230
- | Audit (findings) | \`review_submit\` with \`request-changes\` |
18231
- | Audit (clean) | \`review_list\` |
18232
- | \`review_list\` | \`review_submit\` |
18233
- | \`review_submit\` (approve/accept) | \`build_list\` |
18234
- | \`review_submit\` (request-changes) | \`build_execute\` (redo) or \`build_list\` |
18235
- | \`strategy_review\` | \`strategy_change\` (if needed) |
18236
- | \`idea\` | Next \`plan\` picks it up |
18237
-
18238
- ## Post-Build Audit
18239
-
18240
- After every \`build_execute\` (complete), audit the branch before presenting for human review. This catches bugs and convention violations early.
18241
-
18242
- 1. **Identify changed files:** Run \`git diff origin/main --name-only\` to find modified files. If no changes, report "No changes to audit" and skip.
18243
- 2. **Review each changed file** for:
18244
- - Logic errors, off-by-one mistakes, incorrect conditions
18245
- - Unhandled edge cases (null, undefined, empty inputs)
18246
- - Convention violations defined in this CLAUDE.md
18247
- - Incorrect type narrowing or unsafe casts
18248
- 3. **Documentation check:** If any \`docs/\` files describe behaviour that the change modified, flag as "Doc drift".
18249
- 4. **Report:** For each issue: file path, severity (Bug/Convention/Doc drift), what's wrong, how to fix.
18250
- 5. **If findings exist:** Run \`review_submit\` with \`request-changes\` and the findings. Fix before human review.
18251
- 6. **If clean:** Present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
18252
-
18253
- ## When to Start a New Conversation
18254
-
18255
- Start a fresh window when:
18256
- - **After a release** \u2014 cycle is done, context is heavy. New window orients in seconds via \`orient\`.
18257
- - **After 3+ tasks built** \u2014 accumulated file reads, diffs, and discussions bloat context. Quality degrades.
18258
- - **Switching modes** \u2014 going from building to planning, or from strategy review to building. Each mode benefits from clean context.
18259
- - **After context compression fires** \u2014 if you notice earlier messages are missing, the window is getting stale. Open fresh.
18260
-
18261
- Stay in the same window when:
18262
- - Building sequential tasks in a batch (especially XS/S tasks)
18263
- - Mid-task and not yet complete
18264
- - Having a strategic discussion that informs the next action
18265
-
18266
- **Rule of thumb:** If you've been in the same window for 30+ minutes or 3+ tasks, it's time for a fresh one.
18267
-
18268
- ## Housekeeping \u2014 Opt-In Deep Sweep
18269
-
18270
- \`orient\` runs a fast cheap-checks-only path by default. The deep sweep \u2014 orphaned branches, In Review tasks with no PR, stale In Progress branches, unrecorded commits, unregistered docs \u2014 is opt-in via \`deep_housekeeping: true\`.
18271
-
18272
- When to run with \`deep_housekeeping: true\`:
18273
- 1. Before \`release\` \u2014 catch board/branch drift.
18274
- 2. After a long break (>1 day since last session) \u2014 surface anything that fell off.
18275
- 3. When you suspect drift \u2014 odd cycle counts, missing PRs.
18276
-
18277
- **Don't run deep on every session start.** It pollutes early context with cross-reference output that's noise 80% of the time. The default fast path tells you what cycle you're on, what's in flight, and what to do next; that's the daily-driver shape.
18278
-
18279
- If the deep sweep surfaces something fixable (orphaned branches, missing PRs), fix it silently and report after \u2014 same autonomous-plumbing rule as before.
18280
-
18281
- ## Plumbing Is Autonomous
18282
-
18283
- Board status updates, branch cleanup, orphaned task fixes, commit/PR/merge for housekeeping \u2014 these are mechanical plumbing. **Do them end-to-end without stopping to ask.** Report after the fact.
18284
-
18285
- ## Context Compression Recovery
18286
-
18287
- When the system compresses prior messages, immediately:
18288
- 1. **Run \`orient\`** \u2014 single call for cycle state
18289
- 2. Check your todo list for in-progress work
18290
- 3. Run housekeeping checks
18291
- 4. **NEVER re-build a task that is already In Review or Done.**
18292
- 5. Continue where you left off \u2014 don't restart or re-plan
18293
-
18294
- ## Branching & PR Convention
18295
-
18296
- - **All in-cycle, in-module tasks share \`feat/cycle-N-<module>\`** regardless of complexity. One branch per module per cycle, merged together. Module-less tasks fall back to a per-task branch.
18297
- - **Dependent tasks (any size):** When a task's BUILD HANDOFF lists a \`DEPENDS ON\` task from the same cycle, \`build_execute\` automatically reuses the upstream task's branch so commits stack for a single PR. Do not create a separate branch manually.
18298
- - **Commit per task within grouped branches** \u2014 traceable git history.
18299
- - **Never use \`build_execute\` with \`light=true\` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed \u2014 any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
18300
-
18301
- ## Quick Work vs PAPI Work
18302
-
18303
- PAPI is for planned work. Quick fixes \u2014 just do them. No need for plan or build_execute.
18304
-
18305
- **After completing quick/ad-hoc work** (bug fixes, config changes, small improvements done outside the cycle), call \`ad_hoc\` to record it. This creates a Done task + build report so the work appears in cycle history and metrics. Don't skip this \u2014 unrecorded work is invisible work.
18306
-
18307
- ## Data Integrity
18308
-
18309
- - **Use MCP tools for all project data operations.** DB is the source of truth when using the pg adapter.
18310
- - Do NOT read \`.papi/\` files for context \u2014 use MCP tools.
18311
- - \`.papi/\` files may be stale when using pg adapter. This is expected.
18312
- - **\`board_edit\` handles cycle membership automatically.** Pass \`cycle: <n>\` to assign, \`cycle: null\` to clear (also flips status to Backlog), or \`status: "In Cycle"\` to auto-assign the active cycle. No manual SQL needed.
18313
-
18314
- ## Code Before Claims \u2014 No Assumptions
18315
-
18316
- **Before making any claim about how the codebase works, read the relevant file first.**
18317
-
18318
- This includes:
18319
- - How a feature is implemented ("it works like X") \u2192 read the source
18320
- - Whether something exists ("there's no baseline migration") \u2192 check the directory
18321
- - Whether a flow is broken or working \u2192 trace it in code
18322
- - What a user would experience \u2192 check the actual page/component
18323
-
18324
- Do NOT rely on memory, prior conversation, or inference. Read first, then answer.
18325
- If the answer requires checking 2-3 files, check them all before responding.
18326
-
18327
- ## Tool Use Discipline
18328
-
18329
- Most tool errors are habit issues, not capability issues. Avoid them up front:
18330
-
18331
- - **Ranged reads for large files.** Before \`Read\`-ing a file you haven't already touched this session, check its size. For files over ~1000 lines, or known-large surfaces (generated SQL, lockfiles, HTML reports, large templates), use \`offset\` + \`limit\` from the start instead of hitting the token ceiling.
18332
- - **Search-before-read for unverified paths.** If a path comes from memory or inference rather than a file you've read this session, run \`Glob\` or list the parent directory first. Don't \`Read\` paths you haven't confirmed exist.
18333
- - **Prefer Read/Glob/Grep over Bash for file operations.** Bash \`cat\`/\`grep\`/\`find\`/\`ls\` is the most common source of failed commands and produces unstructured output. Reserve Bash for genuinely shell-only operations (git, gh, package managers, SQL, real pipelines).
18334
- - **Verify-before-recommend.** Before suggesting a new task, hook, or skill, check whether it already exists: \`board_view\` for tasks, \`doc_search\` for docs, \`ls .claude/hooks/\` for hooks. Recommending duplicates of already-shipped work wastes a build slot.
18335
-
18336
- ## User-Facing Replies \u2014 Default Brief
18337
-
18338
- When drafting any external user-facing copy (Discord post, support reply, email, release note, marketing blurb): default to \u22644 sentences, friendly tone, no hidden questions buried in prose. Offer to expand if the user wants more depth. Verbose drafts that need trimming are a recurring friction.
18339
-
18340
- This applies to *output for users*, not internal commit messages, build reports, or technical explanations \u2014 those follow normal conventions.
18341
-
18342
- ## Process Rules
18343
-
18344
- These rules come from 80+ cycles of dogfooding. They prevent the most common sources of wasted time and rework.
18345
-
18346
- ### Building
18347
- - **Verify before claiming done.** Hit the endpoint, check the rendered output, confirm the data round-trips. Never say "should work" \u2014 prove it works.
18348
- - **Preview frontend changes.** After any UI/styling build, provide the localhost URL so the user can visually review. Don't make them ask for it.
18349
- - **Debug one change at a time.** When fixing issues, make one change, verify it, then move on. Don't stack multiple untested fixes.
18350
- - **Test the write-read roundtrip.** Every data write path must have a verified read path. If you write to DB, confirm the read query returns what was written. This is the #1 source of silent failures.
18351
- - **Test after every build.** Run the project's test suite after implementing. Suggest follow-up tasks from learnings when meaningful.
18352
- - **Build patiently.** Validate each phase against the last. Don't rush through implementation \u2014 test through the UI, not just the API.
18353
-
18354
- ### Security
18355
- - **Audit before widening access.** Before any build that adds endpoints, modifies auth/RLS, introduces new user types, or changes access controls \u2014 review the security implications first. Fix findings before shipping.
18356
- - **Flag access-widening changes.** If a build touches auth, RLS policies, API keys, or user-facing access, note "Security surface reviewed" in the build report's \`discovered_issues\` or \`architecture_notes\`.
18357
- - **Never ship secrets.** Do not commit .env files, API keys, or credentials. Check \`.gitignore\` covers sensitive files before pushing.
18358
- - **Telemetry opt-out.** PAPI collects anonymous usage data (tool name, duration, project ID). To disable, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
18294
+ var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
18295
+ var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
18296
+ var CLAUDE_MD_STUB = `# {{project_name}}
18359
18297
 
18360
- ### Planning & Scope
18361
- - **NEVER run \`plan\` more than once per cycle.** Adjust the cycle with \`board_deprioritise\` or \`idea\` instead.
18362
- - **NEVER skip cycles.** Complete and release the current cycle before running the next \`plan\`.
18363
- - **Large plan/handoff outputs:** If the prepare-phase output is too large to pass inline (>50 KB), write it to a file and pass the absolute path via \`llm_response_file\` instead of \`llm_response\`. The \`plan\`, \`strategy_review\`, and \`handoff_generate\` apply modes all accept \`llm_response_file\`. The two parameters are mutually exclusive.
18364
- - **Only build tasks assigned to the current cycle.** Use \`build_list\` \u2014 it filters to current-cycle tasks with handoffs.
18365
- - **Don't ask premature questions.** If the project is in early cycles, don't ask about deployment accounts, hosting providers, OAuth setup, or commercial features. Focus on building core functionality first.
18366
- - **Split large ideas.** If an idea has 3+ concerns, submit it as 2-3 separate ideas so the planner creates properly scoped tasks \u2014 not kitchen-sink handoffs.
18367
- - **Auto-release completed cycles.** When all cycle tasks are Done and reviews accepted, run \`release\` immediately. Forgetting causes cycle number drift and merge conflicts in the next session.
18368
- - **Verify cycle readiness before releasing.** Before calling \`release\`, run \`board_view\` to confirm every task in the current cycle has status Done or Cancelled. The review queue is NOT sufficient evidence \u2014 \`review_list\` only shows built-and-pending-review tasks; it does not show Backlog or In Progress tasks. If any task is Backlog or In Progress: (a) build it, (b) move it to the next cycle via \`board_edit({ task_id, cycle: N+1 })\`, or (c) cancel it via \`board_edit\`. Do not call \`release\` until the cycle has no pending work. The \`release\` tool enforces this server-side and will block with a task list if the check fails.
18298
+ This project is managed with **PAPI**. The agent harness \u2014 session workflow, the plan \u2192 build \u2192 review cycle, branching, and conventions \u2014 lives in **\`AGENTS.md\`** (always loaded) plus lazy-loaded phase skills under \`.agents/skills/papi-cycle/\`.
18369
18299
 
18370
- ### Communication
18371
- - **Show task names, not just IDs.** When summarising board state or reconciliation, include task names \u2014 e.g. "task-42: Add supplier form" not just "task-42".
18372
- - **Surface the next command.** After each step, tell the user what comes next. Commands should be surfaced, not memorised.
18300
+ **At session start: read \`AGENTS.md\`, then run \`orient\`.** Phase mechanics (planning, building, strategy, ideas) load on demand from the skills bundle. Add project-specific notes below this line \u2014 they will not be overwritten.
18373
18301
 
18374
- ### Stage Readiness
18375
- - **Access-widening stages require auth/security phases.** Before declaring a stage complete, check if it widens who can access the product (e.g. Alpha Distribution, Alpha Cohort). If so, auth hardening and security review must be completed first \u2014 not discovered after the fact.
18376
- - **Pattern:** Audit access surface \u2192 fix vulnerabilities \u2192 then widen access. Never ship access-widening without a security phase.
18302
+ ${CLAUDE_MD_ENRICHMENT_SENTINEL_T1}
18303
+ ${CLAUDE_MD_ENRICHMENT_SENTINEL_T2}
18377
18304
  `;
18378
- var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
18379
- var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
18380
18305
  var CLAUDE_MD_TIER_1 = `
18381
18306
  ${CLAUDE_MD_ENRICHMENT_SENTINEL_T1}
18382
18307
 
@@ -18605,7 +18530,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18605
18530
  await mkdir(config2.papiDir, { recursive: true });
18606
18531
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
18607
18532
  const content = substitute(template, vars);
18608
- await writeFile2(join4(config2.papiDir, filename), content, "utf-8");
18533
+ await writeFile2(join5(config2.papiDir, filename), content, "utf-8");
18609
18534
  }
18610
18535
  }
18611
18536
  } else {
@@ -18620,18 +18545,18 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18620
18545
  } catch {
18621
18546
  }
18622
18547
  }
18623
- const commandsDir = join4(config2.projectRoot, ".claude", "commands");
18624
- const docsDir = join4(config2.projectRoot, "docs");
18548
+ const commandsDir = join5(config2.projectRoot, ".claude", "commands");
18549
+ const docsDir = join5(config2.projectRoot, "docs");
18625
18550
  await mkdir(commandsDir, { recursive: true });
18626
18551
  await mkdir(docsDir, { recursive: true });
18627
- const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
18552
+ const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
18628
18553
  let claudeMdExists = false;
18629
18554
  try {
18630
18555
  await access2(claudeMdPath);
18631
18556
  claudeMdExists = true;
18632
18557
  } catch {
18633
18558
  }
18634
- const docsIndexPath = join4(docsDir, "INDEX.md");
18559
+ const docsIndexPath = join5(docsDir, "INDEX.md");
18635
18560
  let docsIndexExists = false;
18636
18561
  try {
18637
18562
  await access2(docsIndexPath);
@@ -18639,26 +18564,30 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18639
18564
  } catch {
18640
18565
  }
18641
18566
  const scaffoldFiles = {
18642
- [join4(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
18643
- [join4(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
18644
- [join4(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
18567
+ [join5(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
18568
+ [join5(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
18569
+ [join5(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
18645
18570
  };
18646
18571
  if (!docsIndexExists) {
18647
18572
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
18648
18573
  }
18649
18574
  if (!claudeMdExists) {
18650
- scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_TEMPLATE, vars);
18575
+ scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_STUB, vars);
18651
18576
  } else {
18652
18577
  try {
18653
18578
  const existing = await readFile4(claudeMdPath, "utf-8");
18654
- if (!existing.includes("## Workflow Sequences") && !existing.includes("### The Cycle (main flow)")) {
18655
- const papiSection = "\n\n" + substitute(CLAUDE_MD_TEMPLATE, vars).split("\n").slice(1).join("\n");
18656
- scaffoldFiles[claudeMdPath] = existing + papiSection;
18579
+ if (!existing.includes("AGENTS.md")) {
18580
+ const pointer = "> This project uses PAPI. The agent harness lives in `AGENTS.md` + `.agents/skills/papi-cycle/`. Read `AGENTS.md` at session start, then run `orient`.\n\n";
18581
+ scaffoldFiles[claudeMdPath] = pointer + existing;
18657
18582
  }
18658
18583
  } catch {
18659
18584
  }
18660
18585
  }
18661
- const cursorDir = join4(config2.projectRoot, ".cursor");
18586
+ for (const [dest, content] of Object.entries(planBundleInstall(config2.projectRoot, input.projectName, { skipExisting: true }))) {
18587
+ await mkdir(dirname2(dest), { recursive: true });
18588
+ scaffoldFiles[dest] = content;
18589
+ }
18590
+ const cursorDir = join5(config2.projectRoot, ".cursor");
18662
18591
  let cursorDetected = false;
18663
18592
  try {
18664
18593
  await access2(cursorDir);
@@ -18666,8 +18595,8 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18666
18595
  } catch {
18667
18596
  }
18668
18597
  if (cursorDetected) {
18669
- const cursorRulesDir = join4(cursorDir, "rules");
18670
- const cursorRulesPath = join4(cursorRulesDir, "papi.mdc");
18598
+ const cursorRulesDir = join5(cursorDir, "rules");
18599
+ const cursorRulesPath = join5(cursorRulesDir, "papi.mdc");
18671
18600
  await mkdir(cursorRulesDir, { recursive: true });
18672
18601
  try {
18673
18602
  await access2(cursorRulesPath);
@@ -18693,7 +18622,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18693
18622
  }
18694
18623
  var PAPI_PERMISSION = "mcp__papi__*";
18695
18624
  async function ensurePapiPermission(projectRoot) {
18696
- const settingsPath = join4(projectRoot, ".claude", "settings.json");
18625
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
18697
18626
  try {
18698
18627
  let settings = {};
18699
18628
  try {
@@ -18712,7 +18641,7 @@ async function ensurePapiPermission(projectRoot) {
18712
18641
  if (!allow.includes(PAPI_PERMISSION)) {
18713
18642
  allow.push(PAPI_PERMISSION);
18714
18643
  }
18715
- await mkdir(join4(projectRoot, ".claude"), { recursive: true });
18644
+ await mkdir(join5(projectRoot, ".claude"), { recursive: true });
18716
18645
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
18717
18646
  } catch {
18718
18647
  }
@@ -18720,7 +18649,7 @@ async function ensurePapiPermission(projectRoot) {
18720
18649
  async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
18721
18650
  const warnings = [];
18722
18651
  if (config2.adapterType !== "pg") {
18723
- await writeFile2(join4(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
18652
+ await writeFile2(join5(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
18724
18653
  }
18725
18654
  await adapter2.updateProductBrief(briefText);
18726
18655
  const briefPhases = parsePhases(briefText);
@@ -18785,7 +18714,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
18785
18714
  }
18786
18715
  if (conventionsText?.trim()) {
18787
18716
  try {
18788
- const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
18717
+ const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
18789
18718
  const existing = await readFile4(claudeMdPath, "utf-8");
18790
18719
  await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
18791
18720
  } catch {
@@ -18865,13 +18794,13 @@ async function scanCodebase(projectRoot) {
18865
18794
  }
18866
18795
  let packageJson;
18867
18796
  try {
18868
- const content = await readFile4(join4(projectRoot, "package.json"), "utf-8");
18797
+ const content = await readFile4(join5(projectRoot, "package.json"), "utf-8");
18869
18798
  packageJson = JSON.parse(content);
18870
18799
  } catch {
18871
18800
  }
18872
18801
  let readme;
18873
18802
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
18874
- const content = await safeReadFile(join4(projectRoot, name), 5e3);
18803
+ const content = await safeReadFile(join5(projectRoot, name), 5e3);
18875
18804
  if (content) {
18876
18805
  readme = content;
18877
18806
  break;
@@ -18881,7 +18810,7 @@ async function scanCodebase(projectRoot) {
18881
18810
  let totalFiles = topLevelFiles.length;
18882
18811
  for (const dir of topLevelDirs) {
18883
18812
  try {
18884
- const entries = await readdir(join4(projectRoot, dir), { withFileTypes: true });
18813
+ const entries = await readdir(join5(projectRoot, dir), { withFileTypes: true });
18885
18814
  const files = entries.filter((e) => e.isFile());
18886
18815
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
18887
18816
  totalFiles += files.length;
@@ -19145,7 +19074,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19145
19074
  }
19146
19075
  }
19147
19076
  try {
19148
- const claudeMdPath = join4(config2.projectRoot, "CLAUDE.md");
19077
+ const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
19149
19078
  const existing = await readFile4(claudeMdPath, "utf-8");
19150
19079
  if (!existing.includes("Dogfood Logging")) {
19151
19080
  const dogfoodSection = [
@@ -19190,7 +19119,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19190
19119
  const gitignoreNote = await ensureMcpJsonGitignored(config2.projectRoot);
19191
19120
  let cursorScaffolded = false;
19192
19121
  try {
19193
- await access2(join4(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
19122
+ await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
19194
19123
  cursorScaffolded = true;
19195
19124
  } catch {
19196
19125
  }
@@ -19208,11 +19137,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19208
19137
  }
19209
19138
  async function ensureMcpJsonGitignored(projectRoot) {
19210
19139
  try {
19211
- await access2(join4(projectRoot, ".git"));
19140
+ await access2(join5(projectRoot, ".git"));
19212
19141
  } catch {
19213
19142
  return void 0;
19214
19143
  }
19215
- const gitignorePath = join4(projectRoot, ".gitignore");
19144
+ const gitignorePath = join5(projectRoot, ".gitignore");
19216
19145
  let existing = "";
19217
19146
  try {
19218
19147
  existing = await readFile4(gitignorePath, "utf-8");
@@ -19551,8 +19480,8 @@ init_dist2();
19551
19480
  init_git();
19552
19481
  init_git();
19553
19482
  import { randomUUID as randomUUID9 } from "crypto";
19554
- import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
19555
- import { join as join5 } from "path";
19483
+ import { readdirSync as readdirSync4, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync, unlinkSync, mkdirSync } from "fs";
19484
+ import { join as join6 } from "path";
19556
19485
  var buildStartTimes = /* @__PURE__ */ new Map();
19557
19486
  var taskBranchMap = /* @__PURE__ */ new Map();
19558
19487
  var taskStartShaMap = /* @__PURE__ */ new Map();
@@ -19953,17 +19882,17 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
19953
19882
  return { task, branchLines, phaseChanges };
19954
19883
  }
19955
19884
  function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched) {
19956
- const papiDir = join5(projectRoot, ".papi");
19957
- if (!existsSync3(papiDir)) {
19885
+ const papiDir = join6(projectRoot, ".papi");
19886
+ if (!existsSync4(papiDir)) {
19958
19887
  mkdirSync(papiDir, { recursive: true });
19959
19888
  }
19960
- const scopePath = join5(papiDir, "active-task-scope.txt");
19889
+ const scopePath = join6(papiDir, "active-task-scope.txt");
19961
19890
  const lines = [taskId, ...filesLikelyTouched ?? []];
19962
19891
  writeFileSync(scopePath, lines.join("\n") + "\n", "utf-8");
19963
19892
  }
19964
19893
  function clearActiveTaskScope(projectRoot) {
19965
- const scopePath = join5(projectRoot, ".papi", "active-task-scope.txt");
19966
- if (existsSync3(scopePath)) {
19894
+ const scopePath = join6(projectRoot, ".papi", "active-task-scope.txt");
19895
+ if (existsSync4(scopePath)) {
19967
19896
  unlinkSync(scopePath);
19968
19897
  }
19969
19898
  }
@@ -19977,7 +19906,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
19977
19906
  else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
19978
19907
  else if (relativePath.startsWith("docs/audits/")) type = "audit";
19979
19908
  try {
19980
- const content = readFileSync(absolutePath, "utf-8").slice(0, 2e3);
19909
+ const content = readFileSync2(absolutePath, "utf-8").slice(0, 2e3);
19981
19910
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
19982
19911
  if (fmMatch) {
19983
19912
  const fm = fmMatch[1];
@@ -20283,14 +20212,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
20283
20212
  let docWarning;
20284
20213
  try {
20285
20214
  if (adapter2.searchDocs) {
20286
- const docsDir = join5(config2.projectRoot, "docs");
20287
- if (existsSync3(docsDir)) {
20215
+ const docsDir = join6(config2.projectRoot, "docs");
20216
+ if (existsSync4(docsDir)) {
20288
20217
  const scanDir = (dir, depth = 0) => {
20289
20218
  if (depth > 8) return [];
20290
- const entries = readdirSync3(dir, { withFileTypes: true });
20219
+ const entries = readdirSync4(dir, { withFileTypes: true });
20291
20220
  const files = [];
20292
20221
  for (const e of entries) {
20293
- const full = join5(dir, e.name);
20222
+ const full = join6(dir, e.name);
20294
20223
  if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
20295
20224
  else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
20296
20225
  }
@@ -20305,7 +20234,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
20305
20234
  const failed = [];
20306
20235
  for (const docPath of unregistered) {
20307
20236
  try {
20308
- const meta = extractDocMeta(join5(config2.projectRoot, docPath), docPath, cycleNumber);
20237
+ const meta = extractDocMeta(join6(config2.projectRoot, docPath), docPath, cycleNumber);
20309
20238
  await adapter2.registerDoc({
20310
20239
  title: meta.title,
20311
20240
  type: meta.type,
@@ -21938,12 +21867,12 @@ _To correct: board_edit ${result.task.id} with updated fields._`
21938
21867
  init_git();
21939
21868
 
21940
21869
  // src/services/reconcile.ts
21941
- import { readFileSync as readFileSync2 } from "fs";
21942
- import { join as join6 } from "path";
21870
+ import { readFileSync as readFileSync3 } from "fs";
21871
+ import { join as join7 } from "path";
21943
21872
  function loadDocsIndex(projectRoot) {
21944
21873
  try {
21945
- const indexPath = join6(projectRoot, "docs", "INDEX.md");
21946
- const raw = readFileSync2(indexPath, "utf8");
21874
+ const indexPath = join7(projectRoot, "docs", "INDEX.md");
21875
+ const raw = readFileSync3(indexPath, "utf8");
21947
21876
  const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
21948
21877
  if (rows.length === 0) return "";
21949
21878
  const entries = rows.map((row) => {
@@ -22519,16 +22448,16 @@ Produce your analysis and structured output above. Present Part 1 to the user an
22519
22448
 
22520
22449
  // src/services/release.ts
22521
22450
  import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
22522
- import { join as join8 } from "path";
22451
+ import { join as join9 } from "path";
22523
22452
  import { execFileSync as execFileSync4 } from "child_process";
22524
22453
 
22525
22454
  // src/lib/install-id.ts
22526
22455
  import { randomUUID as randomUUID13 } from "crypto";
22527
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, chmodSync } from "fs";
22456
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, chmodSync } from "fs";
22528
22457
  import { homedir as homedir2 } from "os";
22529
- import { join as join7 } from "path";
22530
- var PAPI_HOME_DIR = join7(homedir2(), ".papi");
22531
- var INSTALL_ID_FILE = join7(PAPI_HOME_DIR, "install-id.json");
22458
+ import { join as join8 } from "path";
22459
+ var PAPI_HOME_DIR = join8(homedir2(), ".papi");
22460
+ var INSTALL_ID_FILE = join8(PAPI_HOME_DIR, "install-id.json");
22532
22461
  var cachedInstallId = null;
22533
22462
  function isValidUuid(s) {
22534
22463
  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);
@@ -22536,7 +22465,7 @@ function isValidUuid(s) {
22536
22465
  function getInstallId() {
22537
22466
  if (cachedInstallId) return cachedInstallId;
22538
22467
  try {
22539
- const raw = readFileSync3(INSTALL_ID_FILE, "utf-8");
22468
+ const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
22540
22469
  const parsed = JSON.parse(raw);
22541
22470
  if (isValidUuid(parsed.install_id)) {
22542
22471
  cachedInstallId = parsed.install_id;
@@ -22912,7 +22841,7 @@ To override, pass force=true (emits a telemetry warning).`
22912
22841
  throw new Error(`tag "${version}" already exists. Use a different version.`);
22913
22842
  }
22914
22843
  const latestTag = getLatestTag(config2.projectRoot);
22915
- const changelogPath = join8(config2.projectRoot, "CHANGELOG.md");
22844
+ const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
22916
22845
  if (!latestTag) {
22917
22846
  const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
22918
22847
  await writeFile3(changelogPath, initialContent, "utf-8");
@@ -23092,8 +23021,8 @@ async function handleRelease(adapter2, config2, args) {
23092
23021
  }
23093
23022
 
23094
23023
  // src/tools/review.ts
23095
- import { existsSync as existsSync4 } from "fs";
23096
- import { join as join9 } from "path";
23024
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
23025
+ import { join as join10 } from "path";
23097
23026
  init_git();
23098
23027
 
23099
23028
  // src/services/review.ts
@@ -23312,6 +23241,57 @@ async function buildSessionGuidance() {
23312
23241
  }
23313
23242
 
23314
23243
  // src/tools/review.ts
23244
+ var REVIEW_DISPATCH_THRESHOLD = 50 * 1024;
23245
+ var REVIEW_RUBRIC = [
23246
+ "You are reviewing a completed PAPI build for acceptance. Judge:",
23247
+ "- Correctness: does the change do what the build report claims, without obvious bugs?",
23248
+ "- Scope adherence: does the diff match the BUILD HANDOFF scope (no unrelated changes, no skipped acceptance criteria)?",
23249
+ "- Security: any auth/data/secret/injection risk introduced?",
23250
+ "- Quality: tests present and meaningful, no obvious debt or dead code.",
23251
+ "Be specific and cite file/line where you can. Recommend fail only for blocking issues."
23252
+ ].join("\n");
23253
+ async function buildReviewDispatch(adapter2, config2, taskId) {
23254
+ const task = await adapter2.getTask(taskId);
23255
+ if (!task) {
23256
+ return { ok: false, error: `Task ${taskId} not found \u2014 cannot assemble review context.` };
23257
+ }
23258
+ const handoff = task.buildHandoff ? `### BUILD HANDOFF (scope to check against)
23259
+ ${JSON.stringify(task.buildHandoff, null, 2)}` : "### BUILD HANDOFF\n(none recorded)";
23260
+ const report = task.buildReport ? `### Build Report
23261
+ ${task.buildReport}` : "### Build Report\n(none recorded)";
23262
+ const diff = getBranchDiff(config2.projectRoot);
23263
+ const diffBlock = diff ? `### Branch diff vs base
23264
+ \`\`\`diff
23265
+ ${diff}
23266
+ \`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
23267
+ let projectContext = "";
23268
+ const ctxPath = join10(config2.projectRoot, ".agents", "papi-context.md");
23269
+ if (existsSync5(ctxPath)) {
23270
+ try {
23271
+ projectContext = `### Project context (.agents/papi-context.md)
23272
+ ${readFileSync5(ctxPath, "utf-8")}
23273
+
23274
+ `;
23275
+ } catch {
23276
+ }
23277
+ }
23278
+ const userMessage = `## Task ${task.id}: ${task.title}
23279
+
23280
+ ${projectContext}${handoff}
23281
+
23282
+ ${report}
23283
+
23284
+ ${diffBlock}`;
23285
+ const contextBytes = Buffer.byteLength(userMessage, "utf-8");
23286
+ const prompt2 = buildSubagentDispatchPrompt({
23287
+ tool: "review_submit",
23288
+ taskId,
23289
+ systemPrompt: REVIEW_RUBRIC,
23290
+ userMessage,
23291
+ contextBytes
23292
+ });
23293
+ return { ok: true, prompt: prompt2, contextBytes };
23294
+ }
23315
23295
  var reviewListTool = {
23316
23296
  name: "review_list",
23317
23297
  description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
@@ -23356,6 +23336,11 @@ var reviewSubmitTool = {
23356
23336
  type: "string",
23357
23337
  description: 'Reviewer name (default: "human").'
23358
23338
  },
23339
+ dispatch: {
23340
+ type: "string",
23341
+ enum: ["inline", "subagent"],
23342
+ description: `task-1864: set "subagent" (build-acceptance only) to offload code review to a fresh sub-agent. Returns a Task() invocation prompt that feeds the build report + branch diff to the sub-agent, which returns structured auto_review findings. Verdict is NOT required on this call \u2014 you call review_submit again with the human verdict + the sub-agent's auto_review. Default "inline" (record the verdict directly).`
23343
+ },
23359
23344
  reviewer_confirmed: {
23360
23345
  type: "boolean",
23361
23346
  description: "Set to true to confirm you have reviewed the build (read the build report or the pending list via review_list) before submitting an accept verdict. Required to accept a build-acceptance review unless review_list was called in the same session within the last 15 minutes. Defense-in-depth against SUP-2026-010 (Codex prematurely accepted a task because review_list was missing from its tool surface)."
@@ -23434,8 +23419,8 @@ function mergeAfterAccept(config2, taskId) {
23434
23419
  };
23435
23420
  }
23436
23421
  const details = [];
23437
- const papiDir = join9(config2.projectRoot, ".papi");
23438
- if (existsSync4(papiDir)) {
23422
+ const papiDir = join10(config2.projectRoot, ".papi");
23423
+ if (existsSync5(papiDir)) {
23439
23424
  try {
23440
23425
  const commitResult = stageDirAndCommit(
23441
23426
  config2.projectRoot,
@@ -23553,6 +23538,19 @@ async function handleReviewSubmit(adapter2, config2, args) {
23553
23538
  if (!stage) {
23554
23539
  return errorResponse('stage is required. Use "handoff-review" or "build-acceptance".');
23555
23540
  }
23541
+ const explicitDispatch = args.dispatch === "subagent";
23542
+ const autoDispatchEligible = !verdict && args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false";
23543
+ if ((explicitDispatch || autoDispatchEligible) && stage === "build-acceptance" && taskId) {
23544
+ const dispatch = await buildReviewDispatch(adapter2, config2, taskId);
23545
+ if (!dispatch.ok) {
23546
+ if (explicitDispatch) return errorResponse(dispatch.error);
23547
+ } else if (explicitDispatch || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
23548
+ return textResponse(dispatch.prompt);
23549
+ }
23550
+ }
23551
+ if (explicitDispatch && stage !== "build-acceptance") {
23552
+ return errorResponse('dispatch:"subagent" is only supported for stage "build-acceptance".');
23553
+ }
23556
23554
  if (!verdict) {
23557
23555
  return errorResponse('verdict is required. Use "approve", "accept", "request-changes", or "reject".');
23558
23556
  }
@@ -24567,13 +24565,13 @@ function formatUnblockSection(candidates) {
24567
24565
  }
24568
24566
 
24569
24567
  // src/lib/skill-detection.ts
24570
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
24571
- import { join as join10 } from "path";
24568
+ import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
24569
+ import { join as join11 } from "path";
24572
24570
  function readPackageJson(projectRoot) {
24573
- const path7 = join10(projectRoot, "package.json");
24574
- if (!existsSync5(path7)) return null;
24571
+ const path7 = join11(projectRoot, "package.json");
24572
+ if (!existsSync6(path7)) return null;
24575
24573
  try {
24576
- const raw = readFileSync4(path7, "utf-8");
24574
+ const raw = readFileSync6(path7, "utf-8");
24577
24575
  return JSON.parse(raw);
24578
24576
  } catch {
24579
24577
  return null;
@@ -24590,31 +24588,31 @@ function hasDependencyMatching(deps, pattern) {
24590
24588
  return false;
24591
24589
  }
24592
24590
  function hasGitHubWorkflows(projectRoot) {
24593
- const dir = join10(projectRoot, ".github", "workflows");
24594
- if (!existsSync5(dir)) return false;
24591
+ const dir = join11(projectRoot, ".github", "workflows");
24592
+ if (!existsSync6(dir)) return false;
24595
24593
  try {
24596
- const entries = readdirSync4(dir);
24594
+ const entries = readdirSync5(dir);
24597
24595
  return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
24598
24596
  } catch {
24599
24597
  return false;
24600
24598
  }
24601
24599
  }
24602
24600
  function envExampleMentionsStaging(projectRoot) {
24603
- const path7 = join10(projectRoot, ".env.example");
24604
- if (!existsSync5(path7)) return false;
24601
+ const path7 = join11(projectRoot, ".env.example");
24602
+ if (!existsSync6(path7)) return false;
24605
24603
  try {
24606
- const raw = readFileSync4(path7, "utf-8");
24604
+ const raw = readFileSync6(path7, "utf-8");
24607
24605
  return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
24608
24606
  } catch {
24609
24607
  return false;
24610
24608
  }
24611
24609
  }
24612
24610
  function hasVercelConfig(projectRoot) {
24613
- if (existsSync5(join10(projectRoot, "vercel.json"))) return true;
24614
- const vercelDir = join10(projectRoot, ".vercel");
24615
- if (!existsSync5(vercelDir)) return false;
24611
+ if (existsSync6(join11(projectRoot, "vercel.json"))) return true;
24612
+ const vercelDir = join11(projectRoot, ".vercel");
24613
+ if (!existsSync6(vercelDir)) return false;
24616
24614
  try {
24617
- return statSync3(vercelDir).isDirectory();
24615
+ return statSync4(vercelDir).isDirectory();
24618
24616
  } catch {
24619
24617
  return false;
24620
24618
  }
@@ -24672,9 +24670,82 @@ function formatSkillProposals(proposals) {
24672
24670
  return "\n" + lines.join("\n");
24673
24671
  }
24674
24672
 
24673
+ // src/tools/agent-list.ts
24674
+ import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
24675
+ import { join as join12 } from "path";
24676
+ 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.";
24677
+ function parseAgentFrontmatter(content) {
24678
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
24679
+ if (!match) return null;
24680
+ const fm = match[1];
24681
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
24682
+ const descMatch = fm.match(/^description:\s*[>|]?\n?([\s\S]*?)(?=\n\w|\n---|$)/m);
24683
+ const description = descMatch ? descMatch[1].replace(/^\s{2}/gm, "").replace(/\n+$/, "").replace(/\n/g, " ").trim() : "";
24684
+ return { name: nameMatch?.[1].trim(), description };
24685
+ }
24686
+ async function listAgents(projectRoot) {
24687
+ const agentsDir = join12(projectRoot, ".claude", "agents");
24688
+ let files;
24689
+ try {
24690
+ files = await readdir2(agentsDir);
24691
+ } catch {
24692
+ return [];
24693
+ }
24694
+ const agents = [];
24695
+ for (const file of files.filter((f) => f.endsWith(".md"))) {
24696
+ let content;
24697
+ try {
24698
+ content = await readFile7(join12(agentsDir, file), "utf-8");
24699
+ } catch {
24700
+ continue;
24701
+ }
24702
+ const meta = parseAgentFrontmatter(content);
24703
+ agents.push({
24704
+ name: meta?.name ?? file.replace(/\.md$/, ""),
24705
+ description: meta?.description ?? "",
24706
+ path: join12(".claude", "agents", file)
24707
+ });
24708
+ }
24709
+ agents.sort((a, b2) => a.name.localeCompare(b2.name));
24710
+ return agents;
24711
+ }
24712
+ var agentListTool = {
24713
+ name: "agent_list",
24714
+ description: "List the sub-agents discovered in the project's `.claude/agents/*.md` files (read-only). Returns each agent's name and description so you can see which specialised sub-agents are available before dispatching one via the Task/Agent tool. Discovery only \u2014 does not invoke or manage agents.",
24715
+ annotations: { readOnlyHint: true, destructiveHint: false },
24716
+ inputSchema: {
24717
+ type: "object",
24718
+ properties: {
24719
+ include_path: {
24720
+ type: "boolean",
24721
+ description: "Include each agent's project-relative file path in the output. Default false."
24722
+ }
24723
+ },
24724
+ required: []
24725
+ }
24726
+ };
24727
+ async function handleAgentList(config2, args) {
24728
+ const includePath = args.include_path === true;
24729
+ const agents = await listAgents(config2.projectRoot);
24730
+ if (agents.length === 0) {
24731
+ return textResponse(`# Sub-agents
24732
+
24733
+ ${NO_AGENTS_HINT}`);
24734
+ }
24735
+ const formatted = agents.map((a) => {
24736
+ const desc = a.description ? ` \u2014 ${a.description}` : "";
24737
+ const pathSuffix = includePath ? `
24738
+ \`${a.path}\`` : "";
24739
+ return `- **${a.name}**${desc}${pathSuffix}`;
24740
+ }).join("\n");
24741
+ return textResponse(`# Sub-agents (${agents.length})
24742
+
24743
+ ${formatted}`);
24744
+ }
24745
+
24675
24746
  // src/tools/doc-registry.ts
24676
- import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
24677
- import { join as join11, relative } from "path";
24747
+ import { readdirSync as readdirSync6, existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
24748
+ import { join as join13, relative } from "path";
24678
24749
  import { homedir as homedir3 } from "os";
24679
24750
  import { randomUUID as randomUUID16 } from "crypto";
24680
24751
  var docRegisterTool = {
@@ -24819,12 +24890,12 @@ ${d.summary}
24819
24890
  ${lines.join("\n---\n\n")}`);
24820
24891
  }
24821
24892
  function scanMdFiles(dir, rootDir) {
24822
- if (!existsSync6(dir)) return [];
24893
+ if (!existsSync7(dir)) return [];
24823
24894
  const files = [];
24824
24895
  try {
24825
- const entries = readdirSync5(dir, { withFileTypes: true });
24896
+ const entries = readdirSync6(dir, { withFileTypes: true });
24826
24897
  for (const entry of entries) {
24827
- const full = join11(dir, entry.name);
24898
+ const full = join13(dir, entry.name);
24828
24899
  if (entry.isDirectory()) {
24829
24900
  files.push(...scanMdFiles(full, rootDir));
24830
24901
  } else if (entry.name.endsWith(".md")) {
@@ -24837,7 +24908,7 @@ function scanMdFiles(dir, rootDir) {
24837
24908
  }
24838
24909
  function extractTitle(filePath) {
24839
24910
  try {
24840
- const content = readFileSync5(filePath, "utf-8").slice(0, 1e3);
24911
+ const content = readFileSync7(filePath, "utf-8").slice(0, 1e3);
24841
24912
  const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
24842
24913
  if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
24843
24914
  const headingMatch = content.match(/^#+\s+(.+)$/m);
@@ -24853,17 +24924,17 @@ async function handleDocScan(adapter2, config2, args) {
24853
24924
  const includePlans = args.include_plans ?? false;
24854
24925
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
24855
24926
  const registeredPaths = new Set(registered.map((d) => d.path));
24856
- const docsDir = join11(config2.projectRoot, "docs");
24927
+ const docsDir = join13(config2.projectRoot, "docs");
24857
24928
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
24858
24929
  const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
24859
24930
  let unregisteredPlans = [];
24860
24931
  if (includePlans) {
24861
- const plansDir = join11(homedir3(), ".claude", "plans");
24862
- if (existsSync6(plansDir)) {
24932
+ const plansDir = join13(homedir3(), ".claude", "plans");
24933
+ if (existsSync7(plansDir)) {
24863
24934
  const planFiles = scanMdFiles(plansDir, plansDir);
24864
24935
  unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
24865
24936
  path: f,
24866
- title: extractTitle(join11(plansDir, f.replace("plans/", "")))
24937
+ title: extractTitle(join13(plansDir, f.replace("plans/", "")))
24867
24938
  }));
24868
24939
  }
24869
24940
  }
@@ -24874,7 +24945,7 @@ async function handleDocScan(adapter2, config2, args) {
24874
24945
  if (unregisteredDocs.length > 0) {
24875
24946
  lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
24876
24947
  for (const f of unregisteredDocs) {
24877
- const title = extractTitle(join11(config2.projectRoot, f));
24948
+ const title = extractTitle(join13(config2.projectRoot, f));
24878
24949
  lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
24879
24950
  }
24880
24951
  }
@@ -25038,8 +25109,8 @@ function dedupeEnrichmentBlob(existing, blob) {
25038
25109
  // src/tools/orient.ts
25039
25110
  import { execFile } from "child_process";
25040
25111
  import { promisify } from "util";
25041
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "fs";
25042
- import { join as join12 } from "path";
25112
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, existsSync as existsSync8 } from "fs";
25113
+ import { join as join14 } from "path";
25043
25114
  var execFileAsync = promisify(execFile);
25044
25115
  var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
25045
25116
  var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
@@ -25102,7 +25173,7 @@ var orientTool = {
25102
25173
  required: []
25103
25174
  }
25104
25175
  };
25105
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown") {
25176
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = []) {
25106
25177
  const lines = [];
25107
25178
  const cycleIsComplete = health.latestCycleStatus === "complete";
25108
25179
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -25159,6 +25230,16 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
25159
25230
  lines.push("## Board");
25160
25231
  lines.push(health.boardSummary);
25161
25232
  lines.push("");
25233
+ if (subAgents.length > 0) {
25234
+ lines.push(`**Sub-agents (${subAgents.length}):** ` + subAgents.map((a) => {
25235
+ const desc = a.description ? ` (${a.description.slice(0, 60)})` : "";
25236
+ return `\`${a.name}\`${desc}`;
25237
+ }).join(", "));
25238
+ lines.push("");
25239
+ } else {
25240
+ lines.push(`_${NO_AGENTS_HINT}_`);
25241
+ lines.push("");
25242
+ }
25162
25243
  if (buildInfo.cycleTasks.total > 0) {
25163
25244
  const parts = [];
25164
25245
  if (buildInfo.cycleTasks.inProgress > 0) parts.push(`${buildInfo.cycleTasks.inProgress} in progress`);
@@ -25281,8 +25362,8 @@ async function getLatestGitTag(projectRoot) {
25281
25362
  }
25282
25363
  async function checkNpmVersionDrift() {
25283
25364
  try {
25284
- const pkgPath = join12(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
25285
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
25365
+ const pkgPath = join14(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
25366
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
25286
25367
  const localVersion = pkg.version;
25287
25368
  const packageName = pkg.name;
25288
25369
  const { stdout } = await execFileAsync("npm", ["view", packageName, "version"], {
@@ -25570,8 +25651,9 @@ async function handleOrient(adapter2, config2, args = {}) {
25570
25651
  return "\n\n> **Early-cycle tip:** No delivery-shape AD found. Consider whether this project is a *service* (manual delivery, human-in-the-loop) or a *platform* (self-serve). The distinction shapes task priorities. Log it with `idea` to mint an AD.";
25571
25652
  }),
25572
25653
  // task-1652: deep housekeeping — opt-in sweep (board, unrecorded commits, unregistered docs)
25654
+ // task-1865: also detects stale skill forks vs the @papi-ai/skills registry.
25573
25655
  tracked("deep-housekeeping", async () => {
25574
- if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "" };
25656
+ if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
25575
25657
  let reconciliationNote2 = "";
25576
25658
  try {
25577
25659
  const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
@@ -25607,7 +25689,7 @@ async function handleOrient(adapter2, config2, args = {}) {
25607
25689
  let unregisteredDocsNote2 = "";
25608
25690
  try {
25609
25691
  if (adapter2.searchDocs) {
25610
- const docsDir = join12(config2.projectRoot, "docs");
25692
+ const docsDir = join14(config2.projectRoot, "docs");
25611
25693
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
25612
25694
  if (docsFiles.length > 0) {
25613
25695
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -25619,7 +25701,22 @@ async function handleOrient(adapter2, config2, args = {}) {
25619
25701
  }
25620
25702
  } catch {
25621
25703
  }
25622
- return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2 };
25704
+ let staleSkillsNote2 = "";
25705
+ try {
25706
+ const { detectStaleForks, loadManifest } = await import("@papi-ai/skills/manifest");
25707
+ const stale = detectStaleForks(config2.projectRoot);
25708
+ if (stale.length > 0) {
25709
+ const version = loadManifest().packageVersion;
25710
+ const lines = [`
25711
+
25712
+ ## Stale Skill Forks (${stale.length})`];
25713
+ lines.push(`These skills differ from the pinned \`@papi-ai/skills@${version}\` registry. Re-run \`npx @papi-ai/skills install .\` to refresh, or move a skill to \`.claude/skills.local/\` to keep your fork. Replacement is offered, never forced.`);
25714
+ for (const f of stale) lines.push(`- \u26A0\uFE0F **${f.name}** \u2014 local copy diverged from registry`);
25715
+ staleSkillsNote2 = lines.join("\n");
25716
+ }
25717
+ } catch {
25718
+ }
25719
+ return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
25623
25720
  })
25624
25721
  ]);
25625
25722
  const proxyWarning = proxyVersionOutcome.status === "fulfilled" ? proxyVersionOutcome.value : void 0;
@@ -25640,7 +25737,7 @@ ${versionDrift}` : "";
25640
25737
  const projectBannerNote = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : "";
25641
25738
  const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
25642
25739
  const deliveryShapeNote = deliveryShapeOutcome.status === "fulfilled" ? deliveryShapeOutcome.value : "";
25643
- const { reconciliationNote, unrecordedNote, unregisteredDocsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "" };
25740
+ const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
25644
25741
  let enrichmentNote = "";
25645
25742
  try {
25646
25743
  enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
@@ -25679,8 +25776,9 @@ ${section}`;
25679
25776
  } catch {
25680
25777
  }
25681
25778
  tracker.mark("format-summary");
25682
- const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, and unregistered docs.*";
25683
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint);
25779
+ const subAgents = await listAgents(config2.projectRoot);
25780
+ const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks.*";
25781
+ return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint);
25684
25782
  } catch (err) {
25685
25783
  const message = err instanceof Error ? err.message : String(err);
25686
25784
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -25698,9 +25796,9 @@ ${section}`;
25698
25796
  }
25699
25797
  }
25700
25798
  function enrichClaudeMd(projectRoot, cycleNumber) {
25701
- const claudeMdPath = join12(projectRoot, "CLAUDE.md");
25702
- if (!existsSync7(claudeMdPath)) return "";
25703
- const content = readFileSync6(claudeMdPath, "utf-8");
25799
+ const claudeMdPath = join14(projectRoot, "CLAUDE.md");
25800
+ if (!existsSync8(claudeMdPath)) return "";
25801
+ const content = readFileSync8(claudeMdPath, "utf-8");
25704
25802
  const additions = [];
25705
25803
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
25706
25804
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
@@ -26386,7 +26484,7 @@ ${result.userMessage}
26386
26484
 
26387
26485
  // src/services/scope-brief.ts
26388
26486
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
26389
- import { join as join13, dirname } from "path";
26487
+ import { join as join15, dirname as dirname3 } from "path";
26390
26488
  import Anthropic from "@anthropic-ai/sdk";
26391
26489
  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.
26392
26490
 
@@ -26441,8 +26539,8 @@ async function runScopeBrief(adapter2, input) {
26441
26539
  }
26442
26540
  const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
26443
26541
  const relPath = `docs/scopes/${slug}.md`;
26444
- const absPath = join13(input.projectRoot, relPath);
26445
- mkdirSync3(dirname(absPath), { recursive: true });
26542
+ const absPath = join15(input.projectRoot, relPath);
26543
+ mkdirSync3(dirname3(absPath), { recursive: true });
26446
26544
  writeFileSync4(absPath, addFrontmatter(docContent, task, input.cycleNumber), "utf-8");
26447
26545
  const taskCount = countSubTasks(docContent);
26448
26546
  const summary = buildSummary(task, taskCount);
@@ -26818,6 +26916,166 @@ Update PAPI_PROJECT_ID (local) or x-papi-project-id (remote) to \`${result.proje
26818
26916
  );
26819
26917
  }
26820
26918
 
26919
+ // src/services/harness-inventory.ts
26920
+ import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
26921
+ import { join as join16 } from "path";
26922
+ import { createHash as createHash3 } from "crypto";
26923
+ var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
26924
+ async function computeFingerprint(root) {
26925
+ const parts = [];
26926
+ for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
26927
+ const dir = join16(root, sub);
26928
+ try {
26929
+ const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
26930
+ for (const name of names) {
26931
+ let mtime = "";
26932
+ try {
26933
+ mtime = String(Math.floor((await stat3(join16(dir, name))).mtimeMs));
26934
+ } catch {
26935
+ }
26936
+ parts.push(`${sub}/${name}:${mtime}`);
26937
+ }
26938
+ } catch {
26939
+ parts.push(`${sub}:absent`);
26940
+ }
26941
+ }
26942
+ try {
26943
+ const { loadManifest } = await import("@papi-ai/skills/manifest");
26944
+ parts.push(`manifest:${loadManifest().packageVersion}`);
26945
+ } catch {
26946
+ parts.push("manifest:none");
26947
+ }
26948
+ return createHash3("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
26949
+ }
26950
+ async function readSkillDescription(skillDir) {
26951
+ try {
26952
+ const content = await readFile8(join16(skillDir, "SKILL.md"), "utf-8");
26953
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
26954
+ if (!fm) return void 0;
26955
+ const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
26956
+ if (!desc) return void 0;
26957
+ return desc[1].replace(/^\s+/gm, "").replace(/\n+/g, " ").trim() || void 0;
26958
+ } catch {
26959
+ return void 0;
26960
+ }
26961
+ }
26962
+ async function scanInventory(root, toolDefs) {
26963
+ const entries = [];
26964
+ const stale = /* @__PURE__ */ new Set();
26965
+ let version;
26966
+ try {
26967
+ const { detectStaleForks, loadManifest } = await import("@papi-ai/skills/manifest");
26968
+ for (const fork of detectStaleForks(root)) stale.add(fork.name);
26969
+ version = loadManifest().packageVersion;
26970
+ } catch {
26971
+ }
26972
+ const skillsDir = join16(root, ".claude", "skills");
26973
+ try {
26974
+ const dirents = await readdir3(skillsDir, { withFileTypes: true });
26975
+ for (const d of dirents.filter((e) => e.isDirectory())) {
26976
+ entries.push({
26977
+ kind: "skill",
26978
+ name: d.name,
26979
+ description: await readSkillDescription(join16(skillsDir, d.name)),
26980
+ version,
26981
+ status: stale.has(d.name) ? "stale_fork" : "ok",
26982
+ path: join16(".claude", "skills", d.name)
26983
+ });
26984
+ }
26985
+ } catch {
26986
+ }
26987
+ for (const a of await listAgents(root)) {
26988
+ entries.push({
26989
+ kind: "agent",
26990
+ name: a.name,
26991
+ description: a.description || void 0,
26992
+ status: "ok",
26993
+ path: a.path
26994
+ });
26995
+ }
26996
+ const present = /* @__PURE__ */ new Set();
26997
+ try {
26998
+ for (const f of (await readdir3(join16(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
26999
+ present.add(f);
27000
+ entries.push({ kind: "hook", name: f, status: "ok", path: join16(".claude", "hooks", f) });
27001
+ }
27002
+ } catch {
27003
+ }
27004
+ for (const rec of RECOMMENDED_HOOKS) {
27005
+ if (!present.has(rec)) {
27006
+ entries.push({ kind: "hook", name: rec, description: "Recommended hook \u2014 not installed", status: "missing" });
27007
+ }
27008
+ }
27009
+ for (const t of toolDefs ?? []) {
27010
+ entries.push({ kind: "mcp_tool", name: t.name, description: t.description, status: "ok" });
27011
+ }
27012
+ return entries;
27013
+ }
27014
+ async function syncHarnessInventory(adapter2, config2, opts) {
27015
+ if (!adapter2.getHarnessState || !adapter2.setHarnessState || !adapter2.replaceHarnessInventory) {
27016
+ return { changed: false, skipped: true };
27017
+ }
27018
+ const fingerprint = await computeFingerprint(config2.projectRoot);
27019
+ if (!opts?.force) {
27020
+ const state2 = await adapter2.getHarnessState();
27021
+ if (state2 && state2.fingerprint === fingerprint) return { changed: false };
27022
+ }
27023
+ const entries = await scanInventory(config2.projectRoot, opts?.toolDefs);
27024
+ await adapter2.replaceHarnessInventory(entries);
27025
+ await adapter2.setHarnessState(fingerprint);
27026
+ const counts = { skill: 0, agent: 0, hook: 0, mcp_tool: 0 };
27027
+ let staleForks = 0;
27028
+ let missingHooks = 0;
27029
+ for (const e of entries) {
27030
+ counts[e.kind]++;
27031
+ if (e.status === "stale_fork") staleForks++;
27032
+ if (e.status === "missing") missingHooks++;
27033
+ }
27034
+ return { changed: true, counts, staleForks, missingHooks };
27035
+ }
27036
+
27037
+ // src/tools/inventory-sync.ts
27038
+ var inventorySyncTool = {
27039
+ name: "inventory_sync",
27040
+ description: "Sync this project's harness inventory \u2014 skills, sub-agents, hooks, and MCP tools \u2014 to the database so the dashboard can surface it. Gated by a cheap change-fingerprint: a no-op when the harness hasn't changed since the last sync. Set force=true to re-scan and write regardless. Runs automatically at setup and release; use this for an explicit refresh after editing your harness.",
27041
+ annotations: { readOnlyHint: false, destructiveHint: false },
27042
+ inputSchema: {
27043
+ type: "object",
27044
+ properties: {
27045
+ force: {
27046
+ type: "boolean",
27047
+ description: "Re-scan and write even if the change-fingerprint is unchanged. Default false."
27048
+ }
27049
+ },
27050
+ required: []
27051
+ }
27052
+ };
27053
+ async function handleInventorySync(adapter2, config2, args, toolDefs) {
27054
+ const force = args.force === true;
27055
+ const result = await syncHarnessInventory(adapter2, config2, { force, toolDefs });
27056
+ if (result.skipped) {
27057
+ return textResponse(
27058
+ "# Harness inventory\n\nNo database adapter available \u2014 harness inventory is only persisted when running against the hosted database (pg/proxy adapter). Nothing to sync."
27059
+ );
27060
+ }
27061
+ if (!result.changed) {
27062
+ return textResponse(
27063
+ "# Harness inventory\n\n\u2713 Already up to date \u2014 the harness has not changed since the last sync (no write needed)."
27064
+ );
27065
+ }
27066
+ const c = result.counts;
27067
+ const lines = [
27068
+ "# Harness inventory synced",
27069
+ "",
27070
+ `Wrote **${c.skill}** skills, **${c.agent}** sub-agents, **${c.hook}** hooks, **${c.mcp_tool}** MCP tools to the dashboard.`
27071
+ ];
27072
+ if (result.staleForks) lines.push(`
27073
+ \u26A0\uFE0F **${result.staleForks}** skill(s) flagged as stale forks (drifted from the pinned registry).`);
27074
+ if (result.missingHooks) lines.push(`
27075
+ \u26A0\uFE0F **${result.missingHooks}** recommended hook(s) not installed.`);
27076
+ return textResponse(lines.join("\n"));
27077
+ }
27078
+
26821
27079
  // src/server.ts
26822
27080
  var DEFAULT_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_TOOL_TIMEOUT_MS ?? "30000", 10);
26823
27081
  var LONG_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_LONG_TOOL_TIMEOUT_MS ?? "180000", 10);
@@ -26925,14 +27183,19 @@ var PAPI_TOOLS = [
26925
27183
  learningActionTool,
26926
27184
  projectCreateTool,
26927
27185
  projectListTool,
26928
- projectSwitchTool
27186
+ projectSwitchTool,
27187
+ agentListTool,
27188
+ inventorySyncTool
26929
27189
  ];
27190
+ function getToolMetadata() {
27191
+ return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
27192
+ }
26930
27193
  function createServer(adapter2, config2) {
26931
- const __pkgFilename = fileURLToPath(import.meta.url);
26932
- const __pkgDir = dirname2(__pkgFilename);
27194
+ const __pkgFilename = fileURLToPath2(import.meta.url);
27195
+ const __pkgDir = dirname4(__pkgFilename);
26933
27196
  let serverVersion = "unknown";
26934
27197
  try {
26935
- const pkg = JSON.parse(readFileSync7(join14(__pkgDir, "..", "package.json"), "utf-8"));
27198
+ const pkg = JSON.parse(readFileSync9(join17(__pkgDir, "..", "package.json"), "utf-8"));
26936
27199
  serverVersion = pkg.version ?? "unknown";
26937
27200
  } catch {
26938
27201
  }
@@ -26945,9 +27208,9 @@ function createServer(adapter2, config2) {
26945
27208
  "\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"
26946
27209
  );
26947
27210
  }
26948
- const __filename = fileURLToPath(import.meta.url);
26949
- const __dirname2 = dirname2(__filename);
26950
- const skillsDir = join14(__dirname2, "..", "skills");
27211
+ const __filename = fileURLToPath2(import.meta.url);
27212
+ const __dirname2 = dirname4(__filename);
27213
+ const skillsDir = join17(__dirname2, "..", "skills");
26951
27214
  function parseSkillFrontmatter(content) {
26952
27215
  const match = content.match(/^---\n([\s\S]*?)\n---/);
26953
27216
  if (!match) return null;
@@ -26961,11 +27224,11 @@ function createServer(adapter2, config2) {
26961
27224
  }
26962
27225
  server2.setRequestHandler(ListPromptsRequestSchema, async () => {
26963
27226
  try {
26964
- const files = await readdir2(skillsDir);
27227
+ const files = await readdir4(skillsDir);
26965
27228
  const mdFiles = files.filter((f) => f.endsWith(".md"));
26966
27229
  const prompts = [];
26967
27230
  for (const file of mdFiles) {
26968
- const content = await readFile7(join14(skillsDir, file), "utf-8");
27231
+ const content = await readFile9(join17(skillsDir, file), "utf-8");
26969
27232
  const meta = parseSkillFrontmatter(content);
26970
27233
  if (meta) {
26971
27234
  prompts.push({ name: meta.name, description: meta.description });
@@ -26979,9 +27242,9 @@ function createServer(adapter2, config2) {
26979
27242
  server2.setRequestHandler(GetPromptRequestSchema, async (request) => {
26980
27243
  const { name } = request.params;
26981
27244
  try {
26982
- const files = await readdir2(skillsDir);
27245
+ const files = await readdir4(skillsDir);
26983
27246
  for (const file of files.filter((f) => f.endsWith(".md"))) {
26984
- const content = await readFile7(join14(skillsDir, file), "utf-8");
27247
+ const content = await readFile9(join17(skillsDir, file), "utf-8");
26985
27248
  const meta = parseSkillFrontmatter(content);
26986
27249
  if (meta?.name === name) {
26987
27250
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -27100,6 +27363,10 @@ function createServer(adapter2, config2) {
27100
27363
  return handleProjectList(adapter2, config2, safeArgs);
27101
27364
  case "project_switch":
27102
27365
  return handleProjectSwitch(adapter2, config2, safeArgs);
27366
+ case "agent_list":
27367
+ return handleAgentList(config2, safeArgs);
27368
+ case "inventory_sync":
27369
+ return handleInventorySync(adapter2, config2, safeArgs, getToolMetadata());
27103
27370
  default:
27104
27371
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
27105
27372
  }
@@ -27153,6 +27420,11 @@ function createServer(adapter2, config2) {
27153
27420
  }
27154
27421
  }
27155
27422
  }
27423
+ if (!isError && (name === "setup" || name === "release")) {
27424
+ const force = name === "setup";
27425
+ void syncHarnessInventory(adapter2, config2, { force, toolDefs: getToolMetadata() }).catch(() => {
27426
+ });
27427
+ }
27156
27428
  const footer = formatMetricsFooter(elapsed, usage, contextBytes);
27157
27429
  result.content.push({ type: "text", text: footer });
27158
27430
  return result;
@@ -27456,10 +27728,10 @@ async function dispatchRequest(args) {
27456
27728
  }
27457
27729
 
27458
27730
  // src/index.ts
27459
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
27731
+ var __dirname = dirname5(fileURLToPath3(import.meta.url));
27460
27732
  var pkgVersion = "unknown";
27461
27733
  try {
27462
- const pkg = JSON.parse(readFileSync10(join17(__dirname, "..", "package.json"), "utf-8"));
27734
+ const pkg = JSON.parse(readFileSync12(join20(__dirname, "..", "package.json"), "utf-8"));
27463
27735
  pkgVersion = pkg.version;
27464
27736
  } catch {
27465
27737
  }