@papi-ai/server 0.7.11 → 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/README.md CHANGED
@@ -39,6 +39,14 @@ That's it. You're planning.
39
39
 
40
40
  PAPI collects anonymous usage data (tool name, duration, project UUID — no code or task content). To opt out, add `PAPI_TELEMETRY=off` to your `.mcp.json` env block.
41
41
 
42
+ ### md mode warning
43
+
44
+ If you start the server without `DATABASE_URL` (or with `PAPI_ADAPTER=md`), you'll see a stderr warning: *"PAPI is running in md mode — your cycles are not visible on the hosted dashboard."* This is intentional. md mode stores everything locally in `.papi/` files, but the hosted dashboard at [getpapi.ai](https://getpapi.ai/) only sees projects backed by the database adapter.
45
+
46
+ When the warning fires, PAPI emits an anonymous install-level ping (random UUID stored at `~/.papi/install-id.json`, chmod 600) per tool call so we can count installs that haven't connected yet. The ping contains: install UUID, tool name, server version, duration. **No project content, no file paths, no user identifiers.** Setting `PAPI_TELEMETRY=off` disables both the hosted-dashboard telemetry and the md-mode ping.
47
+
48
+ The warning is informational, not an error — md mode is a fully supported way to self-host PAPI without the hosted backend.
49
+
42
50
  ## License
43
51
 
