@papi-ai/server 0.7.11 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.js +296 -56
- package/dist/prompts.js +5 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -39,6 +39,14 @@ That's it. You're planning.
|
|
|
39
39
|
|
|
40
40
|
PAPI collects anonymous usage data (tool name, duration, project UUID — no code or task content). To opt out, add `PAPI_TELEMETRY=off` to your `.mcp.json` env block.
|
|
41
41
|
|
|
42
|
+
### md mode warning
|
|
43
|
+
|
|
44
|
+
If you start the server without `DATABASE_URL` (or with `PAPI_ADAPTER=md`), you'll see a stderr warning: *"PAPI is running in md mode — your cycles are not visible on the hosted dashboard."* This is intentional. md mode stores everything locally in `.papi/` files, but the hosted dashboard at [getpapi.ai](https://getpapi.ai/) only sees projects backed by the database adapter.
|
|
45
|
+
|
|
46
|
+
When the warning fires, PAPI emits an anonymous install-level ping (random UUID stored at `~/.papi/install-id.json`, chmod 600) per tool call so we can count installs that haven't connected yet. The ping contains: install UUID, tool name, server version, duration. **No project content, no file paths, no user identifiers.** Setting `PAPI_TELEMETRY=off` disables both the hosted-dashboard telemetry and the md-mode ping.
|
|
47
|
+
|
|
48
|
+
The warning is informational, not an error — md mode is a fully supported way to self-host PAPI without the hosted backend.
|
|
49
|
+
|
|
42
50
|
## License
|
|
43
51
|
|
|
44
52
|
[Elastic License 2.0](https://www.elastic.co/licensing/elastic-license) — free to use, self-host, and modify. Commercial hosting requires a license.
|
package/dist/index.js
CHANGED
|
@@ -1453,7 +1453,12 @@ var init_dist2 = __esm({
|
|
|
1453
1453
|
research: 2,
|
|
1454
1454
|
spike: 2,
|
|
1455
1455
|
idea: 3,
|
|
1456
|
-
discovery: 1
|
|
1456
|
+
discovery: 1,
|
|
1457
|
+
// Non-code brief types for non-technical Owners (AD-12)
|
|
1458
|
+
"design-brief": 1,
|
|
1459
|
+
"research-brief": 1,
|
|
1460
|
+
"marketing-brief": 1,
|
|
1461
|
+
"ops-brief": 1
|
|
1457
1462
|
};
|
|
1458
1463
|
VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
1459
1464
|
SECTION_HEADERS = [
|
|
@@ -1575,11 +1580,13 @@ ${TABLE_SEPARATOR}
|
|
|
1575
1580
|
}
|
|
1576
1581
|
/** Prepend a new cycle log entry at the top of the Cycle Log section. */
|
|
1577
1582
|
async writeCycleLogEntry(entry) {
|
|
1578
|
-
|
|
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) {
|
|
@@ -7813,7 +7822,8 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7813
7822
|
await this.sql`
|
|
7814
7823
|
INSERT INTO plan_runs (
|
|
7815
7824
|
project_id, cycle_number, context_bytes, duration_ms,
|
|
7816
|
-
task_count_in, task_count_out, backlog_depth, notes
|
|
7825
|
+
task_count_in, task_count_out, backlog_depth, notes,
|
|
7826
|
+
token_usage, source
|
|
7817
7827
|
) VALUES (
|
|
7818
7828
|
${this.projectId},
|
|
7819
7829
|
${entry.cycleNumber},
|
|
@@ -7822,7 +7832,9 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7822
7832
|
${entry.taskCountIn ?? null},
|
|
7823
7833
|
${entry.taskCountOut ?? null},
|
|
7824
7834
|
${entry.backlogDepth ?? null},
|
|
7825
|
-
${entry.notes ?? null}
|
|
7835
|
+
${entry.notes ?? null},
|
|
7836
|
+
${entry.tokenUsage ? this.sql.json(entry.tokenUsage) : null},
|
|
7837
|
+
${entry.source ?? null}
|
|
7826
7838
|
)
|
|
7827
7839
|
`;
|
|
7828
7840
|
}
|
|
@@ -7958,7 +7970,8 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7958
7970
|
title = EXCLUDED.title,
|
|
7959
7971
|
content = EXCLUDED.content,
|
|
7960
7972
|
carry_forward = EXCLUDED.carry_forward,
|
|
7961
|
-
notes = EXCLUDED.notes
|
|
7973
|
+
notes = EXCLUDED.notes,
|
|
7974
|
+
updated_at = now()
|
|
7962
7975
|
`;
|
|
7963
7976
|
if (payload.healthUpdates.boardHealth != null || payload.healthUpdates.strategicDirection != null) {
|
|
7964
7977
|
const [latest] = await tx`
|
|
@@ -9234,9 +9247,9 @@ var init_git = __esm({
|
|
|
9234
9247
|
});
|
|
9235
9248
|
|
|
9236
9249
|
// src/index.ts
|
|
9237
|
-
import { readFileSync as
|
|
9250
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
9238
9251
|
import { createServer as createHttpServer } from "http";
|
|
9239
|
-
import { dirname as dirname2, join as
|
|
9252
|
+
import { dirname as dirname2, join as join13 } from "path";
|
|
9240
9253
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9241
9254
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9242
9255
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -9268,7 +9281,17 @@ function loadConfig() {
|
|
|
9268
9281
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
9269
9282
|
const databaseUrl = process.env.DATABASE_URL;
|
|
9270
9283
|
const explicitAdapter = process.env.PAPI_ADAPTER;
|
|
9271
|
-
const
|
|
9284
|
+
const projectId = process.env.PAPI_PROJECT_ID;
|
|
9285
|
+
let adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
|
|
9286
|
+
if (projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
|
|
9287
|
+
adapterType = "proxy";
|
|
9288
|
+
console.error("[papi] PAPI_PROJECT_ID detected \u2014 switching to proxy adapter (md adapter blocked for external users).");
|
|
9289
|
+
}
|
|
9290
|
+
if (!projectId && !databaseUrl && !papiEndpoint && adapterType === "md") {
|
|
9291
|
+
throw new Error(
|
|
9292
|
+
"PAPI_PROJECT_ID is required to connect to your project.\n\nGet yours at https://getpapi.ai/setup\n\nAlready have one? Make sure PAPI_PROJECT_ID is set in your .mcp.json env config."
|
|
9293
|
+
);
|
|
9294
|
+
}
|
|
9272
9295
|
return {
|
|
9273
9296
|
projectRoot,
|
|
9274
9297
|
papiDir: path.join(projectRoot, ".papi"),
|
|
@@ -9482,8 +9505,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
9482
9505
|
}
|
|
9483
9506
|
|
|
9484
9507
|
// src/server.ts
|
|
9508
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
9485
9509
|
import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
9486
|
-
import { join as
|
|
9510
|
+
import { join as join12, dirname } from "path";
|
|
9487
9511
|
import { fileURLToPath } from "url";
|
|
9488
9512
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9489
9513
|
import {
|
|
@@ -9689,6 +9713,17 @@ ${formatted}`;
|
|
|
9689
9713
|
}
|
|
9690
9714
|
return sections.join("\n\n");
|
|
9691
9715
|
}
|
|
9716
|
+
function formatCandidateTaskFullNotes(tasks) {
|
|
9717
|
+
const candidates = tasks.filter((t) => !PLAN_EXCLUDED_STATUSES.has(t.status)).filter((t) => (t.notes?.length ?? 0) > PLAN_NOTES_MAX_LENGTH);
|
|
9718
|
+
if (candidates.length === 0) return void 0;
|
|
9719
|
+
const lines = candidates.map((t) => `**${t.id}** \u2014 ${t.title}
|
|
9720
|
+
${t.notes}`);
|
|
9721
|
+
return [
|
|
9722
|
+
`${candidates.length} candidate task(s) have notes longer than ${PLAN_NOTES_MAX_LENGTH} chars. Full untruncated notes below \u2014 reference these when generating BUILD HANDOFFs so submitter context, constraints, and reasoning are preserved. The Board section above uses truncated notes for concise task selection; this section supplies the missing detail for tasks you choose to schedule.`,
|
|
9723
|
+
"",
|
|
9724
|
+
...lines
|
|
9725
|
+
].join("\n\n");
|
|
9726
|
+
}
|
|
9692
9727
|
function formatBoardForReview(tasks) {
|
|
9693
9728
|
if (tasks.length === 0) return "No tasks on the board.";
|
|
9694
9729
|
return tasks.map(
|
|
@@ -10510,6 +10545,7 @@ Standard planning cycle with full board review.
|
|
|
10510
10545
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
10511
10546
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
10512
10547
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
10548
|
+
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
10513
10549
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10514
10550
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
10515
10551
|
**Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
|
|
@@ -10740,6 +10776,7 @@ Standard planning cycle with full board review.
|
|
|
10740
10776
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
10741
10777
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
10742
10778
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
10779
|
+
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
10743
10780
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10744
10781
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
10745
10782
|
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
@@ -10843,6 +10880,9 @@ function buildPlanUserMessage(ctx) {
|
|
|
10843
10880
|
if (ctx.board) {
|
|
10844
10881
|
parts.push("### Board", "", ctx.board, "");
|
|
10845
10882
|
}
|
|
10883
|
+
if (ctx.candidateTaskFullNotes) {
|
|
10884
|
+
parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
|
|
10885
|
+
}
|
|
10846
10886
|
if (ctx.preAssignedTasks) {
|
|
10847
10887
|
parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
|
|
10848
10888
|
}
|
|
@@ -12238,6 +12278,10 @@ ${lines.join("\n")}`;
|
|
|
12238
12278
|
if (reportsForCapsResult.status === "fulfilled") {
|
|
12239
12279
|
recentlyShippedLean = formatRecentlyShippedCapabilities(reportsForCapsResult.value);
|
|
12240
12280
|
}
|
|
12281
|
+
let candidateTaskFullNotesLean;
|
|
12282
|
+
if (preAssignedResult.status === "fulfilled") {
|
|
12283
|
+
candidateTaskFullNotesLean = formatCandidateTaskFullNotes(preAssignedResult.value);
|
|
12284
|
+
}
|
|
12241
12285
|
logDataSourceSummary("plan (lean)", [
|
|
12242
12286
|
{ label: "cycleHealth", hasData: !!health },
|
|
12243
12287
|
{ label: "productBrief", hasData: warnIfEmpty("productBrief", productBrief) },
|
|
@@ -12275,7 +12319,8 @@ ${lines.join("\n")}`;
|
|
|
12275
12319
|
carryForwardStaleness: carryForwardStalenessLean,
|
|
12276
12320
|
preAssignedTasks: preAssignedTextLean,
|
|
12277
12321
|
recentlyShippedCapabilities: recentlyShippedLean,
|
|
12278
|
-
strategyReviewCadence
|
|
12322
|
+
strategyReviewCadence,
|
|
12323
|
+
candidateTaskFullNotes: candidateTaskFullNotesLean
|
|
12279
12324
|
};
|
|
12280
12325
|
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12281
12326
|
ctx2.contextTier = leanTierLabel;
|
|
@@ -12436,7 +12481,8 @@ ${logLines}`);
|
|
|
12436
12481
|
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
12437
12482
|
preAssignedTasks: preAssignedText,
|
|
12438
12483
|
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
|
|
12439
|
-
strategyReviewCadence: strategyReviewCadenceFull
|
|
12484
|
+
strategyReviewCadence: strategyReviewCadenceFull,
|
|
12485
|
+
candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks)
|
|
12440
12486
|
};
|
|
12441
12487
|
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12442
12488
|
ctx.contextTier = fullTierLabel;
|
|
@@ -21044,21 +21090,94 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21044
21090
|
return textResponse(lines.join("\n"));
|
|
21045
21091
|
}
|
|
21046
21092
|
|
|
21093
|
+
// src/services/session-guidance.ts
|
|
21094
|
+
import { existsSync as existsSync6 } from "fs";
|
|
21095
|
+
import { join as join9 } from "path";
|
|
21096
|
+
var state = {
|
|
21097
|
+
toolCallCount: 0,
|
|
21098
|
+
lastOrientAt: null,
|
|
21099
|
+
releaseSinceLastOrient: false,
|
|
21100
|
+
sessionStartedAt: Date.now()
|
|
21101
|
+
};
|
|
21102
|
+
var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
|
|
21103
|
+
var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
|
|
21104
|
+
function recordToolCall(name) {
|
|
21105
|
+
state.toolCallCount++;
|
|
21106
|
+
if (name === "release") state.releaseSinceLastOrient = true;
|
|
21107
|
+
}
|
|
21108
|
+
function markOrient() {
|
|
21109
|
+
state.lastOrientAt = Date.now();
|
|
21110
|
+
state.releaseSinceLastOrient = false;
|
|
21111
|
+
}
|
|
21112
|
+
async function buildSessionGuidance(adapter2, projectRoot) {
|
|
21113
|
+
const signals = [];
|
|
21114
|
+
try {
|
|
21115
|
+
if (adapter2.searchDocs) {
|
|
21116
|
+
const researchDir = join9(projectRoot, "docs", "research");
|
|
21117
|
+
if (existsSync6(researchDir)) {
|
|
21118
|
+
const files = scanMdFiles(researchDir, projectRoot);
|
|
21119
|
+
if (files.length > 0) {
|
|
21120
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21121
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21122
|
+
const unregistered = files.filter((f) => !registeredPaths.has(f));
|
|
21123
|
+
if (unregistered.length > 0) {
|
|
21124
|
+
signals.push(
|
|
21125
|
+
`${unregistered.length} research doc(s) in docs/research/ not registered \u2014 run \`doc_register\` so the planner can surface them.`
|
|
21126
|
+
);
|
|
21127
|
+
}
|
|
21128
|
+
}
|
|
21129
|
+
}
|
|
21130
|
+
}
|
|
21131
|
+
} catch {
|
|
21132
|
+
}
|
|
21133
|
+
if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
|
|
21134
|
+
signals.push(
|
|
21135
|
+
`${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
|
|
21136
|
+
);
|
|
21137
|
+
}
|
|
21138
|
+
if (state.lastOrientAt && Date.now() - state.lastOrientAt > ORIENT_GAP_MS) {
|
|
21139
|
+
const hours = Math.round((Date.now() - state.lastOrientAt) / (60 * 60 * 1e3));
|
|
21140
|
+
signals.push(
|
|
21141
|
+
`${hours}h since last orient \u2014 session may be stale. Consider a fresh window for best results.`
|
|
21142
|
+
);
|
|
21143
|
+
}
|
|
21144
|
+
if (state.releaseSinceLastOrient) {
|
|
21145
|
+
signals.push(
|
|
21146
|
+
"Release just ran \u2014 start a fresh session before the next `plan` to keep planning context clean."
|
|
21147
|
+
);
|
|
21148
|
+
}
|
|
21149
|
+
return signals.slice(0, 3);
|
|
21150
|
+
}
|
|
21151
|
+
|
|
21047
21152
|
// src/tools/orient.ts
|
|
21048
21153
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
21049
|
-
import { readFileSync as readFileSync3, writeFileSync, existsSync as
|
|
21050
|
-
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
|
+
}
|
|
21051
21164
|
var orientTool = {
|
|
21052
21165
|
name: "orient",
|
|
21053
|
-
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.",
|
|
21166
|
+
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. Pass `environment` to qualify git-dependent recommendations (build_execute, release, review_submit) for non-Claude-Code callers.",
|
|
21054
21167
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21055
21168
|
inputSchema: {
|
|
21056
21169
|
type: "object",
|
|
21057
|
-
properties: {
|
|
21170
|
+
properties: {
|
|
21171
|
+
environment: {
|
|
21172
|
+
type: "string",
|
|
21173
|
+
enum: ["claude-code", "cowork", "api", "unknown"],
|
|
21174
|
+
description: 'Caller environment. "claude-code" (local CLI with git) sees all recommendations unchanged. "cowork" or "api" (hosted/remote, no local git) suppresses/qualifies build_execute, release, and review_submit recommendations because those require a local Claude Code session to execute. Default "unknown" = show everything (safe default).'
|
|
21175
|
+
}
|
|
21176
|
+
},
|
|
21058
21177
|
required: []
|
|
21059
21178
|
}
|
|
21060
21179
|
};
|
|
21061
|
-
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot) {
|
|
21180
|
+
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown") {
|
|
21062
21181
|
const lines = [];
|
|
21063
21182
|
const cycleIsComplete = health.latestCycleStatus === "complete";
|
|
21064
21183
|
const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
|
|
@@ -21076,7 +21195,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
21076
21195
|
}
|
|
21077
21196
|
lines.push("");
|
|
21078
21197
|
}
|
|
21079
|
-
|
|
21198
|
+
const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
|
|
21199
|
+
if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
|
|
21200
|
+
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
21201
|
+
lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local Claude Code session with git access. From \`${environment}\` you can view state, log ideas, and edit the board \u2014 but the build/review/release loop must run through Claude Code._`);
|
|
21202
|
+
} else {
|
|
21203
|
+
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
21204
|
+
}
|
|
21080
21205
|
lines.push("");
|
|
21081
21206
|
lines.push(`**Strategy Review:** ${health.reviewWarning}`);
|
|
21082
21207
|
lines.push("");
|
|
@@ -21227,7 +21352,7 @@ function getLatestGitTag(projectRoot) {
|
|
|
21227
21352
|
}
|
|
21228
21353
|
function checkNpmVersionDrift() {
|
|
21229
21354
|
try {
|
|
21230
|
-
const pkgPath =
|
|
21355
|
+
const pkgPath = join10(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
21231
21356
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
21232
21357
|
const localVersion = pkg.version;
|
|
21233
21358
|
const packageName = pkg.name;
|
|
@@ -21244,7 +21369,8 @@ function checkNpmVersionDrift() {
|
|
|
21244
21369
|
return null;
|
|
21245
21370
|
}
|
|
21246
21371
|
}
|
|
21247
|
-
async function handleOrient(adapter2, config2) {
|
|
21372
|
+
async function handleOrient(adapter2, config2, args = {}) {
|
|
21373
|
+
const environment = normaliseEnvironment(args.environment);
|
|
21248
21374
|
try {
|
|
21249
21375
|
try {
|
|
21250
21376
|
await propagatePhaseStatus(adapter2);
|
|
@@ -21353,7 +21479,7 @@ ${versionDrift}` : "";
|
|
|
21353
21479
|
let unregisteredDocsNote = "";
|
|
21354
21480
|
try {
|
|
21355
21481
|
if (adapter2.searchDocs) {
|
|
21356
|
-
const docsDir =
|
|
21482
|
+
const docsDir = join10(config2.projectRoot, "docs");
|
|
21357
21483
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21358
21484
|
if (docsFiles.length > 0) {
|
|
21359
21485
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -21443,13 +21569,35 @@ ${versionDrift}` : "";
|
|
|
21443
21569
|
}
|
|
21444
21570
|
} catch {
|
|
21445
21571
|
}
|
|
21572
|
+
let alertsNote = "";
|
|
21446
21573
|
let unactionedIssuesNote = "";
|
|
21447
21574
|
try {
|
|
21448
|
-
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit:
|
|
21575
|
+
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
21449
21576
|
if (learnings) {
|
|
21450
|
-
const
|
|
21451
|
-
|
|
21452
|
-
|
|
21577
|
+
const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
|
|
21578
|
+
const unactionedAll = learnings.filter((l) => !l.actionTaken).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
|
|
21579
|
+
const allAlerts = unactionedAll.filter((l) => l.severity === "P0" || l.severity === "P1").sort(byRecency);
|
|
21580
|
+
const allLowSev = unactionedAll.filter((l) => l.severity === "P2" || l.severity === "P3").sort(byRecency);
|
|
21581
|
+
const totalP2 = allLowSev.filter((l) => l.severity === "P2").length;
|
|
21582
|
+
const totalP3 = allLowSev.filter((l) => l.severity === "P3").length;
|
|
21583
|
+
const ALERT_CAP = 10;
|
|
21584
|
+
const UNACTIONED_CAP = 5;
|
|
21585
|
+
const alerts = allAlerts.slice(0, ALERT_CAP);
|
|
21586
|
+
const unactioned = allLowSev.slice(0, UNACTIONED_CAP);
|
|
21587
|
+
if (allAlerts.length > 0) {
|
|
21588
|
+
const header = allAlerts.length > ALERT_CAP ? `${allAlerts.length} P0/P1 discovered issues awaiting action (showing ${ALERT_CAP} most recent).` : `${allAlerts.length} P0/P1 discovered issue${allAlerts.length !== 1 ? "s" : ""} awaiting action.`;
|
|
21589
|
+
const lines = ["\n\n## \u{1F6A8} Alerts", header];
|
|
21590
|
+
for (const issue of alerts) {
|
|
21591
|
+
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
21592
|
+
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
21593
|
+
}
|
|
21594
|
+
lines.push("_Escalate: run `idea` with P1 priority, or `board_edit` if already handled._");
|
|
21595
|
+
alertsNote = lines.join("\n");
|
|
21596
|
+
}
|
|
21597
|
+
if (allLowSev.length > 0) {
|
|
21598
|
+
const totalLow = totalP2 + totalP3;
|
|
21599
|
+
const header = totalLow > UNACTIONED_CAP ? `${totalP2} P2 \xB7 ${totalP3} P3 (showing ${UNACTIONED_CAP} most recent)` : `${totalP2} P2 \xB7 ${totalP3} P3`;
|
|
21600
|
+
const lines = ["\n\n## Unactioned Issues", header];
|
|
21453
21601
|
for (const issue of unactioned) {
|
|
21454
21602
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
21455
21603
|
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
@@ -21460,15 +21608,26 @@ ${versionDrift}` : "";
|
|
|
21460
21608
|
}
|
|
21461
21609
|
} catch {
|
|
21462
21610
|
}
|
|
21463
|
-
|
|
21611
|
+
let sessionGuidanceNote = "";
|
|
21612
|
+
try {
|
|
21613
|
+
const signals = await buildSessionGuidance(adapter2, config2.projectRoot);
|
|
21614
|
+
if (signals.length > 0) {
|
|
21615
|
+
const lines = ["\n\n## Session Guidance"];
|
|
21616
|
+
for (const s of signals) lines.push(`- ${s}`);
|
|
21617
|
+
sessionGuidanceNote = lines.join("\n");
|
|
21618
|
+
}
|
|
21619
|
+
markOrient();
|
|
21620
|
+
} catch {
|
|
21621
|
+
}
|
|
21622
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment) + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + sessionGuidanceNote + versionNote + enrichmentNote);
|
|
21464
21623
|
} catch (err) {
|
|
21465
21624
|
const message = err instanceof Error ? err.message : String(err);
|
|
21466
21625
|
return errorResponse(`Orient failed: ${message}`);
|
|
21467
21626
|
}
|
|
21468
21627
|
}
|
|
21469
21628
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
21470
|
-
const claudeMdPath =
|
|
21471
|
-
if (!
|
|
21629
|
+
const claudeMdPath = join10(projectRoot, "CLAUDE.md");
|
|
21630
|
+
if (!existsSync7(claudeMdPath)) return "";
|
|
21472
21631
|
const content = readFileSync3(claudeMdPath, "utf-8");
|
|
21473
21632
|
const additions = [];
|
|
21474
21633
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
@@ -22250,6 +22409,47 @@ ${result.userMessage}
|
|
|
22250
22409
|
}
|
|
22251
22410
|
}
|
|
22252
22411
|
|
|
22412
|
+
// src/lib/install-id.ts
|
|
22413
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
22414
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, chmodSync } from "fs";
|
|
22415
|
+
import { homedir as homedir3 } from "os";
|
|
22416
|
+
import { join as join11 } from "path";
|
|
22417
|
+
var PAPI_HOME_DIR = join11(homedir3(), ".papi");
|
|
22418
|
+
var INSTALL_ID_FILE = join11(PAPI_HOME_DIR, "install-id.json");
|
|
22419
|
+
var cachedInstallId = null;
|
|
22420
|
+
function isValidUuid(s) {
|
|
22421
|
+
return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
22422
|
+
}
|
|
22423
|
+
function getInstallId() {
|
|
22424
|
+
if (cachedInstallId) return cachedInstallId;
|
|
22425
|
+
try {
|
|
22426
|
+
const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
|
|
22427
|
+
const parsed = JSON.parse(raw);
|
|
22428
|
+
if (isValidUuid(parsed.install_id)) {
|
|
22429
|
+
cachedInstallId = parsed.install_id;
|
|
22430
|
+
return cachedInstallId;
|
|
22431
|
+
}
|
|
22432
|
+
} catch {
|
|
22433
|
+
}
|
|
22434
|
+
try {
|
|
22435
|
+
mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
|
|
22436
|
+
const id = randomUUID15();
|
|
22437
|
+
const contents = {
|
|
22438
|
+
install_id: id,
|
|
22439
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
22440
|
+
};
|
|
22441
|
+
writeFileSync2(INSTALL_ID_FILE, JSON.stringify(contents, null, 2), { mode: 384 });
|
|
22442
|
+
try {
|
|
22443
|
+
chmodSync(INSTALL_ID_FILE, 384);
|
|
22444
|
+
} catch {
|
|
22445
|
+
}
|
|
22446
|
+
cachedInstallId = id;
|
|
22447
|
+
return cachedInstallId;
|
|
22448
|
+
} catch {
|
|
22449
|
+
return null;
|
|
22450
|
+
}
|
|
22451
|
+
}
|
|
22452
|
+
|
|
22253
22453
|
// src/lib/telemetry.ts
|
|
22254
22454
|
var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
|
|
22255
22455
|
var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
|
|
@@ -22288,6 +22488,29 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
|
|
|
22288
22488
|
metadata: { duration_ms: durationMs, ...extra }
|
|
22289
22489
|
});
|
|
22290
22490
|
}
|
|
22491
|
+
function emitMdAdapterPing(toolName, extra) {
|
|
22492
|
+
if (!isEnabled()) return;
|
|
22493
|
+
const installId = getInstallId();
|
|
22494
|
+
if (!installId) return;
|
|
22495
|
+
const body = {
|
|
22496
|
+
install_id: installId,
|
|
22497
|
+
tool_name: toolName,
|
|
22498
|
+
papi_version: process.env["npm_package_version"] ?? null,
|
|
22499
|
+
metadata: extra ?? {}
|
|
22500
|
+
};
|
|
22501
|
+
fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
|
|
22502
|
+
method: "POST",
|
|
22503
|
+
headers: {
|
|
22504
|
+
"Content-Type": "application/json",
|
|
22505
|
+
"apikey": TELEMETRY_API_KEY,
|
|
22506
|
+
"Authorization": `Bearer ${TELEMETRY_API_KEY}`,
|
|
22507
|
+
"Prefer": "return=minimal"
|
|
22508
|
+
},
|
|
22509
|
+
body: JSON.stringify(body),
|
|
22510
|
+
signal: AbortSignal.timeout(5e3)
|
|
22511
|
+
}).catch(() => {
|
|
22512
|
+
});
|
|
22513
|
+
}
|
|
22291
22514
|
function emitMilestone(projectId, milestone, extra) {
|
|
22292
22515
|
emitTelemetryEvent({
|
|
22293
22516
|
project_id: projectId,
|
|
@@ -22322,13 +22545,26 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
22322
22545
|
"handoff_generate"
|
|
22323
22546
|
]);
|
|
22324
22547
|
function createServer(adapter2, config2) {
|
|
22548
|
+
const __pkgFilename = fileURLToPath(import.meta.url);
|
|
22549
|
+
const __pkgDir = dirname(__pkgFilename);
|
|
22550
|
+
let serverVersion = "unknown";
|
|
22551
|
+
try {
|
|
22552
|
+
const pkg = JSON.parse(readFileSync5(join12(__pkgDir, "..", "package.json"), "utf-8"));
|
|
22553
|
+
serverVersion = pkg.version ?? "unknown";
|
|
22554
|
+
} catch {
|
|
22555
|
+
}
|
|
22325
22556
|
const server2 = new Server(
|
|
22326
|
-
{ name: "papi", version:
|
|
22557
|
+
{ name: "papi", version: serverVersion },
|
|
22327
22558
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
22328
22559
|
);
|
|
22560
|
+
if (config2.adapterType === "md") {
|
|
22561
|
+
process.stderr.write(
|
|
22562
|
+
"\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
|
|
22563
|
+
);
|
|
22564
|
+
}
|
|
22329
22565
|
const __filename = fileURLToPath(import.meta.url);
|
|
22330
22566
|
const __dirname2 = dirname(__filename);
|
|
22331
|
-
const skillsDir =
|
|
22567
|
+
const skillsDir = join12(__dirname2, "..", "skills");
|
|
22332
22568
|
function parseSkillFrontmatter(content) {
|
|
22333
22569
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
22334
22570
|
if (!match) return null;
|
|
@@ -22346,7 +22582,7 @@ function createServer(adapter2, config2) {
|
|
|
22346
22582
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
22347
22583
|
const prompts = [];
|
|
22348
22584
|
for (const file of mdFiles) {
|
|
22349
|
-
const content = await readFile5(
|
|
22585
|
+
const content = await readFile5(join12(skillsDir, file), "utf-8");
|
|
22350
22586
|
const meta = parseSkillFrontmatter(content);
|
|
22351
22587
|
if (meta) {
|
|
22352
22588
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -22362,7 +22598,7 @@ function createServer(adapter2, config2) {
|
|
|
22362
22598
|
try {
|
|
22363
22599
|
const files = await readdir2(skillsDir);
|
|
22364
22600
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
22365
|
-
const content = await readFile5(
|
|
22601
|
+
const content = await readFile5(join12(skillsDir, file), "utf-8");
|
|
22366
22602
|
const meta = parseSkillFrontmatter(content);
|
|
22367
22603
|
if (meta?.name === name) {
|
|
22368
22604
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -22435,6 +22671,7 @@ function createServer(adapter2, config2) {
|
|
|
22435
22671
|
}
|
|
22436
22672
|
}
|
|
22437
22673
|
const timer2 = startTimer();
|
|
22674
|
+
recordToolCall(name);
|
|
22438
22675
|
let result;
|
|
22439
22676
|
switch (name) {
|
|
22440
22677
|
case "plan":
|
|
@@ -22498,7 +22735,7 @@ function createServer(adapter2, config2) {
|
|
|
22498
22735
|
result = await handleInit(config2, safeArgs);
|
|
22499
22736
|
break;
|
|
22500
22737
|
case "orient":
|
|
22501
|
-
result = await handleOrient(adapter2, config2);
|
|
22738
|
+
result = await handleOrient(adapter2, config2, safeArgs);
|
|
22502
22739
|
break;
|
|
22503
22740
|
case "hierarchy_update":
|
|
22504
22741
|
result = await handleHierarchyUpdate(adapter2, safeArgs);
|
|
@@ -22539,6 +22776,9 @@ function createServer(adapter2, config2) {
|
|
|
22539
22776
|
});
|
|
22540
22777
|
} catch {
|
|
22541
22778
|
}
|
|
22779
|
+
if (config2.adapterType === "md") {
|
|
22780
|
+
emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError });
|
|
22781
|
+
}
|
|
22542
22782
|
const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
|
|
22543
22783
|
if (telemetryProjectId) {
|
|
22544
22784
|
emitToolCall(telemetryProjectId, name, elapsed, {
|
|
@@ -22566,7 +22806,7 @@ function createServer(adapter2, config2) {
|
|
|
22566
22806
|
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
22567
22807
|
var pkgVersion = "unknown";
|
|
22568
22808
|
try {
|
|
22569
|
-
const pkg = JSON.parse(
|
|
22809
|
+
const pkg = JSON.parse(readFileSync6(join13(__dirname, "..", "package.json"), "utf-8"));
|
|
22570
22810
|
pkgVersion = pkg.version;
|
|
22571
22811
|
} catch {
|
|
22572
22812
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -245,6 +245,7 @@ Standard planning cycle with full board review.
|
|
|
245
245
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
246
246
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
247
247
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
248
|
+
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
248
249
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
249
250
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
250
251
|
**Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
|
|
@@ -475,6 +476,7 @@ Standard planning cycle with full board review.
|
|
|
475
476
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
476
477
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
477
478
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
479
|
+
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
478
480
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
479
481
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
480
482
|
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
@@ -578,6 +580,9 @@ function buildPlanUserMessage(ctx) {
|
|
|
578
580
|
if (ctx.board) {
|
|
579
581
|
parts.push("### Board", "", ctx.board, "");
|
|
580
582
|
}
|
|
583
|
+
if (ctx.candidateTaskFullNotes) {
|
|
584
|
+
parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
|
|
585
|
+
}
|
|
581
586
|
if (ctx.preAssignedTasks) {
|
|
582
587
|
parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
|
|
583
588
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
|
+
"mcpName": "io.github.cathalos92/papi",
|
|
4
5
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
6
|
"license": "Elastic-2.0",
|
|
6
7
|
"type": "module",
|