@papi-ai/server 0.7.10 → 0.7.12

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
@@ -1453,7 +1453,12 @@ var init_dist2 = __esm({
1453
1453
  research: 2,
1454
1454
  spike: 2,
1455
1455
  idea: 3,
1456
- discovery: 1
1456
+ discovery: 1,
1457
+ // Non-code brief types for non-technical Owners (AD-12)
1458
+ "design-brief": 1,
1459
+ "research-brief": 1,
1460
+ "marketing-brief": 1,
1461
+ "ops-brief": 1
1457
1462
  };
1458
1463
  VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
1459
1464
  SECTION_HEADERS = [
@@ -1575,11 +1580,13 @@ ${TABLE_SEPARATOR}
1575
1580
  }
1576
1581
  /** Prepend a new cycle log entry at the top of the Cycle Log section. */
1577
1582
  async writeCycleLogEntry(entry) {
1578
- if (!entry.uuid) {
1579
- entry = { ...entry, uuid: randomUUID6() };
1580
- }
1583
+ const patched = {
1584
+ ...entry,
1585
+ uuid: entry.uuid || randomUUID6(),
1586
+ date: entry.date ?? (/* @__PURE__ */ new Date()).toISOString()
1587
+ };
1581
1588
  const content = await this.read("SPRINT_LOG.md");
1582
- await this.write("SPRINT_LOG.md", prependCycleLogEntry(entry, content));
1589
+ await this.write("SPRINT_LOG.md", prependCycleLogEntry(patched, content));
1583
1590
  }
1584
1591
  /** Write a strategy review — for md adapter, delegates to cycle log. */
1585
1592
  async writeStrategyReview(review) {
@@ -3708,7 +3715,7 @@ var init_connection = __esm({
3708
3715
 
3709
3716
  // ../../node_modules/postgres/src/subscribe.js
3710
3717
  function Subscribe(postgres2, options) {
3711
- const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state = {};
3718
+ const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state2 = {};
3712
3719
  let connection2, stream, ended = false;
3713
3720
  const sql = subscribe.sql = postgres2({
3714
3721
  ...options,
@@ -3725,7 +3732,7 @@ function Subscribe(postgres2, options) {
3725
3732
  if (ended)
3726
3733
  return;
3727
3734
  stream = null;
3728
- state.pid = state.secret = void 0;
3735
+ state2.pid = state2.secret = void 0;
3729
3736
  connected(await init(sql, slot, options.publications));
3730
3737
  subscribers.forEach((event) => event.forEach(({ onsubscribe }) => onsubscribe()));
3731
3738
  },
@@ -3756,13 +3763,13 @@ function Subscribe(postgres2, options) {
3756
3763
  connected(x);
3757
3764
  onsubscribe();
3758
3765
  stream && stream.on("error", onerror);
3759
- return { unsubscribe, state, sql };
3766
+ return { unsubscribe, state: state2, sql };
3760
3767
  });
3761
3768
  }
3762
3769
  function connected(x) {
3763
3770
  stream = x.stream;
3764
- state.pid = x.state.pid;
3765
- state.secret = x.state.secret;
3771
+ state2.pid = x.state.pid;
3772
+ state2.secret = x.state.secret;
3766
3773
  }
3767
3774
  async function init(sql2, slot2, publications) {
3768
3775
  if (!publications)
@@ -3774,7 +3781,7 @@ function Subscribe(postgres2, options) {
3774
3781
  const stream2 = await sql2.unsafe(
3775
3782
  `START_REPLICATION SLOT ${slot2} LOGICAL ${x.consistent_point} (proto_version '1', publication_names '${publications}')`
3776
3783
  ).writable();
3777
- const state2 = {
3784
+ const state3 = {
3778
3785
  lsn: Buffer.concat(x.consistent_point.split("/").map((x2) => Buffer.from(("00000000" + x2).slice(-8), "hex")))
3779
3786
  };
3780
3787
  stream2.on("data", data);
@@ -3786,9 +3793,9 @@ function Subscribe(postgres2, options) {
3786
3793
  }
3787
3794
  function data(x2) {
3788
3795
  if (x2[0] === 119) {
3789
- parse(x2.subarray(25), state2, sql2.options.parsers, handle, options.transform);
3796
+ parse(x2.subarray(25), state3, sql2.options.parsers, handle, options.transform);
3790
3797
  } else if (x2[0] === 107 && x2[17]) {
3791
- state2.lsn = x2.subarray(1, 9);
3798
+ state3.lsn = x2.subarray(1, 9);
3792
3799
  pong();
3793
3800
  }
3794
3801
  }
@@ -3804,7 +3811,7 @@ function Subscribe(postgres2, options) {
3804
3811
  function pong() {
3805
3812
  const x2 = Buffer.alloc(34);
3806
3813
  x2[0] = "r".charCodeAt(0);
3807
- x2.fill(state2.lsn, 1);
3814
+ x2.fill(state3.lsn, 1);
3808
3815
  x2.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2e3, 0, 1)) * BigInt(1e3), 25);
3809
3816
  stream2.write(x2);
3810
3817
  }
@@ -3816,12 +3823,12 @@ function Subscribe(postgres2, options) {
3816
3823
  function Time(x) {
3817
3824
  return new Date(Date.UTC(2e3, 0, 1) + Number(x / BigInt(1e3)));
3818
3825
  }
3819
- function parse(x, state, parsers2, handle, transform) {
3826
+ function parse(x, state2, parsers2, handle, transform) {
3820
3827
  const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc);
3821
3828
  Object.entries({
3822
3829
  R: (x2) => {
3823
3830
  let i = 1;
3824
- const r = state[x2.readUInt32BE(i)] = {
3831
+ const r = state2[x2.readUInt32BE(i)] = {
3825
3832
  schema: x2.toString("utf8", i += 4, i = x2.indexOf(0, i)) || "pg_catalog",
3826
3833
  table: x2.toString("utf8", i + 1, i = x2.indexOf(0, i + 1)),
3827
3834
  columns: Array(x2.readUInt16BE(i += 2)),
@@ -3848,12 +3855,12 @@ function parse(x, state, parsers2, handle, transform) {
3848
3855
  },
3849
3856
  // Origin
3850
3857
  B: (x2) => {
3851
- state.date = Time(x2.readBigInt64BE(9));
3852
- state.lsn = x2.subarray(1, 9);
3858
+ state2.date = Time(x2.readBigInt64BE(9));
3859
+ state2.lsn = x2.subarray(1, 9);
3853
3860
  },
3854
3861
  I: (x2) => {
3855
3862
  let i = 1;
3856
- const relation = state[x2.readUInt32BE(i)];
3863
+ const relation = state2[x2.readUInt32BE(i)];
3857
3864
  const { row } = tuples(x2, relation.columns, i += 7, transform);
3858
3865
  handle(row, {
3859
3866
  command: "insert",
@@ -3862,7 +3869,7 @@ function parse(x, state, parsers2, handle, transform) {
3862
3869
  },
3863
3870
  D: (x2) => {
3864
3871
  let i = 1;
3865
- const relation = state[x2.readUInt32BE(i)];
3872
+ const relation = state2[x2.readUInt32BE(i)];
3866
3873
  i += 4;
3867
3874
  const key = x2[i] === 75;
3868
3875
  handle(
@@ -3876,7 +3883,7 @@ function parse(x, state, parsers2, handle, transform) {
3876
3883
  },
3877
3884
  U: (x2) => {
3878
3885
  let i = 1;
3879
- const relation = state[x2.readUInt32BE(i)];
3886
+ const relation = state2[x2.readUInt32BE(i)];
3880
3887
  i += 4;
3881
3888
  const key = x2[i] === 75;
3882
3889
  const xs = key || x2[i] === 79 ? tuples(x2, relation.columns, i += 3, transform) : null;
@@ -4612,6 +4619,7 @@ function rowToCycleLogEntry(row) {
4612
4619
  if (row.notes != null) entry.notes = row.notes;
4613
4620
  if (row.task_count != null) entry.taskCount = row.task_count;
4614
4621
  if (row.effort_points != null) entry.effortPoints = row.effort_points;
4622
+ if (row.updated_at != null) entry.date = row.updated_at;
4615
4623
  return entry;
4616
4624
  }
4617
4625
  function rowToPhase(row) {
@@ -6276,7 +6284,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6276
6284
  async getCycleLog(limit) {
6277
6285
  if (limit != null) {
6278
6286
  const rows2 = await this.sql`
6279
- SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6287
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points, updated_at
6280
6288
  FROM planning_log_entries
6281
6289
  WHERE project_id = ${this.projectId}
6282
6290
  ORDER BY cycle_number DESC
@@ -6285,7 +6293,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6285
6293
  return rows2.map(rowToCycleLogEntry);
6286
6294
  }
6287
6295
  const rows = await this.sql`
6288
- SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6296
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points, updated_at
6289
6297
  FROM planning_log_entries
6290
6298
  WHERE project_id = ${this.projectId}
6291
6299
  ORDER BY cycle_number DESC
@@ -6295,7 +6303,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
6295
6303
  }
6296
6304
  async getCycleLogSince(cycleNumber) {
6297
6305
  const rows = await this.sql`
6298
- SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
6306
+ SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points, updated_at
6299
6307
  FROM planning_log_entries
6300
6308
  WHERE project_id = ${this.projectId}
6301
6309
  AND cycle_number >= ${cycleNumber}
@@ -6346,7 +6354,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
6346
6354
  carry_forward = EXCLUDED.carry_forward,
6347
6355
  notes = EXCLUDED.notes,
6348
6356
  task_count = EXCLUDED.task_count,
6349
- effort_points = EXCLUDED.effort_points
6357
+ effort_points = EXCLUDED.effort_points,
6358
+ updated_at = now()
6350
6359
  `;
6351
6360
  }
6352
6361
  async writeStrategyReview(review) {
@@ -6724,14 +6733,21 @@ ${newParts.join("\n")}` : newParts.join("\n");
6724
6733
  // Board (Tasks)
6725
6734
  // -------------------------------------------------------------------------
6726
6735
  async queryBoard(options) {
6727
- if (!options) {
6728
- const rows2 = await this.sql`
6729
- SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6730
- FROM cycle_tasks
6731
- WHERE project_id = ${this.projectId}
6732
- ORDER BY display_id
6733
- LIMIT 2000 -- hard ceiling; single project task count won't approach this
6734
- `;
6736
+ const compact = options?.compact === true;
6737
+ if (!options || Object.keys(options).length === 1 && compact) {
6738
+ const rows2 = compact ? await this.sql`
6739
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6740
+ FROM cycle_tasks
6741
+ WHERE project_id = ${this.projectId}
6742
+ ORDER BY display_id
6743
+ LIMIT 2000
6744
+ ` : await this.sql`
6745
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6746
+ FROM cycle_tasks
6747
+ WHERE project_id = ${this.projectId}
6748
+ ORDER BY display_id
6749
+ LIMIT 2000 -- hard ceiling; single project task count won't approach this
6750
+ `;
6735
6751
  return rows2.map(rowToTask);
6736
6752
  }
6737
6753
  const conditions = [
@@ -6766,11 +6782,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
6766
6782
  for (let i = 1; i < conditions.length; i++) {
6767
6783
  where = this.sql`${where} AND ${conditions[i]}`;
6768
6784
  }
6769
- const rows = await this.sql`
6770
- SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6771
- FROM cycle_tasks WHERE ${where} ORDER BY display_id
6772
- LIMIT 2000 -- matches no-options path ceiling
6773
- `;
6785
+ const rows = compact ? await this.sql`
6786
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6787
+ FROM cycle_tasks WHERE ${where} ORDER BY display_id
6788
+ LIMIT 2000
6789
+ ` : await this.sql`
6790
+ SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
6791
+ FROM cycle_tasks WHERE ${where} ORDER BY display_id
6792
+ LIMIT 2000 -- matches no-options path ceiling
6793
+ `;
6774
6794
  return rows.map(rowToTask);
6775
6795
  }
6776
6796
  async getTask(id) {
@@ -7802,7 +7822,8 @@ ${r.content}` + (r.carry_forward ? `
7802
7822
  await this.sql`
7803
7823
  INSERT INTO plan_runs (
7804
7824
  project_id, cycle_number, context_bytes, duration_ms,
7805
- task_count_in, task_count_out, backlog_depth, notes
7825
+ task_count_in, task_count_out, backlog_depth, notes,
7826
+ token_usage, source
7806
7827
  ) VALUES (
7807
7828
  ${this.projectId},
7808
7829
  ${entry.cycleNumber},
@@ -7811,7 +7832,9 @@ ${r.content}` + (r.carry_forward ? `
7811
7832
  ${entry.taskCountIn ?? null},
7812
7833
  ${entry.taskCountOut ?? null},
7813
7834
  ${entry.backlogDepth ?? null},
7814
- ${entry.notes ?? null}
7835
+ ${entry.notes ?? null},
7836
+ ${entry.tokenUsage ? this.sql.json(entry.tokenUsage) : null},
7837
+ ${entry.source ?? null}
7815
7838
  )
7816
7839
  `;
7817
7840
  }
@@ -7822,7 +7845,7 @@ ${r.content}` + (r.carry_forward ? `
7822
7845
  const [row] = await this.sql`
7823
7846
  INSERT INTO bug_reports (project_id, user_id, description, diagnostics, status)
7824
7847
  VALUES (
7825
- ${report.projectId},
7848
+ ${this.projectId},
7826
7849
  ${report.userId ?? null},
7827
7850
  ${report.description},
7828
7851
  ${JSON.stringify(report.diagnostics)},
@@ -7832,7 +7855,7 @@ ${r.content}` + (r.carry_forward ? `
7832
7855
  `;
7833
7856
  return {
7834
7857
  id: row.id,
7835
- projectId: report.projectId,
7858
+ projectId: this.projectId,
7836
7859
  userId: report.userId,
7837
7860
  description: report.description,
7838
7861
  diagnostics: report.diagnostics,
@@ -7947,7 +7970,8 @@ ${r.content}` + (r.carry_forward ? `
7947
7970
  title = EXCLUDED.title,
7948
7971
  content = EXCLUDED.content,
7949
7972
  carry_forward = EXCLUDED.carry_forward,
7950
- notes = EXCLUDED.notes
7973
+ notes = EXCLUDED.notes,
7974
+ updated_at = now()
7951
7975
  `;
7952
7976
  if (payload.healthUpdates.boardHealth != null || payload.healthUpdates.strategicDirection != null) {
7953
7977
  const [latest] = await tx`
@@ -9223,9 +9247,9 @@ var init_git = __esm({
9223
9247
  });
9224
9248
 
9225
9249
  // src/index.ts
9226
- import { readFileSync as readFileSync4 } from "fs";
9250
+ import { readFileSync as readFileSync6 } from "fs";
9227
9251
  import { createServer as createHttpServer } from "http";
9228
- import { dirname as dirname2, join as join11 } from "path";
9252
+ import { dirname as dirname2, join as join13 } from "path";
9229
9253
  import { fileURLToPath as fileURLToPath2 } from "url";
9230
9254
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9231
9255
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -9257,7 +9281,17 @@ function loadConfig() {
9257
9281
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
9258
9282
  const databaseUrl = process.env.DATABASE_URL;
9259
9283
  const explicitAdapter = process.env.PAPI_ADAPTER;
9260
- const adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
9284
+ const projectId = process.env.PAPI_PROJECT_ID;
9285
+ let adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
9286
+ if (projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
9287
+ adapterType = "proxy";
9288
+ console.error("[papi] PAPI_PROJECT_ID detected \u2014 switching to proxy adapter (md adapter blocked for external users).");
9289
+ }
9290
+ if (!projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
9291
+ throw new Error(
9292
+ "PAPI_PROJECT_ID is required to connect to your project.\n\nGet yours at https://getpapi.ai/setup\n\nAlready have one? Make sure PAPI_PROJECT_ID is set in your .mcp.json env config."
9293
+ );
9294
+ }
9261
9295
  return {
9262
9296
  projectRoot,
9263
9297
  papiDir: path.join(projectRoot, ".papi"),
@@ -9471,8 +9505,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
9471
9505
  }
9472
9506
 
9473
9507
  // src/server.ts
9508
+ import { readFileSync as readFileSync5 } from "fs";
9474
9509
  import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
9475
- import { join as join10, dirname } from "path";
9510
+ import { join as join12, dirname } from "path";
9476
9511
  import { fileURLToPath } from "url";
9477
9512
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9478
9513
  import {
@@ -9678,6 +9713,17 @@ ${formatted}`;
9678
9713
  }
9679
9714
  return sections.join("\n\n");
9680
9715
  }
9716
+ function formatCandidateTaskFullNotes(tasks) {
9717
+ const candidates = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status)).filter((t) => (t.notes?.length ?? 0) > PLAN_NOTES_MAX_LENGTH);
9718
+ if (candidates.length === 0) return void 0;
9719
+ const lines = candidates.map((t) => `**${t.id}** \u2014 ${t.title}
9720
+ ${t.notes}`);
9721
+ return [
9722
+ `${candidates.length} candidate task(s) have notes longer than ${PLAN_NOTES_MAX_LENGTH} chars. Full untruncated notes below \u2014 reference these when generating BUILD HANDOFFs so submitter context, constraints, and reasoning are preserved. The Board section above uses truncated notes for concise task selection; this section supplies the missing detail for tasks you choose to schedule.`,
9723
+ "",
9724
+ ...lines
9725
+ ].join("\n\n");
9726
+ }
9681
9727
  function formatBoardForReview(tasks) {
9682
9728
  if (tasks.length === 0) return "No tasks on the board.";
9683
9729
  return tasks.map(
@@ -10495,9 +10541,11 @@ Standard planning cycle with full board review.
10495
10541
  - Add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch.
10496
10542
  - Keep the SCOPE sections independent (each task still has its own deliverable) but note the ordering in "Why now" \u2014 e.g. "depends on task-123 completing the adapter method".
10497
10543
  Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 do not attempt to resolve multi-level graphs. When in doubt, omit the dependency and let the builder discover it.
10544
+ **Dependency Chain section (Part 1 markdown):** When intra-cycle dependencies are detected, include a visible **## Dependency Chain** section in Part 1 markdown immediately before the first BUILD HANDOFF block. List each dependency as an arrow chain with a brief reason: \`task-A \u2192 task-B (B calls the adapter method A creates)\`. Then show the full recommended build sequence for all cycle tasks, including standalone tasks: e.g. \`Build order: task-A \u2192 task-B; task-C standalone; task-D standalone\`. Flag circular dependencies with \u26A0\uFE0F and a note. Omit this section entirely when no intra-cycle dependencies exist \u2014 do not include an empty section.
10498
10545
  **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".
10499
10546
  **Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. 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).
10500
10547
  **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.
10548
+ **Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
10501
10549
  **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.
10502
10550
  **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
10503
10551
  **Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
@@ -10589,6 +10637,38 @@ var PLAN_FRAGMENT_SPIKE = `
10589
10637
  - **DONE CONDITION:** "Question answered OR time-box hit, whichever comes first."
10590
10638
  - Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10591
10639
  - Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
10640
+ var PLAN_FRAGMENT_DESIGN_BRIEF = `
10641
+ **Design brief task detection:** When a task's task type is "design-brief", generate a DESIGN BRIEF handoff. Replace the standard SCOPE (DO THIS) section with these type-specific sections:
10642
+ - AUDIENCE: Who this design is for \u2014 persona and context of use (e.g. "non-technical Owner, first dashboard visit")
10643
+ - BRAND CONSTRAINTS: Palette, typography, tone \u2014 pull from \`.impeccable.md\` and \`docs/design/brand-system.md\` if present. If neither exists, state "No brand doc \u2014 Owner should define constraints before starting."
10644
+ - DELIVERABLE FORMAT: What the output looks like \u2014 Claude Design handoff package / annotated mockup / style spec. Be specific so the person doing the work knows what "done" means.
10645
+ - REVIEW POINTS: What the Owner must approve before the design is considered done (e.g. layout, copy, colour, imagery).
10646
+ Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION sections as normal.
10647
+ Add to ACCEPTANCE CRITERIA: "[ ] Deliverable format confirmed with Owner before starting" and "[ ] Design output is self-contained \u2014 includes enough context for a developer to implement without further clarification."`;
10648
+ var PLAN_FRAGMENT_RESEARCH_BRIEF = `
10649
+ **Research brief task detection:** When a task's task type is "research-brief", generate a RESEARCH BRIEF handoff. Replace the standard SCOPE (DO THIS) section with:
10650
+ - GOAL: The specific question this research answers \u2014 one sentence, phrased as a question (e.g. "What onboarding patterns do our top 3 competitors use?")
10651
+ - TIME-BOX: Maximum effort allowed \u2014 XS or S. Stop when the time-box is hit and report what was found, even if incomplete.
10652
+ - OUTPUT: Where findings land \u2014 a doc at \`docs/research/[topic]-findings.md\` or inline in the build report. State the path.
10653
+ - FOLLOW-UP PROTOCOL: Do NOT submit follow-up backlog tasks until the Owner reviews and confirms the findings are actionable.
10654
+ Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10655
+ Add to ACCEPTANCE CRITERIA: "[ ] Question answered OR time-box hit \u2014 whichever comes first" and "[ ] Findings doc saved before any follow-up tasks are submitted."`;
10656
+ var PLAN_FRAGMENT_MARKETING_BRIEF = `
10657
+ **Marketing brief task detection:** When a task's task type is "marketing-brief", generate a MARKETING BRIEF handoff. Replace the standard SCOPE (DO THIS) section with:
10658
+ - AUDIENCE: Who this marketing content targets \u2014 persona, awareness level, channel context (e.g. "cold Discord visitor, zero PAPI context")
10659
+ - CHANNEL: Where this content lives \u2014 Discord, landing page, email, social, etc.
10660
+ - MESSAGE FRAME: The core message to land \u2014 one sentence. What does the reader need to believe after seeing this? (e.g. "PAPI makes AI-assisted building systematic, not chaotic.")
10661
+ - SUCCESS SIGNAL: How the Owner knows the content worked \u2014 clicks, signups, replies, saves, DMs. Be specific.
10662
+ Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10663
+ Add to ACCEPTANCE CRITERIA: "[ ] Message Frame confirmed with Owner before drafting" and "[ ] Final content reviewed by Owner before publishing."`;
10664
+ var PLAN_FRAGMENT_OPS_BRIEF = `
10665
+ **Ops brief task detection:** When a task's task type is "ops-brief", generate an OPS BRIEF handoff. Replace the standard SCOPE (DO THIS) section with:
10666
+ - SYSTEM: Which system or service this ops task touches \u2014 Vercel, Railway, Supabase, GitHub Actions, DNS, etc.
10667
+ - RISK: What could go wrong \u2014 data loss, downtime, broken deployments. Include estimated blast radius (e.g. "affects all authenticated users").
10668
+ - ROLLBACK PLAN: Exact steps to undo the change if something breaks. Must be specific enough to execute under pressure.
10669
+ - DONE CONDITION: The specific observable state that confirms the task is complete \u2014 a health check URL, a metric, a log line, a manual verification step.
10670
+ Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
10671
+ Add to ACCEPTANCE CRITERIA: "[ ] Rollback plan confirmed before executing" and "[ ] Done condition verified after completing."`;
10592
10672
  var PLAN_FRAGMENT_UI = `
10593
10673
  **UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
10594
10674
  When a task IS a UI task (primary scope is visual/frontend):
@@ -10696,6 +10776,7 @@ Standard planning cycle with full board review.
10696
10776
  **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".
10697
10777
  **Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. 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).
10698
10778
  **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.
10779
+ **Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
10699
10780
  **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.
10700
10781
  **Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
10701
10782
  **Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
@@ -10705,6 +10786,10 @@ Standard planning cycle with full board review.
10705
10786
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
10706
10787
  if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
10707
10788
  if (flags.hasTaskTasks) parts.push(PLAN_FRAGMENT_TASK);
10789
+ if (flags.hasDesignBriefTasks) parts.push(PLAN_FRAGMENT_DESIGN_BRIEF);
10790
+ if (flags.hasResearchBriefTasks) parts.push(PLAN_FRAGMENT_RESEARCH_BRIEF);
10791
+ if (flags.hasMarketingBriefTasks) parts.push(PLAN_FRAGMENT_MARKETING_BRIEF);
10792
+ if (flags.hasOpsBriefTasks) parts.push(PLAN_FRAGMENT_OPS_BRIEF);
10708
10793
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
10709
10794
  parts.push(`
10710
10795
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
@@ -10795,6 +10880,9 @@ function buildPlanUserMessage(ctx) {
10795
10880
  if (ctx.board) {
10796
10881
  parts.push("### Board", "", ctx.board, "");
10797
10882
  }
10883
+ if (ctx.candidateTaskFullNotes) {
10884
+ parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
10885
+ }
10798
10886
  if (ctx.preAssignedTasks) {
10799
10887
  parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
10800
10888
  }
@@ -11961,6 +12049,10 @@ function detectBoardFlags(tasks) {
11961
12049
  let hasSpikeTasks = false;
11962
12050
  let hasTaskTasks = false;
11963
12051
  let hasUITasks = false;
12052
+ let hasDesignBriefTasks = false;
12053
+ let hasResearchBriefTasks = false;
12054
+ let hasMarketingBriefTasks = false;
12055
+ let hasOpsBriefTasks = false;
11964
12056
  const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
11965
12057
  for (const t of tasks) {
11966
12058
  if (t.taskType === "bug" || /^(Bug:|Fix:)/i.test(t.title)) hasBugTasks = true;
@@ -11968,9 +12060,13 @@ function detectBoardFlags(tasks) {
11968
12060
  if (t.taskType === "idea") hasIdeaTasks = true;
11969
12061
  if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
11970
12062
  if (t.taskType === "task") hasTaskTasks = true;
12063
+ if (t.taskType === "design-brief") hasDesignBriefTasks = true;
12064
+ if (t.taskType === "research-brief") hasResearchBriefTasks = true;
12065
+ if (t.taskType === "marketing-brief") hasMarketingBriefTasks = true;
12066
+ if (t.taskType === "ops-brief") hasOpsBriefTasks = true;
11971
12067
  if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
11972
12068
  }
11973
- return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks };
12069
+ return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks, hasDesignBriefTasks, hasResearchBriefTasks, hasMarketingBriefTasks, hasOpsBriefTasks };
11974
12070
  }
11975
12071
  function detectBoardFlagsFromText(boardText) {
11976
12072
  return {
@@ -11979,7 +12075,11 @@ function detectBoardFlagsFromText(boardText) {
11979
12075
  hasIdeaTasks: /\bidea\b/i.test(boardText),
11980
12076
  hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
11981
12077
  hasTaskTasks: /\btask\b/i.test(boardText),
11982
- hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
12078
+ hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText),
12079
+ hasDesignBriefTasks: /\bdesign-brief\b/i.test(boardText),
12080
+ hasResearchBriefTasks: /\bresearch-brief\b/i.test(boardText),
12081
+ hasMarketingBriefTasks: /\bmarketing-brief\b/i.test(boardText),
12082
+ hasOpsBriefTasks: /\bops-brief\b/i.test(boardText)
11983
12083
  };
11984
12084
  }
11985
12085
  function hashSection(content) {
@@ -12125,7 +12225,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
12125
12225
  ]),
12126
12226
  adapter2.searchDocs?.({ status: "active", limit: 5 }),
12127
12227
  adapter2.getCycleLog(5),
12128
- adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] }),
12228
+ adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"], compact: true }),
12129
12229
  Promise.resolve(leanBuildReports.slice(0, 10)),
12130
12230
  adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null)
12131
12231
  ]);
@@ -12178,6 +12278,10 @@ ${lines.join("\n")}`;
12178
12278
  if (reportsForCapsResult.status === "fulfilled") {
12179
12279
  recentlyShippedLean = formatRecentlyShippedCapabilities(reportsForCapsResult.value);
12180
12280
  }
12281
+ let candidateTaskFullNotesLean;
12282
+ if (preAssignedResult.status === "fulfilled") {
12283
+ candidateTaskFullNotesLean = formatCandidateTaskFullNotes(preAssignedResult.value);
12284
+ }
12181
12285
  logDataSourceSummary("plan (lean)", [
12182
12286
  { label: "cycleHealth", hasData: !!health },
12183
12287
  { label: "productBrief", hasData: warnIfEmpty("productBrief", productBrief) },
@@ -12215,7 +12319,8 @@ ${lines.join("\n")}`;
12215
12319
  carryForwardStaleness: carryForwardStalenessLean,
12216
12320
  preAssignedTasks: preAssignedTextLean,
12217
12321
  recentlyShippedCapabilities: recentlyShippedLean,
12218
- strategyReviewCadence
12322
+ strategyReviewCadence,
12323
+ candidateTaskFullNotes: candidateTaskFullNotesLean
12219
12324
  };
12220
12325
  const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12221
12326
  ctx2.contextTier = leanTierLabel;
@@ -12235,7 +12340,7 @@ ${lines.join("\n")}`;
12235
12340
  adapter2.getActiveDecisions(),
12236
12341
  adapter2.getBuildReportsSince(health.totalCycles ?? 0),
12237
12342
  adapter2.getCycleLog(3),
12238
- adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"], contextTier: 2 }),
12343
+ adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"], contextTier: 2, compact: true }),
12239
12344
  adapter2.readCycleMetrics(),
12240
12345
  adapter2.getRecentReviews(5),
12241
12346
  adapter2.readPhases(),
@@ -12259,7 +12364,7 @@ ${lines.join("\n")}`;
12259
12364
  adapter2.getPendingRecommendations(),
12260
12365
  assembleDiscoveryCanvasText(adapter2),
12261
12366
  assembleTaskComments(adapter2),
12262
- adapter2.searchDocs?.({ status: "active", limit: 5 }),
12367
+ adapter2.searchDocs?.({ status: "active", limit: 15 }),
12263
12368
  adapter2.getContextHashes?.(health.totalCycles) ?? Promise.resolve(null)
12264
12369
  ]);
12265
12370
  timings["parallelAdvisory"] = t();
@@ -12287,10 +12392,31 @@ ${lines.join("\n")}`;
12287
12392
  const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
12288
12393
  let registeredDocsTextFull;
12289
12394
  if (docsResultFull.status === "fulfilled" && docsResultFull.value && docsResultFull.value.length > 0) {
12290
- const docs = docsResultFull.value;
12291
- const lines = docs.map((d) => `- **${d.title}** (${d.type}) \u2014 ${d.summary}`);
12292
- registeredDocsTextFull = `${docs.length} active research doc(s):
12395
+ const allDocs = docsResultFull.value;
12396
+ const taskModules = new Set(plannerTasks.map((t2) => t2.module?.toLowerCase()).filter(Boolean));
12397
+ const taskEpics = new Set(plannerTasks.map((t2) => t2.epic?.toLowerCase()).filter(Boolean));
12398
+ const HIGH_VALUE_TYPES = /* @__PURE__ */ new Set(["architecture", "guide", "research"]);
12399
+ const scored = allDocs.map((d) => {
12400
+ let score = 0;
12401
+ const docTags = d.tags.map((tag) => tag.toLowerCase());
12402
+ const docType = d.type?.toLowerCase() ?? "";
12403
+ for (const tag of docTags) {
12404
+ if (taskModules.has(tag) || taskEpics.has(tag)) score += 2;
12405
+ }
12406
+ if (HIGH_VALUE_TYPES.has(docType)) score += 1;
12407
+ if (d.actions?.some((a) => a.status === "pending")) score += 3;
12408
+ const age = health.totalCycles - (d.cycleUpdated ?? d.cycleCreated ?? 0);
12409
+ if (age <= 3) score += 1;
12410
+ return { doc: d, score };
12411
+ });
12412
+ scored.sort((a, b2) => b2.score - a.score);
12413
+ const selected = scored.slice(0, 5);
12414
+ const lines = selected.map(({ doc, score }) => `- **${doc.title}** (${doc.type}) \u2014 ${doc.summary}`);
12415
+ registeredDocsTextFull = `${selected.length} active research doc(s):
12293
12416
  ${lines.join("\n")}`;
12417
+ const logLines = selected.map(({ doc, score }) => ` ${doc.title} [score=${score}]`).join("\n");
12418
+ console.error(`[plan-perf] doc intelligence: selected ${selected.length}/${allDocs.length} docs by relevance:
12419
+ ${logLines}`);
12294
12420
  }
12295
12421
  logDataSourceSummary("plan (full)", [
12296
12422
  { label: "cycleHealth", hasData: !!health },
@@ -12315,8 +12441,18 @@ ${lines.join("\n")}`;
12315
12441
  const strippedTasks = stripTasksForPlan(tasks);
12316
12442
  const boardFlagsFull = detectBoardFlags(tasks);
12317
12443
  const horizonCtx = buildHorizonContext(phases, tasks) ?? void 0;
12444
+ const ACTIVE_STATUSES2 = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
12445
+ const p3Excluded = strippedTasks.filter(
12446
+ (t2) => t2.priority === "P3 Low" && !ACTIVE_STATUSES2.has(t2.status)
12447
+ );
12448
+ const plannerTasks = strippedTasks.filter(
12449
+ (t2) => t2.priority !== "P3 Low" || ACTIVE_STATUSES2.has(t2.status)
12450
+ );
12451
+ if (p3Excluded.length > 0) {
12452
+ console.error(`[plan-perf] board tiering: excluded ${p3Excluded.length} P3 Low tasks from planner context`);
12453
+ }
12318
12454
  const targetCycle = health.totalCycles + 1;
12319
- const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
12455
+ const preAssigned = plannerTasks.filter((t2) => t2.cycle === targetCycle);
12320
12456
  const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
12321
12457
  const gapFull = health.cyclesSinceLastStrategyReview;
12322
12458
  const lastReviewCycleFull = health.totalCycles - gapFull;
@@ -12328,7 +12464,7 @@ ${lines.join("\n")}`;
12328
12464
  activeDecisions: formatActiveDecisionsForPlan(decisions),
12329
12465
  recentBuildReports: formatBuildReports(cappedReports),
12330
12466
  cycleLog: formatCycleLog(log),
12331
- board: formatBoardForPlan(strippedTasks, filters, health.totalCycles),
12467
+ board: formatBoardForPlan(plannerTasks, filters, health.totalCycles),
12332
12468
  northStar,
12333
12469
  methodologyMetrics: formatCycleMetrics(metricsSnapshots),
12334
12470
  recentReviews: formatReviews(reviews),
@@ -12345,7 +12481,8 @@ ${lines.join("\n")}`;
12345
12481
  carryForwardStaleness: computeCarryForwardStaleness(log),
12346
12482
  preAssignedTasks: preAssignedText,
12347
12483
  recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12348
- strategyReviewCadence: strategyReviewCadenceFull
12484
+ strategyReviewCadence: strategyReviewCadenceFull,
12485
+ candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks)
12349
12486
  };
12350
12487
  const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12351
12488
  ctx.contextTier = fullTierLabel;
@@ -12356,6 +12493,9 @@ ${lines.join("\n")}`;
12356
12493
  if (savedBytes > 0) {
12357
12494
  console.error(`[plan-perf] context diff saved ${savedBytes} bytes`);
12358
12495
  }
12496
+ const boardChars = ctx.board?.length ?? 0;
12497
+ const totalChars = Object.values(ctx).reduce((sum, v) => sum + (typeof v === "string" ? v.length : 0), 0);
12498
+ console.error(`[plan-perf] context budget: board=${boardChars} chars, total fields=${totalChars} chars (excl. system prompt)`);
12359
12499
  return { context: ctx, contextHashes: newHashes };
12360
12500
  }
12361
12501
  async function transactionalWriteBack(adapter2, cycleNumber, data, contextHashes) {
@@ -12393,7 +12533,7 @@ ${cleanContent}`;
12393
12533
  };
12394
12534
  let dedupedNewTasks = data.newTasks ?? [];
12395
12535
  if (dedupedNewTasks.length > 0) {
12396
- const existingTasks = await adapter2.queryBoard();
12536
+ const existingTasks = await adapter2.queryBoard({ compact: true });
12397
12537
  const normalise = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
12398
12538
  const existingTitles = existingTasks.map((t) => normalise(t.title));
12399
12539
  const MIN_SUBSTRING_LEN = 20;
@@ -12475,7 +12615,7 @@ ${cleanContent}`;
12475
12615
  try {
12476
12616
  const [cycles, boardTasks] = await Promise.all([
12477
12617
  adapter2.readCycles(),
12478
- adapter2.queryBoard({ status: ["In Cycle", "Backlog", "In Progress", "In Review"] })
12618
+ adapter2.queryBoard({ status: ["In Cycle", "Backlog", "In Progress", "In Review"], compact: true })
12479
12619
  ]);
12480
12620
  const newCycle = cycles.find((s) => s.number === newCycleNumber);
12481
12621
  if (!newCycle) {
@@ -12551,7 +12691,7 @@ ${cleanContent}`;
12551
12691
  const newTaskIdMap = /* @__PURE__ */ new Map();
12552
12692
  const createTasksPromise = (async () => {
12553
12693
  if (!data.newTasks || data.newTasks.length === 0) return;
12554
- const existingTasks = await adapter2.queryBoard();
12694
+ const existingTasks = await adapter2.queryBoard({ compact: true });
12555
12695
  const normalise = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
12556
12696
  const existingTitles = existingTasks.map((t) => normalise(t.title));
12557
12697
  for (let i = 0; i < data.newTasks.length; i++) {
@@ -12786,7 +12926,8 @@ async function validateAndPrepare(adapter2, force) {
12786
12926
  mode = determineMode(health.totalCycles);
12787
12927
  if (health.latestCycleStatus && health.latestCycleStatus !== "complete" && health.latestCycleStatus !== "released" && !force) {
12788
12928
  const activeTasks = await adapter2.queryBoard({
12789
- status: ["In Progress", "In Review"]
12929
+ status: ["In Progress", "In Review"],
12930
+ compact: true
12790
12931
  });
12791
12932
  const taskNote = activeTasks.length > 0 ? ` ${activeTasks.length} task(s) still active: ${activeTasks.map((t) => t.id).join(", ")}.` : "";
12792
12933
  throw new Error(
@@ -12797,7 +12938,8 @@ Run \`release\` first, or pass \`force: true\` to bypass this block.`
12797
12938
  }
12798
12939
  if (!force) {
12799
12940
  const inReviewTasks = await adapter2.queryBoard({
12800
- status: ["In Review"]
12941
+ status: ["In Review"],
12942
+ compact: true
12801
12943
  });
12802
12944
  const staleTasks = inReviewTasks.filter(
12803
12945
  (t) => t.cycle !== void 0 && t.cycle <= cycleNumber - 2
@@ -12871,7 +13013,9 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
12871
13013
  durationMs,
12872
13014
  taskCountIn: planRunMeta?.taskCountIn,
12873
13015
  taskCountOut: (writeSummary?.handoffs ?? 0) + (writeSummary?.newTasks ?? 0),
12874
- backlogDepth: planRunMeta?.backlogDepth
13016
+ backlogDepth: planRunMeta?.backlogDepth,
13017
+ tokenUsage: planRunMeta?.tokenUsage,
13018
+ source: planRunMeta?.source ?? "mcp-server"
12875
13019
  }).catch(() => {
12876
13020
  });
12877
13021
  }
@@ -12968,7 +13112,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
12968
13112
  if (skipHandoffs) context.skipHandoffs = true;
12969
13113
  t = startTimer();
12970
13114
  try {
12971
- const scanTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] });
13115
+ const scanTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"], compact: true });
12972
13116
  const candidates = scanTasks.filter((task) => task.priority !== "P3 Low").slice(0, 15).map((task) => ({ id: task.id, title: task.title, notes: task.notes }));
12973
13117
  const scanResult = scanCodebaseForTasks(config2.projectRoot, candidates);
12974
13118
  if (scanResult) context.codebaseScan = scanResult;
@@ -13122,7 +13266,7 @@ function formatPhaseChanges(changes) {
13122
13266
  async function propagatePhaseStatus(adapter2) {
13123
13267
  const [phases, tasks] = await Promise.all([
13124
13268
  adapter2.readPhases(),
13125
- adapter2.queryBoard()
13269
+ adapter2.queryBoard({ compact: true })
13126
13270
  ]);
13127
13271
  if (phases.length === 0) return [];
13128
13272
  const horizons = await adapter2.readHorizons?.() ?? [];
@@ -13306,7 +13450,6 @@ async function handlePlan(adapter2, config2, args) {
13306
13450
  lastPrepareContextBytes = void 0;
13307
13451
  lastPrepareCycleNumber = void 0;
13308
13452
  lastPrepareSkipHandoffs = void 0;
13309
- const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
13310
13453
  let utilisation;
13311
13454
  if (inputContext) {
13312
13455
  try {
@@ -13314,6 +13457,12 @@ async function handlePlan(adapter2, config2, args) {
13314
13457
  } catch {
13315
13458
  }
13316
13459
  }
13460
+ const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, {
13461
+ contextBytes: contextBytes ?? void 0,
13462
+ skipHandoffs: skipHandoffs || void 0,
13463
+ tokenUsage: utilisation !== void 0 ? { utilisation } : void 0,
13464
+ source: "mcp-server"
13465
+ });
13317
13466
  const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs });
13318
13467
  return {
13319
13468
  ...response,
@@ -20941,21 +21090,94 @@ async function handleDocScan(adapter2, config2, args) {
20941
21090
  return textResponse(lines.join("\n"));
20942
21091
  }
20943
21092
 
21093
+ // src/services/session-guidance.ts
21094
+ import { existsSync as existsSync6 } from "fs";
21095
+ import { join as join9 } from "path";
21096
+ var state = {
21097
+ toolCallCount: 0,
21098
+ lastOrientAt: null,
21099
+ releaseSinceLastOrient: false,
21100
+ sessionStartedAt: Date.now()
21101
+ };
21102
+ var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
21103
+ var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
21104
+ function recordToolCall(name) {
21105
+ state.toolCallCount++;
21106
+ if (name === "release") state.releaseSinceLastOrient = true;
21107
+ }
21108
+ function markOrient() {
21109
+ state.lastOrientAt = Date.now();
21110
+ state.releaseSinceLastOrient = false;
21111
+ }
21112
+ async function buildSessionGuidance(adapter2, projectRoot) {
21113
+ const signals = [];
21114
+ try {
21115
+ if (adapter2.searchDocs) {
21116
+ const researchDir = join9(projectRoot, "docs", "research");
21117
+ if (existsSync6(researchDir)) {
21118
+ const files = scanMdFiles(researchDir, projectRoot);
21119
+ if (files.length > 0) {
21120
+ const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
21121
+ const registeredPaths = new Set(registered.map((d) => d.path));
21122
+ const unregistered = files.filter((f) => !registeredPaths.has(f));
21123
+ if (unregistered.length > 0) {
21124
+ signals.push(
21125
+ `${unregistered.length} research doc(s) in docs/research/ not registered \u2014 run \`doc_register\` so the planner can surface them.`
21126
+ );
21127
+ }
21128
+ }
21129
+ }
21130
+ }
21131
+ } catch {
21132
+ }
21133
+ if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
21134
+ signals.push(
21135
+ `${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
21136
+ );
21137
+ }
21138
+ if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
21139
+ const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
21140
+ signals.push(
21141
+ `${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
21142
+ );
21143
+ }
21144
+ if (state.releaseSinceLastOrient) {
21145
+ signals.push(
21146
+ "Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
21147
+ );
21148
+ }
21149
+ return signals.slice(0, 3);
21150
+ }
21151
+
20944
21152
  // src/tools/orient.ts
20945
21153
  import { execFileSync as execFileSync3 } from "child_process";
20946
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
20947
- import { join as join9 } from "path";
21154
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync7 } from "fs";
21155
+ import { join as join10 } from "path";
21156
+ var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["cowork", "api"]);
21157
+ var VALID_ENVS = /* @__PURE__ */ new Set(["claude-code", "cowork", "api", "unknown"]);
21158
+ function normaliseEnvironment(raw) {
21159
+ if (typeof raw === "string" && VALID_ENVS.has(raw)) {
21160
+ return raw;
21161
+ }
21162
+ return "unknown";
21163
+ }
20948
21164
  var orientTool = {
20949
21165
  name: "orient",
20950
- description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.",
21166
+ description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. Pass `environment` to qualify git-dependent recommendations (build_execute, release, review_submit) for non-Claude-Code callers.",
20951
21167
  annotations: { readOnlyHint: true, destructiveHint: false },
20952
21168
  inputSchema: {
20953
21169
  type: "object",
20954
- properties: {},
21170
+ properties: {
21171
+ environment: {
21172
+ type: "string",
21173
+ enum: ["claude-code", "cowork", "api", "unknown"],
21174
+ description: 'Caller environment. "claude-code" (local CLI with git) sees all recommendations unchanged. "cowork" or "api" (hosted/remote, no local git) suppresses/qualifies build_execute, release, and review_submit recommendations because those require a local Claude Code session to execute. Default "unknown" = show everything (safe default).'
21175
+ }
21176
+ },
20955
21177
  required: []
20956
21178
  }
20957
21179
  };
20958
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot) {
21180
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown") {
20959
21181
  const lines = [];
20960
21182
  const cycleIsComplete = health.latestCycleStatus === "complete";
20961
21183
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -20973,7 +21195,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
20973
21195
  }
20974
21196
  lines.push("");
20975
21197
  }
20976
- lines.push(`> **Next action:** ${health.recommendedMode}`);
21198
+ const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
21199
+ if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
21200
+ lines.push(`> **Next action:** ${health.recommendedMode}`);
21201
+ lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local Claude Code session with git access. From \`${environment}\` you can view state, log ideas, and edit the board \u2014 but the build/review/release loop must run through Claude Code._`);
21202
+ } else {
21203
+ lines.push(`> **Next action:** ${health.recommendedMode}`);
21204
+ }
20977
21205
  lines.push("");
20978
21206
  lines.push(`**Strategy Review:** ${health.reviewWarning}`);
20979
21207
  lines.push("");
@@ -21124,7 +21352,7 @@ function getLatestGitTag(projectRoot) {
21124
21352
  }
21125
21353
  function checkNpmVersionDrift() {
21126
21354
  try {
21127
- const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21355
+ const pkgPath = join10(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21128
21356
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
21129
21357
  const localVersion = pkg.version;
21130
21358
  const packageName = pkg.name;
@@ -21141,7 +21369,8 @@ function checkNpmVersionDrift() {
21141
21369
  return null;
21142
21370
  }
21143
21371
  }
21144
- async function handleOrient(adapter2, config2) {
21372
+ async function handleOrient(adapter2, config2, args = {}) {
21373
+ const environment = normaliseEnvironment(args.environment);
21145
21374
  try {
21146
21375
  try {
21147
21376
  await propagatePhaseStatus(adapter2);
@@ -21250,7 +21479,7 @@ ${versionDrift}` : "";
21250
21479
  let unregisteredDocsNote = "";
21251
21480
  try {
21252
21481
  if (adapter2.searchDocs) {
21253
- const docsDir = join9(config2.projectRoot, "docs");
21482
+ const docsDir = join10(config2.projectRoot, "docs");
21254
21483
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
21255
21484
  if (docsFiles.length > 0) {
21256
21485
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -21264,6 +21493,49 @@ ${versionDrift}` : "";
21264
21493
  }
21265
21494
  } catch {
21266
21495
  }
21496
+ let researchSignalsNote = "";
21497
+ try {
21498
+ if (adapter2.searchDocs) {
21499
+ const cycleHealth = await adapter2.getCycleHealth();
21500
+ const lastReviewCycle = Math.max(0, cycleHealth.totalCycles - cycleHealth.cyclesSinceLastStrategyReview);
21501
+ const researchDocs = await adapter2.searchDocs({
21502
+ type: "research",
21503
+ hasPendingActions: true,
21504
+ sinceCycle: lastReviewCycle,
21505
+ limit: 10
21506
+ });
21507
+ if (researchDocs.length > 0) {
21508
+ let activeAds = [];
21509
+ try {
21510
+ activeAds = (await adapter2.getActiveDecisions()).filter((a) => !a.superseded);
21511
+ } catch {
21512
+ }
21513
+ const stopWords = /* @__PURE__ */ new Set(["about", "above", "after", "again", "being", "could", "doing", "during", "every", "first", "front", "going", "great", "helps", "large", "later", "level", "local", "makes", "model", "needs", "never", "other", "place", "right", "since", "small", "still", "there", "these", "thing", "those", "three", "under", "until", "using", "where", "which", "while", "would"]);
21514
+ const lines = ["\n\n## Research Signals"];
21515
+ for (const doc of researchDocs) {
21516
+ const docText = [doc.title, doc.summary, ...doc.tags].join(" ").toLowerCase();
21517
+ const relatedAds = activeAds.filter((ad) => {
21518
+ const adWords = ad.title.toLowerCase().split(/\W+/).filter((w) => w.length >= 5 && !stopWords.has(w));
21519
+ return adWords.some((w) => docText.includes(w));
21520
+ });
21521
+ const cycleLabel = `C${doc.cycleUpdated ?? doc.cycleCreated}`;
21522
+ const adRef = relatedAds.length > 0 ? ` \u2014 may relate to ${relatedAds.map((a) => a.displayId).join(", ")}` : "";
21523
+ const pendingActions = doc.actions?.filter((a) => a.status === "pending") ?? [];
21524
+ lines.push(`- **${doc.title}** [${cycleLabel}${adRef}]`);
21525
+ for (const action of pendingActions.slice(0, 2)) {
21526
+ const desc = action.description.length > 100 ? `${action.description.slice(0, 97)}\u2026` : action.description;
21527
+ lines.push(` \u2192 ${desc}`);
21528
+ }
21529
+ if (pendingActions.length > 2) {
21530
+ lines.push(` \u2192 \u2026and ${pendingActions.length - 2} more`);
21531
+ }
21532
+ }
21533
+ lines.push("_Factor into next `strategy_review` or run `doc_search` for details._");
21534
+ researchSignalsNote = lines.join("\n");
21535
+ }
21536
+ }
21537
+ } catch {
21538
+ }
21267
21539
  let recsNote = "";
21268
21540
  try {
21269
21541
  const pendingRecs = await adapter2.getPendingRecommendations();
@@ -21297,13 +21569,35 @@ ${versionDrift}` : "";
21297
21569
  }
21298
21570
  } catch {
21299
21571
  }
21572
+ let alertsNote = "";
21300
21573
  let unactionedIssuesNote = "";
21301
21574
  try {
21302
- const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
21575
+ const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
21303
21576
  if (learnings) {
21304
- const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
21305
- if (unactioned.length > 0) {
21306
- const lines = ["\n\n## Unactioned Issues"];
21577
+ const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
21578
+ const unactionedAll = learnings.filter((l) => !l.actionTaken).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
21579
+ const allAlerts = unactionedAll.filter((l) => l.severity === "P0" || l.severity === "P1").sort(byRecency);
21580
+ const allLowSev = unactionedAll.filter((l) => l.severity === "P2" || l.severity === "P3").sort(byRecency);
21581
+ const totalP2 = allLowSev.filter((l) => l.severity === "P2").length;
21582
+ const totalP3 = allLowSev.filter((l) => l.severity === "P3").length;
21583
+ const ALERT_CAP = 10;
21584
+ const UNACTIONED_CAP = 5;
21585
+ const alerts = allAlerts.slice(0, ALERT_CAP);
21586
+ const unactioned = allLowSev.slice(0, UNACTIONED_CAP);
21587
+ if (allAlerts.length > 0) {
21588
+ const header = allAlerts.length > ALERT_CAP ? `${allAlerts.length} P0/P1 discovered issues awaiting action (showing ${ALERT_CAP} most recent).` : `${allAlerts.length} P0/P1 discovered issue${allAlerts.length !== 1 ? "s" : ""} awaiting action.`;
21589
+ const lines = ["\n\n## \u{1F6A8} Alerts", header];
21590
+ for (const issue of alerts) {
21591
+ const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
21592
+ lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
21593
+ }
21594
+ lines.push("_Escalate: run `idea` with P1 priority, or `board_edit` if already handled._");
21595
+ alertsNote = lines.join("\n");
21596
+ }
21597
+ if (allLowSev.length > 0) {
21598
+ const totalLow = totalP2 + totalP3;
21599
+ const header = totalLow > UNACTIONED_CAP ? `${totalP2} P2 \xB7 ${totalP3} P3 (showing ${UNACTIONED_CAP} most recent)` : `${totalP2} P2 \xB7 ${totalP3} P3`;
21600
+ const lines = ["\n\n## Unactioned Issues", header];
21307
21601
  for (const issue of unactioned) {
21308
21602
  const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
21309
21603
  lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
@@ -21314,15 +21608,26 @@ ${versionDrift}` : "";
21314
21608
  }
21315
21609
  } catch {
21316
21610
  }
21317
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + versionNote + enrichmentNote);
21611
+ let sessionGuidanceNote = "";
21612
+ try {
21613
+ const signals = await buildSessionGuidance(adapter2, config2.projectRoot);
21614
+ if (signals.length > 0) {
21615
+ const lines = ["\n\n## Session Guidance"];
21616
+ for (const s of signals) lines.push(`- ${s}`);
21617
+ sessionGuidanceNote = lines.join("\n");
21618
+ }
21619
+ markOrient();
21620
+ } catch {
21621
+ }
21622
+ return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment) + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + sessionGuidanceNote + versionNote + enrichmentNote);
21318
21623
  } catch (err) {
21319
21624
  const message = err instanceof Error ? err.message : String(err);
21320
21625
  return errorResponse(`Orient failed: ${message}`);
21321
21626
  }
21322
21627
  }
21323
21628
  function enrichClaudeMd(projectRoot, cycleNumber) {
21324
- const claudeMdPath = join9(projectRoot, "CLAUDE.md");
21325
- if (!existsSync6(claudeMdPath)) return "";
21629
+ const claudeMdPath = join10(projectRoot, "CLAUDE.md");
21630
+ if (!existsSync7(claudeMdPath)) return "";
21326
21631
  const content = readFileSync3(claudeMdPath, "utf-8");
21327
21632
  const additions = [];
21328
21633
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
@@ -22104,6 +22409,47 @@ ${result.userMessage}
22104
22409
  }
22105
22410
  }
22106
22411
 
22412
+ // src/lib/install-id.ts
22413
+ import { randomUUID as randomUUID15 } from "crypto";
22414
+ import { mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, chmodSync } from "fs";
22415
+ import { homedir as homedir3 } from "os";
22416
+ import { join as join11 } from "path";
22417
+ var PAPI_HOME_DIR = join11(homedir3(), ".papi");
22418
+ var INSTALL_ID_FILE = join11(PAPI_HOME_DIR, "install-id.json");
22419
+ var cachedInstallId = null;
22420
+ function isValidUuid(s) {
22421
+ return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
22422
+ }
22423
+ function getInstallId() {
22424
+ if (cachedInstallId) return cachedInstallId;
22425
+ try {
22426
+ const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
22427
+ const parsed = JSON.parse(raw);
22428
+ if (isValidUuid(parsed.install_id)) {
22429
+ cachedInstallId = parsed.install_id;
22430
+ return cachedInstallId;
22431
+ }
22432
+ } catch {
22433
+ }
22434
+ try {
22435
+ mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
22436
+ const id = randomUUID15();
22437
+ const contents = {
22438
+ install_id: id,
22439
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
22440
+ };
22441
+ writeFileSync2(INSTALL_ID_FILE, JSON.stringify(contents, null, 2), { mode: 384 });
22442
+ try {
22443
+ chmodSync(INSTALL_ID_FILE, 384);
22444
+ } catch {
22445
+ }
22446
+ cachedInstallId = id;
22447
+ return cachedInstallId;
22448
+ } catch {
22449
+ return null;
22450
+ }
22451
+ }
22452
+
22107
22453
  // src/lib/telemetry.ts
22108
22454
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
22109
22455
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
@@ -22142,6 +22488,29 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
22142
22488
  metadata: { duration_ms: durationMs, ...extra }
22143
22489
  });
22144
22490
  }
22491
+ function emitMdAdapterPing(toolName, extra) {
22492
+ if (!isEnabled()) return;
22493
+ const installId = getInstallId();
22494
+ if (!installId) return;
22495
+ const body = {
22496
+ install_id: installId,
22497
+ tool_name: toolName,
22498
+ papi_version: process.env["npm_package_version"] ?? null,
22499
+ metadata: extra ?? {}
22500
+ };
22501
+ fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
22502
+ method: "POST",
22503
+ headers: {
22504
+ "Content-Type": "application/json",
22505
+ "apikey": TELEMETRY_API_KEY,
22506
+ "Authorization": `Bearer ${TELEMETRY_API_KEY}`,
22507
+ "Prefer": "return=minimal"
22508
+ },
22509
+ body: JSON.stringify(body),
22510
+ signal: AbortSignal.timeout(5e3)
22511
+ }).catch(() => {
22512
+ });
22513
+ }
22145
22514
  function emitMilestone(projectId, milestone, extra) {
22146
22515
  emitTelemetryEvent({
22147
22516
  project_id: projectId,
@@ -22176,13 +22545,26 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
22176
22545
  "handoff_generate"
22177
22546
  ]);
22178
22547
  function createServer(adapter2, config2) {
22548
+ const __pkgFilename = fileURLToPath(import.meta.url);
22549
+ const __pkgDir = dirname(__pkgFilename);
22550
+ let serverVersion = "unknown";
22551
+ try {
22552
+ const pkg = JSON.parse(readFileSync5(join12(__pkgDir, "..", "package.json"), "utf-8"));
22553
+ serverVersion = pkg.version ?? "unknown";
22554
+ } catch {
22555
+ }
22179
22556
  const server2 = new Server(
22180
- { name: "papi", version: "0.1.0" },
22557
+ { name: "papi", version: serverVersion },
22181
22558
  { capabilities: { tools: {}, prompts: {} } }
22182
22559
  );
22560
+ if (config2.adapterType === "md") {
22561
+ process.stderr.write(
22562
+ "\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
22563
+ );
22564
+ }
22183
22565
  const __filename = fileURLToPath(import.meta.url);
22184
22566
  const __dirname2 = dirname(__filename);
22185
- const skillsDir = join10(__dirname2, "..", "skills");
22567
+ const skillsDir = join12(__dirname2, "..", "skills");
22186
22568
  function parseSkillFrontmatter(content) {
22187
22569
  const match = content.match(/^---\n([\s\S]*?)\n---/);
22188
22570
  if (!match) return null;
@@ -22200,7 +22582,7 @@ function createServer(adapter2, config2) {
22200
22582
  const mdFiles = files.filter((f) => f.endsWith(".md"));
22201
22583
  const prompts = [];
22202
22584
  for (const file of mdFiles) {
22203
- const content = await readFile5(join10(skillsDir, file), "utf-8");
22585
+ const content = await readFile5(join12(skillsDir, file), "utf-8");
22204
22586
  const meta = parseSkillFrontmatter(content);
22205
22587
  if (meta) {
22206
22588
  prompts.push({ name: meta.name, description: meta.description });
@@ -22216,7 +22598,7 @@ function createServer(adapter2, config2) {
22216
22598
  try {
22217
22599
  const files = await readdir2(skillsDir);
22218
22600
  for (const file of files.filter((f) => f.endsWith(".md"))) {
22219
- const content = await readFile5(join10(skillsDir, file), "utf-8");
22601
+ const content = await readFile5(join12(skillsDir, file), "utf-8");
22220
22602
  const meta = parseSkillFrontmatter(content);
22221
22603
  if (meta?.name === name) {
22222
22604
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -22289,6 +22671,7 @@ function createServer(adapter2, config2) {
22289
22671
  }
22290
22672
  }
22291
22673
  const timer2 = startTimer();
22674
+ recordToolCall(name);
22292
22675
  let result;
22293
22676
  switch (name) {
22294
22677
  case "plan":
@@ -22352,7 +22735,7 @@ function createServer(adapter2, config2) {
22352
22735
  result = await handleInit(config2, safeArgs);
22353
22736
  break;
22354
22737
  case "orient":
22355
- result = await handleOrient(adapter2, config2);
22738
+ result = await handleOrient(adapter2, config2, safeArgs);
22356
22739
  break;
22357
22740
  case "hierarchy_update":
22358
22741
  result = await handleHierarchyUpdate(adapter2, safeArgs);
@@ -22393,6 +22776,9 @@ function createServer(adapter2, config2) {
22393
22776
  });
22394
22777
  } catch {
22395
22778
  }
22779
+ if (config2.adapterType === "md") {
22780
+ emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError });
22781
+ }
22396
22782
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
22397
22783
  if (telemetryProjectId) {
22398
22784
  emitToolCall(telemetryProjectId, name, elapsed, {
@@ -22420,7 +22806,7 @@ function createServer(adapter2, config2) {
22420
22806
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
22421
22807
  var pkgVersion = "unknown";
22422
22808
  try {
22423
- const pkg = JSON.parse(readFileSync4(join11(__dirname, "..", "package.json"), "utf-8"));
22809
+ const pkg = JSON.parse(readFileSync6(join13(__dirname, "..", "package.json"), "utf-8"));
22424
22810
  pkgVersion = pkg.version;
22425
22811
  } catch {
22426
22812
  }
@@ -22536,41 +22922,7 @@ if (httpPort) {
22536
22922
  if (!httpToken) {
22537
22923
  process.stderr.write("[papi] WARNING: PAPI_HTTP_TOKEN is not set. HTTP transport is unauthenticated \u2014 anyone with the URL can call your PAPI tools. Set PAPI_HTTP_TOKEN to a secret string.\n");
22538
22924
  }
22539
- const createServerForRequest = () => {
22540
- if (adapter && !setupError) {
22541
- return createServer(adapter, config);
22542
- }
22543
- const errorServer = new Server2(
22544
- { name: "papi", version: pkgVersion },
22545
- { capabilities: { tools: {} } }
22546
- );
22547
- const errorMessage = setupError || "Unknown startup error";
22548
- errorServer.setRequestHandler(ListToolsRequestSchema2, async () => ({
22549
- tools: [{
22550
- name: "setup",
22551
- description: "PAPI is not connected \u2014 run this tool for setup instructions.",
22552
- inputSchema: { type: "object", properties: {}, required: [] }
22553
- }]
22554
- }));
22555
- errorServer.setRequestHandler(CallToolRequestSchema2, async () => ({
22556
- content: [{
22557
- type: "text",
22558
- text: `# PAPI Connection Error
22559
-
22560
- ${errorMessage}
22561
-
22562
- ## Quick Fix
22563
-
22564
- If you haven't set up PAPI yet:
22565
- 1. Go to https://getpapi.ai/login and sign up
22566
- 2. Complete the onboarding wizard \u2014 it generates your config
22567
- 3. Copy the config to your project and restart your AI tool
22568
-
22569
- If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_DATA_API_KEY** are set in your .mcp.json env config.`
22570
- }]
22571
- }));
22572
- return errorServer;
22573
- };
22925
+ const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
22574
22926
  const httpServer = createHttpServer((req, res) => {
22575
22927
  if (req.method === "GET" && req.url === "/healthz") {
22576
22928
  res.writeHead(200, { "Content-Type": "text/plain" });
@@ -22579,26 +22931,14 @@ If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_D
22579
22931
  }
22580
22932
  if (httpToken) {
22581
22933
  const authHeader = req.headers["authorization"] ?? "";
22582
- const bearerMatch = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
22583
- const urlMatch = req.url?.match(/^\/(mcp|sse)\/(.+)$/);
22584
- const urlToken = urlMatch?.[2];
22585
- if (bearerMatch === httpToken) {
22586
- } else if (urlToken === httpToken) {
22587
- req.url = `/${urlMatch[1]}`;
22588
- } else {
22934
+ const provided = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
22935
+ if (provided !== httpToken) {
22589
22936
  res.writeHead(401, { "Content-Type": "application/json" });
22590
22937
  res.end(JSON.stringify({ error: "Unauthorized" }));
22591
22938
  return;
22592
22939
  }
22593
22940
  }
22594
22941
  if (req.url === "/mcp" || req.url === "/sse") {
22595
- req.headers["accept"] = "application/json, text/event-stream";
22596
- const acceptIdx = req.rawHeaders.findIndex((h) => h.toLowerCase() === "accept");
22597
- if (acceptIdx >= 0) {
22598
- req.rawHeaders[acceptIdx + 1] = "application/json, text/event-stream";
22599
- } else {
22600
- req.rawHeaders.push("Accept", "application/json, text/event-stream");
22601
- }
22602
22942
  if (req.method === "POST") {
22603
22943
  const chunks = [];
22604
22944
  req.on("data", (chunk) => chunks.push(chunk));
@@ -22611,42 +22951,23 @@ If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_D
22611
22951
  res.end(JSON.stringify({ error: "Invalid JSON body" }));
22612
22952
  return;
22613
22953
  }
22614
- (async () => {
22615
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
22616
- const reqServer = createServerForRequest();
22617
- await reqServer.connect(transport);
22618
- await transport.handleRequest(req, res, parsedBody);
22619
- await reqServer.close();
22620
- })().catch((err) => {
22954
+ httpTransport.handleRequest(req, res, parsedBody).catch((err) => {
22621
22955
  process.stderr.write(`[papi] HTTP transport error: ${err instanceof Error ? err.message : String(err)}
22622
22956
  `);
22623
- if (!res.headersSent) {
22624
- res.writeHead(500, { "Content-Type": "application/json" });
22625
- res.end(JSON.stringify({ error: "Internal server error" }));
22626
- }
22627
22957
  });
22628
22958
  });
22629
22959
  return;
22630
22960
  }
22631
- (async () => {
22632
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
22633
- const reqServer = createServerForRequest();
22634
- await reqServer.connect(transport);
22635
- await transport.handleRequest(req, res);
22636
- await reqServer.close();
22637
- })().catch((err) => {
22961
+ httpTransport.handleRequest(req, res).catch((err) => {
22638
22962
  process.stderr.write(`[papi] HTTP transport error: ${err instanceof Error ? err.message : String(err)}
22639
22963
  `);
22640
- if (!res.headersSent) {
22641
- res.writeHead(500, { "Content-Type": "application/json" });
22642
- res.end(JSON.stringify({ error: "Internal server error" }));
22643
- }
22644
22964
  });
22645
22965
  return;
22646
22966
  }
22647
22967
  res.writeHead(404, { "Content-Type": "text/plain" });
22648
22968
  res.end("Not found");
22649
22969
  });
22970
+ await server.connect(httpTransport);
22650
22971
  httpServer.listen(httpPort, httpHost, () => {
22651
22972
  process.stderr.write(`[papi] HTTP transport listening on http://${httpHost}:${httpPort}/mcp
22652
22973
  `);