44
52
  [Elastic License 2.0](https://www.elastic.co/licensing/elastic-license) — free to use, self-host, and modify. Commercial hosting requires a license.
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) {
@@ -7813,7 +7822,8 @@ ${r.content}` + (r.carry_forward ? `
7813
7822
  await this.sql`
7814
7823
  INSERT INTO plan_runs (
7815
7824
  project_id, cycle_number, context_bytes, duration_ms,
7816
- task_count_in, task_count_out, backlog_depth, notes
7825
+ task_count_in, task_count_out, backlog_depth, notes,
7826
+ token_usage, source
7817
7827
  ) VALUES (
7818
7828
  ${this.projectId},
7819
7829
  ${entry.cycleNumber},
@@ -7822,7 +7832,9 @@ ${r.content}` + (r.carry_forward ? `
7822
7832
  ${entry.taskCountIn ?? null},
7823
7833
  ${entry.taskCountOut ?? null},
7824
7834
  ${entry.backlogDepth ?? null},
7825
- ${entry.notes ?? null}
7835
+ ${entry.notes ?? null},
7836
+ ${entry.tokenUsage ? this.sql.json(entry.tokenUsage) : null},
7837
+ ${entry.source ?? null}
7826
7838
  )
7827
7839
  `;
7828
7840
  }
@@ -7958,7 +7970,8 @@ ${r.content}` + (r.carry_forward ? `
7958
7970
  title = EXCLUDED.title,
7959
7971
  content = EXCLUDED.content,
7960
7972
  carry_forward = EXCLUDED.carry_forward,
7961
- notes = EXCLUDED.notes
7973
+ notes = EXCLUDED.notes,
7974
+ updated_at = now()
7962
7975
  `;
7963
7976
  if (payload.healthUpdates.boardHealth != null || payload.healthUpdates.strategicDirection != null) {
7964
7977
  const [latest] = await tx`
@@ -9234,9 +9247,9 @@ var init_git = __esm({
9234
9247
  });
9235
9248
 
9236
9249
  // src/index.ts
9237
- import { readFileSync as readFileSync4 } from "fs";
9250
+ import { readFileSync as readFileSync6 } from "fs";
9238
9251
  import { createServer as createHttpServer } from "http";
9239
- import { dirname as dirname2, join as join11 } from "path";
9252
+ import { dirname as dirname2, join as join13 } from "path";
9240
9253
  import { fileURLToPath as fileURLToPath2 } from "url";
9241
9254
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9242
9255
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -9268,7 +9281,17 @@ function loadConfig() {
9268
9281
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
9269
9282
  const databaseUrl = process.env.DATABASE_URL;
9270
9283
  const explicitAdapter = process.env.PAPI_ADAPTER;
9271
- 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
+ }
9272
9295
  return {
9273
9296
  projectRoot,
9274
9297
  papiDir: path.join(projectRoot, ".papi"),
@@ -9482,8 +9505,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
9482
9505
  }
9483
9506
 
9484
9507
  // src/server.ts
9508
+ import { readFileSync as readFileSync5 } from "fs";
9485
9509
  import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
9486
- import { join as join10, dirname } from "path";
9510
+ import { join as join12, dirname } from "path";
9487
9511
  import { fileURLToPath } from "url";
9488
9512
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9489
9513
  import {
@@ -9689,6 +9713,17 @@ ${formatted}`;
9689
9713
  }
9690
9714
  return sections.join("\n\n");
9691
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
+ }
9692
9727
  function formatBoardForReview(tasks) {
9693
9728
  if (tasks.length === 0) return "No tasks on the board.";
9694
9729
  return tasks.map(
@@ -10510,6 +10545,7 @@ Standard planning cycle with full board review.
10510
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".
10511
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).
10512
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.
10513
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.
10514
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.
10515
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.
@@ -10740,6 +10776,7 @@ Standard planning cycle with full board review.
10740
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".
10741
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).
10742
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.
10743
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.
10744
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.
10745
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.
@@ -10843,6 +10880,9 @@ function buildPlanUserMessage(ctx) {
10843
10880
  if (ctx.board) {
10844
10881
  parts.push("### Board", "", ctx.board, "");
10845
10882
  }
10883
+ if (ctx.candidateTaskFullNotes) {
10884
+ parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
10885
+ }
10846
10886
  if (ctx.preAssignedTasks) {
10847
10887
  parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
10848
10888
  }
@@ -12238,6 +12278,10 @@ ${lines.join("\n")}`;
12238
12278
  if (reportsForCapsResult.status === "fulfilled") {
12239
12279
  recentlyShippedLean = formatRecentlyShippedCapabilities(reportsForCapsResult.value);
12240
12280
  }
12281
+ let candidateTaskFullNotesLean;
12282
+ if (preAssignedResult.status === "fulfilled") {
12283
+ candidateTaskFullNotesLean = formatCandidateTaskFullNotes(preAssignedResult.value);
12284
+ }
12241
12285
  logDataSourceSummary("plan (lean)", [
12242
12286
  { label: "cycleHealth", hasData: !!health },
12243
12287
  { label: "productBrief", hasData: warnIfEmpty("productBrief", productBrief) },
@@ -12275,7 +12319,8 @@ ${lines.join("\n")}`;
12275
12319
  carryForwardStaleness: carryForwardStalenessLean,
12276
12320
  preAssignedTasks: preAssignedTextLean,
12277
12321
  recentlyShippedCapabilities: recentlyShippedLean,
12278
- strategyReviewCadence
12322
+ strategyReviewCadence,
12323
+ candidateTaskFullNotes: candidateTaskFullNotesLean
12279
12324
  };
12280
12325
  const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
12281
12326
  ctx2.contextTier = leanTierLabel;
@@ -12436,7 +12481,8 @@ ${logLines}`);
12436
12481
  carryForwardStaleness: computeCarryForwardStaleness(log),
12437
12482
  preAssignedTasks: preAssignedText,
12438
12483
  recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
12439
- strategyReviewCadence: strategyReviewCadenceFull
12484
+ strategyReviewCadence: strategyReviewCadenceFull,
12485
+ candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks)
12440
12486
  };
12441
12487
  const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
12442
12488
  ctx.contextTier = fullTierLabel;
@@ -21044,21 +21090,94 @@ async function handleDocScan(adapter2, config2, args) {
21044
21090
  return textResponse(lines.join("\n"));
21045
21091
  }
21046
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
+
21047
21152
  // src/tools/orient.ts
21048
21153
  import { execFileSync as execFileSync3 } from "child_process";
21049
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
21050
- 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
+ }
21051
21164
  var orientTool = {
21052
21165
  name: "orient",
21053
- 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.",
21054
21167
  annotations: { readOnlyHint: true, destructiveHint: false },
21055
21168
  inputSchema: {
21056
21169
  type: "object",
21057
- 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
+ },
21058
21177
  required: []
21059
21178
  }
21060
21179
  };
