@papi-ai/server 0.7.78 → 0.7.79

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
@@ -2248,7 +2248,7 @@ var init_reap_orphans = __esm({
2248
2248
  }
2249
2249
  });
2250
2250
 
2251
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/query.js
2251
+ // ../../node_modules/postgres/src/query.js
2252
2252
  function cachedError(xs) {
2253
2253
  if (originCache.has(xs))
2254
2254
  return originCache.get(xs);
@@ -2260,7 +2260,7 @@ function cachedError(xs) {
2260
2260
  }
2261
2261
  var originCache, originStackCache, originError, CLOSE, Query;
2262
2262
  var init_query = __esm({
2263
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/query.js"() {
2263
+ "../../node_modules/postgres/src/query.js"() {
2264
2264
  "use strict";
2265
2265
  originCache = /* @__PURE__ */ new Map();
2266
2266
  originStackCache = /* @__PURE__ */ new Map();
@@ -2391,7 +2391,7 @@ var init_query = __esm({
2391
2391
  }
2392
2392
  });
2393
2393
 
2394
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/errors.js
2394
+ // ../../node_modules/postgres/src/errors.js
2395
2395
  function connection(x, options, socket) {
2396
2396
  const { host, port } = socket || options;
2397
2397
  const error = Object.assign(
@@ -2429,7 +2429,7 @@ function notSupported(x) {
2429
2429
  }
2430
2430
  var PostgresError, Errors;
2431
2431
  var init_errors = __esm({
2432
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/errors.js"() {
2432
+ "../../node_modules/postgres/src/errors.js"() {
2433
2433
  "use strict";
2434
2434
  PostgresError = class extends Error {
2435
2435
  constructor(x) {
@@ -2447,7 +2447,7 @@ var init_errors = __esm({
2447
2447
  }
2448
2448
  });
2449
2449
 
2450
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/types.js
2450
+ // ../../node_modules/postgres/src/types.js
2451
2451
  function handleValue(x, parameters, types2, options) {
2452
2452
  let value = x instanceof Parameter ? x.value : x;
2453
2453
  if (value === void 0) {
@@ -2562,7 +2562,7 @@ function createJsonTransform(fn) {
2562
2562
  }
2563
2563
  var types, NotTagged, Identifier, Parameter, Builder, defaultHandlers, builders, serializers, parsers, mergeUserTypes, escapeIdentifier, inferType, escapeBackslash, escapeQuote, arraySerializer, arrayParserState, arrayParser, toCamel, toPascal, toKebab, fromCamel, fromPascal, fromKebab, camel, pascal, kebab;
2564
2564
  var init_types = __esm({
2565
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/types.js"() {
2565
+ "../../node_modules/postgres/src/types.js"() {
2566
2566
  "use strict";
2567
2567
  init_query();
2568
2568
  init_errors();
@@ -2741,10 +2741,10 @@ var init_types = __esm({
2741
2741
  }
2742
2742
  });
2743
2743
 
2744
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/result.js
2744
+ // ../../node_modules/postgres/src/result.js
2745
2745
  var Result;
2746
2746
  var init_result = __esm({
2747
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/result.js"() {
2747
+ "../../node_modules/postgres/src/result.js"() {
2748
2748
  "use strict";
2749
2749
  Result = class extends Array {
2750
2750
  constructor() {
@@ -2764,7 +2764,7 @@ var init_result = __esm({
2764
2764
  }
2765
2765
  });
2766
2766
 
2767
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/queue.js
2767
+ // ../../node_modules/postgres/src/queue.js
2768
2768
  function Queue(initial = []) {
2769
2769
  let xs = initial.slice();
2770
2770
  let index = 0;
@@ -2791,13 +2791,13 @@ function Queue(initial = []) {
2791
2791
  }
2792
2792
  var queue_default;
2793
2793
  var init_queue = __esm({
2794
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/queue.js"() {
2794
+ "../../node_modules/postgres/src/queue.js"() {
2795
2795
  "use strict";
2796
2796
  queue_default = Queue;
2797
2797
  }
2798
2798
  });
2799
2799
 
2800
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/bytes.js
2800
+ // ../../node_modules/postgres/src/bytes.js
2801
2801
  function fit(x) {
2802
2802
  if (buffer.length - b.i < x) {
2803
2803
  const prev = buffer, length = prev.length;
@@ -2811,7 +2811,7 @@ function reset() {
2811
2811
  }
2812
2812
  var size, buffer, messages, b, bytes_default;
2813
2813
  var init_bytes = __esm({
2814
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/bytes.js"() {
2814
+ "../../node_modules/postgres/src/bytes.js"() {
2815
2815
  "use strict";
2816
2816
  size = 256;
2817
2817
  buffer = Buffer.allocUnsafe(size);
@@ -2876,7 +2876,7 @@ var init_bytes = __esm({
2876
2876
  }
2877
2877
  });
2878
2878
 
2879
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/connection.js
2879
+ // ../../node_modules/postgres/src/connection.js
2880
2880
  import net from "net";
2881
2881
  import tls from "tls";
2882
2882
  import crypto2 from "crypto";
@@ -3670,7 +3670,7 @@ function timer(fn, seconds) {
3670
3670
  }
3671
3671
  var connection_default, uid, Sync, Flush, SSLRequest, ExecuteUnnamed, DescribeUnnamed, noop, retryRoutines, errorFields;
3672
3672
  var init_connection = __esm({
3673
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/connection.js"() {
3673
+ "../../node_modules/postgres/src/connection.js"() {
3674
3674
  "use strict";
3675
3675
  init_types();
3676
3676
  init_errors();
@@ -3733,7 +3733,7 @@ var init_connection = __esm({
3733
3733
  }
3734
3734
  });
3735
3735
 
3736
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/subscribe.js
3736
+ // ../../node_modules/postgres/src/subscribe.js
3737
3737
  function Subscribe(postgres2, options) {
3738
3738
  const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state = {};
3739
3739
  let connection2, stream, ended = false;
@@ -3944,14 +3944,14 @@ function parseEvent(x) {
3944
3944
  }
3945
3945
  var noop2;
3946
3946
  var init_subscribe = __esm({
3947
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/subscribe.js"() {
3947
+ "../../node_modules/postgres/src/subscribe.js"() {
3948
3948
  "use strict";
3949
3949
  noop2 = () => {
3950
3950
  };
3951
3951
  }
3952
3952
  });
3953
3953
 
3954
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/large.js
3954
+ // ../../node_modules/postgres/src/large.js
3955
3955
  import Stream2 from "stream";
3956
3956
  function largeObject(sql, oid, mode = 131072 | 262144) {
3957
3957
  return new Promise(async (resolve4, reject) => {
@@ -4017,12 +4017,12 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
4017
4017
  });
4018
4018
  }
4019
4019
  var init_large = __esm({
4020
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/large.js"() {
4020
+ "../../node_modules/postgres/src/large.js"() {
4021
4021
  "use strict";
4022
4022
  }
4023
4023
  });
4024
4024
 
4025
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/index.js
4025
+ // ../../node_modules/postgres/src/index.js
4026
4026
  var src_exports = {};
4027
4027
  __export(src_exports, {
4028
4028
  default: () => src_default
@@ -4412,7 +4412,7 @@ function osUsername() {
4412
4412
  }
4413
4413
  var src_default;
4414
4414
  var init_src = __esm({
4415
- "../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/node_modules/postgres/src/index.js"() {
4415
+ "../../node_modules/postgres/src/index.js"() {
4416
4416
  "use strict";
4417
4417
  init_types();
4418
4418
  init_connection();
@@ -4452,7 +4452,7 @@ __export(doctor_exports, {
4452
4452
  });
4453
4453
  import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4454
4454
  import { homedir as homedir4 } from "os";
4455
- import { join as join21 } from "path";
4455
+ import { join as join22 } from "path";
4456
4456
  function redact(name, value) {
4457
4457
  if (!value) return "(empty)";
4458
4458
  if (SECRET_VARS.has(name)) {
@@ -4463,9 +4463,9 @@ function redact(name, value) {
4463
4463
  }
4464
4464
  function findMcpJson() {
4465
4465
  const candidates = [
4466
- join21(process.cwd(), ".mcp.json"),
4467
- join21(homedir4(), ".claude", ".mcp.json"),
4468
- join21(homedir4(), ".mcp.json")
4466
+ join22(process.cwd(), ".mcp.json"),
4467
+ join22(homedir4(), ".claude", ".mcp.json"),
4468
+ join22(homedir4(), ".mcp.json")
4469
4469
  ];
4470
4470
  for (const path7 of candidates) {
4471
4471
  if (!existsSync12(path7)) continue;
@@ -4748,9 +4748,9 @@ __export(reset_exports, {
4748
4748
  removePapiEntry: () => removePapiEntry,
4749
4749
  runReset: () => runReset
4750
4750
  });
4751
- import { existsSync as existsSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "fs";
4751
+ import { existsSync as existsSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
4752
4752
  import { homedir as homedir5 } from "os";
4753
- import { join as join22 } from "path";
4753
+ import { join as join23 } from "path";
4754
4754
  import { createInterface } from "readline/promises";
4755
4755
  function findResetTarget() {
4756
4756
  for (const path7 of CANDIDATE_PATHS()) {
@@ -4829,7 +4829,7 @@ async function runReset(args = []) {
4829
4829
  }
4830
4830
  }
4831
4831
  try {
4832
- writeFileSync8(target.path, removePapiEntry(target), "utf-8");
4832
+ writeFileSync9(target.path, removePapiEntry(target), "utf-8");
4833
4833
  process.stdout.write(`
4834
4834
  \u2713 Removed papi entry from ${target.path}
4835
4835
  `);
@@ -4846,9 +4846,9 @@ var init_reset = __esm({
4846
4846
  "src/cli/reset.ts"() {
4847
4847
  "use strict";
4848
4848
  CANDIDATE_PATHS = () => [
4849
- join22(process.cwd(), ".mcp.json"),
4850
- join22(homedir5(), ".claude", ".mcp.json"),
4851
- join22(homedir5(), ".mcp.json")
4849
+ join23(process.cwd(), ".mcp.json"),
4850
+ join23(homedir5(), ".claude", ".mcp.json"),
4851
+ join23(homedir5(), ".mcp.json")
4852
4852
  ];
4853
4853
  }
4854
4854
  });
@@ -4861,7 +4861,7 @@ __export(audit_exports, {
4861
4861
  });
4862
4862
  import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync7 } from "fs";
4863
4863
  import { homedir as homedir6 } from "os";
4864
- import { join as join23 } from "path";
4864
+ import { join as join24 } from "path";
4865
4865
  function safeListDirs(dir) {
4866
4866
  try {
4867
4867
  return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
@@ -4877,7 +4877,7 @@ function safeListFiles(dir, ext) {
4877
4877
  }
4878
4878
  }
4879
4879
  function readMcp(projectPath) {
4880
- const path7 = join23(projectPath, ".mcp.json");
4880
+ const path7 = join24(projectPath, ".mcp.json");
4881
4881
  if (!existsSync14(path7)) return { servers: [] };
4882
4882
  try {
4883
4883
  const parsed = JSON.parse(readFileSync17(path7, "utf-8"));
@@ -4912,18 +4912,18 @@ function auditProjectSync(projectPath, name) {
4912
4912
  path: projectPath,
4913
4913
  papiProjectId,
4914
4914
  mcpServers: servers,
4915
- skills: safeListDirs(join23(projectPath, ".claude", "skills")),
4916
- agentSkills: safeListDirs(join23(projectPath, ".agents", "skills")),
4917
- agents: safeListFiles(join23(projectPath, ".claude", "agents"), ".md"),
4918
- hooks: safeListFiles(join23(projectPath, ".claude", "hooks"), ".sh")
4915
+ skills: safeListDirs(join24(projectPath, ".claude", "skills")),
4916
+ agentSkills: safeListDirs(join24(projectPath, ".agents", "skills")),
4917
+ agents: safeListFiles(join24(projectPath, ".claude", "agents"), ".md"),
4918
+ hooks: safeListFiles(join24(projectPath, ".claude", "hooks"), ".sh")
4919
4919
  };
4920
4920
  }
4921
4921
  function discoverProjects() {
4922
4922
  const out = [];
4923
4923
  for (const root of PROJECT_ROOTS) {
4924
4924
  for (const name of safeListDirs(root)) {
4925
- const path7 = join23(root, name);
4926
- if (existsSync14(join23(path7, ".mcp.json")) || existsSync14(join23(path7, ".claude"))) {
4925
+ const path7 = join24(root, name);
4926
+ if (existsSync14(join24(path7, ".mcp.json")) || existsSync14(join24(path7, ".claude"))) {
4927
4927
  out.push({ name, path: path7 });
4928
4928
  }
4929
4929
  }
@@ -5088,9 +5088,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
5088
5088
  var init_audit = __esm({
5089
5089
  "src/cli/audit.ts"() {
5090
5090
  "use strict";
5091
- PROJECT_ROOTS = [join23(homedir6(), "Ai-App-Projects"), join23(homedir6(), "android-projects")];
5092
- GLOBAL_SKILLS_DIR = join23(homedir6(), ".claude", "skills");
5093
- GLOBAL_CLAUDE_JSON = join23(homedir6(), ".claude.json");
5091
+ PROJECT_ROOTS = [join24(homedir6(), "Ai-App-Projects"), join24(homedir6(), "android-projects")];
5092
+ GLOBAL_SKILLS_DIR = join24(homedir6(), ".claude", "skills");
5093
+ GLOBAL_CLAUDE_JSON = join24(homedir6(), ".claude.json");
5094
5094
  IDLE_WINDOW_DAYS = 30;
5095
5095
  GLOBALIZE_THRESHOLD = 3;
5096
5096
  __testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
@@ -5102,8 +5102,8 @@ var setup_exports = {};
5102
5102
  __export(setup_exports, {
5103
5103
  runSetup: () => runSetup
5104
5104
  });
5105
- import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9, chmodSync as chmodSync2, statSync as statSync9 } from "fs";
5106
- import { join as join24 } from "path";
5105
+ import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync10, chmodSync as chmodSync2, statSync as statSync9 } from "fs";
5106
+ import { join as join25 } from "path";
5107
5107
  function baseUrl() {
5108
5108
  const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
5109
5109
  if (fromEnv) return fromEnv.replace(/\/$/, "");
@@ -5135,7 +5135,7 @@ function sleep(ms) {
5135
5135
  return new Promise((resolve4) => setTimeout(resolve4, ms));
5136
5136
  }
5137
5137
  function writeMcpJson(opts) {
5138
- const path7 = join24(process.cwd(), ".mcp.json");
5138
+ const path7 = join25(process.cwd(), ".mcp.json");
5139
5139
  let parsed = {};
5140
5140
  if (existsSync15(path7)) {
5141
5141
  try {
@@ -5160,7 +5160,7 @@ function writeMcpJson(opts) {
5160
5160
  }
5161
5161
  mcpServers.papi = papiEntry;
5162
5162
  parsed.mcpServers = mcpServers;
5163
- writeFileSync9(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5163
+ writeFileSync10(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5164
5164
  try {
5165
5165
  const mode = statSync9(path7).mode & 511;
5166
5166
  if (mode !== 384) chmodSync2(path7, 384);
@@ -5271,7 +5271,7 @@ var init_setup = __esm({
5271
5271
 
5272
5272
  // src/index.ts
5273
5273
  import { readFileSync as readFileSync19 } from "fs";
5274
- import { dirname as dirname7, join as join25, basename as basename2 } from "path";
5274
+ import { dirname as dirname7, join as join26, basename as basename2 } from "path";
5275
5275
  import { fileURLToPath as fileURLToPath5 } from "url";
5276
5276
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5277
5277
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
@@ -5425,7 +5425,7 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
5425
5425
  import path3 from "path";
5426
5426
  import { execSync } from "child_process";
5427
5427
 
5428
- // ../../../../../Users/cathaloullivan/Ai-App-Projects/PapiUI/packages/adapter-md/dist/index.js
5428
+ // ../adapter-md/dist/index.js
5429
5429
  import { readFile, writeFile, access } from "fs/promises";
5430
5430
  import { randomUUID as randomUUID6 } from "crypto";
5431
5431
  import { join } from "path";
@@ -8400,7 +8400,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8400
8400
  // src/server.ts
8401
8401
  import { readFileSync as readFileSync14 } from "fs";
8402
8402
  import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
8403
- import { join as join20, dirname as dirname6 } from "path";
8403
+ import { join as join21, dirname as dirname6 } from "path";
8404
8404
  import { fileURLToPath as fileURLToPath4 } from "url";
8405
8405
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8406
8406
  import {
@@ -9193,6 +9193,19 @@ If a candidate AD body could be invalidated by running a SQL query, refreshing a
9193
9193
  **Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
9194
9194
 
9195
9195
  This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
9196
+ var AD_CONFLICT_SURFACING_RULES = `**A contradiction is NOT a veto \u2014 surface it, never silently shelve it.**
9197
+
9198
+ Active Decisions are *active*: they can be superseded, modified, or abandoned. You do NOT have authority to kill a piece of work simply because it cuts against one. That is the user's call, and they can only make it if you show it to them.
9199
+
9200
+ **Never do any of these to a task solely because it contradicts, competes with, or duplicates the intent of an existing AD:** cancel it; drop it to P3; defer it silently; omit it from the cycle without comment; or reduce its scope down to the part that fits the AD.
9201
+
9202
+ **Instead, emit a \`decisionConflicts\` entry** for it (see the structured-output schema) with: the task, the AD it collides with, one line on what the AD says versus what the task implies, 2-3 concrete options, and your recommendation. Also render a \`## Decisions Needed\` section in Part 1 listing the same, immediately before the BUILD HANDOFFs.
9203
+
9204
+ Apply will park each conflicted task as **Blocked** with a \`decision-gate\` blocker pointing at that AD. It clears automatically the moment the AD is resolved, confirmed, validated, or superseded \u2014 so the work returns to the board on its own once the user has decided. The task is preserved, not killed.
9205
+
9206
+ **Emit a conflict ONLY when the AD is the reason the work would not otherwise proceed.** If the task is fine to schedule, schedule it. If it is genuinely obsolete, duplicated, or already shipped, cancel it on THOSE grounds and say so \u2014 that is not an AD conflict. Do not manufacture conflicts to look thorough; a plan with zero real conflicts should emit an empty array.
9207
+
9208
+ This applies equally to *competing* work: two tasks proposing rival approaches to the same problem is a decision for the user, not a coin-flip for you.`;
9196
9209
  var OUTPUT_QUALITY_RUBRIC = `Quality bar \u2014 before emitting, self-score the artifact 1-10 on five dimensions: (1) **Clarity** \u2014 could a third LLM act on it with no extra context? (2) **Scope tightness** \u2014 one focused unit of work, not three bundled together. (3) **Specificity** \u2014 it names concrete files/paths/tasks/ADs, not vague references like "the auth module". (4) **Dependency surfacing** \u2014 prerequisite or upstream items are called out explicitly. (5) **Success-criteria concreteness** \u2014 "done" is testable and observable, not "looks good". Sum to /50. If any single dimension scores \u22644, or the total is <35, revise the artifact and re-score before emitting \u2014 do not ship a below-threshold artifact. This is a self-check gate, not an output field: do NOT add the scores to the artifact or the structured JSON.`;
9197
9210
  var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
9198
9211
  You receive project context and produce a planning cycle output with a BUILD HANDOFF.
@@ -9273,7 +9286,8 @@ After your natural language output, include this EXACT format on its own line:
9273
9286
  "newTasks": [],
9274
9287
  "boardCorrections": [],
9275
9288
  "productBrief": null,
9276
- "activeDecisions": []
9289
+ "activeDecisions": [],
9290
+ "decisionConflicts": [{"taskId": "string \u2014 an existing task ID or a newTasks tempId", "adId": "string \u2014 AD-N this collides with", "conflict": "string \u2014 one line: what the AD says vs what this task implies", "options": ["string \u2014 concrete option, e.g. 'Supersede AD-12 and schedule the task'"], "recommendation": "string \u2014 which option you would take and why, in one sentence"}]
9277
9291
  }
9278
9292
  \`\`\`
9279
9293
 
@@ -9310,6 +9324,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
9310
9324
  - Updated or created Active Decisions in Part 1? \u2192 Put them in \`activeDecisions\` array (with id and full body including ### heading)
9311
9325
  - Found board corrections (wrong priority, missing fields, stale status) in Part 1? \u2192 Put them in \`boardCorrections\` array
9312
9326
  - Generated BUILD HANDOFFs in Part 1? \u2192 Put them in \`cycleHandoffs\` array
9327
+ - Wrote a \`## Decisions Needed\` section in Part 1? \u2192 Put every entry in \`decisionConflicts\`. Nothing in that array means the user is never asked, and the work is lost. Omit the array (or leave it empty) when there are genuinely no AD conflicts.
9313
9328
  - **\`complexity\` uses the LONG forms only** \u2014 "XS", "Small", "Medium", "Large", "XL". Do NOT reuse the handoff EFFORT short-forms (S/M/L) for task complexity.
9314
9329
 
9315
9330
  **Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
@@ -9497,7 +9512,7 @@ Standard planning cycle with full board review.
9497
9512
  **\u26A0\uFE0F PRIORITY RECALIBRATION \u2014 do NOT rubber-stamp the submitted priority.** The priority set at idea submission reflects the submitter's view at that time, which may be outdated by the time the planner runs. For EVERY unreviewed task, evaluate its priority FROM SCRATCH against: (a) current horizon/stage/phase goals, (b) recent Active Decision changes, (c) recently shipped functionality that makes this task more or less urgent. If your assessed priority differs from the submitted one, set the new priority in \`boardCorrections\` and include the change in a **Priority Recalibration** paragraph in your cycle log (Step 8): list each changed task by ID, old priority \u2192 new priority, and a 1-sentence rationale. This paragraph is how the user sees what the planner recalibrated and why. If no priorities changed during triage, omit the paragraph.
9498
9513
  Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
9499
9514
  **Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
9500
- If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
9515
+ If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why. **"It contradicts an Active Decision" is NOT one of those reasons** \u2014 route it to \`decisionConflicts\` instead (see the AD Conflict Surfacing rule below step 7).
9501
9516
  **\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
9502
9517
 
9503
9518
  3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
@@ -9540,6 +9555,10 @@ Standard planning cycle with full board review.
9540
9555
  **Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
9541
9556
  **Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
9542
9557
 
9558
+ ${AD_CONFLICT_SURFACING_RULES}
9559
+
9560
+ **Where this fires:** during Inbox Triage (step 2), the Priority Drift Check (step 3), and selection (step 7). A task whose notes already carry an \`AD-CONFLICT:\` line, or an "AD-N alignment/conflict candidate" note from the idea tool, is a mandatory candidate \u2014 read it, and either schedule it, cancel it on non-AD grounds, or emit the conflict. Do not leave it sitting silently in Backlog for a third cycle.
9561
+
9543
9562
  8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
9544
9563
  **Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
9545
9564
 
@@ -9989,6 +10008,13 @@ function coerceStructuredOutput(parsed) {
9989
10008
  })) : [];
9990
10009
  const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
9991
10010
  const carryForward = coerceCarryForward(parsed.cycleLogCarryForward);
10011
+ const decisionConflicts = Array.isArray(parsed.decisionConflicts) ? parsed.decisionConflicts.map((c) => ({
10012
+ taskId: coerceToString(c.taskId).trim(),
10013
+ adId: coerceToString(c.adId).trim(),
10014
+ conflict: coerceToString(c.conflict).trim(),
10015
+ options: Array.isArray(c.options) ? c.options.map((o) => coerceToString(o)) : [],
10016
+ recommendation: coerceToString(c.recommendation).trim()
10017
+ })).filter((c) => c.taskId.length > 0 && c.adId.length > 0) : void 0;
9992
10018
  return {
9993
10019
  cycleLogTitle: coerceToString(parsed.cycleLogTitle),
9994
10020
  cycleLogContent: coerceToString(parsed.cycleLogContent),
@@ -10004,7 +10030,8 @@ function coerceStructuredOutput(parsed) {
10004
10030
  newTasks,
10005
10031
  boardCorrections,
10006
10032
  productBrief: parsed.productBrief === null ? null : coerceToString(parsed.productBrief),
10007
- activeDecisions
10033
+ activeDecisions,
10034
+ decisionConflicts
10008
10035
  };
10009
10036
  }
10010
10037
  var REVIEW_SYSTEM_COMPRESSION_SECTION = `
@@ -10079,6 +10106,7 @@ You MUST cover these 5 sections. Each is mandatory.
10079
10106
  - Only flag ADs that represent a genuine strategic question requiring owner input
10080
10107
  - Note any hierarchy/phase issues worth correcting (1-2 bullets max)
10081
10108
  - Delete ADs that are legacy, process-level, or redundant without discussion
10109
+ - **Blocked-on-decision tasks are evidence (task-3219).** If the board carries tasks Blocked behind a \`decision-gate\` on an AD, that AD is actively costing the project work. Resolve it here \u2014 supersede, modify, or explicitly reaffirm it with a one-line reason. Reaffirming is a legitimate answer; leaving it unaddressed is not, because the blocked work stays parked until someone rules.
10082
10110
 
10083
10111
  ${AD_REJECTION_RULES}
10084
10112
 
@@ -11692,6 +11720,52 @@ function applyContextTier(ctx, cycleCount) {
11692
11720
  }
11693
11721
  return { tier, label };
11694
11722
  }
11723
+ var PARKABLE_STATUSES = /* @__PURE__ */ new Set([
11724
+ "Backlog",
11725
+ "In Cycle",
11726
+ "Ready",
11727
+ "Deferred"
11728
+ ]);
11729
+ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedCycle, scheduledTaskIds = []) {
11730
+ if (conflicts.length === 0) return [];
11731
+ const scheduled = new Set(scheduledTaskIds.map((id) => id.toLowerCase()));
11732
+ const results = [];
11733
+ for (const conflict of conflicts) {
11734
+ const resolvedTaskId = newTaskIdMap.get(conflict.taskId) ?? conflict.taskId;
11735
+ const base = { ...conflict, resolvedTaskId, parked: false };
11736
+ if (scheduled.has(resolvedTaskId.toLowerCase())) {
11737
+ results.push({ ...base, skipReason: "scheduled in this cycle \u2014 left in the cycle, conflict still surfaced" });
11738
+ continue;
11739
+ }
11740
+ try {
11741
+ const [task] = await adapter2.getTasks([resolvedTaskId]);
11742
+ if (!task) {
11743
+ results.push({ ...base, skipReason: "task not found" });
11744
+ continue;
11745
+ }
11746
+ if (!PARKABLE_STATUSES.has(task.status)) {
11747
+ results.push({ ...base, skipReason: `status is ${task.status} \u2014 left as-is` });
11748
+ continue;
11749
+ }
11750
+ if (task.status === "Deferred") {
11751
+ await adapter2.updateTask(resolvedTaskId, { status: "Backlog" });
11752
+ }
11753
+ await adapter2.updateTask(resolvedTaskId, {
11754
+ status: "Blocked",
11755
+ blocker: {
11756
+ type: "decision-gate",
11757
+ ref: conflict.adId,
11758
+ reason: conflict.conflict || `Contradicts ${conflict.adId} \u2014 awaiting the owner's decision.`,
11759
+ blockedCycle
11760
+ }
11761
+ });
11762
+ results.push({ ...base, parked: true });
11763
+ } catch (err) {
11764
+ results.push({ ...base, skipReason: err instanceof Error ? err.message : String(err) });
11765
+ }
11766
+ }
11767
+ return results;
11768
+ }
11695
11769
  function determineMode(totalCycles) {
11696
11770
  if (totalCycles === 0) return "bootstrap";
11697
11771
  return "full";
@@ -13203,6 +13277,7 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
13203
13277
  let writeBackWarnings;
13204
13278
  let writeSummary;
13205
13279
  let skippedCancellations;
13280
+ let decisionConflicts;
13206
13281
  const persistenceFailed = !data;
13207
13282
  if (data) {
13208
13283
  try {
@@ -13229,6 +13304,19 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
13229
13304
  for (const [placeholder, realId] of newTaskIdMap) {
13230
13305
  resolvedDisplayText = resolvedDisplayText.replaceAll(placeholder, realId);
13231
13306
  }
13307
+ if (data.decisionConflicts && data.decisionConflicts.length > 0) {
13308
+ try {
13309
+ const parked = await parkDecisionConflicts(
13310
+ adapter2,
13311
+ data.decisionConflicts,
13312
+ newTaskIdMap,
13313
+ cycleNumber + 1,
13314
+ ws.taskIds
13315
+ );
13316
+ if (parked.length > 0) decisionConflicts = parked;
13317
+ } catch {
13318
+ }
13319
+ }
13232
13320
  if (adapter2.insertPlanRun) {
13233
13321
  const durationMs = Date.now() - applyStartMs + (planRunMeta?.prepareStartMs !== void 0 ? Date.now() - planRunMeta.prepareStartMs : 0);
13234
13322
  adapter2.insertPlanRun({
@@ -13281,7 +13369,8 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
13281
13369
  writeBackFailed,
13282
13370
  writeBackWarnings,
13283
13371
  writeSummary,
13284
- skippedCancellations
13372
+ skippedCancellations,
13373
+ decisionConflicts
13285
13374
  };
13286
13375
  }
13287
13376
  async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker, density) {
@@ -13900,6 +13989,47 @@ async function resolveLlmResponse(inlineResponse, filePath) {
13900
13989
  return { ok: true, llmResponse: contents };
13901
13990
  }
13902
13991
 
13992
+ // src/lib/overflow.ts
13993
+ import { writeFileSync as writeFileSync2 } from "fs";
13994
+ import { tmpdir } from "os";
13995
+ import { join as join3 } from "path";
13996
+ import { createHash as createHash2 } from "crypto";
13997
+ var DISPATCH_THRESHOLD_BYTES = 50 * 1024;
13998
+ var ELIDE_THRESHOLD_BYTES = 90 * 1024;
13999
+ var INLINE_CEILING_BYTES = 40 * 1024;
14000
+ function byteLength(s) {
14001
+ return Buffer.byteLength(s, "utf-8");
14002
+ }
14003
+ function shouldDispatch(contextBytes, explicit) {
14004
+ if (explicit === true) return true;
14005
+ if (explicit === false) return false;
14006
+ if (process.env.PAPI_AUTO_DISPATCH === "false") return false;
14007
+ return contextBytes > DISPATCH_THRESHOLD_BYTES;
14008
+ }
14009
+ function canSpill(adapterType) {
14010
+ return adapterType !== "proxy";
14011
+ }
14012
+ function spillPath(kind, projectId, callerKey) {
14013
+ const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? "default"}`).digest("hex").slice(0, 16);
14014
+ return join3(tmpdir(), `papi-${kind}-${id}.md`);
14015
+ }
14016
+ function spill(kind, content, projectId, callerKey) {
14017
+ const path7 = spillPath(kind, projectId, callerKey);
14018
+ writeFileSync2(path7, content, { mode: 384 });
14019
+ return path7;
14020
+ }
14021
+ function planDelivery(input) {
14022
+ const bytes = byteLength(input.payload);
14023
+ if (bytes <= INLINE_CEILING_BYTES) return { mode: "inline", payload: input.payload };
14024
+ if (!canSpill(input.adapterType)) return { mode: "undeliverable", bytes };
14025
+ try {
14026
+ const path7 = spill(input.kind, input.payload, input.projectId, input.callerKey);
14027
+ return { mode: "spilled", path: path7, bytes };
14028
+ } catch {
14029
+ return { mode: "undeliverable", bytes };
14030
+ }
14031
+ }
14032
+
13903
14033
  // src/services/session-guidance.ts
13904
14034
  var DEFAULT_CALLER_KEY = "__default__";
13905
14035
  var sessionStates = /* @__PURE__ */ new Map();
@@ -14100,20 +14230,20 @@ var PerCallerCache = class {
14100
14230
  };
14101
14231
 
14102
14232
  // src/lib/plan-prepare-store.ts
14103
- import { createHash as createHash2 } from "crypto";
14104
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, existsSync, statSync } from "fs";
14105
- import { tmpdir } from "os";
14106
- import { join as join3 } from "path";
14233
+ import { createHash as createHash3 } from "crypto";
14234
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, unlinkSync, existsSync, statSync } from "fs";
14235
+ import { tmpdir as tmpdir2 } from "os";
14236
+ import { join as join4 } from "path";
14107
14237
  var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
14108
14238
  var DEFAULT_CALLER_KEY3 = "__default__";
14109
- function spillPath(projectId, callerKey) {
14110
- const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
14111
- return join3(tmpdir(), `papi-plan-prepare-${id}.json`);
14239
+ function spillPath2(projectId, callerKey) {
14240
+ const id = createHash3("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
14241
+ return join4(tmpdir2(), `papi-plan-prepare-${id}.json`);
14112
14242
  }
14113
14243
  function savePrepareSpill(projectId, callerKey, state) {
14114
14244
  try {
14115
- writeFileSync2(
14116
- spillPath(projectId, callerKey),
14245
+ writeFileSync3(
14246
+ spillPath2(projectId, callerKey),
14117
14247
  JSON.stringify({ savedAt: Date.now(), state }),
14118
14248
  { mode: 384 }
14119
14249
  );
@@ -14121,7 +14251,7 @@ function savePrepareSpill(projectId, callerKey, state) {
14121
14251
  }
14122
14252
  }
14123
14253
  function loadPrepareSpill(projectId, callerKey) {
14124
- const path7 = spillPath(projectId, callerKey);
14254
+ const path7 = spillPath2(projectId, callerKey);
14125
14255
  try {
14126
14256
  if (!existsSync(path7)) return void 0;
14127
14257
  if (Date.now() - statSync(path7).mtimeMs > SPILL_TTL_MS) {
@@ -14136,18 +14266,18 @@ function loadPrepareSpill(projectId, callerKey) {
14136
14266
  }
14137
14267
  function clearPrepareSpill(projectId, callerKey) {
14138
14268
  try {
14139
- const path7 = spillPath(projectId, callerKey);
14269
+ const path7 = spillPath2(projectId, callerKey);
14140
14270
  if (existsSync(path7)) unlinkSync(path7);
14141
14271
  } catch {
14142
14272
  }
14143
14273
  }
14144
14274
  function contextPath(projectId, callerKey) {
14145
- const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
14146
- return join3(tmpdir(), `papi-plan-context-${id}.md`);
14275
+ const id = createHash3("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
14276
+ return join4(tmpdir2(), `papi-plan-context-${id}.md`);
14147
14277
  }
14148
14278
  function savePrepareContextFile(projectId, callerKey, content) {
14149
14279
  const path7 = contextPath(projectId, callerKey);
14150
- writeFileSync2(path7, content, { mode: 384 });
14280
+ writeFileSync3(path7, content, { mode: 384 });
14151
14281
  return path7;
14152
14282
  }
14153
14283
 
@@ -14287,6 +14417,20 @@ function formatPlanResult(result) {
14287
14417
  lines.push('To apply these cancellations, re-run `plan` with `mode: "apply"` and `confirm_cancellations: true`.');
14288
14418
  lines.push("To keep these tasks, do nothing \u2014 they remain on the board.");
14289
14419
  }
14420
+ const conflicts = result.decisionConflicts ?? [];
14421
+ if (conflicts.length > 0) {
14422
+ lines.push("");
14423
+ lines.push(`\u{1F500} **${conflicts.length} decision(s) need your call \u2014 work was NOT dropped.**`);
14424
+ lines.push("These tasks cut against a live Active Decision. An AD is *active* \u2014 it can be superseded, so the planner parked the work instead of shelving it:");
14425
+ for (const c of conflicts) {
14426
+ const state = c.parked ? `Blocked behind ${c.adId}` : `still ${c.skipReason ?? "unparked"}`;
14427
+ lines.push(`- **${c.resolvedTaskId} vs ${c.adId}** \u2014 ${c.conflict} _(${state})_`);
14428
+ if (c.options.length > 0) lines.push(` Options: ${c.options.join(" | ")}`);
14429
+ if (c.recommendation) lines.push(` Planner recommends: ${c.recommendation}`);
14430
+ }
14431
+ lines.push("");
14432
+ lines.push('Decide: `strategy_change` with `mode: "capture"` to supersede or modify the AD (parked tasks auto-unblock once it moves), or `board_edit` to cancel the task if the AD stands.');
14433
+ }
14290
14434
  if (result.skipHandoffs) {
14291
14435
  const taskCount = result.writeSummary?.taskIds.length ?? 0;
14292
14436
  lines.push("", `Next: run \`handoff_generate\` to create BUILD HANDOFFs for your ${taskCount} cycle task(s), then \`build_list\` to start building.`);
@@ -14409,16 +14553,8 @@ async function handlePlan(adapter2, config2, args) {
14409
14553
  };
14410
14554
  planPrepareCache.set(callerKey, prepareState);
14411
14555
  savePrepareSpill(adapter2.getProjectId?.(), callerKey, prepareState);
14412
- const autoDispatchEnabled = process.env.PAPI_AUTO_DISPATCH !== "false";
14413
- const autoDispatchThreshold = 50 * 1024;
14414
- let dispatch;
14415
- if (args.dispatch === "inline" || args.dispatch === "subagent") {
14416
- dispatch = args.dispatch;
14417
- } else if (autoDispatchEnabled && result.contextBytes !== void 0 && result.contextBytes > autoDispatchThreshold) {
14418
- dispatch = "subagent";
14419
- } else {
14420
- dispatch = "inline";
14421
- }
14556
+ const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
14557
+ const dispatch = shouldDispatch(result.contextBytes ?? 0, explicit) ? "subagent" : "inline";
14422
14558
  const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
14423
14559
  const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
14424
14560
  ` : "";
@@ -14532,10 +14668,10 @@ ${result.userMessage}
14532
14668
  }
14533
14669
 
14534
14670
  // src/services/strategy.ts
14535
- import { randomUUID as randomUUID10, createHash as createHash3 } from "crypto";
14671
+ import { randomUUID as randomUUID10, createHash as createHash4 } from "crypto";
14536
14672
  import { execFileSync as execFileSync2 } from "child_process";
14537
14673
  import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
14538
- import { join as join4 } from "path";
14674
+ import { join as join5 } from "path";
14539
14675
  import { homedir as homedir2 } from "os";
14540
14676
 
14541
14677
  // src/services/idea.ts
@@ -14885,14 +15021,22 @@ ${lines.join("\n")}
14885
15021
  }
14886
15022
  }
14887
15023
  let notesWithAlignment = input.notes || "";
15024
+ let detectedAdIds = [];
14888
15025
  try {
14889
15026
  const ads = await adapter2.getActiveDecisions();
14890
15027
  const ideaCombined = `${input.text} ${input.notes ?? ""}`;
14891
15028
  const alignmentMatches = findAdAlignmentMatches(ideaCombined, ads);
14892
15029
  if (alignmentMatches.length > 0) {
14893
15030
  notesWithAlignment = `${notesWithAlignment}${formatAdAlignmentNote(alignmentMatches)}`.trim();
15031
+ detectedAdIds = alignmentMatches.map((m) => m.displayId);
14894
15032
  }
14895
15033
  } catch {
15034
+ }
15035
+ const conflictsWithAd = input.conflictsWithAd?.trim();
15036
+ if (conflictsWithAd) {
15037
+ notesWithAlignment = `${notesWithAlignment}
15038
+
15039
+ AD-CONFLICT: ${conflictsWithAd} \u2014 submitter flagged this as cutting against that decision. Owner decides: supersede the AD or drop the task.`.trim();
14896
15040
  }
14897
15041
  const inherited = await resolveVisibilityFromDocs(adapter2, [input.docRef]);
14898
15042
  const task = await adapter2.createTask({
@@ -14961,17 +15105,52 @@ ${lines.join("\n")}
14961
15105
  } catch {
14962
15106
  }
14963
15107
  }
15108
+ let gated = false;
15109
+ if (conflictsWithAd) {
15110
+ try {
15111
+ await adapter2.updateTask(task.id, {
15112
+ status: "Blocked",
15113
+ blocker: {
15114
+ type: "decision-gate",
15115
+ ref: conflictsWithAd,
15116
+ reason: `Submitted as cutting against ${conflictsWithAd}. Awaiting the owner's decision.`,
15117
+ blockedCycle: health.totalCycles
15118
+ }
15119
+ });
15120
+ gated = true;
15121
+ } catch {
15122
+ }
15123
+ }
14964
15124
  const typeNote = typeInferred ? ` [type: ${taskType} \u2014 inferred from text]` : "";
14965
15125
  const visibilityNote = inherited.visibility !== "public" ? ` [visibility: ${inherited.visibility} \u2014 inherited from source doc]` : "";
14966
15126
  const mismatchNote = inherited.mismatchWarning ? `
14967
15127
 
14968
15128
  \u26A0\uFE0F ${inherited.mismatchWarning}` : "";
15129
+ const adIds = conflictsWithAd ? [conflictsWithAd] : detectedAdIds;
15130
+ const landing = gated ? "created and gated on the decision" : "added to backlog";
14969
15131
  return {
14970
15132
  routing: "task",
14971
15133
  task,
14972
- message: `${task.id}: "${task.title}" \u2014 added to backlog${typeNote}${visibilityNote}${mismatchNote}`
15134
+ message: `${task.id}: "${task.title}" \u2014 ${landing}${typeNote}${visibilityNote}${mismatchNote}`,
15135
+ ...adIds.length > 0 ? { adConflicts: { adIds, explicit: Boolean(conflictsWithAd), gated } } : {}
14973
15136
  };
14974
15137
  }
15138
+ function buildAdConflictNote(conflictsWithAd, detectedAdIds, gated) {
15139
+ if (conflictsWithAd) {
15140
+ const state = gated ? `Parked as **Blocked** behind a decision gate on ${conflictsWithAd} \u2014 it auto-unblocks once that AD is superseded, modified, or reaffirmed.` : `Recorded against ${conflictsWithAd} (gate could not be applied \u2014 the AD-CONFLICT note is on the task).`;
15141
+ return `
15142
+
15143
+ \u{1F500} **Conflicts with ${conflictsWithAd}.** ${state}
15144
+ **Tell the user now** \u2014 an AD is *active*, so this is their call to make, not yours. Offer both paths: supersede the AD via \`strategy_change\`, or leave the AD standing and drop the task.`;
15145
+ }
15146
+ if (detectedAdIds.length > 0) {
15147
+ return `
15148
+
15149
+ \u{1F500} **Possible conflict with ${detectedAdIds.join(", ")}** (keyword match \u2014 verify it is real).
15150
+ If it IS a real contradiction, **do not shelve the idea on those grounds**: surface it to the user and re-submit with \`conflicts_with_ad\` so the task is gated on the decision instead of quietly competing in the backlog.`;
15151
+ }
15152
+ return "";
15153
+ }
14975
15154
  var CANVAS_SECTION_LABELS = {
14976
15155
  landscape: "Landscape References",
14977
15156
  journeys: "User Journeys",
@@ -15542,7 +15721,7 @@ ${surpriseDigestText}` : buildReportsText;
15542
15721
  try {
15543
15722
  const fullCanvasText = formatDiscoveryCanvas(canvas);
15544
15723
  if (fullCanvasText) {
15545
- const canvasHash = createHash3("md5").update(fullCanvasText).digest("hex");
15724
+ const canvasHash = createHash4("md5").update(fullCanvasText).digest("hex");
15546
15725
  const lastReview = previousStrategyReviews?.[0];
15547
15726
  const prevHash = lastReview?.structuredData?.canvasHash;
15548
15727
  if (prevHash && prevHash === canvasHash) {
@@ -15612,11 +15791,11 @@ ${lines.join("\n")}`;
15612
15791
  }
15613
15792
  let recentPlansText;
15614
15793
  try {
15615
- const plansDir = join4(homedir2(), ".claude", "plans");
15794
+ const plansDir = join5(homedir2(), ".claude", "plans");
15616
15795
  if (existsSync2(plansDir)) {
15617
15796
  const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
15618
15797
  const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
15619
- const fullPath = join4(plansDir, f);
15798
+ const fullPath = join5(plansDir, f);
15620
15799
  const stat4 = statSync2(fullPath);
15621
15800
  return { name: f, modified: stat4.mtime, size: stat4.size };
15622
15801
  }).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
@@ -15633,7 +15812,7 @@ ${lines.join("\n")}`;
15633
15812
  }
15634
15813
  let unregisteredDocsText;
15635
15814
  try {
15636
- const docsDir = join4(projectRoot, "docs");
15815
+ const docsDir = join5(projectRoot, "docs");
15637
15816
  if (hasLocalWorkspace() && existsSync2(docsDir)) {
15638
15817
  const registeredPaths = new Set(
15639
15818
  (registeredDocs ?? []).map((d) => d.path).filter(Boolean)
@@ -15641,7 +15820,7 @@ ${lines.join("\n")}`;
15641
15820
  const allDocFiles = [];
15642
15821
  const scanDir = (dir, prefix) => {
15643
15822
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
15644
- if (entry.isDirectory()) scanDir(join4(dir, entry.name), `${prefix}${entry.name}/`);
15823
+ if (entry.isDirectory()) scanDir(join5(dir, entry.name), `${prefix}${entry.name}/`);
15645
15824
  else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
15646
15825
  }
15647
15826
  };
@@ -15981,7 +16160,7 @@ ${cleanContent}`;
15981
16160
  const currentCanvas = await adapter2.readDiscoveryCanvas();
15982
16161
  const canvasText = formatDiscoveryCanvas(currentCanvas);
15983
16162
  if (canvasText) {
15984
- return { ...sd, canvasHash: createHash3("md5").update(canvasText).digest("hex") };
16163
+ return { ...sd, canvasHash: createHash4("md5").update(canvasText).digest("hex") };
15985
16164
  }
15986
16165
  } catch {
15987
16166
  }
@@ -17105,16 +17284,8 @@ ${recLines.join("\n")}
17105
17284
  userMessage: result.userMessage,
17106
17285
  contextBytes: reviewContextBytes
17107
17286
  });
17108
- const autoDispatchEnabled = process.env.PAPI_AUTO_DISPATCH !== "false";
17109
- const autoDispatchThreshold = 50 * 1024;
17110
- let dispatch;
17111
- if (args.dispatch === "inline" || args.dispatch === "subagent") {
17112
- dispatch = args.dispatch;
17113
- } else if (autoDispatchEnabled && reviewContextBytes > autoDispatchThreshold) {
17114
- dispatch = "subagent";
17115
- } else {
17116
- dispatch = "inline";
17117
- }
17287
+ const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
17288
+ const dispatch = shouldDispatch(reviewContextBytes, explicit) ? "subagent" : "inline";
17118
17289
  if (dispatch === "subagent") {
17119
17290
  const dispatchPrompt = buildSubagentDispatchPrompt({
17120
17291
  tool: "strategy_review",
@@ -18125,15 +18296,15 @@ ${existing}` : entry;
18125
18296
 
18126
18297
  // src/services/setup.ts
18127
18298
  import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
18128
- import { join as join9, basename, extname, dirname as dirname3 } from "path";
18299
+ import { join as join10, basename, extname, dirname as dirname3, relative } from "path";
18129
18300
  import { execFileSync as execFileSync3 } from "child_process";
18130
18301
 
18131
18302
  // src/lib/detect-codebase.ts
18132
18303
  import { existsSync as existsSync3 } from "fs";
18133
18304
  import { readdirSync as readdirSync2, statSync as statSync3 } from "fs";
18134
- import { join as join5 } from "path";
18305
+ import { join as join6 } from "path";
18135
18306
  function detectCodebaseType(projectRoot) {
18136
- if (existsSync3(join5(projectRoot, ".git"))) {
18307
+ if (existsSync3(join6(projectRoot, ".git"))) {
18137
18308
  return "existing_codebase";
18138
18309
  }
18139
18310
  const manifests = [
@@ -18147,7 +18318,7 @@ function detectCodebaseType(projectRoot) {
18147
18318
  "CMakeLists.txt"
18148
18319
  ];
18149
18320
  for (const manifest of manifests) {
18150
- if (existsSync3(join5(projectRoot, manifest))) {
18321
+ if (existsSync3(join6(projectRoot, manifest))) {
18151
18322
  return "existing_codebase";
18152
18323
  }
18153
18324
  }
@@ -18155,7 +18326,7 @@ function detectCodebaseType(projectRoot) {
18155
18326
  const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
18156
18327
  const fileCount = entries.filter((f) => {
18157
18328
  try {
18158
- return statSync3(join5(projectRoot, f)).isFile();
18329
+ return statSync3(join6(projectRoot, f)).isFile();
18159
18330
  } catch {
18160
18331
  return false;
18161
18332
  }
@@ -18168,20 +18339,20 @@ function detectCodebaseType(projectRoot) {
18168
18339
 
18169
18340
  // src/lib/agents-bundle.ts
18170
18341
  import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
18171
- import { dirname, join as join6, resolve } from "path";
18342
+ import { dirname, join as join7, resolve } from "path";
18172
18343
  import { fileURLToPath } from "url";
18173
- var PROJECT_BUNDLE_REL = join6(".agents", "skills", "papi-cycle");
18344
+ var PROJECT_BUNDLE_REL = join7(".agents", "skills", "papi-cycle");
18174
18345
  function bundleDestRel(rel) {
18175
- return rel === "AGENTS.md" ? "AGENTS.md" : join6(PROJECT_BUNDLE_REL, rel);
18346
+ return rel === "AGENTS.md" ? "AGENTS.md" : join7(PROJECT_BUNDLE_REL, rel);
18176
18347
  }
18177
18348
  function claudeSkillDestRel(rel) {
18178
- return rel === "AGENTS.md" ? void 0 : join6(".claude", "skills", rel);
18349
+ return rel === "AGENTS.md" ? void 0 : join7(".claude", "skills", rel);
18179
18350
  }
18180
18351
  function resolveBundleDir() {
18181
18352
  let dir = dirname(fileURLToPath(import.meta.url));
18182
18353
  for (let i = 0; i < 5; i++) {
18183
- const candidate = join6(dir, "skills", "papi-cycle");
18184
- if (existsSync4(join6(candidate, "AGENTS.md"))) return candidate;
18354
+ const candidate = join7(dir, "skills", "papi-cycle");
18355
+ if (existsSync4(join7(candidate, "AGENTS.md"))) return candidate;
18185
18356
  const parent = resolve(dir, "..");
18186
18357
  if (parent === dir) break;
18187
18358
  dir = parent;
@@ -18193,8 +18364,8 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
18193
18364
  const files = [];
18194
18365
  const walk = (abs, rel) => {
18195
18366
  for (const entry of readdirSync3(abs, { withFileTypes: true })) {
18196
- const childAbs = join6(abs, entry.name);
18197
- const childRel = rel ? join6(rel, entry.name) : entry.name;
18367
+ const childAbs = join7(abs, entry.name);
18368
+ const childRel = rel ? join7(rel, entry.name) : entry.name;
18198
18369
  if (entry.isDirectory()) walk(childAbs, childRel);
18199
18370
  else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync3(childAbs, "utf8") });
18200
18371
  }
@@ -18203,13 +18374,14 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
18203
18374
  return files;
18204
18375
  }
18205
18376
  function planBundleInstall(projectRoot, projectName, opts = {}) {
18377
+ const canProbe = opts.skipExisting === true && projectRoot !== "";
18206
18378
  const out = {};
18207
18379
  for (const f of readBundleFiles()) {
18208
18380
  const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
18209
18381
  for (const rel of [bundleDestRel(f.rel), claudeSkillDestRel(f.rel)]) {
18210
18382
  if (!rel) continue;
18211
- const dest = join6(projectRoot, rel);
18212
- if (opts.skipExisting && existsSync4(dest) && statSync4(dest).isFile()) continue;
18383
+ const dest = join7(projectRoot, rel);
18384
+ if (canProbe && existsSync4(dest) && statSync4(dest).isFile()) continue;
18213
18385
  out[dest] = content;
18214
18386
  }
18215
18387
  }
@@ -18218,19 +18390,19 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
18218
18390
 
18219
18391
  // src/lib/design-bundle.ts
18220
18392
  import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync5 } from "fs";
18221
- import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
18393
+ import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
18222
18394
  import { fileURLToPath as fileURLToPath2 } from "url";
18223
18395
  var DESIGN_ASSETS = [
18224
- { srcRel: join7("agents", "frontend-design-engineer.md"), destRel: join7(".claude", "agents", "frontend-design-engineer.md"), executable: false },
18225
- { srcRel: join7("skills", "design-critique", "SKILL.md"), destRel: join7(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
18226
- { srcRel: join7("hooks", "frontend-design-guard.sh"), destRel: join7(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
18396
+ { srcRel: join8("agents", "frontend-design-engineer.md"), destRel: join8(".claude", "agents", "frontend-design-engineer.md"), executable: false },
18397
+ { srcRel: join8("skills", "design-critique", "SKILL.md"), destRel: join8(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
18398
+ { srcRel: join8("hooks", "frontend-design-guard.sh"), destRel: join8(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
18227
18399
  ];
18228
18400
  var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
18229
18401
  function resolveDesignAssetsDir() {
18230
18402
  let dir = dirname2(fileURLToPath2(import.meta.url));
18231
18403
  for (let i = 0; i < 5; i++) {
18232
- const candidate = join7(dir, "design-assets");
18233
- if (existsSync5(join7(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
18404
+ const candidate = join8(dir, "design-assets");
18405
+ if (existsSync5(join8(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
18234
18406
  const parent = resolve2(dir, "..");
18235
18407
  if (parent === dir) break;
18236
18408
  dir = parent;
@@ -18242,9 +18414,9 @@ function planDesignInstall(projectRoot, opts = {}) {
18242
18414
  if (!assetsDir) return [];
18243
18415
  const out = [];
18244
18416
  for (const asset of DESIGN_ASSETS) {
18245
- const srcAbs = join7(assetsDir, asset.srcRel);
18417
+ const srcAbs = join8(assetsDir, asset.srcRel);
18246
18418
  if (!existsSync5(srcAbs)) continue;
18247
- const dest = projectRoot ? join7(projectRoot, asset.destRel) : asset.destRel;
18419
+ const dest = projectRoot ? join8(projectRoot, asset.destRel) : asset.destRel;
18248
18420
  if (opts.skipExisting && projectRoot && existsSync5(dest) && statSync5(dest).isFile()) continue;
18249
18421
  out.push({ dest, content: readFileSync4(srcAbs, "utf8"), executable: asset.executable });
18250
18422
  }
@@ -18253,9 +18425,9 @@ function planDesignInstall(projectRoot, opts = {}) {
18253
18425
 
18254
18426
  // src/lib/skill-detection.ts
18255
18427
  import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync6 } from "fs";
18256
- import { join as join8 } from "path";
18428
+ import { join as join9 } from "path";
18257
18429
  function readPackageJson(projectRoot) {
18258
- const path7 = join8(projectRoot, "package.json");
18430
+ const path7 = join9(projectRoot, "package.json");
18259
18431
  if (!existsSync6(path7)) return null;
18260
18432
  try {
18261
18433
  const raw = readFileSync5(path7, "utf-8");
@@ -18279,7 +18451,7 @@ function detectsFrontendStack(projectRoot) {
18279
18451
  return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
18280
18452
  }
18281
18453
  function hasGitHubWorkflows(projectRoot) {
18282
- const dir = join8(projectRoot, ".github", "workflows");
18454
+ const dir = join9(projectRoot, ".github", "workflows");
18283
18455
  if (!existsSync6(dir)) return false;
18284
18456
  try {
18285
18457
  const entries = readdirSync4(dir);
@@ -18289,7 +18461,7 @@ function hasGitHubWorkflows(projectRoot) {
18289
18461
  }
18290
18462
  }
18291
18463
  function envExampleMentionsStaging(projectRoot) {
18292
- const path7 = join8(projectRoot, ".env.example");
18464
+ const path7 = join9(projectRoot, ".env.example");
18293
18465
  if (!existsSync6(path7)) return false;
18294
18466
  try {
18295
18467
  const raw = readFileSync5(path7, "utf-8");
@@ -18299,8 +18471,8 @@ function envExampleMentionsStaging(projectRoot) {
18299
18471
  }
18300
18472
  }
18301
18473
  function hasVercelConfig(projectRoot) {
18302
- if (existsSync6(join8(projectRoot, "vercel.json"))) return true;
18303
- const vercelDir = join8(projectRoot, ".vercel");
18474
+ if (existsSync6(join9(projectRoot, "vercel.json"))) return true;
18475
+ const vercelDir = join9(projectRoot, ".vercel");
18304
18476
  if (!existsSync6(vercelDir)) return false;
18305
18477
  try {
18306
18478
  return statSync6(vercelDir).isDirectory();
@@ -18547,6 +18719,7 @@ Active Decisions (ADs) track architectural and product choices with confidence l
18547
18719
  - ADs are for product/architecture choices only, not process preferences.
18548
18720
  - When new evidence appears, update AD confidence via \`strategy_change\`.
18549
18721
  - Supersede rather than overwrite \u2014 old decisions stay as history.
18722
+ - **A contradiction is not a veto.** If an idea or task cuts against a live AD, that is a decision for the user \u2014 never your reason to drop it. Submit it with \`idea\` + \`conflicts_with_ad: "AD-N"\`: the task is created and parked behind a decision gate, surfaces in \`orient\` as waiting on the owner, and returns to the board on its own once the AD is superseded, modified, or reaffirmed. Shelving the work instead means the AD never gets tested by the evidence that would have challenged it.
18550
18723
  - New ADs should include a \`### Reversal Trigger\` section: specify the signal that would invalidate the stance, the action to take (modify/supersede/abandon), and why writing it now prevents sunk-cost drift later.
18551
18724
  `;
18552
18725
  var CLAUDE_MD_TIER_2 = `
@@ -18744,6 +18917,13 @@ function formatFilesToWriteSection(collector) {
18744
18917
  '- `mode: "create"` \u2014 create the file with `content`. If `skip_if_exists: true`, skip when the file already exists.',
18745
18918
  '- `mode: "overwrite"` \u2014 replace the file fully with `content`.',
18746
18919
  '- `mode: "append"` \u2014 append `content` to the existing file (create it first if missing).',
18920
+ '- `mode: "merge-json"` \u2014 the file is JSON. If it does not exist, create it with `content`.',
18921
+ " If it DOES exist, parse both, merge `content` INTO the existing object, and write the",
18922
+ " result back. Merge rules: objects merge key-by-key (recursing into nested objects);",
18923
+ " arrays UNION, keeping the existing order and appending only values not already present;",
18924
+ " scalars in `content` do not overwrite an existing value. NEVER remove, reorder or rewrite",
18925
+ " anything already in the file \u2014 this mode only ADDS. If the existing file is malformed",
18926
+ " JSON, leave it untouched and tell the user rather than replacing their file.",
18747
18927
  ""
18748
18928
  ].join("\n");
18749
18929
  }
@@ -18775,6 +18955,7 @@ function shouldWriteClaudeMd(clientName) {
18775
18955
  return !clientName || /claude/i.test(clientName);
18776
18956
  }
18777
18957
  async function scaffoldPapiDir(adapter2, config2, input, collector) {
18958
+ const preparedFiles = [];
18778
18959
  const isPg = config2.adapterType === "pg" || config2.adapterType === "proxy";
18779
18960
  const vars = {
18780
18961
  project_name: input.projectName,
@@ -18784,12 +18965,12 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18784
18965
  if (!isPg) {
18785
18966
  try {
18786
18967
  await access2(config2.papiDir);
18787
- return false;
18968
+ return { createdProject: false, preparedFiles };
18788
18969
  } catch {
18789
18970
  await mkdir(config2.papiDir, { recursive: true });
18790
18971
  for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
18791
18972
  const content = substitute(template, vars);
18792
- await writeFile2(join9(config2.papiDir, filename), content, "utf-8");
18973
+ await writeFile2(join10(config2.papiDir, filename), content, "utf-8");
18793
18974
  }
18794
18975
  }
18795
18976
  } else {
@@ -18799,7 +18980,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18799
18980
  const templateContent = substitute(PRODUCT_BRIEF_TEMPLATE, vars);
18800
18981
  await adapter2.updateProductBrief(templateContent);
18801
18982
  } else {
18802
- return false;
18983
+ return { createdProject: false, preparedFiles };
18803
18984
  }
18804
18985
  } catch {
18805
18986
  }
@@ -18807,13 +18988,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18807
18988
  const useCollector = config2.adapterType === "proxy";
18808
18989
  const docsRel = "docs";
18809
18990
  const commandsRel = ".claude/commands";
18810
- const commandsDir = useCollector ? commandsRel : join9(config2.projectRoot, ".claude", "commands");
18811
- const docsDir = useCollector ? docsRel : join9(config2.projectRoot, "docs");
18991
+ const commandsDir = useCollector ? commandsRel : join10(config2.projectRoot, ".claude", "commands");
18992
+ const docsDir = useCollector ? docsRel : join10(config2.projectRoot, "docs");
18812
18993
  if (!useCollector) {
18813
18994
  await mkdir(commandsDir, { recursive: true });
18814
18995
  await mkdir(docsDir, { recursive: true });
18815
18996
  }
18816
- const claudeMdPath = useCollector ? "CLAUDE.md" : join9(config2.projectRoot, "CLAUDE.md");
18997
+ const claudeMdPath = useCollector ? "CLAUDE.md" : join10(config2.projectRoot, "CLAUDE.md");
18817
18998
  let claudeMdExists = false;
18818
18999
  if (!useCollector) {
18819
19000
  try {
@@ -18822,7 +19003,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18822
19003
  } catch {
18823
19004
  }
18824
19005
  }
18825
- const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join9(docsDir, "INDEX.md");
19006
+ const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join10(docsDir, "INDEX.md");
18826
19007
  let docsIndexExists = false;
18827
19008
  if (!useCollector) {
18828
19009
  try {
@@ -18832,9 +19013,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18832
19013
  }
18833
19014
  }
18834
19015
  const scaffoldFiles = {
18835
- [useCollector ? `${commandsRel}/papi-audit.md` : join9(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
18836
- [useCollector ? `${commandsRel}/test.md` : join9(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
18837
- [useCollector ? `${docsRel}/README.md` : join9(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
19016
+ [useCollector ? `${commandsRel}/papi-audit.md` : join10(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
19017
+ [useCollector ? `${commandsRel}/test.md` : join10(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
19018
+ [useCollector ? `${docsRel}/README.md` : join10(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
18838
19019
  };
18839
19020
  if (!docsIndexExists) {
18840
19021
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -18861,7 +19042,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18861
19042
  if (useCollector) {
18862
19043
  scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
18863
19044
  } else {
18864
- const cursorDir = join9(config2.projectRoot, ".cursor");
19045
+ const cursorDir = join10(config2.projectRoot, ".cursor");
18865
19046
  let cursorDetected = false;
18866
19047
  try {
18867
19048
  await access2(cursorDir);
@@ -18869,8 +19050,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18869
19050
  } catch {
18870
19051
  }
18871
19052
  if (cursorDetected) {
18872
- const cursorRulesDir = join9(cursorDir, "rules");
18873
- const cursorRulesPath = join9(cursorRulesDir, "papi.mdc");
19053
+ const cursorRulesDir = join10(cursorDir, "rules");
19054
+ const cursorRulesPath = join10(cursorRulesDir, "papi.mdc");
18874
19055
  await mkdir(cursorRulesDir, { recursive: true });
18875
19056
  try {
18876
19057
  await access2(cursorRulesPath);
@@ -18888,6 +19069,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18888
19069
  await writeFile2(filepath, content, "utf-8");
18889
19070
  }
18890
19071
  }
19072
+ for (const p of Object.keys(scaffoldFiles)) {
19073
+ preparedFiles.push(useCollector ? p : relative(config2.projectRoot, p));
19074
+ }
18891
19075
  if (!useCollector && detectsFrontendStack(config2.projectRoot)) {
18892
19076
  for (const entry of planDesignInstall(config2.projectRoot, { skipExisting: true })) {
18893
19077
  await mkdir(dirname3(entry.dest), { recursive: true });
@@ -18912,20 +19096,38 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18912
19096
  }]);
18913
19097
  }
18914
19098
  if (useCollector) {
18915
- collector.add({
18916
- path: ".claude/settings.json",
18917
- content: JSON.stringify({ permissions: { allow: [...PAPI_PERMISSIONS] } }, null, 2) + "\n",
18918
- mode: "create",
18919
- skip_if_exists: true
18920
- });
19099
+ collector.add(papiSettingsCollectorEntry());
18921
19100
  } else {
18922
19101
  await ensurePapiPermission(config2.projectRoot);
18923
19102
  }
18924
- return true;
19103
+ return { createdProject: true, preparedFiles };
18925
19104
  }
18926
19105
  var PAPI_PERMISSIONS = ["mcp__papi__*", "mcp__plugin_papi_papi__*"];
19106
+ function papiSettingsCollectorEntry() {
19107
+ return {
19108
+ path: ".claude/settings.json",
19109
+ content: JSON.stringify(mergePapiPermissions({}), null, 2) + "\n",
19110
+ mode: "merge-json"
19111
+ };
19112
+ }
19113
+ function mergePapiPermissions(settings) {
19114
+ if (!settings.permissions || typeof settings.permissions !== "object") {
19115
+ settings.permissions = {};
19116
+ }
19117
+ const perms = settings.permissions;
19118
+ if (!Array.isArray(perms.allow)) {
19119
+ perms.allow = [];
19120
+ }
19121
+ const allow = perms.allow;
19122
+ for (const permission of PAPI_PERMISSIONS) {
19123
+ if (!allow.includes(permission)) {
19124
+ allow.push(permission);
19125
+ }
19126
+ }
19127
+ return settings;
19128
+ }
18927
19129
  async function ensurePapiPermission(projectRoot) {
18928
- const settingsPath = join9(projectRoot, ".claude", "settings.json");
19130
+ const settingsPath = join10(projectRoot, ".claude", "settings.json");
18929
19131
  try {
18930
19132
  let settings = {};
18931
19133
  try {
@@ -18933,26 +19135,14 @@ async function ensurePapiPermission(projectRoot) {
18933
19135
  settings = JSON.parse(existing);
18934
19136
  } catch {
18935
19137
  }
18936
- if (!settings.permissions || typeof settings.permissions !== "object") {
18937
- settings.permissions = {};
18938
- }
18939
- const perms = settings.permissions;
18940
- if (!Array.isArray(perms.allow)) {
18941
- perms.allow = [];
18942
- }
18943
- const allow = perms.allow;
18944
- for (const permission of PAPI_PERMISSIONS) {
18945
- if (!allow.includes(permission)) {
18946
- allow.push(permission);
18947
- }
18948
- }
18949
- await mkdir(join9(projectRoot, ".claude"), { recursive: true });
19138
+ mergePapiPermissions(settings);
19139
+ await mkdir(join10(projectRoot, ".claude"), { recursive: true });
18950
19140
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
18951
19141
  } catch {
18952
19142
  }
18953
19143
  }
18954
19144
  async function ensureDesignHookRegistered(projectRoot) {
18955
- const settingsPath = join9(projectRoot, ".claude", "settings.json");
19145
+ const settingsPath = join10(projectRoot, ".claude", "settings.json");
18956
19146
  try {
18957
19147
  let settings = {};
18958
19148
  try {
@@ -18982,7 +19172,7 @@ async function ensureDesignHookRegistered(projectRoot) {
18982
19172
  chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
18983
19173
  }
18984
19174
  }
18985
- await mkdir(join9(projectRoot, ".claude"), { recursive: true });
19175
+ await mkdir(join10(projectRoot, ".claude"), { recursive: true });
18986
19176
  await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
18987
19177
  } catch {
18988
19178
  }
@@ -19088,7 +19278,7 @@ ${conventionsText.trim()}
19088
19278
  );
19089
19279
  } else {
19090
19280
  try {
19091
- const claudeMdPath = join9(config2.projectRoot, "CLAUDE.md");
19281
+ const claudeMdPath = join10(config2.projectRoot, "CLAUDE.md");
19092
19282
  const existing = await readFile4(claudeMdPath, "utf-8");
19093
19283
  if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
19094
19284
  warnings.push(
@@ -19175,13 +19365,13 @@ async function scanCodebase(projectRoot) {
19175
19365
  }
19176
19366
  let packageJson;
19177
19367
  try {
19178
- const content = await readFile4(join9(projectRoot, "package.json"), "utf-8");
19368
+ const content = await readFile4(join10(projectRoot, "package.json"), "utf-8");
19179
19369
  packageJson = JSON.parse(content);
19180
19370
  } catch {
19181
19371
  }
19182
19372
  let readme;
19183
19373
  for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
19184
- const content = await safeReadFile(join9(projectRoot, name), 5e3);
19374
+ const content = await safeReadFile(join10(projectRoot, name), 5e3);
19185
19375
  if (content) {
19186
19376
  readme = content;
19187
19377
  break;
@@ -19191,7 +19381,7 @@ async function scanCodebase(projectRoot) {
19191
19381
  let totalFiles = topLevelFiles.length;
19192
19382
  for (const dir of topLevelDirs) {
19193
19383
  try {
19194
- const entries = await readdir(join9(projectRoot, dir), { withFileTypes: true });
19384
+ const entries = await readdir(join10(projectRoot, dir), { withFileTypes: true });
19195
19385
  const files = entries.filter((e) => e.isFile());
19196
19386
  const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
19197
19387
  totalFiles += files.length;
@@ -19274,7 +19464,7 @@ function formatCodebaseSummary(scan, sourceContents) {
19274
19464
  }
19275
19465
  async function prepareSetup(adapter2, config2, input) {
19276
19466
  const prepareCollector = new FileWriteCollector();
19277
- const createdProject = await scaffoldPapiDir(adapter2, config2, input, prepareCollector);
19467
+ const { createdProject, preparedFiles } = await scaffoldPapiDir(adapter2, config2, input, prepareCollector);
19278
19468
  let existingBrief;
19279
19469
  try {
19280
19470
  existingBrief = await adapter2.readProductBrief();
@@ -19443,12 +19633,13 @@ async function prepareSetup(adapter2, config2, input) {
19443
19633
  preScanInstruction,
19444
19634
  newProjectInstruction,
19445
19635
  warnings: warnings.length > 0 ? warnings : void 0,
19446
- filesToWrite: prepareCollector.isEmpty() ? void 0 : prepareCollector
19636
+ filesToWrite: prepareCollector.isEmpty() ? void 0 : prepareCollector,
19637
+ preparedFiles
19447
19638
  };
19448
19639
  }
19449
19640
  async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText, northStarText) {
19450
19641
  const collector = new FileWriteCollector();
19451
- const createdProject = await scaffoldPapiDir(adapter2, config2, input, collector);
19642
+ const { createdProject, preparedFiles } = await scaffoldPapiDir(adapter2, config2, input, collector);
19452
19643
  let effectiveBriefText = briefText;
19453
19644
  let briefRegenerated = false;
19454
19645
  if (!effectiveBriefText.trim()) {
@@ -19577,7 +19768,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19577
19768
  collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
19578
19769
  } else {
19579
19770
  try {
19580
- const claudeMdPath = join9(config2.projectRoot, "CLAUDE.md");
19771
+ const claudeMdPath = join10(config2.projectRoot, "CLAUDE.md");
19581
19772
  const existing = await readFile4(claudeMdPath, "utf-8");
19582
19773
  if (!existing.includes("Dogfood Logging")) {
19583
19774
  await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
@@ -19622,7 +19813,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19622
19813
  cursorScaffolded = true;
19623
19814
  } else {
19624
19815
  try {
19625
- await access2(join9(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
19816
+ await access2(join10(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
19626
19817
  cursorScaffolded = true;
19627
19818
  } catch {
19628
19819
  }
@@ -19638,16 +19829,17 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19638
19829
  cursorScaffolded,
19639
19830
  gitignoreNote,
19640
19831
  warnings: warnings.length > 0 ? warnings : void 0,
19641
- filesToWrite: collector.isEmpty() ? void 0 : collector
19832
+ filesToWrite: collector.isEmpty() ? void 0 : collector,
19833
+ preparedFiles
19642
19834
  };
19643
19835
  }
19644
19836
  async function ensureMcpJsonGitignored(projectRoot) {
19645
19837
  try {
19646
- await access2(join9(projectRoot, ".git"));
19838
+ await access2(join10(projectRoot, ".git"));
19647
19839
  } catch {
19648
19840
  return void 0;
19649
19841
  }
19650
- const gitignorePath = join9(projectRoot, ".gitignore");
19842
+ const gitignorePath = join10(projectRoot, ".gitignore");
19651
19843
  let existing = "";
19652
19844
  try {
19653
19845
  existing = await readFile4(gitignorePath, "utf-8");
@@ -19796,8 +19988,14 @@ function extractInput(args) {
19796
19988
  codebaseScan: args.codebase_scan && typeof args.codebase_scan === "object" ? args.codebase_scan : void 0
19797
19989
  };
19798
19990
  }
19991
+ function harnessFilesLabel(prepared, writesClaudeMd) {
19992
+ const HARNESS_FILES = ["AGENTS.md", "CLAUDE.md"];
19993
+ const emitted = HARNESS_FILES.filter((f) => prepared?.includes(f));
19994
+ if (emitted.length > 0) return emitted.join(", ");
19995
+ return writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
19996
+ }
19799
19997
  function formatSuccessResponse(result, constraints, writesClaudeMd = true) {
19800
- const harnessFiles = writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
19998
+ const harnessFiles = harnessFilesLabel(result.preparedFiles, writesClaudeMd);
19801
19999
  const prefix = result.createdProject ? `PAPI project "${result.projectName}" initialised and ` : "";
19802
20000
  const briefRegenNote = result.briefRegenerated ? `
19803
20001
 
@@ -20213,8 +20411,8 @@ function buildPapiMetaFramingDirective(caps, inner) {
20213
20411
 
20214
20412
  // src/services/build.ts
20215
20413
  import { randomUUID as randomUUID11 } from "crypto";
20216
- import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
20217
- import { join as join12 } from "path";
20414
+ import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
20415
+ import { join as join13 } from "path";
20218
20416
  import { splitFindings, findingKey } from "@papi-ai/shared";
20219
20417
 
20220
20418
  // src/lib/db-only-notices.ts
@@ -20381,7 +20579,7 @@ init_git();
20381
20579
  // src/services/release.ts
20382
20580
  init_telemetry();
20383
20581
  import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
20384
- import { join as join10 } from "path";
20582
+ import { join as join11 } from "path";
20385
20583
  import { execFileSync as execFileSync4 } from "child_process";
20386
20584
  import { isSensitiveChangelogLine } from "@papi-ai/shared";
20387
20585
  init_git();
@@ -20855,7 +21053,7 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
20855
21053
  }
20856
21054
  }
20857
21055
  const latestTag = getLatestTag(config2.projectRoot);
20858
- const changelogPath = join10(config2.projectRoot, "CHANGELOG.md");
21056
+ const changelogPath = join11(config2.projectRoot, "CHANGELOG.md");
20859
21057
  if (!latestTag) {
20860
21058
  const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
20861
21059
  if (config2.adapterType === "proxy") {
@@ -21011,33 +21209,12 @@ import { execFile } from "child_process";
21011
21209
  import { promisify } from "util";
21012
21210
  var execFileAsync = promisify(execFile);
21013
21211
  var REGISTRY_TIMEOUT_MS = 12e4;
21014
- async function updateOfficialMCPRegistry(version) {
21015
- const token = process.env["MCP_REGISTRY_GITHUB_TOKEN"]?.trim();
21016
- if (!token) {
21017
- return {
21018
- registry: "Official MCP Registry",
21019
- updated: false,
21020
- message: `Listing not updated \u2014 set MCP_REGISTRY_GITHUB_TOKEN to automate, or run manually: npx mcp-publisher publish`
21021
- };
21022
- }
21023
- try {
21024
- await execFileAsync("npx", ["mcp-publisher", "publish", "--yes"], {
21025
- env: { ...process.env, GITHUB_TOKEN: token },
21026
- timeout: REGISTRY_TIMEOUT_MS
21027
- });
21028
- return {
21029
- registry: "Official MCP Registry",
21030
- updated: true,
21031
- message: `Listing updated to ${version}.`
21032
- };
21033
- } catch (err) {
21034
- const detail = err instanceof Error ? err.message.slice(0, 300) : String(err).slice(0, 300);
21035
- return {
21036
- registry: "Official MCP Registry",
21037
- updated: false,
21038
- message: `Update failed \u2014 run manually: npx mcp-publisher publish. (${detail})`
21039
- };
21040
- }
21212
+ function officialMCPRegistryGuidance(version) {
21213
+ return {
21214
+ registry: "Official MCP Registry",
21215
+ updated: false,
21216
+ message: `Listing not updated (manual step). From a checkout of getpapi/papi: \`mcp-publisher login github\` (once), then \`mcp-publisher publish\` \u2014 bump server.json to ${version} first. Requires the Go CLI (brew install mcp-publisher), NOT the npm package of the same name.`
21217
+ };
21041
21218
  }
21042
21219
  async function updateSmithery(version) {
21043
21220
  const key = process.env["SMITHERY_API_KEY"]?.trim();
@@ -21068,11 +21245,8 @@ async function updateSmithery(version) {
21068
21245
  }
21069
21246
  }
21070
21247
  async function updateRegistryListings(version) {
21071
- const [official, smithery] = await Promise.all([
21072
- updateOfficialMCPRegistry(version),
21073
- updateSmithery(version)
21074
- ]);
21075
- return [official, smithery];
21248
+ const [smithery] = await Promise.all([updateSmithery(version)]);
21249
+ return [officialMCPRegistryGuidance(version), smithery];
21076
21250
  }
21077
21251
  async function updateCursorDirectory(version) {
21078
21252
  const token = process.env["CURSOR_DIRECTORY_GITHUB_TOKEN"]?.trim();
@@ -21690,21 +21864,21 @@ function detectWorktreeCollision(input) {
21690
21864
  init_git();
21691
21865
 
21692
21866
  // src/lib/build-checkpoint.ts
21693
- import { createHash as createHash4 } from "crypto";
21694
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
21695
- import { join as join11 } from "path";
21867
+ import { createHash as createHash5 } from "crypto";
21868
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
21869
+ import { join as join12 } from "path";
21696
21870
  var BUILD_CHECKPOINT_VERSION = 1;
21697
21871
  function cwdHash(cwd) {
21698
- return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
21872
+ return createHash5("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
21699
21873
  }
21700
21874
  function safeTaskId(taskId) {
21701
21875
  return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
21702
21876
  }
21703
21877
  function checkpointDir(cwd) {
21704
- return join11(cwd, ".papi", "state");
21878
+ return join12(cwd, ".papi", "state");
21705
21879
  }
21706
21880
  function checkpointPath(cwd, taskId) {
21707
- return join11(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
21881
+ return join12(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
21708
21882
  }
21709
21883
  function writeBuildCheckpoint(input) {
21710
21884
  try {
@@ -21722,7 +21896,7 @@ function writeBuildCheckpoint(input) {
21722
21896
  cwd: realpathOrSelf(input.cwd),
21723
21897
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21724
21898
  };
21725
- writeFileSync3(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
21899
+ writeFileSync4(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
21726
21900
  } catch {
21727
21901
  }
21728
21902
  }
@@ -22616,15 +22790,15 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
22616
22790
  collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
22617
22791
  return;
22618
22792
  }
22619
- const papiDir = join12(projectRoot, ".papi");
22793
+ const papiDir = join13(projectRoot, ".papi");
22620
22794
  if (!existsSync8(papiDir)) {
22621
22795
  mkdirSync3(papiDir, { recursive: true });
22622
22796
  }
22623
- const scopePath = join12(papiDir, "active-task-scope.txt");
22624
- writeFileSync4(scopePath, content, "utf-8");
22797
+ const scopePath = join13(papiDir, "active-task-scope.txt");
22798
+ writeFileSync5(scopePath, content, "utf-8");
22625
22799
  }
22626
22800
  function clearActiveTaskScope(projectRoot) {
22627
- const scopePath = join12(projectRoot, ".papi", "active-task-scope.txt");
22801
+ const scopePath = join13(projectRoot, ".papi", "active-task-scope.txt");
22628
22802
  if (existsSync8(scopePath)) {
22629
22803
  unlinkSync3(scopePath);
22630
22804
  }
@@ -23084,14 +23258,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
23084
23258
  let docWarning;
23085
23259
  try {
23086
23260
  if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
23087
- const docsDir = join12(config2.projectRoot, "docs");
23261
+ const docsDir = join13(config2.projectRoot, "docs");
23088
23262
  if (existsSync8(docsDir)) {
23089
23263
  const scanDir = (dir, depth = 0) => {
23090
23264
  if (depth > 8) return [];
23091
23265
  const entries = readdirSync5(dir, { withFileTypes: true });
23092
23266
  const files = [];
23093
23267
  for (const e of entries) {
23094
- const full = join12(dir, e.name);
23268
+ const full = join13(dir, e.name);
23095
23269
  if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
23096
23270
  else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
23097
23271
  }
@@ -23116,7 +23290,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
23116
23290
  const failed = [];
23117
23291
  for (const docPath of unregistered) {
23118
23292
  try {
23119
- const meta = extractDocMeta(join12(config2.projectRoot, docPath), docPath, cycleNumber);
23293
+ const meta = extractDocMeta(join13(config2.projectRoot, docPath), docPath, cycleNumber);
23120
23294
  await adapter2.registerDoc({
23121
23295
  title: meta.title,
23122
23296
  type: meta.type,
@@ -23286,8 +23460,8 @@ ${instructions}`;
23286
23460
  }
23287
23461
 
23288
23462
  // src/tools/doc-registry.ts
23289
- import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
23290
- import { join as join13, relative, isAbsolute as isAbsolute2, dirname as dirname4, resolve as resolve3, sep } from "path";
23463
+ import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
23464
+ import { join as join14, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname4, resolve as resolve3, sep } from "path";
23291
23465
  import { homedir as homedir3 } from "os";
23292
23466
  import { randomUUID as randomUUID12 } from "crypto";
23293
23467
  import { docDeletionBlockMessage } from "@papi-ai/shared";
@@ -23527,7 +23701,7 @@ function ensureDocDurable(path7, projectRoot) {
23527
23701
  \u26A0\uFE0F **Registered, but NOT durable.** ${reason}
23528
23702
  The registry stores metadata and a summary \u2014 not the body. This doc has one copy, on disk, and a branch switch or stash can take it.
23529
23703
  **Fix:** ${fix}`;
23530
- if (!existsSync9(join13(projectRoot, path7))) {
23704
+ if (!existsSync9(join14(projectRoot, path7))) {
23531
23705
  return warn(
23532
23706
  `No file exists at \`${path7}\`.`,
23533
23707
  `write the doc body to that path, then re-run doc_register.`
@@ -23641,7 +23815,7 @@ async function handleDocRegister(adapter2, args, config2) {
23641
23815
  let body = supplied;
23642
23816
  if (body === void 0 && hasLocalWorkspace()) {
23643
23817
  try {
23644
- const abs = isAbsolute2(entry.path) ? entry.path : join13(config2?.projectRoot ?? process.cwd(), entry.path);
23818
+ const abs = isAbsolute2(entry.path) ? entry.path : join14(config2?.projectRoot ?? process.cwd(), entry.path);
23645
23819
  if (existsSync9(abs)) body = readFileSync8(abs, "utf8");
23646
23820
  } catch {
23647
23821
  }
@@ -23715,7 +23889,7 @@ async function handleDocSearch(adapter2, args, config2) {
23715
23889
  const lines = docs.map((d) => {
23716
23890
  const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
23717
23891
  const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
23718
- const missingNote = root && d.path && !existsSync9(join13(root, d.path)) ? `
23892
+ const missingNote = root && d.path && !existsSync9(join14(root, d.path)) ? `
23719
23893
  > \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there. Recover it with \`doc_read\` (\`id_or_path: "${d.path}", write_to_disk: true\`) if its body was stored, or check \`git stash list\` for a papi-autostash entry.` : "";
23720
23894
  return `### ${d.title}
23721
23895
  **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
@@ -23809,7 +23983,7 @@ function restoreBodyToDisk(projectRoot, docPath, body) {
23809
23983
  `;
23810
23984
  }
23811
23985
  mkdirSync4(dirname4(target.abs), { recursive: true });
23812
- writeFileSync5(target.abs, body, "utf8");
23986
+ writeFileSync6(target.abs, body, "utf8");
23813
23987
  return `- **Restored:** wrote ${Buffer.byteLength(body, "utf8").toLocaleString()} bytes to \`${docPath}\`
23814
23988
  `;
23815
23989
  } catch (err) {
@@ -23824,11 +23998,11 @@ function scanMdFiles(dir, rootDir) {
23824
23998
  try {
23825
23999
  const entries = readdirSync6(dir, { withFileTypes: true });
23826
24000
  for (const entry of entries) {
23827
- const full = join13(dir, entry.name);
24001
+ const full = join14(dir, entry.name);
23828
24002
  if (entry.isDirectory()) {
23829
24003
  files.push(...scanMdFiles(full, rootDir));
23830
24004
  } else if (entry.name.endsWith(".md")) {
23831
- files.push(relative(rootDir, full).replace(/\\/g, "/"));
24005
+ files.push(relative2(rootDir, full).replace(/\\/g, "/"));
23832
24006
  }
23833
24007
  }
23834
24008
  } catch {
@@ -23849,7 +24023,7 @@ function extractTitle(filePath) {
23849
24023
  async function detectUnregisteredDocsNote(adapter2, config2) {
23850
24024
  try {
23851
24025
  if (!adapter2.searchDocs || !hasLocalWorkspace()) return "";
23852
- const docsDir = join13(config2.projectRoot, "docs");
24026
+ const docsDir = join14(config2.projectRoot, "docs");
23853
24027
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
23854
24028
  if (docsFiles.length === 0) return "";
23855
24029
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -23875,17 +24049,17 @@ async function handleDocScan(adapter2, config2, args) {
23875
24049
  const includePlans = args.include_plans ?? false;
23876
24050
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
23877
24051
  const registeredPaths = new Set(registered.map((d) => d.path));
23878
- const docsDir = join13(config2.projectRoot, "docs");
24052
+ const docsDir = join14(config2.projectRoot, "docs");
23879
24053
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
23880
24054
  const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
23881
24055
  let unregisteredPlans = [];
23882
24056
  if (includePlans) {
23883
- const plansDir = join13(homedir3(), ".claude", "plans");
24057
+ const plansDir = join14(homedir3(), ".claude", "plans");
23884
24058
  if (existsSync9(plansDir)) {
23885
24059
  const planFiles = scanMdFiles(plansDir, plansDir);
23886
24060
  unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
23887
24061
  path: f,
23888
- title: extractTitle(join13(plansDir, f.replace("plans/", "")))
24062
+ title: extractTitle(join14(plansDir, f.replace("plans/", "")))
23889
24063
  }));
23890
24064
  }
23891
24065
  }
@@ -23896,7 +24070,7 @@ async function handleDocScan(adapter2, config2, args) {
23896
24070
  if (unregisteredDocs.length > 0) {
23897
24071
  lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
23898
24072
  for (const f of unregisteredDocs) {
23899
- const title = extractTitle(join13(config2.projectRoot, f));
24073
+ const title = extractTitle(join14(config2.projectRoot, f));
23900
24074
  lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
23901
24075
  }
23902
24076
  }
@@ -25082,6 +25256,10 @@ var ideaTool = {
25082
25256
  type: "string",
25083
25257
  description: "What user problem does this solve? Auto-fill from problem context in notes when submitting ideas that describe a user pain point. The planner uses this to cluster backlog tasks by opportunity."
25084
25258
  },
25259
+ conflicts_with_ad: {
25260
+ type: "string",
25261
+ description: 'The Active Decision this idea cuts against, e.g. "AD-12". USE THIS INSTEAD OF DROPPING THE IDEA. An AD is *active* \u2014 it can be superseded \u2014 so a contradiction is a decision for the user, never a reason for you to shelve work during research or scoping. The task is created and parked Blocked behind a decision-gate on that AD: it shows in `orient` as a decision waiting on the owner, and returns to the board automatically once the AD is superseded, modified, or reaffirmed.'
25262
+ },
25085
25263
  project: {
25086
25264
  type: "string",
25087
25265
  description: "Project id (UUID) or slug to write this idea to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
@@ -25114,7 +25292,8 @@ async function handleIdea(adapter2, config2, args) {
25114
25292
  force: args.force === true,
25115
25293
  docRef: args.doc_ref?.trim(),
25116
25294
  type: args.type,
25117
- opportunity: args.opportunity?.trim()
25295
+ opportunity: args.opportunity?.trim(),
25296
+ conflictsWithAd: args.conflicts_with_ad?.trim()
25118
25297
  };
25119
25298
  let target = adapter2;
25120
25299
  let overrideNote = "";
@@ -25159,7 +25338,12 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
25159
25338
  } catch {
25160
25339
  }
25161
25340
  }
25162
- return textResponse(`${result.message}${overrideNote}${branchNote}${truncateWarning}${refNudge}`);
25341
+ const conflictNote = result.adConflicts ? buildAdConflictNote(
25342
+ result.adConflicts.explicit ? result.adConflicts.adIds[0] : void 0,
25343
+ result.adConflicts.explicit ? [] : result.adConflicts.adIds,
25344
+ result.adConflicts.gated
25345
+ ) : "";
25346
+ return textResponse(`${result.message}${overrideNote}${branchNote}${truncateWarning}${refNudge}${conflictNote}`);
25163
25347
  }
25164
25348
  return textResponse(`${result.message}${overrideNote}`);
25165
25349
  }
@@ -26212,11 +26396,11 @@ init_git();
26212
26396
 
26213
26397
  // src/services/reconcile.ts
26214
26398
  import { readFileSync as readFileSync9 } from "fs";
26215
- import { join as join14 } from "path";
26399
+ import { join as join15 } from "path";
26216
26400
  function loadDocsIndex(projectRoot) {
26217
26401
  if (!hasLocalWorkspace()) return "";
26218
26402
  try {
26219
- const indexPath = join14(projectRoot, "docs", "INDEX.md");
26403
+ const indexPath = join15(projectRoot, "docs", "INDEX.md");
26220
26404
  const raw = readFileSync9(indexPath, "utf8");
26221
26405
  const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
26222
26406
  if (rows.length === 0) return "";
@@ -26793,7 +26977,7 @@ Produce your analysis and structured output above. Present Part 1 to the user an
26793
26977
 
26794
26978
  // src/tools/review.ts
26795
26979
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
26796
- import { join as join15 } from "path";
26980
+ import { join as join16 } from "path";
26797
26981
  init_git();
26798
26982
 
26799
26983
  // src/services/review.ts
@@ -26991,8 +27175,6 @@ Re-run build_execute complete with a production_verification field, then re-subm
26991
27175
  }
26992
27176
 
26993
27177
  // src/tools/review.ts
26994
- var REVIEW_DISPATCH_THRESHOLD = 50 * 1024;
26995
- var REVIEW_DISPATCH_CEILING = 40 * 1024;
26996
27178
  var REVIEW_ECHO_CAP = 4e3;
26997
27179
  function trimForEcho(text) {
26998
27180
  if (text.length <= REVIEW_ECHO_CAP) return text;
@@ -27022,7 +27204,7 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
27022
27204
  ${diff}
27023
27205
  \`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
27024
27206
  let projectContext = "";
27025
- const ctxPath = join15(config2.projectRoot, ".agents", "papi-context.md");
27207
+ const ctxPath = join16(config2.projectRoot, ".agents", "papi-context.md");
27026
27208
  if (existsSync10(ctxPath)) {
27027
27209
  try {
27028
27210
  projectContext = `### Project context (.agents/papi-context.md)
@@ -27201,7 +27383,7 @@ function mergeAfterAccept(config2, taskId) {
27201
27383
  };
27202
27384
  }
27203
27385
  const details = [];
27204
- const papiDir = join15(config2.projectRoot, ".papi");
27386
+ const papiDir = join16(config2.projectRoot, ".papi");
27205
27387
  if (existsSync10(papiDir)) {
27206
27388
  try {
27207
27389
  const commitResult = stageDirAndCommit(
@@ -27340,7 +27522,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
27340
27522
  if (typeof rawPreset === "string" && rawPreset.length > 0 && !reviewPreset) {
27341
27523
  return errorResponse(`review_preset "${rawPreset}" is not valid. Use "gate", "full", or "security-focused".`);
27342
27524
  }
27343
- const autoDispatchOptIn = args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false" && isCapabilityEnabled(caps, "prReviewer");
27525
+ const autoDispatchOptIn = args.dispatch !== "inline" && isCapabilityEnabled(caps, "prReviewer");
27344
27526
  const autoDispatchEligible = !verdict && autoDispatchOptIn;
27345
27527
  const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
27346
27528
  let capabilityReviewSkippedNote = "";
@@ -27353,16 +27535,27 @@ async function handleReviewSubmit(adapter2, config2, args) {
27353
27535
  );
27354
27536
  if (!dispatch.ok) {
27355
27537
  if (explicitDispatch) return errorResponse(dispatch.error);
27356
- } else if (explicitDispatch || autoDispatchEligible && dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
27538
+ } else if (explicitDispatch || autoDispatchEligible && shouldDispatch(dispatch.contextBytes)) {
27357
27539
  return textResponse(dispatch.prompt);
27358
27540
  } else if (capabilityAutoReviewEligible) {
27359
- if (dispatch.contextBytes <= REVIEW_DISPATCH_CEILING) {
27360
- return textResponse(dispatch.prompt);
27541
+ const delivery = planDelivery({
27542
+ payload: dispatch.prompt,
27543
+ kind: "review-context",
27544
+ hasPendingWrite: true,
27545
+ adapterType: config2.adapterType,
27546
+ projectId: config2.projectId,
27547
+ callerKey: taskId
27548
+ });
27549
+ if (delivery.mode === "inline") {
27550
+ return textResponse(delivery.payload);
27361
27551
  }
27362
27552
  const kb = (dispatch.contextBytes / 1024).toFixed(0);
27363
- capabilityReviewSkippedNote = `
27553
+ capabilityReviewSkippedNote = delivery.mode === "spilled" ? `
27364
27554
 
27365
- > \u26A0\uFE0F pr-reviewer auto-review skipped \u2014 the diff/build-report (~${kb} KB) exceeds the ${REVIEW_DISPATCH_CEILING / 1024} KB inline-dispatch ceiling, which would overflow the response and drop the accept (task-2854). Verdict recorded directly. To review the diff explicitly, run \`review_submit ${taskId} build-acceptance accept dispatch:"subagent"\`.`;
27555
+ > \u{1F4C4} pr-reviewer auto-review was too large to return inline (~${kb} KB), so it was written to a file instead of being dropped (task-3030). Verdict recorded. To run the review, read:
27556
+ > \`${delivery.path}\`` : `
27557
+
27558
+ > \u26A0\uFE0F pr-reviewer auto-review could not be delivered \u2014 the diff/build-report (~${kb} KB) exceeds the inline ceiling and this connection has no shared filesystem to spill to. Verdict recorded. To review the diff explicitly, run \`review_submit ${taskId} build-acceptance accept dispatch:"subagent"\`.`;
27366
27559
  }
27367
27560
  }
27368
27561
  if (explicitDispatch && stage !== "build-acceptance") {
@@ -28834,7 +29027,7 @@ function formatDeferredGateSection(sweep) {
28834
29027
 
28835
29028
  // src/tools/agent-list.ts
28836
29029
  import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
28837
- import { join as join16 } from "path";
29030
+ import { join as join17 } from "path";
28838
29031
  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.";
28839
29032
  function parseAgentFrontmatter(content) {
28840
29033
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -28846,7 +29039,7 @@ function parseAgentFrontmatter(content) {
28846
29039
  return { name: nameMatch?.[1].trim(), description };
28847
29040
  }
28848
29041
  async function listAgents(projectRoot) {
28849
- const agentsDir = join16(projectRoot, ".claude", "agents");
29042
+ const agentsDir = join17(projectRoot, ".claude", "agents");
28850
29043
  let files;
28851
29044
  try {
28852
29045
  files = await readdir2(agentsDir);
@@ -28857,7 +29050,7 @@ async function listAgents(projectRoot) {
28857
29050
  for (const file of files.filter((f) => f.endsWith(".md"))) {
28858
29051
  let content;
28859
29052
  try {
28860
- content = await readFile7(join16(agentsDir, file), "utf-8");
29053
+ content = await readFile7(join17(agentsDir, file), "utf-8");
28861
29054
  } catch {
28862
29055
  continue;
28863
29056
  }
@@ -28865,7 +29058,7 @@ async function listAgents(projectRoot) {
28865
29058
  agents.push({
28866
29059
  name: meta?.name ?? file.replace(/\.md$/, ""),
28867
29060
  description: meta?.description ?? "",
28868
- path: join16(".claude", "agents", file)
29061
+ path: join17(".claude", "agents", file)
28869
29062
  });
28870
29063
  }
28871
29064
  agents.sort((a, b2) => a.name.localeCompare(b2.name));
@@ -29006,8 +29199,8 @@ async function verifyProject(adapter2) {
29006
29199
  // src/tools/orient.ts
29007
29200
  import { execFile as execFile2 } from "child_process";
29008
29201
  import { promisify as promisify2 } from "util";
29009
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync11 } from "fs";
29010
- import { join as join17 } from "path";
29202
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11 } from "fs";
29203
+ import { join as join18 } from "path";
29011
29204
 
29012
29205
  // src/lib/exit-criteria-evaluators.ts
29013
29206
  var TEST_ACCOUNT_PATTERN = "ftue-%@test.papi.dev";
@@ -29063,6 +29256,7 @@ var UNEVALUATABLE = {
29063
29256
  "c596a082-9ad3-4089-b497-46cd189e892b": "threshold is provisional pending an owner ruling \u2014 no settled bar to evaluate against"
29064
29257
  };
29065
29258
  async function evaluateExitCriteria(adapter2, projectId, criteria) {
29259
+ const hasProject = typeof projectId === "string" && projectId.trim() !== "";
29066
29260
  return Promise.all(criteria.map(async (c) => {
29067
29261
  const evaluator = EVALUATORS[c.id];
29068
29262
  if (!evaluator) {
@@ -29070,6 +29264,14 @@ async function evaluateExitCriteria(adapter2, projectId, criteria) {
29070
29264
  if (!reason) return { ...c, met: c.met, autoEvaluated: false };
29071
29265
  return c.met ? { ...c, met: true, autoEvaluated: false } : { ...c, met: null, unevaluatedReason: reason, autoEvaluated: false };
29072
29266
  }
29267
+ if (!hasProject) {
29268
+ return {
29269
+ ...c,
29270
+ met: c.met ? true : null,
29271
+ unevaluatedReason: "project id unavailable \u2014 this criterion is measured against a specific project and orient could not resolve one",
29272
+ autoEvaluated: false
29273
+ };
29274
+ }
29073
29275
  try {
29074
29276
  const result = await evaluator(adapter2, projectId);
29075
29277
  return {
@@ -29080,10 +29282,11 @@ async function evaluateExitCriteria(adapter2, projectId, criteria) {
29080
29282
  autoEvaluated: result.met !== null
29081
29283
  };
29082
29284
  } catch (err) {
29285
+ console.error(`[exit-criteria] evaluator for criterion ${c.id} threw:`, err);
29083
29286
  return {
29084
29287
  ...c,
29085
29288
  met: c.met ? true : null,
29086
- unevaluatedReason: `evaluator failed: ${err instanceof Error ? err.message : String(err)}`,
29289
+ unevaluatedReason: "evaluator failed \u2014 see server logs. The stored value is shown and is unverified.",
29087
29290
  autoEvaluated: false
29088
29291
  };
29089
29292
  }
@@ -29462,9 +29665,13 @@ async function getHierarchyPosition(adapter2, projectId) {
29462
29665
  // task-3008: EVALUATE rather than read the hand-ticked boolean. Best-effort —
29463
29666
  // a failing evaluator degrades that criterion to unevaluated and leaves the
29464
29667
  // rest intact, because orient is the first call of every session.
29668
+ // task-3226: pass the id THROUGH, undefined and all. This used to coerce to
29669
+ // `''`, which is not a uuid and blew up every SQL-backed evaluator — the
29670
+ // coercion looked like a safe default and was the bug. evaluateExitCriteria
29671
+ // now reports "project id unavailable" for that case, which is the truth.
29465
29672
  stageExitCriteria: await evaluateExitCriteria(
29466
29673
  adapter2,
29467
- projectId ?? "",
29674
+ projectId,
29468
29675
  activeStage.exitCriteria ?? []
29469
29676
  ).catch(() => (activeStage.exitCriteria ?? []).map((c) => ({ ...c, autoEvaluated: false })))
29470
29677
  };
@@ -29475,7 +29682,7 @@ async function getHierarchyPosition(adapter2, projectId) {
29475
29682
  var GIT_TAG_TIMEOUT_MS = 2e3;
29476
29683
  async function checkNpmVersionDrift() {
29477
29684
  try {
29478
- const pkgPath = join17(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
29685
+ const pkgPath = join18(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
29479
29686
  const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
29480
29687
  const localVersion = pkg.version;
29481
29688
  const packageName = pkg.name;
@@ -30176,7 +30383,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
30176
30383
 
30177
30384
  \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
30178
30385
  }
30179
- const claudeMdPath = join17(projectRoot, "CLAUDE.md");
30386
+ const claudeMdPath = join18(projectRoot, "CLAUDE.md");
30180
30387
  if (!existsSync11(claudeMdPath)) return "";
30181
30388
  const content = readFileSync11(claudeMdPath, "utf-8");
30182
30389
  const additions = [];
@@ -30187,7 +30394,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
30187
30394
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
30188
30395
  }
30189
30396
  if (additions.length === 0) return "";
30190
- writeFileSync6(claudeMdPath, content + additions.join(""), "utf-8");
30397
+ writeFileSync7(claudeMdPath, content + additions.join(""), "utf-8");
30191
30398
  const tierNames = [];
30192
30399
  if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
30193
30400
  if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
@@ -30709,6 +30916,10 @@ var zoomOutTool = {
30709
30916
  enum: ["prepare", "apply"],
30710
30917
  description: '"prepare" returns the retrospective prompt. "apply" accepts your output. Defaults to "prepare" when omitted.'
30711
30918
  },
30919
+ llm_response_file: {
30920
+ type: "string",
30921
+ description: 'Absolute path to a file containing the zoom-out output (mode "apply" only). LOCAL stdio servers only \u2014 on the hosted connection the server cannot read files on your machine. Use when the response is too large to pass inline (some hosts cap tool inputs around 50KB). Mutually exclusive with llm_response.'
30922
+ },
30712
30923
  llm_response: {
30713
30924
  type: "string",
30714
30925
  description: 'Your raw output from executing the retrospective prompt (mode "apply" only).'
@@ -30730,10 +30941,12 @@ async function handleZoomOut(adapter2, config2, args) {
30730
30941
  const toolMode = args.mode;
30731
30942
  try {
30732
30943
  if (toolMode === "apply") {
30733
- const llmResponse = args.llm_response;
30734
- if (!llmResponse || !llmResponse.trim()) {
30735
- return errorResponse('llm_response is required for mode "apply".');
30736
- }
30944
+ const resolved = await resolveLlmResponse(
30945
+ args.llm_response,
30946
+ args.llm_response_file
30947
+ );
30948
+ if (!resolved.ok) return errorResponse(resolved.error);
30949
+ const llmResponse = resolved.llmResponse;
30737
30950
  const cycleNumber = typeof args.cycle_number === "number" ? args.cycle_number : 0;
30738
30951
  const result = await applyZoomOut(adapter2, llmResponse, cycleNumber);
30739
30952
  return textResponse(
@@ -30748,16 +30961,8 @@ Retrospective saved. Use insights to inform your next \`strategy_review\` or \`p
30748
30961
  }
30749
30962
  {
30750
30963
  const result = await prepareZoomOut(adapter2, config2.projectRoot);
30751
- const autoDispatchEnabled = process.env.PAPI_AUTO_DISPATCH !== "false";
30752
- const autoDispatchThreshold = 50 * 1024;
30753
- let dispatch;
30754
- if (args.dispatch === "inline" || args.dispatch === "subagent") {
30755
- dispatch = args.dispatch;
30756
- } else if (autoDispatchEnabled && result.contextBytes > autoDispatchThreshold) {
30757
- dispatch = "subagent";
30758
- } else {
30759
- dispatch = "inline";
30760
- }
30964
+ const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
30965
+ const dispatch = shouldDispatch(result.contextBytes, explicit) ? "subagent" : "inline";
30761
30966
  if (dispatch === "subagent") {
30762
30967
  const dispatchPrompt = buildSubagentDispatchPrompt({
30763
30968
  tool: "zoom_out",
@@ -31002,8 +31207,8 @@ ${result.userMessage}
31002
31207
  import { readFileSync as readFileSync12, statSync as statSync7 } from "fs";
31003
31208
 
31004
31209
  // src/services/scope-brief.ts
31005
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
31006
- import { join as join18, dirname as dirname5 } from "path";
31210
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
31211
+ import { join as join19, dirname as dirname5 } from "path";
31007
31212
  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.
31008
31213
 
31009
31214
  A scope document must:
@@ -31060,14 +31265,14 @@ async function applyScopeBrief(adapter2, input) {
31060
31265
  }
31061
31266
  const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
31062
31267
  const relPath = `docs/scopes/${slug}.md`;
31063
- const absPath = join18(input.projectRoot, relPath);
31268
+ const absPath = join19(input.projectRoot, relPath);
31064
31269
  const docBody = addFrontmatter(docContent, task, input.cycleNumber);
31065
31270
  const collector = new FileWriteCollector();
31066
31271
  if (input.adapterType === "proxy") {
31067
31272
  collector.add({ path: relPath, content: docBody, mode: "overwrite" });
31068
31273
  } else {
31069
31274
  mkdirSync5(dirname5(absPath), { recursive: true });
31070
- writeFileSync7(absPath, docBody, "utf-8");
31275
+ writeFileSync8(absPath, docBody, "utf-8");
31071
31276
  }
31072
31277
  const taskCount = countSubTasks(docContent);
31073
31278
  const summary = buildSummary(task, taskCount);
@@ -31354,7 +31559,7 @@ ${formatted}`, meta));
31354
31559
 
31355
31560
  // src/lib/dist-staleness.ts
31356
31561
  import { readFileSync as readFileSync13, statSync as statSync8 } from "fs";
31357
- import { createHash as createHash5 } from "crypto";
31562
+ import { createHash as createHash6 } from "crypto";
31358
31563
  import { fileURLToPath as fileURLToPath3 } from "url";
31359
31564
  var BOOT_MS = Date.now();
31360
31565
  var DEFAULT_SKEW_MS = 2e3;
@@ -31368,7 +31573,7 @@ var SELF_PATH = (() => {
31368
31573
  })();
31369
31574
  function hashFile(path7) {
31370
31575
  try {
31371
- return createHash5("sha256").update(readFileSync13(path7)).digest("hex");
31576
+ return createHash6("sha256").update(readFileSync13(path7)).digest("hex");
31372
31577
  } catch {
31373
31578
  return null;
31374
31579
  }
@@ -32018,19 +32223,19 @@ Its build reports, comments, and history moved with it; the cycle assignment was
32018
32223
 
32019
32224
  // src/services/harness-inventory.ts
32020
32225
  import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
32021
- import { join as join19 } from "path";
32022
- import { createHash as createHash6 } from "crypto";
32226
+ import { join as join20 } from "path";
32227
+ import { createHash as createHash7 } from "crypto";
32023
32228
  var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
32024
32229
  async function computeFingerprint(root) {
32025
32230
  const parts = [];
32026
32231
  for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
32027
- const dir = join19(root, sub);
32232
+ const dir = join20(root, sub);
32028
32233
  try {
32029
32234
  const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
32030
32235
  for (const name of names) {
32031
32236
  let mtime = "";
32032
32237
  try {
32033
- mtime = String(Math.floor((await stat3(join19(dir, name))).mtimeMs));
32238
+ mtime = String(Math.floor((await stat3(join20(dir, name))).mtimeMs));
32034
32239
  } catch {
32035
32240
  }
32036
32241
  parts.push(`${sub}/${name}:${mtime}`);
@@ -32045,11 +32250,11 @@ async function computeFingerprint(root) {
32045
32250
  } catch {
32046
32251
  parts.push("manifest:none");
32047
32252
  }
32048
- return createHash6("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
32253
+ return createHash7("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
32049
32254
  }
32050
32255
  async function readSkillDescription(skillDir) {
32051
32256
  try {
32052
- const content = await readFile8(join19(skillDir, "SKILL.md"), "utf-8");
32257
+ const content = await readFile8(join20(skillDir, "SKILL.md"), "utf-8");
32053
32258
  const fm = content.match(/^---\n([\s\S]*?)\n---/);
32054
32259
  if (!fm) return void 0;
32055
32260
  const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
@@ -32069,17 +32274,17 @@ async function scanInventory(root, toolDefs) {
32069
32274
  version = loadManifest().packageVersion;
32070
32275
  } catch {
32071
32276
  }
32072
- const skillsDir = join19(root, ".claude", "skills");
32277
+ const skillsDir = join20(root, ".claude", "skills");
32073
32278
  try {
32074
32279
  const dirents = await readdir3(skillsDir, { withFileTypes: true });
32075
32280
  for (const d of dirents.filter((e) => e.isDirectory())) {
32076
32281
  entries.push({
32077
32282
  kind: "skill",
32078
32283
  name: d.name,
32079
- description: await readSkillDescription(join19(skillsDir, d.name)),
32284
+ description: await readSkillDescription(join20(skillsDir, d.name)),
32080
32285
  version,
32081
32286
  status: stale.has(d.name) ? "stale_fork" : "ok",
32082
- path: join19(".claude", "skills", d.name)
32287
+ path: join20(".claude", "skills", d.name)
32083
32288
  });
32084
32289
  }
32085
32290
  } catch {
@@ -32095,9 +32300,9 @@ async function scanInventory(root, toolDefs) {
32095
32300
  }
32096
32301
  const present = /* @__PURE__ */ new Set();
32097
32302
  try {
32098
- for (const f of (await readdir3(join19(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
32303
+ for (const f of (await readdir3(join20(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
32099
32304
  present.add(f);
32100
- entries.push({ kind: "hook", name: f, status: "ok", path: join19(".claude", "hooks", f) });
32305
+ entries.push({ kind: "hook", name: f, status: "ok", path: join20(".claude", "hooks", f) });
32101
32306
  }
32102
32307
  } catch {
32103
32308
  }
@@ -32543,7 +32748,7 @@ function createServer(adapter2, config2) {
32543
32748
  const __pkgDir = dirname6(__pkgFilename);
32544
32749
  let serverVersion = "unknown";
32545
32750
  try {
32546
- const pkg = JSON.parse(readFileSync14(join20(__pkgDir, "..", "package.json"), "utf-8"));
32751
+ const pkg = JSON.parse(readFileSync14(join21(__pkgDir, "..", "package.json"), "utf-8"));
32547
32752
  serverVersion = pkg.version ?? "unknown";
32548
32753
  } catch {
32549
32754
  }
@@ -32562,7 +32767,7 @@ function createServer(adapter2, config2) {
32562
32767
  }
32563
32768
  const __filename = fileURLToPath4(import.meta.url);
32564
32769
  const __dirname2 = dirname6(__filename);
32565
- const skillsDir = join20(__dirname2, "..", "skills");
32770
+ const skillsDir = join21(__dirname2, "..", "skills");
32566
32771
  function parseSkillFrontmatter(content) {
32567
32772
  const match = content.match(/^---\n([\s\S]*?)\n---/);
32568
32773
  if (!match) return null;
@@ -32580,7 +32785,7 @@ function createServer(adapter2, config2) {
32580
32785
  const mdFiles = files.filter((f) => f.endsWith(".md"));
32581
32786
  const prompts = [];
32582
32787
  for (const file of mdFiles) {
32583
- const content = await readFile9(join20(skillsDir, file), "utf-8");
32788
+ const content = await readFile9(join21(skillsDir, file), "utf-8");
32584
32789
  const meta = parseSkillFrontmatter(content);
32585
32790
  if (meta) {
32586
32791
  prompts.push({ name: meta.name, description: meta.description });
@@ -32596,7 +32801,7 @@ function createServer(adapter2, config2) {
32596
32801
  try {
32597
32802
  const files = await readdir4(skillsDir);
32598
32803
  for (const file of files.filter((f) => f.endsWith(".md"))) {
32599
- const content = await readFile9(join20(skillsDir, file), "utf-8");
32804
+ const content = await readFile9(join21(skillsDir, file), "utf-8");
32600
32805
  const meta = parseSkillFrontmatter(content);
32601
32806
  if (meta?.name === name) {
32602
32807
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -32882,7 +33087,7 @@ ${usageLine(decision.usage)}`;
32882
33087
  // src/transport-http.ts
32883
33088
  init_proxy_adapter();
32884
33089
  import { createServer as createHttpServer } from "http";
32885
- import { createHash as createHash7 } from "crypto";
33090
+ import { createHash as createHash8 } from "crypto";
32886
33091
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
32887
33092
  var BEARER_PREFIX = "papi_";
32888
33093
  var BEARER_REGEX = /^(papi_|papi_oauth_)[a-f0-9]{64}$/;
@@ -33006,7 +33211,7 @@ var AUTH_REJECTED_TTL_MS = 6e4;
33006
33211
  var AUTH_CACHE_MAX = 5e3;
33007
33212
  var authVerdicts = /* @__PURE__ */ new Map();
33008
33213
  function bearerKey(bearer) {
33009
- return createHash7("sha256").update(bearer).digest("hex");
33214
+ return createHash8("sha256").update(bearer).digest("hex");
33010
33215
  }
33011
33216
  function readAuthVerdict(bearer, now = Date.now()) {
33012
33217
  const hit = authVerdicts.get(bearerKey(bearer));
@@ -33421,7 +33626,7 @@ async function dispatchRequest(args) {
33421
33626
  var __dirname = dirname7(fileURLToPath5(import.meta.url));
33422
33627
  var pkgVersion = "unknown";
33423
33628
  try {
33424
- const pkg = JSON.parse(readFileSync19(join25(__dirname, "..", "package.json"), "utf-8"));
33629
+ const pkg = JSON.parse(readFileSync19(join26(__dirname, "..", "package.json"), "utf-8"));
33425
33630
  pkgVersion = pkg.version;
33426
33631
  } catch {
33427
33632
  }