@papi-ai/server 0.5.0 → 0.5.1

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.
Files changed (2) hide show
  1. package/dist/index.js +587 -44
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -280,6 +280,9 @@ function splitSections(text) {
280
280
  function parseBulletList(text) {
281
281
  return text.split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
282
282
  }
283
+ function parseBulletsOnly(text) {
284
+ return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
285
+ }
283
286
  function parseChecklist(text) {
284
287
  return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
285
288
  }
@@ -314,6 +317,7 @@ function parseBuildHandoff(markdown) {
314
317
  scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
315
318
  acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
316
319
  securityConsiderations: (sections.get("SECURITY CONSIDERATIONS") ?? "").trim(),
320
+ verificationFiles: parseBulletsOnly(sections.get("PRE-BUILD VERIFICATION") ?? ""),
317
321
  filesLikelyTouched: parseBulletList(sections.get("FILES LIKELY TOUCHED") ?? ""),
318
322
  effort
319
323
  };
@@ -345,6 +349,15 @@ function serializeBuildHandoff(handoff) {
345
349
  lines.push("");
346
350
  lines.push("SECURITY CONSIDERATIONS");
347
351
  lines.push(handoff.securityConsiderations);
352
+ if (handoff.verificationFiles && handoff.verificationFiles.length > 0) {
353
+ lines.push("");
354
+ lines.push("PRE-BUILD VERIFICATION");
355
+ lines.push("Before implementing, read these files and check if the functionality already exists:");
356
+ for (const item of handoff.verificationFiles) {
357
+ lines.push(`- ${item}`);
358
+ }
359
+ lines.push('If >80% of the scope is already implemented, call build_execute with completed="yes" and note "already built" in surprises instead of re-implementing.');
360
+ }
348
361
  lines.push("");
349
362
  lines.push("FILES LIKELY TOUCHED");
350
363
  for (const item of handoff.filesLikelyTouched) {
@@ -1422,6 +1435,7 @@ var init_dist2 = __esm({
1422
1435
  "SCOPE BOUNDARY (DO NOT DO THIS)",
1423
1436
  "ACCEPTANCE CRITERIA",
1424
1437
  "SECURITY CONSIDERATIONS",
1438
+ "PRE-BUILD VERIFICATION",
1425
1439
  "FILES LIKELY TOUCHED",
1426
1440
  "EFFORT"
1427
1441
  ];
@@ -4540,7 +4554,7 @@ function rowToDecisionScore(row) {
4540
4554
  createdAt: row.created_at
4541
4555
  };
4542
4556
  }
4543
- function rowToSprintLogEntry(row) {
4557
+ function rowToCycleLogEntry(row) {
4544
4558
  const entry = {
4545
4559
  uuid: row.id,
4546
4560
  cycleNumber: row.cycle_number,
@@ -6133,14 +6147,14 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6133
6147
  ORDER BY cycle_number DESC
6134
6148
  LIMIT ${limit}
6135
6149
  `;
6136
- return rows2.map(rowToSprintLogEntry);
6150
+ return rows2.map(rowToCycleLogEntry);
6137
6151
  }
6138
6152
  const rows = await this.sql`
6139
6153
  SELECT * FROM planning_log_entries
6140
6154
  WHERE project_id = ${this.projectId}
6141
6155
  ORDER BY cycle_number DESC
6142
6156
  `;
6143
- return rows.map(rowToSprintLogEntry);
6157
+ return rows.map(rowToCycleLogEntry);
6144
6158
  }
6145
6159
  async getCycleLogSince(cycleNumber) {
6146
6160
  const rows = await this.sql`
@@ -6149,7 +6163,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6149
6163
  AND cycle_number >= ${cycleNumber}
6150
6164
  ORDER BY cycle_number DESC
6151
6165
  `;
6152
- return rows.map(rowToSprintLogEntry);
6166
+ return rows.map(rowToCycleLogEntry);
6153
6167
  }
6154
6168
  async setCycleHealth(updates) {
6155
6169
  if (updates.boardHealth != null || updates.strategicDirection != null) {
@@ -6243,7 +6257,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
6243
6257
  board_health, strategic_direction, full_analysis,
6244
6258
  velocity_assessment, structured_data, created_at
6245
6259
  FROM strategy_reviews
6246
- WHERE project_id = ${this.projectId}
6260
+ WHERE project_id = ${this.projectId} AND cycle_number > 0
6247
6261
  ORDER BY cycle_number DESC
6248
6262
  LIMIT ${limit}
6249
6263
  `;
@@ -6283,6 +6297,139 @@ ${newParts.join("\n")}` : newParts.join("\n");
6283
6297
  createdAt: r.created_at ?? void 0
6284
6298
  }));
6285
6299
  }
6300
+ async savePendingReviewResponse(cycleNumber, rawResponse) {
6301
+ await this.sql`
6302
+ INSERT INTO strategy_reviews (
6303
+ project_id, cycle_number, title, content, full_analysis
6304
+ ) VALUES (
6305
+ ${this.projectId}, ${0}, ${"[PENDING] Strategy Review"}, ${"Pending write-back retry"}, ${rawResponse}
6306
+ )
6307
+ ON CONFLICT (project_id, cycle_number)
6308
+ DO UPDATE SET
6309
+ full_analysis = ${rawResponse},
6310
+ notes = ${`original_cycle:${cycleNumber}`}
6311
+ `;
6312
+ }
6313
+ async getPendingReviewResponse() {
6314
+ const rows = await this.sql`
6315
+ SELECT full_analysis, notes FROM strategy_reviews
6316
+ WHERE project_id = ${this.projectId} AND cycle_number = 0
6317
+ LIMIT 1
6318
+ `;
6319
+ if (rows.length === 0 || !rows[0].full_analysis) return null;
6320
+ const cycleMatch = rows[0].notes?.match(/original_cycle:(\d+)/);
6321
+ const cycleNumber = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
6322
+ return { cycleNumber, rawResponse: rows[0].full_analysis };
6323
+ }
6324
+ async clearPendingReviewResponse() {
6325
+ await this.sql`
6326
+ DELETE FROM strategy_reviews
6327
+ WHERE project_id = ${this.projectId} AND cycle_number = 0
6328
+ `;
6329
+ }
6330
+ // -------------------------------------------------------------------------
6331
+ // Doc Registry
6332
+ // -------------------------------------------------------------------------
6333
+ async registerDoc(entry) {
6334
+ const [row] = await this.sql`
6335
+ INSERT INTO doc_registry (
6336
+ project_id, title, type, path, status, summary, tags,
6337
+ cycle_created, cycle_updated, superseded_by, actions
6338
+ ) VALUES (
6339
+ ${this.projectId}, ${entry.title}, ${entry.type}, ${entry.path},
6340
+ ${entry.status}, ${entry.summary}, ${entry.tags},
6341
+ ${entry.cycleCreated}, ${entry.cycleUpdated ?? null},
6342
+ ${entry.supersededBy ?? null},
6343
+ ${entry.actions ? JSON.stringify(entry.actions) : "[]"}
6344
+ )
6345
+ ON CONFLICT (project_id, path)
6346
+ DO UPDATE SET
6347
+ title = EXCLUDED.title,
6348
+ type = EXCLUDED.type,
6349
+ status = EXCLUDED.status,
6350
+ summary = EXCLUDED.summary,
6351
+ tags = EXCLUDED.tags,
6352
+ cycle_updated = EXCLUDED.cycle_updated,
6353
+ superseded_by = EXCLUDED.superseded_by,
6354
+ actions = EXCLUDED.actions,
6355
+ updated_at = now()
6356
+ RETURNING id, created_at, updated_at
6357
+ `;
6358
+ return {
6359
+ ...entry,
6360
+ id: row.id,
6361
+ createdAt: row.created_at,
6362
+ updatedAt: row.updated_at
6363
+ };
6364
+ }
6365
+ async searchDocs(input) {
6366
+ const status = input.status ?? "active";
6367
+ const limit = input.limit ?? 10;
6368
+ const keyword = input.keyword ? `%${input.keyword}%` : null;
6369
+ const sinceCycle = input.sinceCycle ?? 0;
6370
+ const hasPending = input.hasPendingActions ?? false;
6371
+ const rows = await this.sql`
6372
+ SELECT * FROM doc_registry
6373
+ WHERE project_id = ${this.projectId}
6374
+ AND status = ${status}
6375
+ AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
6376
+ AND (${keyword}::text IS NULL OR (title ILIKE ${keyword} OR summary ILIKE ${keyword}))
6377
+ AND (${sinceCycle} = 0 OR COALESCE(cycle_updated, cycle_created) >= ${sinceCycle})
6378
+ AND (${hasPending} = false OR actions::text LIKE '%"pending"%')
6379
+ AND (${input.tags ?? []}::text[] = '{}' OR tags && ${input.tags ?? []})
6380
+ ORDER BY COALESCE(cycle_updated, cycle_created) DESC
6381
+ LIMIT ${limit}
6382
+ `;
6383
+ return rows.map((r) => ({
6384
+ id: r.id,
6385
+ title: r.title,
6386
+ type: r.type,
6387
+ path: r.path,
6388
+ status: r.status,
6389
+ summary: r.summary,
6390
+ tags: r.tags ?? [],
6391
+ cycleCreated: r.cycle_created,
6392
+ cycleUpdated: r.cycle_updated ?? void 0,
6393
+ supersededBy: r.superseded_by ?? void 0,
6394
+ actions: r.actions,
6395
+ createdAt: r.created_at,
6396
+ updatedAt: r.updated_at
6397
+ }));
6398
+ }
6399
+ async getDoc(idOrPath) {
6400
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrPath);
6401
+ const rows = isUuid ? await this.sql`
6402
+ SELECT * FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
6403
+ ` : await this.sql`
6404
+ SELECT * FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
6405
+ `;
6406
+ if (rows.length === 0) return null;
6407
+ const r = rows[0];
6408
+ return {
6409
+ id: r.id,
6410
+ title: r.title,
6411
+ type: r.type,
6412
+ path: r.path,
6413
+ status: r.status,
6414
+ summary: r.summary,
6415
+ tags: r.tags ?? [],
6416
+ cycleCreated: r.cycle_created,
6417
+ cycleUpdated: r.cycle_updated ?? void 0,
6418
+ supersededBy: r.superseded_by ?? void 0,
6419
+ actions: r.actions,
6420
+ createdAt: r.created_at,
6421
+ updatedAt: r.updated_at
6422
+ };
6423
+ }
6424
+ async updateDocStatus(id, status, supersededBy) {
6425
+ await this.sql`
6426
+ UPDATE doc_registry
6427
+ SET status = ${status},
6428
+ superseded_by = ${supersededBy ?? null},
6429
+ updated_at = now()
6430
+ WHERE id = ${id} AND project_id = ${this.projectId}
6431
+ `;
6432
+ }
6286
6433
  async writeDogfoodEntries(entries) {
6287
6434
  if (entries.length === 0) return;
6288
6435
  const values2 = entries.map((entry) => ({
@@ -7045,7 +7192,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
7045
7192
  status: "pending",
7046
7193
  content: r.content,
7047
7194
  createdCycle: r.created_cycle,
7048
- actionedSprint: r.actioned_cycle ?? void 0,
7195
+ actionedCycle: r.actioned_cycle ?? void 0,
7049
7196
  target: r.target ?? void 0
7050
7197
  }));
7051
7198
  }
@@ -7328,7 +7475,7 @@ ${r.content}` + (r.carry_forward ? `
7328
7475
  }));
7329
7476
  await this.sql`INSERT INTO entity_references ${this.sql(values2)}`;
7330
7477
  }
7331
- async getDecisionUsage(currentSprint) {
7478
+ async getDecisionUsage(currentCycle) {
7332
7479
  const rows = await this.sql`
7333
7480
  SELECT decision_id, reference_count, last_referenced_cycle
7334
7481
  FROM v_decision_usage
@@ -7338,7 +7485,7 @@ ${r.content}` + (r.carry_forward ? `
7338
7485
  decisionId: r.decision_id,
7339
7486
  referenceCount: parseInt(r.reference_count, 10),
7340
7487
  lastReferencedCycle: r.last_referenced_cycle,
7341
- cyclesSinceLastReference: currentSprint - r.last_referenced_cycle
7488
+ cyclesSinceLastReference: currentCycle - r.last_referenced_cycle
7342
7489
  }));
7343
7490
  }
7344
7491
  async getContextUtilisation() {
@@ -7979,6 +8126,7 @@ function loadConfig() {
7979
8126
  const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
7980
8127
  const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
7981
8128
  const autoPR = process.env.PAPI_AUTO_PR !== "false";
8129
+ const lightMode = process.env.PAPI_LIGHT_MODE === "true";
7982
8130
  const papiEndpoint = process.env.PAPI_ENDPOINT;
7983
8131
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
7984
8132
  const databaseUrl = process.env.DATABASE_URL;
@@ -7992,7 +8140,8 @@ function loadConfig() {
7992
8140
  baseBranch,
7993
8141
  autoPR,
7994
8142
  adapterType,
7995
- papiEndpoint
8143
+ papiEndpoint,
8144
+ lightMode
7996
8145
  };
7997
8146
  }
7998
8147
 
@@ -8000,7 +8149,6 @@ function loadConfig() {
8000
8149
  init_dist2();
8001
8150
  import path2 from "path";
8002
8151
  var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
8003
- var HOSTED_PROXY_KEY = "e9891a0a2225ac376f88ebdad78b4814b52ce0a39a41c5ec";
8004
8152
  var PLACEHOLDER_PATTERNS = [
8005
8153
  "<YOUR_DATABASE_URL>",
8006
8154
  "your-database-url",
@@ -8091,7 +8239,12 @@ async function createAdapter(optionsOrType, maybePapiDir) {
8091
8239
  );
8092
8240
  }
8093
8241
  const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
8094
- const dataApiKey = process.env["PAPI_DATA_API_KEY"] || HOSTED_PROXY_KEY;
8242
+ const dataApiKey = process.env["PAPI_DATA_API_KEY"];
8243
+ if (!dataApiKey) {
8244
+ throw new Error(
8245
+ "PAPI_DATA_API_KEY is required for proxy mode.\nTo get your API key:\n 1. Sign in at https://papi-web-three.vercel.app with GitHub\n 2. Your API key is shown on the onboarding page (save it \u2014 shown only once)\n 3. Add PAPI_DATA_API_KEY to your .mcp.json env config\nIf you already have a key, set it in your MCP configuration."
8246
+ );
8247
+ }
8095
8248
  const adapter2 = new ProxyPapiAdapter2({
8096
8249
  endpoint: dataEndpoint,
8097
8250
  apiKey: dataApiKey,
@@ -8985,6 +9138,9 @@ SECURITY CONSIDERATIONS
8985
9138
  REFERENCE DOCS
8986
9139
  [Optional \u2014 paths to docs/ files that provide background context for this task. Include only when the task originated from research or scoping work and the doc contains context the builder will need beyond what is in this handoff. Omit this section entirely for tasks that don't need supplementary context.]
8987
9140
 
9141
+ PRE-BUILD VERIFICATION
9142
+ [List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
9143
+
8988
9144
  FILES LIKELY TOUCHED
8989
9145
  [files]
8990
9146
 
@@ -9112,7 +9268,8 @@ Standard planning cycle with full board review.
9112
9268
  - Tier 4: Data visualization
9113
9269
  - Tier 5: New capability
9114
9270
  Within a tier: smaller effort wins. Justify in 2-3 sentences.
9115
- **Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 3-5 tasks. A 1-task cycle is almost never correct \u2014 if only 1 task qualifies, check if lower-priority tasks could also ship.
9271
+ **Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
9272
+ **Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 4-6 tasks. Cycles with fewer than 4 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 5+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 4 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct.
9116
9273
 
9117
9274
  8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates.
9118
9275
  **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.
@@ -9122,11 +9279,13 @@ Standard planning cycle with full board review.
9122
9279
 
9123
9280
  10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
9124
9281
  **SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
9282
+ **Scope pre-check:** Before writing the SCOPE section of each handoff, check whether the described functionality already exists based on the task's context, recent build reports, and the FILES LIKELY TOUCHED. If the infrastructure likely exists (e.g. a status type, a DB constraint, an API route), reduce the scope to only the missing pieces and explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
9125
9283
  **Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
9126
9284
  **Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6. This includes raw tasks (\`maturity: "raw"\`) and tasks whose phase prerequisites are not met. Only cycle-ready tasks should receive handoffs.
9127
9285
  **Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
9128
9286
  **Estimation calibration:** Tasks that wire existing adapter methods, add API routes following established patterns, modify prompts, or make documentation-only changes should be estimated **S** unless they require new abstractions, new DB tables, or multi-file architectural changes. Default to S for pattern-following work. Only use M when genuine new architecture is needed. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
9129
9287
  **Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
9288
+ **Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
9130
9289
  **UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout"), apply these handoff additions:
9131
9290
  - Add to SCOPE: "Use the \`frontend-design\` skill for implementation \u2014 it produces higher-quality visual output than manual styling."
9132
9291
  - Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser before reporting done \u2014 provide localhost URL or screenshot to the user for review."
@@ -9147,7 +9306,7 @@ Standard planning cycle with full board review.
9147
9306
  - The North Star changed or was validated in a way that the brief doesn't reflect
9148
9307
  - A phase completed that shifts what the product IS (not just what was built)
9149
9308
  - The brief describes capabilities, architecture, or direction that are no longer accurate
9150
- - **STALENESS CHECK:** Look at the "Last updated" line in the brief. If it references a cycle number more than 10 cycles behind the current cycle, the brief is stale and MUST be updated \u2014 even if no single trajectory-changing event occurred, cumulative drift over 10+ cycles means the brief no longer represents the product. This is the #1 source of planner drift.
9309
+ - **DRIFT CHECK:** Compare the brief's content against current reality. The brief is drifted if: (a) it describes capabilities that don't exist or have been removed, (b) it references user types, architecture, or positioning that ADs have since changed, (c) the current phase/stage has shifted from what the brief describes, or (d) key metrics or success criteria no longer match the project's direction. Cycle count since last update is a secondary signal only \u2014 a brief updated 15 cycles ago that still accurately describes the product is NOT stale. A brief updated 3 cycles ago that contradicts a recent AD IS drifted.
9151
9310
  If any of these apply, include an updated \`productBrief\` in the structured output. Include the FULL updated brief (not a diff). Preserve all existing sections and user-added content; update facts, numbers, and status to reflect current reality. Do not regenerate the brief every cycle \u2014 but do not let it go stale either.
9152
9311
 
9153
9312
  13. **Forward Horizon** \u2014 If a Forward Horizon section is provided in the context below, write a "## Forward Horizon" section in Part 1. Surface 2-3 decisions the team should make before the next phase starts. Each item must be:
@@ -9345,7 +9504,7 @@ You MUST cover these 6 sections. Each is mandatory unless explicitly noted as co
9345
9504
  - Is the product brief still an accurate description of what this product IS and WHERE it's going? If ADs have been created or superseded since the brief was last updated, the brief may be wrong.
9346
9505
  - Has the target user changed? Has the scope expanded or contracted in ways the brief doesn't capture?
9347
9506
  - Are we building for the right problem? Has evidence emerged (from builds, feedback, or market) that the core problem statement needs revision?
9348
- - If the North Star hasn't been referenced in 10+ cycles, flag it as potentially stale.
9507
+ - Assess North Star drift: Does the North Star's key metric and success definition still align with the current phase, active ADs, and recent build directions? A North Star is drifted when: the metric it tracks is no longer the team's focus, the success criteria reference capabilities that have been deprioritised, or ADs have shifted the product direction away from what the North Star describes. Cycle count since last update is a secondary signal \u2014 a stable, accurate North Star is not stale regardless of age.
9349
9508
  If this analysis reveals the brief needs updating, you MUST include updated content in \`productBriefUpdates\` in Part 2. Don't just note "the brief is stale" \u2014 write the update.
9350
9509
 
9351
9510
  6. **Active Decision Review + Scoring** \u2014 For each non-superseded AD: is the confidence level still correct? Has evidence emerged that changes anything? Score on 5 dimensions (1-5, lower = better):
@@ -9397,11 +9556,11 @@ ${compressionJob}
9397
9556
  - If no phase data is provided, skip this section.
9398
9557
  Report findings in a "Hierarchy Assessment" section in Part 1. Persist findings in the \`stalePhases\` array in Part 2 (include stage/horizon observations too). If no issues found, omit the section and use an empty array.
9399
9558
 
9400
- 12. **Structural Staleness Detection** \u2014 If decision usage data is provided in context, identify structural decay:
9401
- - ADs that haven't been **referenced in 10+ cycles** \u2192 flag as potentially obsolete, recommend review or resolution.
9402
- - Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
9403
- - ADs with LOW confidence that have persisted for 5+ cycles without evidence \u2192 flag as unvalidated.
9404
- Reference the decision usage data for quantitative staleness signals rather than guessing. Report findings in a "Structural Staleness" section in Part 1. Persist findings in the \`staleDecisions\` array in Part 2. If no issues found, omit the section and use an empty array.
9559
+ 12. **Structural Drift Detection** \u2014 If decision usage data is provided in context, identify structural decay using drift-based criteria (not pure cycle counts):
9560
+ - **AD drift:** An AD is drifted when its content contradicts recent build evidence, references architecture/capabilities that no longer exist, or has been made redundant by newer ADs. Reference frequency is a secondary signal \u2014 an unreferenced AD that is still accurate is not necessarily stale; an AD referenced last cycle that contradicts shipped code IS drifted.
9561
+ - **Carry-forward drift:** Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
9562
+ - **Confidence drift:** ADs with LOW confidence that have not gained supporting evidence within 5 cycles \u2192 flag as unvalidated. ADs where build reports contradict the decision \u2192 flag as confidence should decrease.
9563
+ Use decision usage data as a secondary signal (unreferenced ADs are more likely to be drifted, but verify by checking content alignment). Report findings in a "Structural Drift" section in Part 1. Persist findings in the \`staleDecisions\` array in Part 2. If no issues found, omit the section and use an empty array.
9405
9564
 
9406
9565
  ## OUTPUT FORMAT
9407
9566
 
@@ -9422,7 +9581,7 @@ Then include conditional sections only if relevant:
9422
9581
  - **Architecture Health** \u2014 only if issues found
9423
9582
  - **Discovery Canvas Audit** \u2014 only if gaps or staleness found
9424
9583
  - **Hierarchy Assessment** \u2014 only if hierarchy staleness, phase closure, or stage progression signals detected
9425
- - **Structural Staleness** \u2014 only if unreferenced ADs or stuck carry-forwards found${compressionPart1}
9584
+ - **Structural Drift** \u2014 only if drifted ADs or stuck carry-forwards found${compressionPart1}
9426
9585
 
9427
9586
  ### Part 2: Structured Data Block
9428
9587
  After your natural language output, include this EXACT format on its own line:
@@ -9584,6 +9743,12 @@ function buildReviewUserMessage(ctx) {
9584
9743
  if (ctx.recommendationEffectiveness) {
9585
9744
  parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
9586
9745
  }
9746
+ if (ctx.adHocCommits) {
9747
+ parts.push("### Ad-hoc Work (Non-Task Commits)", "", ctx.adHocCommits, "");
9748
+ }
9749
+ if (ctx.pendingRecommendations) {
9750
+ parts.push("### Pending Strategy Recommendations", "", ctx.pendingRecommendations, "");
9751
+ }
9587
9752
  return parts.join("\n");
9588
9753
  }
9589
9754
  function parseReviewStructuredOutput(raw) {
@@ -10069,9 +10234,19 @@ function autoCommitPapi(config2, cycleNumber, mode) {
10069
10234
  return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
10070
10235
  }
10071
10236
  }
10072
- function formatStrategyRecommendations(recs) {
10073
- const byType = /* @__PURE__ */ new Map();
10237
+ var REC_EXPIRY_CYCLES = 3;
10238
+ function formatStrategyRecommendations(recs, currentCycle) {
10239
+ const active = [];
10240
+ const expired = [];
10074
10241
  for (const rec of recs) {
10242
+ if (currentCycle !== void 0 && rec.createdCycle && currentCycle - rec.createdCycle > REC_EXPIRY_CYCLES) {
10243
+ expired.push(rec);
10244
+ } else {
10245
+ active.push(rec);
10246
+ }
10247
+ }
10248
+ const byType = /* @__PURE__ */ new Map();
10249
+ for (const rec of active) {
10075
10250
  const list = byType.get(rec.type) ?? [];
10076
10251
  list.push(rec);
10077
10252
  byType.set(rec.type, list);
@@ -10091,6 +10266,12 @@ function formatStrategyRecommendations(recs) {
10091
10266
  sections.push(`- (Cycle ${item.createdCycle}) ${item.content}${targetSuffix}`);
10092
10267
  }
10093
10268
  }
10269
+ if (expired.length > 0) {
10270
+ sections.push(`**Expired (${expired.length} recs skipped \u2014 older than ${REC_EXPIRY_CYCLES} cycles):**`);
10271
+ for (const item of expired) {
10272
+ sections.push(`- (Cycle ${item.createdCycle}) ${item.content.slice(0, 80)}...`);
10273
+ }
10274
+ }
10094
10275
  return sections.join("\n");
10095
10276
  }
10096
10277
  function formatDiscoveryCanvas(canvas) {
@@ -10311,7 +10492,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
10311
10492
  try {
10312
10493
  const pendingRecs = await adapter2.getPendingRecommendations();
10313
10494
  if (pendingRecs.length > 0) {
10314
- strategyRecommendationsText2 = formatStrategyRecommendations(pendingRecs);
10495
+ strategyRecommendationsText2 = formatStrategyRecommendations(pendingRecs, health.totalCycles);
10315
10496
  }
10316
10497
  } catch {
10317
10498
  }
@@ -11240,6 +11421,7 @@ ${result.userMessage}
11240
11421
  // src/services/strategy.ts
11241
11422
  init_dist2();
11242
11423
  import { randomUUID as randomUUID8, createHash as createHash2 } from "crypto";
11424
+ import { execFileSync as execFileSync2 } from "child_process";
11243
11425
 
11244
11426
  // src/lib/phase-realign.ts
11245
11427
  function extractPhaseNumber(phaseField) {
@@ -11556,6 +11738,45 @@ function formatTaskCompact(t) {
11556
11738
  Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}` + (notesSnippet ? `
11557
11739
  Notes: ${notesSnippet}` : "") + "\n";
11558
11740
  }
11741
+ function getAdHocCommits(projectRoot, sinceTag) {
11742
+ try {
11743
+ const logArgs = ["log", "--oneline", "--no-merges", "-100"];
11744
+ if (sinceTag) {
11745
+ logArgs.splice(1, 0, `${sinceTag}..HEAD`);
11746
+ }
11747
+ const output = execFileSync2("git", logArgs, {
11748
+ cwd: projectRoot,
11749
+ encoding: "utf-8",
11750
+ timeout: 5e3
11751
+ }).trim();
11752
+ if (!output) return void 0;
11753
+ const allCommits = output.split("\n");
11754
+ const taskPattern = /task-\d+/i;
11755
+ const nonTaskCommits = allCommits.filter((line) => !taskPattern.test(line));
11756
+ if (nonTaskCommits.length === 0) return void 0;
11757
+ const capped = nonTaskCommits.slice(0, 20);
11758
+ const groups = {};
11759
+ const typePattern = /^[a-f0-9]+ (feat|fix|chore|refactor|docs|style|test|ci|perf|build|release)[\s(:]/i;
11760
+ for (const line of capped) {
11761
+ const match = line.match(typePattern);
11762
+ const type = match ? match[1].toLowerCase() : "other";
11763
+ (groups[type] ??= []).push(line);
11764
+ }
11765
+ const lines = [];
11766
+ lines.push(`${nonTaskCommits.length} non-task commits found${nonTaskCommits.length > 20 ? " (showing 20 most recent)" : ""}:
11767
+ `);
11768
+ for (const [type, commits] of Object.entries(groups).sort((a, b2) => b2[1].length - a[1].length)) {
11769
+ lines.push(`**${type}** (${commits.length}):`);
11770
+ for (const c of commits) {
11771
+ lines.push(`- ${c}`);
11772
+ }
11773
+ lines.push("");
11774
+ }
11775
+ return lines.join("\n").trimEnd();
11776
+ } catch {
11777
+ return void 0;
11778
+ }
11779
+ }
11559
11780
  async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot) {
11560
11781
  const lastReviewCycleNum = cycleNumber - cyclesSinceLastReview;
11561
11782
  const [
@@ -11571,7 +11792,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11571
11792
  currentNorthStar,
11572
11793
  canvas,
11573
11794
  decisionUsage,
11574
- recData
11795
+ recData,
11796
+ pendingRecs
11575
11797
  ] = await Promise.all([
11576
11798
  adapter2.readProductBrief(),
11577
11799
  adapter2.getActiveDecisions(),
@@ -11592,7 +11814,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11592
11814
  // Previously sequential — now parallel
11593
11815
  adapter2.readDiscoveryCanvas().catch(() => ({})),
11594
11816
  adapter2.getDecisionUsage(cycleNumber).catch(() => []),
11595
- adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([])
11817
+ adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([]),
11818
+ adapter2.getPendingRecommendations().catch(() => [])
11596
11819
  ]);
11597
11820
  const tasks = [...activeTasks, ...recentDoneTasks];
11598
11821
  const recentLog = log;
@@ -11667,12 +11890,32 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11667
11890
  }
11668
11891
  } catch {
11669
11892
  }
11893
+ let pendingRecsText;
11894
+ try {
11895
+ if (pendingRecs.length > 0) {
11896
+ const lines = pendingRecs.map((r) => {
11897
+ const targetSuffix = r.target ? ` \u2192 ${r.target}` : "";
11898
+ return `- [${r.status}] (Cycle ${r.createdCycle}, ${r.type}) ${r.content}${targetSuffix}`;
11899
+ });
11900
+ pendingRecsText = `${pendingRecs.length} pending recommendation(s) from prior reviews:
11901
+ ${lines.join("\n")}`;
11902
+ }
11903
+ } catch {
11904
+ }
11905
+ let adHocCommitsText;
11906
+ try {
11907
+ const sinceTag = `v0.${lastReviewCycleNum}.0`;
11908
+ adHocCommitsText = getAdHocCommits(projectRoot, sinceTag);
11909
+ } catch {
11910
+ }
11670
11911
  logDataSourceSummary("strategy_review_audit", [
11671
11912
  { label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
11672
11913
  { label: "briefImplications", hasData: briefImplicationsText !== void 0 },
11673
11914
  { label: "phases", hasData: phasesText !== void 0 },
11674
11915
  { label: "decisionUsage", hasData: decisionUsageText !== void 0 },
11675
- { label: "recEffectiveness", hasData: recEffectivenessText !== void 0 }
11916
+ { label: "recEffectiveness", hasData: recEffectivenessText !== void 0 },
11917
+ { label: "pendingRecs", hasData: pendingRecsText !== void 0 },
11918
+ { label: "adHocCommits", hasData: adHocCommitsText !== void 0 }
11676
11919
  ]);
11677
11920
  const context = {
11678
11921
  sessionNumber: cycleNumber,
@@ -11692,7 +11935,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
11692
11935
  phases: phasesText,
11693
11936
  decisionUsage: decisionUsageText,
11694
11937
  northStar: currentNorthStar ?? void 0,
11695
- recommendationEffectiveness: recEffectivenessText
11938
+ recommendationEffectiveness: recEffectivenessText,
11939
+ adHocCommits: adHocCommitsText,
11940
+ pendingRecommendations: pendingRecsText
11696
11941
  };
11697
11942
  const BUDGET_SOFT2 = 8e4;
11698
11943
  const BUDGET_HARD2 = 1e5;
@@ -11904,8 +12149,16 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
11904
12149
  phaseChanges = await writeBack2(adapter2, cycleNumber, data, displayText);
11905
12150
  } catch (err) {
11906
12151
  writeBackFailed = err instanceof Error ? err.message : String(err);
12152
+ try {
12153
+ await adapter2.savePendingReviewResponse?.(cycleNumber, rawOutput);
12154
+ } catch {
12155
+ }
11907
12156
  }
11908
12157
  if (!writeBackFailed) {
12158
+ try {
12159
+ await adapter2.clearPendingReviewResponse?.();
12160
+ } catch {
12161
+ }
11909
12162
  const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
11910
12163
  slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
11911
12164
  }
@@ -11953,6 +12206,28 @@ async function prepareStrategyReview(adapter2, force, projectRoot, adapterType)
11953
12206
  isPg ? "Could not read cycle health from the database. Check your DATABASE_URL and verify the project exists." : "Could not read cycle health from PLANNING_LOG.md. Run setup first to initialise your PAPI project."
11954
12207
  );
11955
12208
  }
12209
+ try {
12210
+ const pending = await adapter2.getPendingReviewResponse?.();
12211
+ if (pending) {
12212
+ return {
12213
+ cycleNumber: pending.cycleNumber || cycleNumber,
12214
+ systemPrompt: "",
12215
+ userMessage: `\u26A0\uFE0F **Pending Strategy Review Found**
12216
+
12217
+ A previous strategy review (Cycle ${pending.cycleNumber || cycleNumber}) failed to write back. The raw LLM response has been preserved.
12218
+
12219
+ To retry, call \`strategy_review\` with:
12220
+ - \`mode\`: "apply"
12221
+ - \`llm_response\`: (the response below)
12222
+ - \`cycle_number\`: ${pending.cycleNumber || cycleNumber}
12223
+
12224
+ ---
12225
+
12226
+ ${pending.rawResponse}`
12227
+ };
12228
+ }
12229
+ } catch {
12230
+ }
11956
12231
  let context;
11957
12232
  try {
11958
12233
  context = await assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot);
@@ -12740,7 +13015,7 @@ var boardViewTool = {
12740
13015
  };
12741
13016
  var boardDeprioritiseTool = {
12742
13017
  name: "board_deprioritise",
12743
- description: `Remove a task from the current cycle. Three actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
13018
+ description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
12744
13019
  inputSchema: {
12745
13020
  type: "object",
12746
13021
  properties: {
@@ -12750,8 +13025,8 @@ var boardDeprioritiseTool = {
12750
13025
  },
12751
13026
  action: {
12752
13027
  type: "string",
12753
- enum: ["backlog", "defer", "cancel"],
12754
- description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
13028
+ enum: ["backlog", "defer", "block", "cancel"],
13029
+ description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "block" = waiting on external dependency (visible but skipped by planner \u2014 reason required). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
12755
13030
  },
12756
13031
  reason: {
12757
13032
  type: "string",
@@ -12872,6 +13147,29 @@ async function handleBoardDeprioritise(adapter2, args) {
12872
13147
  const reason = args.reason;
12873
13148
  const newPriority = args.priority;
12874
13149
  const newPhase = args.phase;
13150
+ if (action === "block") {
13151
+ if (!reason) {
13152
+ return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
13153
+ }
13154
+ try {
13155
+ const task = await adapter2.getTask(taskId);
13156
+ if (!task) return errorResponse(`Task ${taskId} not found.`);
13157
+ const existingNotes = task.notes ? `${task.notes}
13158
+
13159
+ ` : "";
13160
+ await adapter2.updateTask(taskId, {
13161
+ status: "Blocked",
13162
+ notes: `${existingNotes}BLOCKED: ${reason}`
13163
+ });
13164
+ return textResponse(`Blocked **${taskId}** (${task.title}).
13165
+
13166
+ Reason: ${reason}
13167
+
13168
+ Task remains visible on the board but will be skipped by the planner and build_list.`);
13169
+ } catch (err) {
13170
+ return errorResponse(err instanceof Error ? err.message : String(err));
13171
+ }
13172
+ }
12875
13173
  if (action === "cancel") {
12876
13174
  if (!reason) {
12877
13175
  return errorResponse("reason is required when cancelling a task.");
@@ -14008,6 +14306,8 @@ init_dist2();
14008
14306
 
14009
14307
  // src/services/build.ts
14010
14308
  import { randomUUID as randomUUID9 } from "crypto";
14309
+ import { readdirSync, existsSync } from "fs";
14310
+ import { join as join3 } from "path";
14011
14311
  function capitalizeCompleted(value) {
14012
14312
  const map = {
14013
14313
  yes: "Yes",
@@ -14247,6 +14547,13 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14247
14547
  cycleNumber = 0;
14248
14548
  }
14249
14549
  const now = /* @__PURE__ */ new Date();
14550
+ let iterationCount = 1;
14551
+ try {
14552
+ const priorReports = await adapter2.getRecentBuildReports(200);
14553
+ const priorForTask = priorReports.filter((r) => r.taskId === taskId);
14554
+ iterationCount = priorForTask.length + 1;
14555
+ } catch {
14556
+ }
14250
14557
  const report = {
14251
14558
  uuid: randomUUID9(),
14252
14559
  createdAt: now.toISOString(),
@@ -14264,7 +14571,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14264
14571
  handoffAccuracy: input.handoffAccuracy,
14265
14572
  correctionsCount: input.correctionsCount,
14266
14573
  briefImplications: input.briefImplications,
14267
- deadEnds: input.deadEnds
14574
+ deadEnds: input.deadEnds,
14575
+ iterationCount
14268
14576
  };
14269
14577
  if (input.relatedDecisions) {
14270
14578
  const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
@@ -14291,7 +14599,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14291
14599
  }
14292
14600
  const surpriseNote = input.surprises === "None" ? "" : ` Surprises: ${input.surprises}.`;
14293
14601
  const issueNote = input.discoveredIssues === "None" ? "" : ` Issues: ${input.discoveredIssues}.`;
14294
- const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${surpriseNote}${issueNote}`;
14602
+ const iterNote = iterationCount > 1 ? ` Iterations: ${iterationCount} (${iterationCount - 1} pushback${iterationCount > 2 ? "s" : ""}).` : "";
14603
+ const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${iterNote}${surpriseNote}${issueNote}`;
14295
14604
  await adapter2.updateTask(taskId, { buildReport: buildReportSummary });
14296
14605
  if (input.completed === "yes") {
14297
14606
  if (options.light) {
@@ -14332,6 +14641,32 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14332
14641
  phaseChanges = await propagatePhaseStatus(adapter2);
14333
14642
  } catch {
14334
14643
  }
14644
+ let docWarning;
14645
+ try {
14646
+ if (adapter2.searchDocs) {
14647
+ const docsDir = join3(config2.projectRoot, "docs");
14648
+ if (existsSync(docsDir)) {
14649
+ const scanDir = (dir) => {
14650
+ const entries = readdirSync(dir, { withFileTypes: true });
14651
+ const files = [];
14652
+ for (const e of entries) {
14653
+ const full = join3(dir, e.name);
14654
+ if (e.isDirectory()) files.push(...scanDir(full));
14655
+ else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
14656
+ }
14657
+ return files;
14658
+ };
14659
+ const mdFiles = scanDir(docsDir);
14660
+ const registered = await adapter2.searchDocs({ status: "active", limit: 500 });
14661
+ const registeredPaths = new Set(registered.map((d) => d.path));
14662
+ const unregistered = mdFiles.filter((f) => !registeredPaths.has(f));
14663
+ if (unregistered.length > 0) {
14664
+ docWarning = `${unregistered.length} unregistered doc(s) in docs/ \u2014 consider running \`doc_register\` for: ${unregistered.slice(0, 5).join(", ")}${unregistered.length > 5 ? ` (+${unregistered.length - 5} more)` : ""}`;
14665
+ }
14666
+ }
14667
+ }
14668
+ } catch {
14669
+ }
14335
14670
  return {
14336
14671
  task,
14337
14672
  report,
@@ -14343,7 +14678,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
14343
14678
  discoveredIssues: input.discoveredIssues,
14344
14679
  completed: input.completed,
14345
14680
  scopeAccuracy: input.scopeAccuracy,
14346
- phaseChanges
14681
+ phaseChanges,
14682
+ docWarning
14347
14683
  };
14348
14684
  }
14349
14685
  async function cancelBuild(adapter2, taskId, reason) {
@@ -14384,7 +14720,7 @@ var buildDescribeTool = {
14384
14720
  };
14385
14721
  var buildExecuteTool = {
14386
14722
  name: "build_execute",
14387
- description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Does not call the Anthropic API.",
14723
+ description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
14388
14724
  inputSchema: {
14389
14725
  type: "object",
14390
14726
  properties: {
@@ -14550,7 +14886,7 @@ async function handleBuildExecute(adapter2, config2, args) {
14550
14886
  if (!taskId) {
14551
14887
  return errorResponse("task_id is required.");
14552
14888
  }
14553
- const light = args.light === true;
14889
+ const light = args.light === true || config2.lightMode;
14554
14890
  if (hasReportFields(args)) {
14555
14891
  return handleExecuteComplete(adapter2, config2, taskId, args, light);
14556
14892
  }
@@ -14563,8 +14899,16 @@ async function handleBuildExecute(adapter2, config2, args) {
14563
14899
  ---
14564
14900
 
14565
14901
  `;
14902
+ const verificationFiles = result.task.buildHandoff.verificationFiles ?? [];
14903
+ const verificationNote = verificationFiles.length > 0 ? `
14904
+
14905
+ ---
14906
+
14907
+ **PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
14908
+ ${verificationFiles.map((f) => `- ${f}`).join("\n")}
14909
+ If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
14566
14910
  const chainInstruction = "\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.";
14567
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + chainInstruction + phaseNote);
14911
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + verificationNote + chainInstruction + phaseNote);
14568
14912
  } catch (err) {
14569
14913
  if (isNoHandoffError(err)) {
14570
14914
  const lines = [
@@ -14671,6 +15015,9 @@ function formatCompleteResult(result) {
14671
15015
  lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
14672
15016
  }
14673
15017
  }
15018
+ if (result.docWarning) {
15019
+ lines.push("", `\u{1F4C4} ${result.docWarning}`);
15020
+ }
14674
15021
  const hasDiscoveredIssues = result.discoveredIssues !== "None" && result.discoveredIssues.trim() !== "";
14675
15022
  const remaining = result.cycleProgress.total - result.cycleProgress.completed;
14676
15023
  if (result.completed !== "yes") {
@@ -15818,6 +16165,25 @@ async function getHealthSummary(adapter2) {
15818
16165
  } catch (_err) {
15819
16166
  metricsSection = "Could not read methodology metrics.";
15820
16167
  }
16168
+ try {
16169
+ const recentReports = await adapter2.getRecentBuildReports(50);
16170
+ if (recentReports.length > 0) {
16171
+ const taskCounts = /* @__PURE__ */ new Map();
16172
+ for (const r of recentReports) {
16173
+ taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
16174
+ }
16175
+ const iterCounts = [...taskCounts.values()];
16176
+ const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
16177
+ const multiIterTasks = iterCounts.filter((c) => c > 1).length;
16178
+ if (avgIter > 1 || multiIterTasks > 0) {
16179
+ derivedMetricsSection += `
16180
+
16181
+ **Rework**
16182
+ - Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
16183
+ }
16184
+ }
16185
+ } catch {
16186
+ }
15821
16187
  const costSection = "Disabled \u2014 local MCP, no API costs.";
15822
16188
  let decisionUsageSection = "";
15823
16189
  try {
@@ -15991,7 +16357,7 @@ async function handleHealth(adapter2) {
15991
16357
 
15992
16358
  // src/services/release.ts
15993
16359
  import { writeFile as writeFile3 } from "fs/promises";
15994
- import { join as join3 } from "path";
16360
+ import { join as join4 } from "path";
15995
16361
  var INITIAL_RELEASE_NOTES = `# Changelog
15996
16362
 
15997
16363
  ## v0.1.0-alpha \u2014 Initial Release
@@ -16082,7 +16448,7 @@ async function createRelease(config2, branch, version, adapter2) {
16082
16448
  const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
16083
16449
  changelogContent = generateChangelog(version, commits);
16084
16450
  }
16085
- const changelogPath = join3(config2.projectRoot, "CHANGELOG.md");
16451
+ const changelogPath = join4(config2.projectRoot, "CHANGELOG.md");
16086
16452
  await writeFile3(changelogPath, changelogContent, "utf-8");
16087
16453
  const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
16088
16454
  const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
@@ -16163,8 +16529,8 @@ async function handleRelease(adapter2, config2, args) {
16163
16529
  }
16164
16530
 
16165
16531
  // src/tools/review.ts
16166
- import { existsSync } from "fs";
16167
- import { join as join4 } from "path";
16532
+ import { existsSync as existsSync2 } from "fs";
16533
+ import { join as join5 } from "path";
16168
16534
 
16169
16535
  // src/services/review.ts
16170
16536
  init_dist2();
@@ -16402,8 +16768,8 @@ function mergeAfterAccept(config2, taskId) {
16402
16768
  }
16403
16769
  const featureBranch = taskBranchName(taskId);
16404
16770
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
16405
- const papiDir = join4(config2.projectRoot, ".papi");
16406
- if (existsSync(papiDir)) {
16771
+ const papiDir = join5(config2.projectRoot, ".papi");
16772
+ if (existsSync2(papiDir)) {
16407
16773
  try {
16408
16774
  const commitResult = stageDirAndCommit(
16409
16775
  config2.projectRoot,
@@ -16692,6 +17058,9 @@ Path: ${mcpJsonPath}`
16692
17058
  }
16693
17059
 
16694
17060
  // src/tools/orient.ts
17061
+ import { execFileSync as execFileSync3 } from "child_process";
17062
+ import { readFileSync } from "fs";
17063
+ import { join as join6 } from "path";
16695
17064
  var orientTool = {
16696
17065
  name: "orient",
16697
17066
  description: "Session orientation \u2014 single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, and recommended next action. Read-only, does not modify any files.",
@@ -16836,6 +17205,25 @@ async function getHierarchyPosition(adapter2) {
16836
17205
  return void 0;
16837
17206
  }
16838
17207
  }
17208
+ function checkNpmVersionDrift() {
17209
+ try {
17210
+ const pkgPath = join6(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
17211
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
17212
+ const localVersion = pkg.version;
17213
+ const packageName = pkg.name;
17214
+ const published = execFileSync3("npm", ["view", packageName, "version"], {
17215
+ encoding: "utf-8",
17216
+ timeout: 3e3,
17217
+ stdio: ["ignore", "pipe", "ignore"]
17218
+ }).trim();
17219
+ if (published && published !== localVersion) {
17220
+ return `\u26A0\uFE0F npm version drift: local v${localVersion} vs published v${published}`;
17221
+ }
17222
+ return null;
17223
+ } catch {
17224
+ return null;
17225
+ }
17226
+ }
16839
17227
  async function handleOrient(adapter2, config2) {
16840
17228
  try {
16841
17229
  const [buildResult, healthResult, hierarchy] = await Promise.all([
@@ -16884,7 +17272,28 @@ async function handleOrient(adapter2, config2) {
16884
17272
  } catch {
16885
17273
  }
16886
17274
  }
16887
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote);
17275
+ const versionDrift = checkNpmVersionDrift();
17276
+ const versionNote = versionDrift ? `
17277
+ ${versionDrift}` : "";
17278
+ let recsNote = "";
17279
+ try {
17280
+ const pendingRecs = await adapter2.getPendingRecommendations();
17281
+ if (pendingRecs.length > 0) {
17282
+ recsNote = `
17283
+ **Strategy Recommendations:** ${pendingRecs.length} pending action`;
17284
+ }
17285
+ } catch {
17286
+ }
17287
+ let pendingReviewNote = "";
17288
+ try {
17289
+ const pending = await adapter2.getPendingReviewResponse?.();
17290
+ if (pending) {
17291
+ pendingReviewNote = `
17292
+ \u26A0\uFE0F **Pending Strategy Review:** 1 review failed write-back (Cycle ${pending.cycleNumber}) \u2014 run \`strategy_review\` to retry.`;
17293
+ }
17294
+ } catch {
17295
+ }
17296
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + versionNote);
16888
17297
  } catch (err) {
16889
17298
  const message = err instanceof Error ? err.message : String(err);
16890
17299
  return errorResponse(`Orient failed: ${message}`);
@@ -17311,6 +17720,132 @@ ${result.userMessage}
17311
17720
  }
17312
17721
  }
17313
17722
 
17723
+ // src/tools/doc-registry.ts
17724
+ var docRegisterTool = {
17725
+ name: "doc_register",
17726
+ description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
17727
+ inputSchema: {
17728
+ type: "object",
17729
+ properties: {
17730
+ path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
17731
+ title: { type: "string", description: "Document title." },
17732
+ type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
17733
+ status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
17734
+ summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
17735
+ tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
17736
+ cycle: { type: "number", description: "Current cycle number." },
17737
+ actions: {
17738
+ type: "array",
17739
+ items: {
17740
+ type: "object",
17741
+ properties: {
17742
+ description: { type: "string" },
17743
+ status: { type: "string", enum: ["pending", "resolved"] },
17744
+ linkedTaskId: { type: "string" }
17745
+ },
17746
+ required: ["description", "status"]
17747
+ },
17748
+ description: "Actionable findings from the document."
17749
+ },
17750
+ superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
17751
+ },
17752
+ required: ["path", "title", "type", "summary", "cycle"]
17753
+ }
17754
+ };
17755
+ var docSearchTool = {
17756
+ name: "doc_search",
17757
+ description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
17758
+ inputSchema: {
17759
+ type: "object",
17760
+ properties: {
17761
+ type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
17762
+ status: { type: "string", description: 'Filter by status. Defaults to "active".' },
17763
+ tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
17764
+ keyword: { type: "string", description: "Search title and summary text." },
17765
+ has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
17766
+ since_cycle: { type: "number", description: "Docs updated since this cycle." },
17767
+ limit: { type: "number", description: "Max results (default: 10)." }
17768
+ },
17769
+ required: []
17770
+ }
17771
+ };
17772
+ async function handleDocRegister(adapter2, args) {
17773
+ if (!adapter2.registerDoc) {
17774
+ return errorResponse("Doc registry not available \u2014 requires pg adapter.");
17775
+ }
17776
+ const path5 = args.path;
17777
+ const title = args.title;
17778
+ const type = args.type;
17779
+ const status = args.status ?? "active";
17780
+ const summary = args.summary;
17781
+ const tags = args.tags ?? [];
17782
+ const cycle = args.cycle;
17783
+ const actions = args.actions;
17784
+ const supersededByPath = args.superseded_by_path;
17785
+ if (!path5 || !title || !type || !summary || !cycle) {
17786
+ return errorResponse("Required fields: path, title, type, summary, cycle.");
17787
+ }
17788
+ let supersededBy;
17789
+ if (supersededByPath) {
17790
+ const existing = await adapter2.getDoc?.(supersededByPath);
17791
+ if (existing) {
17792
+ supersededBy = existing.id;
17793
+ await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
17794
+ }
17795
+ }
17796
+ const entry = await adapter2.registerDoc({
17797
+ title,
17798
+ type,
17799
+ path: path5,
17800
+ status: supersededByPath ? "superseded" : status,
17801
+ summary,
17802
+ tags,
17803
+ cycleCreated: cycle,
17804
+ cycleUpdated: cycle,
17805
+ supersededBy,
17806
+ actions
17807
+ });
17808
+ return textResponse(
17809
+ `**Registered:** ${entry.title}
17810
+ - **Path:** ${entry.path}
17811
+ - **Type:** ${entry.type} | **Status:** ${entry.status}
17812
+ - **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
17813
+ - **Actions:** ${actions?.length ?? 0} items
17814
+ - **ID:** ${entry.id}`
17815
+ );
17816
+ }
17817
+ async function handleDocSearch(adapter2, args) {
17818
+ if (!adapter2.searchDocs) {
17819
+ return errorResponse("Doc registry not available \u2014 requires pg adapter.");
17820
+ }
17821
+ const input = {
17822
+ type: args.type,
17823
+ status: args.status,
17824
+ tags: args.tags,
17825
+ keyword: args.keyword,
17826
+ hasPendingActions: args.has_pending_actions,
17827
+ sinceCycle: args.since_cycle,
17828
+ limit: args.limit
17829
+ };
17830
+ const docs = await adapter2.searchDocs(input);
17831
+ if (docs.length === 0) {
17832
+ return textResponse("No documents found matching the search criteria.");
17833
+ }
17834
+ const lines = docs.map((d) => {
17835
+ const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
17836
+ const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
17837
+ return `### ${d.title}
17838
+ **Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
17839
+ **Path:** ${d.path}
17840
+ **Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
17841
+ ${d.summary}
17842
+ `;
17843
+ });
17844
+ return textResponse(`**${docs.length} document(s) found:**
17845
+
17846
+ ${lines.join("\n---\n\n")}`);
17847
+ }
17848
+
17314
17849
  // src/lib/telemetry.ts
17315
17850
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
17316
17851
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
@@ -17407,7 +17942,9 @@ function createServer(adapter2, config2) {
17407
17942
  initTool,
17408
17943
  orientTool,
17409
17944
  hierarchyUpdateTool,
17410
- zoomOutTool
17945
+ zoomOutTool,
17946
+ docRegisterTool,
17947
+ docSearchTool
17411
17948
  ]
17412
17949
  }));
17413
17950
  server2.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -17508,6 +18045,12 @@ function createServer(adapter2, config2) {
17508
18045
  case "zoom_out":
17509
18046
  result = await handleZoomOut(adapter2, config2, safeArgs);
17510
18047
  break;
18048
+ case "doc_register":
18049
+ result = await handleDocRegister(adapter2, safeArgs);
18050
+ break;
18051
+ case "doc_search":
18052
+ result = await handleDocSearch(adapter2, safeArgs);
18053
+ break;
17511
18054
  default:
17512
18055
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
17513
18056
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "exports": {
9
- ".": "./dist/index.js",
10
- "./prompts": "./src/prompts.ts"
9
+ ".": "./dist/index.js"
11
10
  },
12
11
  "bin": {
13
12
  "papi-server": "./dist/index.js"