21061
- function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot) {
21180
+ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown") {
21062
21181
  const lines = [];
21063
21182
  const cycleIsComplete = health.latestCycleStatus === "complete";
21064
21183
  const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
@@ -21076,7 +21195,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
21076
21195
  }
21077
21196
  lines.push("");
21078
21197
  }
21079
- 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
+ }
21080
21205
  lines.push("");
21081
21206
  lines.push(`**Strategy Review:** ${health.reviewWarning}`);
21082
21207
  lines.push("");
@@ -21227,7 +21352,7 @@ function getLatestGitTag(projectRoot) {
21227
21352
  }
21228
21353
  function checkNpmVersionDrift() {
21229
21354
  try {
21230
- const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21355
+ const pkgPath = join10(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
21231
21356
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
21232
21357
  const localVersion = pkg.version;
21233
21358
  const packageName = pkg.name;
@@ -21244,7 +21369,8 @@ function checkNpmVersionDrift() {
21244
21369
  return null;
21245
21370
  }
21246
21371
  }
21247
- async function handleOrient(adapter2, config2) {
21372
+ async function handleOrient(adapter2, config2, args = {}) {
21373
+ const environment = normaliseEnvironment(args.environment);
21248
21374
  try {
21249
21375
  try {
21250
21376
  await propagatePhaseStatus(adapter2);
@@ -21353,7 +21479,7 @@ ${versionDrift}` : "";
21353
21479
  let unregisteredDocsNote = "";
21354
21480
  try {
21355
21481
  if (adapter2.searchDocs) {
21356
- const docsDir = join9(config2.projectRoot, "docs");
21482
+ const docsDir = join10(config2.projectRoot, "docs");
21357
21483
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
21358
21484
  if (docsFiles.length > 0) {
21359
21485
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
@@ -21443,13 +21569,35 @@ ${versionDrift}` : "";
21443
21569
  }
21444
21570
  } catch {
21445
21571
  }
21572
+ let alertsNote = "";
21446
21573
  let unactionedIssuesNote = "";
21447
21574
  try {
21448
- const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
21575
+ const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
21449
21576
  if (learnings) {
21450
- const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
21451
- if (unactioned.length > 0) {
21452
- 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];
21453
21601
  for (const issue of unactioned) {
21454
21602
  const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
21455
21603
  lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
@@ -21460,15 +21608,26 @@ ${versionDrift}` : "";
21460
21608
  }
21461
21609
  } catch {
21462
21610
  }
21463
- return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + 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);
21464
21623
  } catch (err) {
21465
21624
  const message = err instanceof Error ? err.message : String(err);
21466
21625
  return errorResponse(`Orient failed: ${message}`);
21467
21626
  }
21468
21627
  }
21469
21628
  function enrichClaudeMd(projectRoot, cycleNumber) {
21470
- const claudeMdPath = join9(projectRoot, "CLAUDE.md");
21471
- if (!existsSync6(claudeMdPath)) return "";
21629
+ const claudeMdPath = join10(projectRoot, "CLAUDE.md");
21630
+ if (!existsSync7(claudeMdPath)) return "";
21472
21631
  const content = readFileSync3(claudeMdPath, "utf-8");
21473
21632
  const additions = [];
21474
21633
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
@@ -22250,6 +22409,47 @@ ${result.userMessage}
22250
22409
  }
22251
22410
  }
22252
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
+
22253
22453
  // src/lib/telemetry.ts
22254
22454
  var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
22255
22455
  var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
@@ -22288,6 +22488,29 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
22288
22488
  metadata: { duration_ms: durationMs, ...extra }
22289
22489
  });
22290
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
+ }
22291
22514
  function emitMilestone(projectId, milestone, extra) {
22292
22515
  emitTelemetryEvent({
22293
22516
  project_id: projectId,
@@ -22322,13 +22545,26 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
22322
22545
  "handoff_generate"
22323
22546
  ]);
22324
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
+ }
22325
22556
  const server2 = new Server(
22326
- { name: "papi", version: "0.1.0" },
22557
+ { name: "papi", version: serverVersion },
22327
22558
  { capabilities: { tools: {}, prompts: {} } }
22328
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
+ }
22329
22565
  const __filename = fileURLToPath(import.meta.url);
22330
22566
  const __dirname2 = dirname(__filename);
22331
- const skillsDir = join10(__dirname2, "..", "skills");
22567
+ const skillsDir = join12(__dirname2, "..", "skills");
22332
22568
  function parseSkillFrontmatter(content) {
22333
22569
  const match = content.match(/^---\n([\s\S]*?)\n---/);
22334
22570
  if (!match) return null;
@@ -22346,7 +22582,7 @@ function createServer(adapter2, config2) {
22346
22582
  const mdFiles = files.filter((f) => f.endsWith(".md"));
22347
22583
  const prompts = [];
22348
22584
  for (const file of mdFiles) {
22349
- const content = await readFile5(join10(skillsDir, file), "utf-8");
22585
+ const content = await readFile5(join12(skillsDir, file), "utf-8");
22350
22586
  const meta = parseSkillFrontmatter(content);
22351
22587
  if (meta) {
22352
22588
  prompts.push({ name: meta.name, description: meta.description });
@@ -22362,7 +22598,7 @@ function createServer(adapter2, config2) {
22362
22598
  try {
22363
22599
  const files = await readdir2(skillsDir);
22364
22600
  for (const file of files.filter((f) => f.endsWith(".md"))) {
22365
- const content = await readFile5(join10(skillsDir, file), "utf-8");
22601
+ const content = await readFile5(join12(skillsDir, file), "utf-8");
22366
22602
  const meta = parseSkillFrontmatter(content);
22367
22603
  if (meta?.name === name) {
22368
22604
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -22435,6 +22671,7 @@ function createServer(adapter2, config2) {
22435
22671
  }
22436
22672
  }
22437
22673
  const timer2 = startTimer();
22674
+ recordToolCall(name);
22438
22675
  let result;
22439
22676
  switch (name) {
22440
22677
  case "plan":
@@ -22498,7 +22735,7 @@ function createServer(adapter2, config2) {
22498
22735
  result = await handleInit(config2, safeArgs);
22499
22736
  break;
22500
22737
  case "orient":
22501
- result = await handleOrient(adapter2, config2);
22738
+ result = await handleOrient(adapter2, config2, safeArgs);
22502
22739
  break;
22503
22740
  case "hierarchy_update":
22504
22741
  result = await handleHierarchyUpdate(adapter2, safeArgs);
@@ -22539,6 +22776,9 @@ function createServer(adapter2, config2) {
22539
22776
  });
22540
22777
  } catch {
22541
22778
  }
22779
+ if (config2.adapterType === "md") {
22780
+ emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError });
22781
+ }
22542
22782
  const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
22543
22783
  if (telemetryProjectId) {
22544
22784
  emitToolCall(telemetryProjectId, name, elapsed, {
@@ -22566,7 +22806,7 @@ function createServer(adapter2, config2) {
22566
22806
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
22567
22807
  var pkgVersion = "unknown";
22568
22808
  try {
22569
- const pkg = JSON.parse(readFileSync4(join11(__dirname, "..", "package.json"), "utf-8"));
22809
+ const pkg = JSON.parse(readFileSync6(join13(__dirname, "..", "package.json"), "utf-8"));
22570
22810
  pkgVersion = pkg.version;
22571
22811
  } catch {
22572
22812
  }
package/dist/prompts.js CHANGED
@@ -245,6 +245,7 @@ Standard planning cycle with full board review.
245
245
  **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".
246
246
  **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).
247
247
  **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.
248
+ **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.
248
249
  **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.
249
250
  **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.
250
251
  **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.
@@ -475,6 +476,7 @@ Standard planning cycle with full board review.
475
476
  **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".
476
477
  **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).
477
478
  **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.
479
+ **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.
478
480
  **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.
479
481
  **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.
480
482
  **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.
@@ -578,6 +580,9 @@ function buildPlanUserMessage(ctx) {
578
580
  if (ctx.board) {
579
581
  parts.push("### Board", "", ctx.board, "");
580
582
  }
583
+ if (ctx.candidateTaskFullNotes) {
584
+ parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
585
+ }
581
586
  if (ctx.preAssignedTasks) {
582
587
  parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
583
588
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
+ "mcpName": "io.github.cathalos92/papi",
4
5
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
6
  "license": "Elastic-2.0",
6
7
  "type": "module",