@papi-ai/server 0.7.85 → 0.7.103

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/prompts.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/lib/carry-forward-shape.ts
2
+ import { splitCarryForward, renderCarryForward } from "@papi-ai/shared";
3
+
1
4
  // src/prompts.ts
2
5
  var AD_REJECTION_RULES = `**AD Minting Guard \u2014 REJECT observations dressed as decisions.**
3
6
 
@@ -96,7 +99,7 @@ SECURITY CONSIDERATIONS
96
99
  [data exposure, secrets handling, auth/access control, dependency risks \u2014 or "None \u2014 no security-relevant changes"]
97
100
 
98
101
  DEPLOY VERIFICATION
99
- [Include this section ONLY when FILES LIKELY TOUCHED contains a TRIGGER-SURFACE path. Trigger surfaces (exact set): lib/install-snippets.ts, packages/server/src/transport-http.ts, packages/server/src/index.ts, app/well-known/**, app/api/auth/oauth/**, app/proxy.ts, app/middleware.ts, lib/auth*, next.config.ts, vercel.json, **/railway.toml, **/Dockerfile, **/Procfile, supabase/functions/**, lib/env*.ts. When required, instruct the builder: "Production verification REQUIRED. After deploy, run a curl against the live URL(s) you changed and pass production_verification on build_execute complete with { urls, curl_command, http_status, response_excerpt, verified_at }. http_status MUST be 2xx; build_execute and review_submit reject otherwise. Server inspects the diff against origin/main to enforce this." Omit this section entirely when no trigger surface is touched.]
102
+ [Include this section ONLY when FILES LIKELY TOUCHED contains a TRIGGER-SURFACE path. Trigger surfaces (exact set): lib/install-snippets.ts, packages/server/src/transport-http.ts, packages/server/src/index.ts, app/well-known/**, app/api/auth/oauth/**, app/proxy.ts, app/middleware.ts, lib/auth*, next.config.ts, **/Dockerfile, **/Procfile, supabase/functions/**, lib/env*.ts. When required, instruct the builder: "Production verification REQUIRED. After deploy, run a curl against the live URL(s) you changed and pass production_verification on build_execute complete with { urls, curl_command, http_status, response_excerpt, verified_at }. http_status MUST be 2xx; build_execute and review_submit reject otherwise. Server inspects the diff against origin/main to enforce this." Omit this section entirely when no trigger surface is touched.]
100
103
 
101
104
  REFERENCE DOCS
102
105
  [Optional \u2014 paths to docs/ files that provide background context for this task. Include only when the task originated from research or scoping work and the doc contains context the builder will need beyond what is in this handoff. Omit this section entirely for tasks that don't need supplementary context.]
@@ -304,7 +307,7 @@ var PLAN_FRAGMENT_MARKETING_BRIEF = `
304
307
  Add to ACCEPTANCE CRITERIA: "[ ] Message Frame confirmed with Owner before drafting" and "[ ] Final content reviewed by Owner before publishing."`;
305
308
  var PLAN_FRAGMENT_OPS_BRIEF = `
306
309
  **Ops brief task detection:** When a task's task type is "ops-brief", generate an OPS BRIEF handoff. Inside SCOPE (DO THIS), use these subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
307
- - SYSTEM: Which system or service this ops task touches \u2014 Vercel, Railway, Supabase, GitHub Actions, DNS, etc.
310
+ - SYSTEM: Which system or service this ops task touches \u2014 Vercel, Supabase, GitHub Actions, DNS, etc.
308
311
  - RISK: What could go wrong \u2014 data loss, downtime, broken deployments. Include estimated blast radius (e.g. "affects all authenticated users").
309
312
  - ROLLBACK PLAN: Exact steps to undo the change if something breaks. Must be specific enough to execute under pressure.
310
313
  - 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.
@@ -332,7 +335,19 @@ var PLAN_FRAGMENT_FORWARD_HORIZON = `
332
335
  - **Actionable** \u2014 frame as a decision to make, not a vague warning (e.g. "Decide whether to use WebSockets or SSE for real-time updates before starting Phase 4: Real-Time Features")
333
336
  - **Tied to trajectory** \u2014 based on current board state, ADs, and velocity, not generic advice
334
337
  If the Forward Horizon context is absent or there are no meaningful decisions to surface, omit this section entirely. Do NOT generate generic advice like "plan ahead" or "consider testing".`;
