@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/README.md +8 -0
- package/dist/index.js +482 -161
- package/dist/prompts.js +42 -0
- package/package.json +3 -2
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
|
-
|
|
1579
|
-
|
|
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(
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
3765
|
-
|
|
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
|
|
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),
|
|
3796
|
+
parse(x2.subarray(25), state3, sql2.options.parsers, handle, options.transform);
|
|
3790
3797
|
} else if (x2[0] === 107 && x2[17]) {
|
|
3791
|
-
|
|
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(
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
3852
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
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
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
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
|
-
${
|
|
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:
|
|
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
|
|
9250
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
9227
9251
|
import { createServer as createHttpServer } from "http";
|
|
9228
|
-
import { dirname as dirname2, join as
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
12291
|
-
const
|
|
12292
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
20947
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
21575
|
+
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
21303
21576
|
if (learnings) {
|
|
21304
|
-
const
|
|
21305
|
-
|
|
21306
|
-
|
|
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
|
-
|
|
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 =
|
|
21325
|
-
if (!
|
|
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:
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
22583
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
`);
|