335
- function composeFullModeInstructions(flags, ctx) {
338
+ function omitSection(source, startMarker, endMarker) {
339
+ const start = source.indexOf(startMarker);
340
+ if (start === -1) return source;
341
+ const end = source.indexOf(endMarker, start + startMarker.length);
342
+ if (end === -1) return source;
343
+ return source.slice(0, start) + source.slice(end);
344
+ }
345
+ var ALL_PLAN_GATES_ON = {
346
+ planHealthCheck: true,
347
+ backlogTriage: true,
348
+ buildOrder: true
349
+ };
350
+ function composeFullModeInstructions(flags, ctx, gates = ALL_PLAN_GATES_ON) {
336
351
  const parts = [
337
352
  `## FULL MODE
338
353
 
@@ -465,7 +480,21 @@ ${AD_ADMISSION_RULES}
465
480
  }
466
481
  parts.push(`
467
482
  **CRITICAL: Review your Part 2 JSON before finishing. Every action from Part 1 must have a corresponding entry in Part 2. If Part 1 mentions corrections, new tasks, AD changes, or handoffs but Part 2 has empty arrays \u2014 you have a persistence bug.**`);
468
- return parts.join("\n");
483
+ let composed = parts.join("\n");
484
+ if (!gates.planHealthCheck) {
485
+ composed = omitSection(composed, "1. **Cycle Health Check**", "\n2. **Inbox Triage**");
486
+ }
487
+ if (!gates.backlogTriage) {
488
+ composed = omitSection(composed, "2. **Inbox Triage**", "\n3. **Board Integrity**");
489
+ }
490
+ if (!gates.buildOrder) {
491
+ composed = omitSection(
492
+ composed,
493
+ " **Intra-cycle dependency detection:**",
494
+ " **Branch-coherence check (task-1908):**"
495
+ );
496
+ }
497
+ return composed;
469
498
  }
470
499
  var PLAN_FULL_BASELINE_FLAGS = {
471
500
  hasBugTasks: true,
@@ -483,9 +512,11 @@ var PLAN_FULL_INSTRUCTIONS = composeFullModeInstructions(PLAN_FULL_BASELINE_FLAG
483
512
  hasDiscoveryCanvas: true,
484
513
  hasHorizonContext: true
485
514
  });
486
- function buildPlanFullInstructionsConditional(flags, ctx) {
487
- if (!flags || !ctx) return PLAN_FULL_INSTRUCTIONS;
488
- return composeFullModeInstructions(flags, ctx);
515
+ function buildPlanFullInstructionsConditional(flags, ctx, gates) {
516
+ if (!flags || !ctx) {
517
+ return gates ? composeFullModeInstructions(PLAN_FULL_BASELINE_FLAGS, { hasDiscoveryCanvas: true, hasHorizonContext: true }, gates) : PLAN_FULL_INSTRUCTIONS;
518
+ }
519
+ return composeFullModeInstructions(flags, ctx, gates);
489
520
  }
490
521
  var CYCLE_DENSITY_TARGETS = {
491
522
  light: { label: "Light", range: "2-3" },
@@ -526,10 +557,14 @@ function buildPlanUserMessage(ctx) {
526
557
  if (ctx.mode === "bootstrap") {
527
558
  parts.push(PLAN_BOOTSTRAP_INSTRUCTIONS);
528
559
  } else {
529
- const instructions = ctx.boardFlags ? buildPlanFullInstructionsConditional(ctx.boardFlags, {
530
- hasDiscoveryCanvas: !!ctx.discoveryCanvas,
531
- hasHorizonContext: !!ctx.horizonContext
532
- }) : PLAN_FULL_INSTRUCTIONS;
560
+ const instructions = ctx.boardFlags ? buildPlanFullInstructionsConditional(
561
+ ctx.boardFlags,
562
+ {
563
+ hasDiscoveryCanvas: !!ctx.discoveryCanvas,
564
+ hasHorizonContext: !!ctx.horizonContext
565
+ },
566
+ ctx.planGates
567
+ ) : buildPlanFullInstructionsConditional(void 0, void 0, ctx.planGates);
533
568
  parts.push(instructions);
534
569
  }
535
570
  if (ctx.foundationalTasksGuidance) {
@@ -802,11 +837,31 @@ function coerceToString(value) {
802
837
  if (value === null || value === void 0) return "";
803
838
  return JSON.stringify(value, null, 2);
804
839
  }
840
+ function normalizePlannerPriority(value) {
841
+ const raw = coerceToString(value).trim();
842
+ const aliases = {
843
+ p0: "P0 Critical",
844
+ "p0 critical": "P0 Critical",
845
+ p1: "P1 High",
846
+ "p1 high": "P1 High",
847
+ p2: "P2 Medium",
848
+ "p2 medium": "P2 Medium",
849
+ p3: "P3 Low",
850
+ "p3 low": "P3 Low"
851
+ };
852
+ return aliases[raw.toLowerCase()] ?? raw;
853
+ }
805
854
  function coerceCarryForward(value) {
806
855
  if (value === null || value === void 0) return { value: null };
807
856
  if (typeof value === "string") {
808
857
  const trimmed = value.trim();
809
- return { value: trimmed.length > 0 ? trimmed : null };
858
+ if (trimmed.length === 0) return { value: null };
859
+ if (!splitCarryForward(trimmed).split) {
860
+ const warning2 = 'cycleLogCarryForward was persisted, but it has neither a "WHAT SHIPS FOR USERS:" nor a "RELEASE MECHANICS:" label \u2014 orient will render it verbatim instead of leading with product signal. Re-run plan apply with both labels in cycleLogCarryForward for this cycle if that split matters here.';
861
+ console.error(`[plan] ${warning2}`);
862
+ return { value: trimmed, warning: warning2 };
863
+ }
864
+ return { value: trimmed };
810
865
  }
811
866
  const shape = Array.isArray(value) ? "array" : typeof value;
812
867
  const warning = `cycleLogCarryForward was ${shape}, not a string \u2014 DROPPED rather than persisted. Carry-forward is prose that orient parses for the WHAT SHIPS FOR USERS / RELEASE MECHANICS labels; a non-string value cannot carry them. Re-run plan apply with cycleLogCarryForward as a single string (or null) to record one for this cycle.`;
@@ -830,7 +885,7 @@ function coerceStructuredOutput(parsed) {
830
885
  tempId: t.tempId !== void 0 && t.tempId !== null ? coerceToString(t.tempId) : void 0,
831
886
  title: coerceToString(t.title),
832
887
  status: coerceToString(t.status),
833
- priority: coerceToString(t.priority),
888
+ priority: normalizePlannerPriority(t.priority),
834
889
  complexity: coerceToString(t.complexity),
835
890
  module: coerceToString(t.module),
836
891
  epic: coerceToString(t.epic),
@@ -843,7 +898,7 @@ function coerceStructuredOutput(parsed) {
843
898
  const updates = typeof c.updates === "object" && c.updates !== null ? c.updates : {};
844
899
  const coercedUpdates = {};
845
900
  for (const [key, val] of Object.entries(updates)) {
846
- coercedUpdates[key] = typeof val === "object" && val !== null ? JSON.stringify(val) : val;
901
+ coercedUpdates[key] = key === "priority" ? normalizePlannerPriority(val) : typeof val === "object" && val !== null ? JSON.stringify(val) : val;
847
902
  }
848
903
  return { taskId: coerceToString(c.taskId), updates: coercedUpdates };
849
904
  }) : [];
@@ -1765,6 +1820,7 @@ export {
1765
1820
  buildVisionTasksPrompt,
1766
1821
  coerceCarryForward,
1767
1822
  extractDependencyChain,
1823
+ normalizePlannerPriority,
1768
1824
  parseReviewStructuredOutput,
1769
1825
  parseStrategyChangeOutput,
1770
1826
  parseStructuredOutput
@@ -0,0 +1,168 @@
1
+ // src/cli/statusline.ts
2
+ import { createHash } from "crypto";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir, tmpdir } from "os";
5
+ import { join } from "path";
6
+ import { pathToFileURL } from "url";
7
+ var CACHE_TTL_MS = 3e4;
8
+ var FETCH_TIMEOUT_MS = 3e3;
9
+ function renderPeekLine(c) {
10
+ const parts = [`PAPI c${c.cycle ?? "?"}`];
11
+ parts.push(`build ${c.inFlight}`);
12
+ parts.push(`review ${c.inReview}`);
13
+ if (c.ownerActions !== null) parts.push(`you ${c.ownerActions}`);
14
+ return parts.join(" | ");
15
+ }
16
+ function cacheFilePath(endpoint, projectId) {
17
+ const hash = createHash("sha256").update(`${endpoint}|${projectId ?? ""}`).digest("hex").slice(0, 16);
18
+ return join(tmpdir(), `papi-statusline-${hash}.json`);
19
+ }
20
+ function readClientCache(path, now = Date.now()) {
21
+ try {
22
+ const hit = JSON.parse(readFileSync(path, "utf-8"));
23
+ if (!hit || typeof hit.expires !== "number" || hit.expires <= now) return void 0;
24
+ return hit.payload;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ function writeClientCache(path, payload, now = Date.now()) {
30
+ try {
31
+ writeFileSync(path, JSON.stringify({ expires: now + CACHE_TTL_MS, payload }));
32
+ } catch {
33
+ }
34
+ }
35
+ function configFromMcpJson(body) {
36
+ if (!body || typeof body !== "object") return void 0;
37
+ const servers = body.mcpServers;
38
+ if (!servers || typeof servers !== "object") return void 0;
39
+ for (const entry of Object.values(servers)) {
40
+ if (!entry || typeof entry !== "object") continue;
41
+ const s = entry;
42
+ if (typeof s.url !== "string" || !/papi/i.test(s.url)) continue;
43
+ const auth = s.headers?.["Authorization"];
44
+ const authString = typeof auth === "string" ? auth : "";
45
+ const bearer = authString.startsWith("Bearer ") ? authString.slice(7).trim() : "";
46
+ const projectHeader = s.headers?.["x-papi-project-id"];
47
+ if (!bearer) continue;
48
+ return {
49
+ endpoint: new URL(s.url).origin,
50
+ bearer,
51
+ projectId: typeof projectHeader === "string" && projectHeader.length > 0 ? projectHeader : void 0
52
+ };
53
+ }
54
+ return void 0;
55
+ }
56
+ function mergeStatusLine(body, command) {
57
+ const obj = body && typeof body === "object" ? body : {};
58
+ if (obj["statusLine"] !== void 0) return null;
59
+ return { ...obj, statusLine: { type: "command", command } };
60
+ }
61
+ function resolveConfig(argv) {
62
+ const flag = (name) => {
63
+ const i = argv.indexOf(name);
64
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
65
+ };
66
+ const endpoint = flag("--endpoint") ?? process.env["PAPI_ENDPOINT"];
67
+ const bearer = flag("--bearer") ?? process.env["PAPI_BEARER"];
68
+ const projectId = flag("--project") ?? process.env["PAPI_PROJECT_ID"];
69
+ if (endpoint && bearer) return { endpoint: new URL(endpoint).origin, bearer, projectId };
70
+ const mcpJsonPath = join(process.cwd(), ".mcp.json");
71
+ if (existsSync(mcpJsonPath)) {
72
+ try {
73
+ return configFromMcpJson(JSON.parse(readFileSync(mcpJsonPath, "utf-8")));
74
+ } catch {
75
+ return void 0;
76
+ }
77
+ }
78
+ return void 0;
79
+ }
80
+ async function fetchPeek(config) {
81
+ const response = await fetch(`${config.endpoint}/peek`, {
82
+ headers: {
83
+ Authorization: `Bearer ${config.bearer}`,
84
+ ...config.projectId ? { "x-papi-project-id": config.projectId } : {}
85
+ },
86
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
87
+ });
88
+ if (!response.ok) throw new Error(`peek returned ${response.status}`);
89
+ const body = await response.json();
90
+ return {
91
+ cycle: typeof body.cycle === "number" ? body.cycle : null,
92
+ inFlight: typeof body.inFlight === "number" ? body.inFlight : 0,
93
+ inReview: typeof body.inReview === "number" ? body.inReview : 0,
94
+ ownerActions: typeof body.ownerActions === "number" ? body.ownerActions : null
95
+ };
96
+ }
97
+ var RENDER_COMMAND = "npx -y @papi-ai/cli papi-statusline";
98
+ async function install() {
99
+ const settingsPath = join(homedir(), ".claude", "settings.json");
100
+ let body = {};
101
+ if (existsSync(settingsPath)) {
102
+ try {
103
+ body = JSON.parse(readFileSync(settingsPath, "utf-8"));
104
+ } catch (err) {
105
+ console.error(`papi-statusline: could not parse ${settingsPath}: ${err.message}`);
106
+ return 1;
107
+ }
108
+ }
109
+ const merged = mergeStatusLine(body, RENDER_COMMAND);
110
+ if (merged === null) {
111
+ console.log(
112
+ `papi-statusline: ${settingsPath} already has a statusLine entry \u2014 left untouched.
113
+ To switch it to PAPI manually, set:
114
+ "statusLine": { "type": "command", "command": "${RENDER_COMMAND}" }
115
+
116
+ Any other MCP client with a custom status-bar command can run the same command.`
117
+ );
118
+ return 0;
119
+ }
120
+ writeFileSync(settingsPath, `${JSON.stringify(merged, null, 2)}
121
+ `);
122
+ console.log(`papi-statusline: installed into ${settingsPath}. Restart your AI client to see it.`);
123
+ return 0;
124
+ }
125
+ async function runStatusline(argv, stdin) {
126
+ if (argv.includes("--install")) return install();
127
+ void stdin;
128
+ const config = resolveConfig(argv);
129
+ if (!config) return 0;
130
+ const cachePath = cacheFilePath(config.endpoint, config.projectId);
131
+ const cached = readClientCache(cachePath);
132
+ if (cached) {
133
+ process.stdout.write(`${renderPeekLine(cached)}
134
+ `);
135
+ return 0;
136
+ }
137
+ try {
138
+ const payload = await fetchPeek(config);
139
+ writeClientCache(cachePath, payload);
140
+ process.stdout.write(`${renderPeekLine(payload)}
141
+ `);
142
+ } catch {
143
+ }
144
+ return 0;
145
+ }
146
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
147
+ const chunks = [];
148
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
149
+ process.stdin.on("end", () => {
150
+ void runStatusline(process.argv.slice(2), Buffer.concat(chunks).toString("utf-8")).then((code) => {
151
+ process.exitCode = code;
152
+ });
153
+ });
154
+ setTimeout(() => {
155
+ void runStatusline(process.argv.slice(2), Buffer.concat(chunks).toString("utf-8")).then((code) => {
156
+ process.exitCode = code;
157
+ });
158
+ }, 250);
159
+ }
160
+ export {
161
+ cacheFilePath,
162
+ configFromMcpJson,
163
+ mergeStatusLine,
164
+ readClientCache,
165
+ renderPeekLine,
166
+ runStatusline,
167
+ writeClientCache
168
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.85",
4
- "description": "PAPI MCP server AI-powered sprint planning, build execution, and strategy review for software projects",
3
+ "version": "0.7.103",
4
+ "description": "PAPI MCP server \u2014 AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "mcpName": "io.github.getpapi/papi",
7
7
  "type": "module",
@@ -49,17 +49,16 @@
49
49
  "node": ">=18.0.0"
50
50
  },
51
51
  "dependencies": {
52
- "@anthropic-ai/sdk": "^0.82.0",
52
+ "@anthropic-ai/sdk": "^0.91.1",
53
53
  "@modelcontextprotocol/sdk": "^1.27.1",
54
54
  "@papi-ai/adapter-pg": "^0.2.16",
55
55
  "@papi-ai/shared": "^0.2.5",
56
- "@papi-ai/skills": "^0.1.4",
57
- "js-yaml": "^4.1.0"
56
+ "@papi-ai/skills": "^0.1.4"
58
57
  },
59
58
  "devDependencies": {
60
59
  "@papi-ai/adapter-md": "^0.2.0",
61
- "@types/js-yaml": "^4.0.9",
62
60
  "@types/node": "^22.0.0",
61
+ "ajv": "^8.20.0",
63
62
  "tsup": "^8.0.0",
64
63
  "typescript": "^5.5.0",
65
64
  "vitest": "^4.0.18"