@papi-ai/server 0.7.20 → 0.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +714 -442
- package/package.json +4 -2
- package/skills/papi-cycle/AGENTS.md +81 -0
- package/skills/papi-cycle/papi-advanced/SKILL.md +28 -0
- package/skills/papi-cycle/papi-build/SKILL.md +52 -0
- package/skills/papi-cycle/papi-idea/SKILL.md +37 -0
- package/skills/papi-cycle/papi-plan/SKILL.md +163 -0
- package/skills/papi-cycle/papi-strategy/SKILL.md +28 -0
package/dist/index.js
CHANGED
|
@@ -2364,9 +2364,9 @@ var init_query = __esm({
|
|
|
2364
2364
|
CLOSE = {};
|
|
2365
2365
|
Query = class extends Promise {
|
|
2366
2366
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
2367
|
-
let
|
|
2367
|
+
let resolve2, reject;
|
|
2368
2368
|
super((a, b2) => {
|
|
2369
|
-
|
|
2369
|
+
resolve2 = a;
|
|
2370
2370
|
reject = b2;
|
|
2371
2371
|
});
|
|
2372
2372
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -2377,7 +2377,7 @@ var init_query = __esm({
|
|
|
2377
2377
|
this.options = options;
|
|
2378
2378
|
this.state = null;
|
|
2379
2379
|
this.statement = null;
|
|
2380
|
-
this.resolve = (x) => (this.active = false,
|
|
2380
|
+
this.resolve = (x) => (this.active = false, resolve2(x));
|
|
2381
2381
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2382
2382
|
this.active = false;
|
|
2383
2383
|
this.cancelled = null;
|
|
@@ -2425,12 +2425,12 @@ var init_query = __esm({
|
|
|
2425
2425
|
if (this.executed && !this.active)
|
|
2426
2426
|
return { done: true };
|
|
2427
2427
|
prev && prev();
|
|
2428
|
-
const promise = new Promise((
|
|
2428
|
+
const promise = new Promise((resolve2, reject) => {
|
|
2429
2429
|
this.cursorFn = (value) => {
|
|
2430
|
-
|
|
2430
|
+
resolve2({ value, done: false });
|
|
2431
2431
|
return new Promise((r) => prev = r);
|
|
2432
2432
|
};
|
|
2433
|
-
this.resolve = () => (this.active = false,
|
|
2433
|
+
this.resolve = () => (this.active = false, resolve2({ done: true }));
|
|
2434
2434
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2435
2435
|
});
|
|
2436
2436
|
this.execute();
|
|
@@ -3028,12 +3028,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
3028
3028
|
x.on("drain", drain);
|
|
3029
3029
|
return x;
|
|
3030
3030
|
}
|
|
3031
|
-
async function cancel({ pid, secret },
|
|
3031
|
+
async function cancel({ pid, secret }, resolve2, reject) {
|
|
3032
3032
|
try {
|
|
3033
3033
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
3034
3034
|
await connect();
|
|
3035
3035
|
socket.once("error", reject);
|
|
3036
|
-
socket.once("close",
|
|
3036
|
+
socket.once("close", resolve2);
|
|
3037
3037
|
} catch (error2) {
|
|
3038
3038
|
reject(error2);
|
|
3039
3039
|
}
|
|
@@ -4050,7 +4050,7 @@ var init_subscribe = __esm({
|
|
|
4050
4050
|
// ../../node_modules/postgres/src/large.js
|
|
4051
4051
|
import Stream2 from "stream";
|
|
4052
4052
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
4053
|
-
return new Promise(async (
|
|
4053
|
+
return new Promise(async (resolve2, reject) => {
|
|
4054
4054
|
await sql.begin(async (sql2) => {
|
|
4055
4055
|
let finish;
|
|
4056
4056
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -4076,7 +4076,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
4076
4076
|
) seek
|
|
4077
4077
|
`
|
|
4078
4078
|
};
|
|
4079
|
-
|
|
4079
|
+
resolve2(lo);
|
|
4080
4080
|
return new Promise(async (r) => finish = r);
|
|
4081
4081
|
async function readable({
|
|
4082
4082
|
highWaterMark = 2048 * 8,
|
|
@@ -4237,8 +4237,8 @@ function Postgres(a, b2) {
|
|
|
4237
4237
|
}
|
|
4238
4238
|
async function reserve() {
|
|
4239
4239
|
const queue = queue_default();
|
|
4240
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
4241
|
-
const query = { reserve:
|
|
4240
|
+
const c = open.length ? open.shift() : await new Promise((resolve2, reject) => {
|
|
4241
|
+
const query = { reserve: resolve2, reject };
|
|
4242
4242
|
queries.push(query);
|
|
4243
4243
|
closed.length && connect(closed.shift(), query);
|
|
4244
4244
|
});
|
|
@@ -4275,9 +4275,9 @@ function Postgres(a, b2) {
|
|
|
4275
4275
|
let uncaughtError, result;
|
|
4276
4276
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
4277
4277
|
try {
|
|
4278
|
-
result = await new Promise((
|
|
4278
|
+
result = await new Promise((resolve2, reject) => {
|
|
4279
4279
|
const x = fn2(sql2);
|
|
4280
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
4280
|
+
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve2, reject);
|
|
4281
4281
|
});
|
|
4282
4282
|
if (uncaughtError)
|
|
4283
4283
|
throw uncaughtError;
|
|
@@ -4334,8 +4334,8 @@ function Postgres(a, b2) {
|
|
|
4334
4334
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
4335
4335
|
}
|
|
4336
4336
|
function cancel(query) {
|
|
4337
|
-
return new Promise((
|
|
4338
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
4337
|
+
return new Promise((resolve2, reject) => {
|
|
4338
|
+
query.state ? query.active ? connection_default(options).cancel(query.state, resolve2, reject) : query.cancelled = { resolve: resolve2, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve2());
|
|
4339
4339
|
});
|
|
4340
4340
|
}
|
|
4341
4341
|
async function end({ timeout = null } = {}) {
|
|
@@ -4354,11 +4354,11 @@ function Postgres(a, b2) {
|
|
|
4354
4354
|
async function close() {
|
|
4355
4355
|
await Promise.all(connections.map((c) => c.end()));
|
|
4356
4356
|
}
|
|
4357
|
-
async function destroy(
|
|
4357
|
+
async function destroy(resolve2) {
|
|
4358
4358
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
4359
4359
|
while (queries.length)
|
|
4360
4360
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
4361
|
-
|
|
4361
|
+
resolve2();
|
|
4362
4362
|
}
|
|
4363
4363
|
function connect(c, query) {
|
|
4364
4364
|
move(c, connecting);
|
|
@@ -5757,6 +5757,38 @@ CREATE TABLE IF NOT EXISTS cost_snapshots (
|
|
|
5757
5757
|
UNIQUE (project_id, cycle)
|
|
5758
5758
|
);
|
|
5759
5759
|
|
|
5760
|
+
-- task-1896: per-project harness inventory (skills / sub-agents / hooks / MCP tools).
|
|
5761
|
+
-- Scanned from the local filesystem by the MCP server and persisted so the
|
|
5762
|
+
-- dashboard (no filesystem access) can surface what harness a project runs.
|
|
5763
|
+
CREATE TABLE IF NOT EXISTS project_harness_inventory (
|
|
5764
|
+
id UUID DEFAULT gen_random_uuid() NOT NULL,
|
|
5765
|
+
project_id UUID NOT NULL REFERENCES projects(id),
|
|
5766
|
+
user_id UUID,
|
|
5767
|
+
kind TEXT NOT NULL CHECK (kind IN ('skill', 'agent', 'hook', 'mcp_tool')),
|
|
5768
|
+
name TEXT NOT NULL,
|
|
5769
|
+
description TEXT,
|
|
5770
|
+
version TEXT,
|
|
5771
|
+
checksum TEXT,
|
|
5772
|
+
status TEXT DEFAULT 'ok'::text NOT NULL CHECK (status IN ('ok', 'stale_fork', 'missing')),
|
|
5773
|
+
path TEXT,
|
|
5774
|
+
synced_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
5775
|
+
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
5776
|
+
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
5777
|
+
PRIMARY KEY (id),
|
|
5778
|
+
UNIQUE (project_id, kind, name)
|
|
5779
|
+
);
|
|
5780
|
+
CREATE INDEX IF NOT EXISTS idx_project_harness_inventory_project_kind
|
|
5781
|
+
ON project_harness_inventory (project_id, kind);
|
|
5782
|
+
|
|
5783
|
+
-- task-1896: cheap change-detection marker. The sync writer only does the full
|
|
5784
|
+
-- scan + DB write when this fingerprint changes, so DB writes stay rare.
|
|
5785
|
+
CREATE TABLE IF NOT EXISTS project_harness_state (
|
|
5786
|
+
project_id UUID NOT NULL REFERENCES projects(id),
|
|
5787
|
+
fingerprint TEXT,
|
|
5788
|
+
synced_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
5789
|
+
PRIMARY KEY (project_id)
|
|
5790
|
+
);
|
|
5791
|
+
|
|
5760
5792
|
CREATE TABLE IF NOT EXISTS cycle_metrics_snapshots (
|
|
5761
5793
|
id UUID DEFAULT gen_random_uuid() NOT NULL,
|
|
5762
5794
|
project_id UUID NOT NULL REFERENCES projects(id),
|
|
@@ -6846,6 +6878,63 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6846
6878
|
createdAt: r.created_at
|
|
6847
6879
|
}));
|
|
6848
6880
|
}
|
|
6881
|
+
// --- Harness inventory (task-1896) ---
|
|
6882
|
+
async getHarnessInventory() {
|
|
6883
|
+
const rows = await this.sql`
|
|
6884
|
+
SELECT id, project_id, kind, name, description, version, checksum, status, path, synced_at
|
|
6885
|
+
FROM project_harness_inventory
|
|
6886
|
+
WHERE project_id = ${this.projectId}
|
|
6887
|
+
ORDER BY kind, name
|
|
6888
|
+
`;
|
|
6889
|
+
return rows.map((r) => ({
|
|
6890
|
+
id: r.id,
|
|
6891
|
+
projectId: r.project_id,
|
|
6892
|
+
kind: r.kind,
|
|
6893
|
+
name: r.name,
|
|
6894
|
+
description: r.description ?? void 0,
|
|
6895
|
+
version: r.version ?? void 0,
|
|
6896
|
+
checksum: r.checksum ?? void 0,
|
|
6897
|
+
status: r.status,
|
|
6898
|
+
path: r.path ?? void 0,
|
|
6899
|
+
syncedAt: r.synced_at
|
|
6900
|
+
}));
|
|
6901
|
+
}
|
|
6902
|
+
async replaceHarnessInventory(entries) {
|
|
6903
|
+
const userId = await this.ownerUserId();
|
|
6904
|
+
await this.sql.begin(async (_tx) => {
|
|
6905
|
+
const sql = _tx;
|
|
6906
|
+
await sql`DELETE FROM project_harness_inventory WHERE project_id = ${this.projectId}`;
|
|
6907
|
+
if (entries.length === 0) return;
|
|
6908
|
+
const values2 = entries.map((e) => ({
|
|
6909
|
+
project_id: this.projectId,
|
|
6910
|
+
user_id: userId,
|
|
6911
|
+
kind: e.kind,
|
|
6912
|
+
name: e.name,
|
|
6913
|
+
description: e.description ?? null,
|
|
6914
|
+
version: e.version ?? null,
|
|
6915
|
+
checksum: e.checksum ?? null,
|
|
6916
|
+
status: e.status,
|
|
6917
|
+
path: e.path ?? null
|
|
6918
|
+
}));
|
|
6919
|
+
await sql`INSERT INTO project_harness_inventory ${sql(values2)}`;
|
|
6920
|
+
});
|
|
6921
|
+
}
|
|
6922
|
+
async getHarnessState() {
|
|
6923
|
+
const rows = await this.sql`
|
|
6924
|
+
SELECT fingerprint, synced_at FROM project_harness_state
|
|
6925
|
+
WHERE project_id = ${this.projectId}
|
|
6926
|
+
LIMIT 1
|
|
6927
|
+
`;
|
|
6928
|
+
if (rows.length === 0 || rows[0].fingerprint === null) return null;
|
|
6929
|
+
return { fingerprint: rows[0].fingerprint, syncedAt: rows[0].synced_at };
|
|
6930
|
+
}
|
|
6931
|
+
async setHarnessState(fingerprint) {
|
|
6932
|
+
await this.sql`
|
|
6933
|
+
INSERT INTO project_harness_state (project_id, fingerprint, synced_at)
|
|
6934
|
+
VALUES (${this.projectId}, ${fingerprint}, now())
|
|
6935
|
+
ON CONFLICT (project_id) DO UPDATE SET fingerprint = ${fingerprint}, synced_at = now()
|
|
6936
|
+
`;
|
|
6937
|
+
}
|
|
6849
6938
|
async getUnactionedDogfoodEntries(limit = 20) {
|
|
6850
6939
|
const rows = await this.sql`
|
|
6851
6940
|
SELECT id, project_id, cycle_number, category, content, source_tool, source_ref, created_at
|
|
@@ -9068,6 +9157,19 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
9068
9157
|
updateDogfoodEntryStatus(id, status, linkedTaskId) {
|
|
9069
9158
|
return this.invoke("updateDogfoodEntryStatus", [id, status, linkedTaskId]);
|
|
9070
9159
|
}
|
|
9160
|
+
// --- Harness inventory (task-1896) ---
|
|
9161
|
+
getHarnessInventory() {
|
|
9162
|
+
return this.invoke("getHarnessInventory");
|
|
9163
|
+
}
|
|
9164
|
+
replaceHarnessInventory(entries) {
|
|
9165
|
+
return this.invoke("replaceHarnessInventory", [entries]);
|
|
9166
|
+
}
|
|
9167
|
+
getHarnessState() {
|
|
9168
|
+
return this.invoke("getHarnessState");
|
|
9169
|
+
}
|
|
9170
|
+
setHarnessState(fingerprint) {
|
|
9171
|
+
return this.invoke("setHarnessState", [fingerprint]);
|
|
9172
|
+
}
|
|
9071
9173
|
// --- North Star ---
|
|
9072
9174
|
getCurrentNorthStar() {
|
|
9073
9175
|
return this.invoke("getCurrentNorthStar");
|
|
@@ -9227,6 +9329,7 @@ __export(git_exports, {
|
|
|
9227
9329
|
detectBoardMismatches: () => detectBoardMismatches,
|
|
9228
9330
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
9229
9331
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
9332
|
+
getBranchDiff: () => getBranchDiff,
|
|
9230
9333
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
9231
9334
|
getCurrentBranch: () => getCurrentBranch,
|
|
9232
9335
|
getDocPathsTouchedOnBranch: () => getDocPathsTouchedOnBranch,
|
|
@@ -9370,6 +9473,26 @@ function getModifiedFiles(cwd) {
|
|
|
9370
9473
|
return [];
|
|
9371
9474
|
}
|
|
9372
9475
|
}
|
|
9476
|
+
function getBranchDiff(cwd, base = "origin/main", maxBytes = 2e5) {
|
|
9477
|
+
const refs = [`${base}...HEAD`, "main...HEAD"];
|
|
9478
|
+
for (const ref of refs) {
|
|
9479
|
+
try {
|
|
9480
|
+
const out = execFileSync("git", ["diff", ref], {
|
|
9481
|
+
cwd,
|
|
9482
|
+
encoding: "utf-8",
|
|
9483
|
+
maxBuffer: 32 * 1024 * 1024
|
|
9484
|
+
});
|
|
9485
|
+
if (out) {
|
|
9486
|
+
return out.length > maxBytes ? `${out.slice(0, maxBytes)}
|
|
9487
|
+
|
|
9488
|
+
... [diff truncated at ${Math.round(maxBytes / 1024)} KB]` : out;
|
|
9489
|
+
}
|
|
9490
|
+
return "";
|
|
9491
|
+
} catch {
|
|
9492
|
+
}
|
|
9493
|
+
}
|
|
9494
|
+
return "";
|
|
9495
|
+
}
|
|
9373
9496
|
function getHeadCommitSubject(cwd) {
|
|
9374
9497
|
try {
|
|
9375
9498
|
const out = execFileSync("git", ["log", "-1", "--format=%s"], {
|
|
@@ -10002,9 +10125,9 @@ __export(doctor_exports, {
|
|
|
10002
10125
|
__testing: () => __testing,
|
|
10003
10126
|
runDoctor: () => runDoctor
|
|
10004
10127
|
});
|
|
10005
|
-
import { existsSync as
|
|
10128
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
10006
10129
|
import { homedir as homedir4 } from "os";
|
|
10007
|
-
import { join as
|
|
10130
|
+
import { join as join18 } from "path";
|
|
10008
10131
|
function redact(name, value) {
|
|
10009
10132
|
if (!value) return "(empty)";
|
|
10010
10133
|
if (SECRET_VARS.has(name)) {
|
|
@@ -10015,14 +10138,14 @@ function redact(name, value) {
|
|
|
10015
10138
|
}
|
|
10016
10139
|
function findMcpJson() {
|
|
10017
10140
|
const candidates = [
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10141
|
+
join18(process.cwd(), ".mcp.json"),
|
|
10142
|
+
join18(homedir4(), ".claude", ".mcp.json"),
|
|
10143
|
+
join18(homedir4(), ".mcp.json")
|
|
10021
10144
|
];
|
|
10022
10145
|
for (const path7 of candidates) {
|
|
10023
|
-
if (!
|
|
10146
|
+
if (!existsSync9(path7)) continue;
|
|
10024
10147
|
try {
|
|
10025
|
-
const raw =
|
|
10148
|
+
const raw = readFileSync10(path7, "utf-8");
|
|
10026
10149
|
const parsed = JSON.parse(raw);
|
|
10027
10150
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
10028
10151
|
if (!papiEntry) continue;
|
|
@@ -10180,17 +10303,17 @@ __export(reset_exports, {
|
|
|
10180
10303
|
removePapiEntry: () => removePapiEntry,
|
|
10181
10304
|
runReset: () => runReset
|
|
10182
10305
|
});
|
|
10183
|
-
import { existsSync as
|
|
10306
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
|
|
10184
10307
|
import { homedir as homedir5 } from "os";
|
|
10185
|
-
import { join as
|
|
10308
|
+
import { join as join19 } from "path";
|
|
10186
10309
|
import { createInterface } from "readline/promises";
|
|
10187
10310
|
function findResetTarget() {
|
|
10188
10311
|
for (const path7 of CANDIDATE_PATHS()) {
|
|
10189
|
-
if (!
|
|
10312
|
+
if (!existsSync10(path7)) continue;
|
|
10190
10313
|
let raw;
|
|
10191
10314
|
let parsed;
|
|
10192
10315
|
try {
|
|
10193
|
-
raw =
|
|
10316
|
+
raw = readFileSync11(path7, "utf-8");
|
|
10194
10317
|
parsed = JSON.parse(raw);
|
|
10195
10318
|
} catch {
|
|
10196
10319
|
continue;
|
|
@@ -10278,17 +10401,17 @@ var init_reset = __esm({
|
|
|
10278
10401
|
"src/cli/reset.ts"() {
|
|
10279
10402
|
"use strict";
|
|
10280
10403
|
CANDIDATE_PATHS = () => [
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10404
|
+
join19(process.cwd(), ".mcp.json"),
|
|
10405
|
+
join19(homedir5(), ".claude", ".mcp.json"),
|
|
10406
|
+
join19(homedir5(), ".mcp.json")
|
|
10284
10407
|
];
|
|
10285
10408
|
}
|
|
10286
10409
|
});
|
|
10287
10410
|
|
|
10288
10411
|
// src/index.ts
|
|
10289
|
-
import { readFileSync as
|
|
10290
|
-
import { dirname as
|
|
10291
|
-
import { fileURLToPath as
|
|
10412
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
10413
|
+
import { dirname as dirname5, join as join20 } from "path";
|
|
10414
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10292
10415
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10293
10416
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10294
10417
|
import {
|
|
@@ -10758,10 +10881,10 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
10758
10881
|
}
|
|
10759
10882
|
|
|
10760
10883
|
// src/server.ts
|
|
10761
|
-
import { readFileSync as
|
|
10762
|
-
import { access as access4, readdir as
|
|
10763
|
-
import { join as
|
|
10764
|
-
import { fileURLToPath } from "url";
|
|
10884
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
10885
|
+
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
10886
|
+
import { join as join17, dirname as dirname4 } from "path";
|
|
10887
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10765
10888
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10766
10889
|
import {
|
|
10767
10890
|
CallToolRequestSchema,
|
|
@@ -15083,34 +15206,60 @@ function buildSubagentDispatchPrompt(input) {
|
|
|
15083
15206
|
const {
|
|
15084
15207
|
tool,
|
|
15085
15208
|
applyMode,
|
|
15086
|
-
cycleNumber,
|
|
15209
|
+
cycleNumber = 0,
|
|
15087
15210
|
strategyReviewWarning = "",
|
|
15211
|
+
taskId,
|
|
15088
15212
|
systemPrompt,
|
|
15089
15213
|
userMessage,
|
|
15090
15214
|
contextBytes
|
|
15091
15215
|
} = input;
|
|
15092
15216
|
const sizeNote = contextBytes !== void 0 ? ` (~${Math.round(contextBytes / 1024)} KB context)` : "";
|
|
15093
|
-
const
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15217
|
+
const isReview = tool === "review_submit";
|
|
15218
|
+
let applyNote;
|
|
15219
|
+
if (tool === "plan") {
|
|
15220
|
+
applyNote = `\`plan\` again with mode="apply", llm_response=<sub-agent output>, plan_mode="${applyMode ?? "full"}", cycle_number=${cycleNumber + 1}, strategy_review_warning=${JSON.stringify(strategyReviewWarning)}`;
|
|
15221
|
+
} else if (tool === "strategy_review") {
|
|
15222
|
+
applyNote = `\`strategy_review\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber}`;
|
|
15223
|
+
} else {
|
|
15224
|
+
applyNote = `\`review_submit\` with task_id="${taskId ?? "<task-id>"}", stage="build-acceptance", verdict=<your decision: accept / request-changes / reject>, comments=<your reasoning>, and auto_review set to the sub-agent's JSON object verbatim. The sub-agent's verdict is a RECOMMENDATION \u2014 you make the final call`;
|
|
15225
|
+
}
|
|
15226
|
+
const contractBody = isReview ? `- Review the completed build below. The build report says what was intended; the diff is what actually changed. Check correctness, scope adherence (did it match the handoff?), security, and quality.
|
|
15227
|
+
- Return ONLY a single JSON object \u2014 no preamble, no commentary, no fences \u2014 in exactly this shape:
|
|
15228
|
+
{"verdict":"pass|warn|fail","summary":"<one-line overall assessment>","findings":[{"severity":"error|warning|info","file":"<path>","line":<number>,"message":"<specific issue>"}]}
|
|
15229
|
+
- "pass" = no blocking issues; "warn" = minor/non-blocking nits; "fail" = blocking issues that should send the build back. \`file\`/\`line\` are optional per finding. Empty \`findings\` is valid when clean.` : `- Read the system prompt and context exactly as the main agent would.
|
|
15099
15230
|
- Produce the full structured output the system prompt requires (Part 1 markdown + Part 2 JSON after \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, or whatever the system prompt specifies).
|
|
15100
|
-
- Return ONLY that output \u2014 no preamble, no commentary, no closing summary. The dispatching agent will pass your reply verbatim to the apply call
|
|
15231
|
+
- Return ONLY that output \u2014 no preamble, no commentary, no closing summary. The dispatching agent will pass your reply verbatim to the apply call.`;
|
|
15232
|
+
const contextBlock = isReview ? `<review_rubric>
|
|
15233
|
+
${systemPrompt}
|
|
15234
|
+
</review_rubric>
|
|
15101
15235
|
|
|
15102
|
-
<
|
|
15236
|
+
<build_under_review>
|
|
15237
|
+
${userMessage}
|
|
15238
|
+
</build_under_review>` : `<system_prompt>
|
|
15103
15239
|
${systemPrompt}
|
|
15104
15240
|
</system_prompt>
|
|
15105
15241
|
|
|
15106
15242
|
<context>
|
|
15107
15243
|
${userMessage}
|
|
15108
|
-
</context
|
|
15244
|
+
</context>`;
|
|
15245
|
+
const purpose = isReview ? "perform a PAPI build-acceptance code review" : `execute a PAPI ${tool} call`;
|
|
15246
|
+
const subagentInstructions = `
|
|
15247
|
+
You are a one-shot sub-agent dispatched by Claude Code to ${purpose}.
|
|
15248
|
+
|
|
15249
|
+
CONTRACT:
|
|
15250
|
+
${contractBody}
|
|
15251
|
+
|
|
15252
|
+
${contextBlock}
|
|
15109
15253
|
`.trim();
|
|
15254
|
+
const headerLabel = isReview ? `## PAPI review_submit \u2014 Sub-Agent Dispatch (${taskId ?? "build review"}${sizeNote})` : `## PAPI ${tool} \u2014 Sub-Agent Dispatch (Cycle ${cycleNumber + (tool === "plan" ? 1 : 0)}${sizeNote})`;
|
|
15255
|
+
const intro = isReview ? "This build review carries a large diff/report \u2014 enough to crowd the main session context. Dispatch the analysis via `Task()` to a fresh sub-agent, then act on its structured findings." : `The reasoning for this ${tool} call is heavy enough to crowd the main session context. Dispatch it via \`Task()\` to a fresh sub-agent and pass its reply through to the apply call.`;
|
|
15256
|
+
const step2Note = isReview ? `When the sub-agent returns its JSON, surface the findings, decide your verdict, then call ${applyNote}.` : `When the sub-agent returns, call ${applyNote}.
|
|
15257
|
+
|
|
15258
|
+
Do NOT post-process the sub-agent reply. The apply path expects the full structured output exactly as the sub-agent emits it.`;
|
|
15110
15259
|
return [
|
|
15111
|
-
|
|
15260
|
+
headerLabel,
|
|
15112
15261
|
"",
|
|
15113
|
-
|
|
15262
|
+
intro,
|
|
15114
15263
|
"",
|
|
15115
15264
|
"---",
|
|
15116
15265
|
"",
|
|
@@ -15118,18 +15267,16 @@ ${userMessage}
|
|
|
15118
15267
|
"",
|
|
15119
15268
|
"Use the `Task` tool with:",
|
|
15120
15269
|
`- \`subagent_type\`: \`general-purpose\``,
|
|
15121
|
-
`- \`description\`:
|
|
15270
|
+
`- \`description\`: \`${isReview ? "Review PAPI build" : `Run PAPI ${tool}`}\``,
|
|
15122
15271
|
`- \`prompt\`: the full prompt block below (everything between the BEGIN/END markers, trimmed of the markers themselves).`,
|
|
15123
15272
|
"",
|
|
15124
15273
|
"<<<BEGIN_SUBAGENT_PROMPT>>>",
|
|
15125
15274
|
subagentInstructions,
|
|
15126
15275
|
"<<<END_SUBAGENT_PROMPT>>>",
|
|
15127
15276
|
"",
|
|
15128
|
-
|
|
15129
|
-
"",
|
|
15130
|
-
`When the sub-agent returns, call ${applyNote}.`,
|
|
15277
|
+
`### Step 2 \u2014 ${isReview ? "Act on the review" : "Pass the sub-agent reply to apply"}`,
|
|
15131
15278
|
"",
|
|
15132
|
-
|
|
15279
|
+
step2Note
|
|
15133
15280
|
].join("\n");
|
|
15134
15281
|
}
|
|
15135
15282
|
|
|
@@ -15975,8 +16122,8 @@ ${lines.join("\n")}`;
|
|
|
15975
16122
|
const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
|
|
15976
16123
|
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
15977
16124
|
const fullPath = join2(plansDir, f);
|
|
15978
|
-
const
|
|
15979
|
-
return { name: f, modified:
|
|
16125
|
+
const stat4 = statSync(fullPath);
|
|
16126
|
+
return { name: f, modified: stat4.mtime, size: stat4.size };
|
|
15980
16127
|
}).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
|
|
15981
16128
|
if (planFiles.length > 0) {
|
|
15982
16129
|
const lines = planFiles.map((f) => {
|
|
@@ -17933,7 +18080,7 @@ ${existing}` : entry;
|
|
|
17933
18080
|
// src/services/setup.ts
|
|
17934
18081
|
init_dist2();
|
|
17935
18082
|
import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2 } from "fs/promises";
|
|
17936
|
-
import { join as
|
|
18083
|
+
import { join as join5, basename, extname, dirname as dirname2 } from "path";
|
|
17937
18084
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
17938
18085
|
|
|
17939
18086
|
// src/lib/detect-codebase.ts
|
|
@@ -17974,6 +18121,50 @@ function detectCodebaseType(projectRoot) {
|
|
|
17974
18121
|
return "new_project";
|
|
17975
18122
|
}
|
|
17976
18123
|
|
|
18124
|
+
// src/lib/agents-bundle.ts
|
|
18125
|
+
import { readFileSync, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
18126
|
+
import { dirname, join as join4, resolve } from "path";
|
|
18127
|
+
import { fileURLToPath } from "url";
|
|
18128
|
+
var PROJECT_BUNDLE_REL = join4(".agents", "skills", "papi-cycle");
|
|
18129
|
+
function bundleDestRel(rel) {
|
|
18130
|
+
return rel === "AGENTS.md" ? "AGENTS.md" : join4(PROJECT_BUNDLE_REL, rel);
|
|
18131
|
+
}
|
|
18132
|
+
function resolveBundleDir() {
|
|
18133
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
18134
|
+
for (let i = 0; i < 5; i++) {
|
|
18135
|
+
const candidate = join4(dir, "skills", "papi-cycle");
|
|
18136
|
+
if (existsSync3(join4(candidate, "AGENTS.md"))) return candidate;
|
|
18137
|
+
const parent = resolve(dir, "..");
|
|
18138
|
+
if (parent === dir) break;
|
|
18139
|
+
dir = parent;
|
|
18140
|
+
}
|
|
18141
|
+
return void 0;
|
|
18142
|
+
}
|
|
18143
|
+
function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
18144
|
+
if (!bundleDir || !existsSync3(bundleDir)) return [];
|
|
18145
|
+
const files = [];
|
|
18146
|
+
const walk = (abs, rel) => {
|
|
18147
|
+
for (const entry of readdirSync3(abs, { withFileTypes: true })) {
|
|
18148
|
+
const childAbs = join4(abs, entry.name);
|
|
18149
|
+
const childRel = rel ? join4(rel, entry.name) : entry.name;
|
|
18150
|
+
if (entry.isDirectory()) walk(childAbs, childRel);
|
|
18151
|
+
else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync(childAbs, "utf8") });
|
|
18152
|
+
}
|
|
18153
|
+
};
|
|
18154
|
+
walk(bundleDir, "");
|
|
18155
|
+
return files;
|
|
18156
|
+
}
|
|
18157
|
+
function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
18158
|
+
const out = {};
|
|
18159
|
+
for (const f of readBundleFiles()) {
|
|
18160
|
+
const dest = join4(projectRoot, bundleDestRel(f.rel));
|
|
18161
|
+
if (opts.skipExisting && existsSync3(dest) && statSync3(dest).isFile()) continue;
|
|
18162
|
+
const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
|
|
18163
|
+
out[dest] = content;
|
|
18164
|
+
}
|
|
18165
|
+
return out;
|
|
18166
|
+
}
|
|
18167
|
+
|
|
17977
18168
|
// src/templates.ts
|
|
17978
18169
|
var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
|
|
17979
18170
|
|
|
@@ -18100,283 +18291,17 @@ var CYCLES_TEMPLATE = `# Cycles
|
|
|
18100
18291
|
cycles: []
|
|
18101
18292
|
<!-- PAPI-YAML-END -->
|
|
18102
18293
|
`;
|
|
18103
|
-
var
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
On the first \`orient\` of any session, surface the connected project name to the user (e.g. "Connected to: <project_name>") and confirm it matches what they expect before making any code changes. PAPI projects are scoped by ID; if the wrong PAPI_PROJECT_ID is configured, edits land in the wrong project's history \u2014 a hard-to-undo class of mistake.
|
|
18108
|
-
|
|
18109
|
-
If the user doesn't recognise the project, stop. To fix it from this chat \u2014 no file editing \u2014 use the project tools: \`project_list\` to see their projects, \`project_create\` to make an empty one for this folder, or \`project_switch\` to point at the right one. Or pass \`project=<id>\` on a single call to override for that call only.
|
|
18110
|
-
|
|
18111
|
-
## Documentation Maintenance
|
|
18112
|
-
|
|
18113
|
-
Before creating a new doc, check \`docs/INDEX.md\` \u2014 it may already exist. When creating or archiving docs, update the index.
|
|
18114
|
-
|
|
18115
|
-
After implementing any code change, check if the change affects any documentation in \`docs/\`. If a doc describes behaviour, architecture, or file interactions that your change modified, update the doc to stay accurate.
|
|
18116
|
-
|
|
18117
|
-
When updating a doc, add or update a review header immediately below the title:
|
|
18118
|
-
|
|
18119
|
-
\`\`\`
|
|
18120
|
-
# Document Title
|
|
18121
|
-
> Last reviewed: task-NNN \u2014 DD-MM-YYYY
|
|
18122
|
-
\`\`\`
|
|
18123
|
-
|
|
18124
|
-
Replace \`task-NNN\` with the task ID that triggered the update, and \`DD-MM-YYYY\` with today's date.
|
|
18125
|
-
|
|
18126
|
-
## Session Start
|
|
18127
|
-
|
|
18128
|
-
When a conversation starts \u2014 fresh window, new session, or after context compression \u2014 orient before doing anything else:
|
|
18129
|
-
|
|
18130
|
-
1. **Run \`orient\`** \u2014 single call that returns cycle number, task counts, in-progress/in-review tasks, strategy review cadence, trends, and recommended next action.
|
|
18131
|
-
2. **Fix orphaned tasks silently** \u2014 check for feat/task-XXX branches that don't match board status. Fix and report after.
|
|
18132
|
-
3. **Summarise:** "You're on Cycle N. X tasks to build, Y builds pending review." or "Cycle N is complete \u2014 ready for the next plan."
|
|
18133
|
-
4. **Run \`build_list\` when picking a task** \u2014 \`orient\` shows counts only. \`build_list\` shows the full task list with handoffs.
|
|
18134
|
-
|
|
18135
|
-
**CRITICAL: Check task statuses before acting.**
|
|
18136
|
-
- **In Review** = already built. Suggest \`review_list\` \u2192 \`review_submit\`. **NEVER re-build an In Review task.**
|
|
18137
|
-
- **In Progress** = build started but not completed. Check the branch and existing changes before writing new code.
|
|
18138
|
-
- **Backlog** = not started. But first check if a \`feat/task-XXX\` branch already exists with commits \u2014 fix it, don't rebuild.
|
|
18139
|
-
- If all cycle tasks are Done, suggest \`release\` or next \`plan\`.
|
|
18140
|
-
|
|
18141
|
-
## Workflow Sequences
|
|
18142
|
-
|
|
18143
|
-
PAPI tools follow structured flows. The agent manages the cycle workflow automatically \u2014 the user should never need to type tool names or remember the flow. Handle the plumbing, surface the summaries.
|
|
18144
|
-
|
|
18145
|
-
### Cycle Workflow (auto-managed)
|
|
18146
|
-
|
|
18147
|
-
- **Run tools automatically** \u2014 don't ask the user to invoke MCP tools manually
|
|
18148
|
-
- Before implementing: silently run \`build_execute <task_id>\` (start phase)
|
|
18149
|
-
- After implementing: run \`build_execute <task_id>\` (complete phase) with report fields
|
|
18150
|
-
- After build_execute completes: audit the branch changes for bugs, convention violations, and doc drift (see Post-Build Audit below)
|
|
18151
|
-
- After audit with findings: *MUST* automatically run \`review_submit\` with verdict \`request-changes\` and a concise summary of the audit findings as the changes requested \u2014 the builder fixes these before the task goes to human review
|
|
18152
|
-
- After audit clean: present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
|
|
18153
|
-
- User approves/requests changes \u2192 run \`review_submit\` behind the scenes
|
|
18154
|
-
|
|
18155
|
-
### The Cycle (main flow)
|
|
18156
|
-
|
|
18157
|
-
\`\`\`
|
|
18158
|
-
plan \u2192 build_list \u2192 build_execute \u2192 audit \u2192 review_list \u2192 review_submit \u2192 build_list
|
|
18159
|
-
\`\`\`
|
|
18160
|
-
|
|
18161
|
-
1. **plan** \u2014 Run at the start of each cycle to generate the cycle plan and populate the board.
|
|
18162
|
-
Next: \`build_list\` to see prioritised tasks.
|
|
18163
|
-
2. **build_list** \u2014 View tasks ready for execution, ordered by priority.
|
|
18164
|
-
Next: \`build_execute <task_id>\` to start a task.
|
|
18165
|
-
3. **build_execute** (start) \u2014 Creates a feature branch and marks the task In Progress. Returns the build handoff.
|
|
18166
|
-
Next: Implement the task, then \`build_execute <task_id>\` again with report fields to complete.
|
|
18167
|
-
4. **build_execute** (complete) \u2014 Submits the build report, commits, and marks the task In Review.
|
|
18168
|
-
Next: Run the post-build audit automatically.
|
|
18169
|
-
5. **Post-build audit** \u2014 Review branch changes for bugs, convention violations, and doc drift (see Post-Build Audit section below).
|
|
18170
|
-
Next: If findings exist, run \`review_submit\` with \`request-changes\` and the audit findings. If clean, proceed to \`review_list\`.
|
|
18171
|
-
6. **review_list** \u2014 Shows tasks pending human review (handoff-review or build-acceptance).
|
|
18172
|
-
Next: \`review_submit\` to approve, accept, or request changes.
|
|
18173
|
-
7. **review_submit** \u2014 Records the review verdict and updates task status.
|
|
18174
|
-
Next: \`build_list\` to view next build
|
|
18175
|
-
|
|
18176
|
-
**DO NOT** use \`review_submit\` as a substitute for \`review_list\`. If you need to see what is pending review, always call \`review_list\` first. If \`review_list\` is unavailable in your tool set (e.g. your MCP client filters parameterless tools), STOP and tell the human their MCP integration is incomplete \u2014 never guess at the next pending task. To submit an accept verdict on a build-acceptance review, either pass \`reviewer_confirmed: true\` or ensure \`review_list\` has run in the same session within the last 15 minutes. (SUP-2026-010.)
|
|
18177
|
-
|
|
18178
|
-
### Strategy Review
|
|
18179
|
-
|
|
18180
|
-
\`\`\`
|
|
18181
|
-
strategy_review \u2192 strategy_change
|
|
18182
|
-
\`\`\`
|
|
18183
|
-
|
|
18184
|
-
- **strategy_review** \u2014 Analyses project health, velocity, and estimation accuracy.
|
|
18185
|
-
Next: \`strategy_change\` if the review recommends adjustments.
|
|
18186
|
-
- **strategy_change** \u2014 Updates active decisions, north star, or project direction based on review findings.
|
|
18187
|
-
|
|
18188
|
-
### Detect Strategic Decisions in Conversation
|
|
18189
|
-
|
|
18190
|
-
Watch for: direction changes, architecture shifts, deprioritisation with reasoning, new principles, competitive positioning decisions.
|
|
18191
|
-
|
|
18192
|
-
When detected:
|
|
18193
|
-
1. Flag it: "That sounds like a strategic direction change \u2014 should I run \`strategy_change\`?"
|
|
18194
|
-
2. If confirmed, run \`strategy_change\` immediately.
|
|
18195
|
-
3. If mid-build, finish the current task first.
|
|
18196
|
-
|
|
18197
|
-
### Idea Capture
|
|
18198
|
-
|
|
18199
|
-
\`\`\`
|
|
18200
|
-
idea \u2192 (picked up by next plan)
|
|
18201
|
-
\`\`\`
|
|
18202
|
-
|
|
18203
|
-
- **idea** \u2014 Captures a new task idea and writes it to the backlog.
|
|
18204
|
-
Next: The next \`plan\` run will prioritise and schedule it.
|
|
18205
|
-
|
|
18206
|
-
### Project Bootstrap
|
|
18207
|
-
|
|
18208
|
-
\`\`\`
|
|
18209
|
-
setup \u2192 plan
|
|
18210
|
-
\`\`\`
|
|
18211
|
-
|
|
18212
|
-
- **setup** \u2014 {{setup_description}}
|
|
18213
|
-
Next: \`plan\` to run the first cycle planning session.
|
|
18214
|
-
|
|
18215
|
-
### Board Management
|
|
18216
|
-
|
|
18217
|
-
- **board_view** \u2014 Read-only view of all tasks on the board.
|
|
18218
|
-
- **board_archive** \u2014 Removes completed/cancelled tasks from the board to an archive.
|
|
18219
|
-
- **board_deprioritise** \u2014 Moves a task to a later phase.
|
|
18220
|
-
|
|
18221
|
-
### Quick Reference: Tool \u2192 Next Step
|
|
18222
|
-
|
|
18223
|
-
| Tool | Next Step |
|
|
18224
|
-
|------|-----------|
|
|
18225
|
-
| \`setup\` | \`plan\` |
|
|
18226
|
-
| \`plan\` | \`build_list\` |
|
|
18227
|
-
| \`build_list\` | \`build_execute <task_id>\` |
|
|
18228
|
-
| \`build_execute\` (start) | Implement, then \`build_execute\` (complete) |
|
|
18229
|
-
| \`build_execute\` (complete) | Post-build audit (automatic) |
|
|
18230
|
-
| Audit (findings) | \`review_submit\` with \`request-changes\` |
|
|
18231
|
-
| Audit (clean) | \`review_list\` |
|
|
18232
|
-
| \`review_list\` | \`review_submit\` |
|
|
18233
|
-
| \`review_submit\` (approve/accept) | \`build_list\` |
|
|
18234
|
-
| \`review_submit\` (request-changes) | \`build_execute\` (redo) or \`build_list\` |
|
|
18235
|
-
| \`strategy_review\` | \`strategy_change\` (if needed) |
|
|
18236
|
-
| \`idea\` | Next \`plan\` picks it up |
|
|
18237
|
-
|
|
18238
|
-
## Post-Build Audit
|
|
18239
|
-
|
|
18240
|
-
After every \`build_execute\` (complete), audit the branch before presenting for human review. This catches bugs and convention violations early.
|
|
18241
|
-
|
|
18242
|
-
1. **Identify changed files:** Run \`git diff origin/main --name-only\` to find modified files. If no changes, report "No changes to audit" and skip.
|
|
18243
|
-
2. **Review each changed file** for:
|
|
18244
|
-
- Logic errors, off-by-one mistakes, incorrect conditions
|
|
18245
|
-
- Unhandled edge cases (null, undefined, empty inputs)
|
|
18246
|
-
- Convention violations defined in this CLAUDE.md
|
|
18247
|
-
- Incorrect type narrowing or unsafe casts
|
|
18248
|
-
3. **Documentation check:** If any \`docs/\` files describe behaviour that the change modified, flag as "Doc drift".
|
|
18249
|
-
4. **Report:** For each issue: file path, severity (Bug/Convention/Doc drift), what's wrong, how to fix.
|
|
18250
|
-
5. **If findings exist:** Run \`review_submit\` with \`request-changes\` and the findings. Fix before human review.
|
|
18251
|
-
6. **If clean:** Present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
|
|
18252
|
-
|
|
18253
|
-
## When to Start a New Conversation
|
|
18254
|
-
|
|
18255
|
-
Start a fresh window when:
|
|
18256
|
-
- **After a release** \u2014 cycle is done, context is heavy. New window orients in seconds via \`orient\`.
|
|
18257
|
-
- **After 3+ tasks built** \u2014 accumulated file reads, diffs, and discussions bloat context. Quality degrades.
|
|
18258
|
-
- **Switching modes** \u2014 going from building to planning, or from strategy review to building. Each mode benefits from clean context.
|
|
18259
|
-
- **After context compression fires** \u2014 if you notice earlier messages are missing, the window is getting stale. Open fresh.
|
|
18260
|
-
|
|
18261
|
-
Stay in the same window when:
|
|
18262
|
-
- Building sequential tasks in a batch (especially XS/S tasks)
|
|
18263
|
-
- Mid-task and not yet complete
|
|
18264
|
-
- Having a strategic discussion that informs the next action
|
|
18265
|
-
|
|
18266
|
-
**Rule of thumb:** If you've been in the same window for 30+ minutes or 3+ tasks, it's time for a fresh one.
|
|
18267
|
-
|
|
18268
|
-
## Housekeeping \u2014 Opt-In Deep Sweep
|
|
18269
|
-
|
|
18270
|
-
\`orient\` runs a fast cheap-checks-only path by default. The deep sweep \u2014 orphaned branches, In Review tasks with no PR, stale In Progress branches, unrecorded commits, unregistered docs \u2014 is opt-in via \`deep_housekeeping: true\`.
|
|
18271
|
-
|
|
18272
|
-
When to run with \`deep_housekeeping: true\`:
|
|
18273
|
-
1. Before \`release\` \u2014 catch board/branch drift.
|
|
18274
|
-
2. After a long break (>1 day since last session) \u2014 surface anything that fell off.
|
|
18275
|
-
3. When you suspect drift \u2014 odd cycle counts, missing PRs.
|
|
18276
|
-
|
|
18277
|
-
**Don't run deep on every session start.** It pollutes early context with cross-reference output that's noise 80% of the time. The default fast path tells you what cycle you're on, what's in flight, and what to do next; that's the daily-driver shape.
|
|
18278
|
-
|
|
18279
|
-
If the deep sweep surfaces something fixable (orphaned branches, missing PRs), fix it silently and report after \u2014 same autonomous-plumbing rule as before.
|
|
18280
|
-
|
|
18281
|
-
## Plumbing Is Autonomous
|
|
18282
|
-
|
|
18283
|
-
Board status updates, branch cleanup, orphaned task fixes, commit/PR/merge for housekeeping \u2014 these are mechanical plumbing. **Do them end-to-end without stopping to ask.** Report after the fact.
|
|
18284
|
-
|
|
18285
|
-
## Context Compression Recovery
|
|
18286
|
-
|
|
18287
|
-
When the system compresses prior messages, immediately:
|
|
18288
|
-
1. **Run \`orient\`** \u2014 single call for cycle state
|
|
18289
|
-
2. Check your todo list for in-progress work
|
|
18290
|
-
3. Run housekeeping checks
|
|
18291
|
-
4. **NEVER re-build a task that is already In Review or Done.**
|
|
18292
|
-
5. Continue where you left off \u2014 don't restart or re-plan
|
|
18293
|
-
|
|
18294
|
-
## Branching & PR Convention
|
|
18295
|
-
|
|
18296
|
-
- **All in-cycle, in-module tasks share \`feat/cycle-N-<module>\`** regardless of complexity. One branch per module per cycle, merged together. Module-less tasks fall back to a per-task branch.
|
|
18297
|
-
- **Dependent tasks (any size):** When a task's BUILD HANDOFF lists a \`DEPENDS ON\` task from the same cycle, \`build_execute\` automatically reuses the upstream task's branch so commits stack for a single PR. Do not create a separate branch manually.
|
|
18298
|
-
- **Commit per task within grouped branches** \u2014 traceable git history.
|
|
18299
|
-
- **Never use \`build_execute\` with \`light=true\` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed \u2014 any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
|
|
18300
|
-
|
|
18301
|
-
## Quick Work vs PAPI Work
|
|
18302
|
-
|
|
18303
|
-
PAPI is for planned work. Quick fixes \u2014 just do them. No need for plan or build_execute.
|
|
18304
|
-
|
|
18305
|
-
**After completing quick/ad-hoc work** (bug fixes, config changes, small improvements done outside the cycle), call \`ad_hoc\` to record it. This creates a Done task + build report so the work appears in cycle history and metrics. Don't skip this \u2014 unrecorded work is invisible work.
|
|
18306
|
-
|
|
18307
|
-
## Data Integrity
|
|
18308
|
-
|
|
18309
|
-
- **Use MCP tools for all project data operations.** DB is the source of truth when using the pg adapter.
|
|
18310
|
-
- Do NOT read \`.papi/\` files for context \u2014 use MCP tools.
|
|
18311
|
-
- \`.papi/\` files may be stale when using pg adapter. This is expected.
|
|
18312
|
-
- **\`board_edit\` handles cycle membership automatically.** Pass \`cycle: <n>\` to assign, \`cycle: null\` to clear (also flips status to Backlog), or \`status: "In Cycle"\` to auto-assign the active cycle. No manual SQL needed.
|
|
18313
|
-
|
|
18314
|
-
## Code Before Claims \u2014 No Assumptions
|
|
18315
|
-
|
|
18316
|
-
**Before making any claim about how the codebase works, read the relevant file first.**
|
|
18317
|
-
|
|
18318
|
-
This includes:
|
|
18319
|
-
- How a feature is implemented ("it works like X") \u2192 read the source
|
|
18320
|
-
- Whether something exists ("there's no baseline migration") \u2192 check the directory
|
|
18321
|
-
- Whether a flow is broken or working \u2192 trace it in code
|
|
18322
|
-
- What a user would experience \u2192 check the actual page/component
|
|
18323
|
-
|
|
18324
|
-
Do NOT rely on memory, prior conversation, or inference. Read first, then answer.
|
|
18325
|
-
If the answer requires checking 2-3 files, check them all before responding.
|
|
18326
|
-
|
|
18327
|
-
## Tool Use Discipline
|
|
18328
|
-
|
|
18329
|
-
Most tool errors are habit issues, not capability issues. Avoid them up front:
|
|
18330
|
-
|
|
18331
|
-
- **Ranged reads for large files.** Before \`Read\`-ing a file you haven't already touched this session, check its size. For files over ~1000 lines, or known-large surfaces (generated SQL, lockfiles, HTML reports, large templates), use \`offset\` + \`limit\` from the start instead of hitting the token ceiling.
|
|
18332
|
-
- **Search-before-read for unverified paths.** If a path comes from memory or inference rather than a file you've read this session, run \`Glob\` or list the parent directory first. Don't \`Read\` paths you haven't confirmed exist.
|
|
18333
|
-
- **Prefer Read/Glob/Grep over Bash for file operations.** Bash \`cat\`/\`grep\`/\`find\`/\`ls\` is the most common source of failed commands and produces unstructured output. Reserve Bash for genuinely shell-only operations (git, gh, package managers, SQL, real pipelines).
|
|
18334
|
-
- **Verify-before-recommend.** Before suggesting a new task, hook, or skill, check whether it already exists: \`board_view\` for tasks, \`doc_search\` for docs, \`ls .claude/hooks/\` for hooks. Recommending duplicates of already-shipped work wastes a build slot.
|
|
18335
|
-
|
|
18336
|
-
## User-Facing Replies \u2014 Default Brief
|
|
18337
|
-
|
|
18338
|
-
When drafting any external user-facing copy (Discord post, support reply, email, release note, marketing blurb): default to \u22644 sentences, friendly tone, no hidden questions buried in prose. Offer to expand if the user wants more depth. Verbose drafts that need trimming are a recurring friction.
|
|
18339
|
-
|
|
18340
|
-
This applies to *output for users*, not internal commit messages, build reports, or technical explanations \u2014 those follow normal conventions.
|
|
18341
|
-
|
|
18342
|
-
## Process Rules
|
|
18343
|
-
|
|
18344
|
-
These rules come from 80+ cycles of dogfooding. They prevent the most common sources of wasted time and rework.
|
|
18345
|
-
|
|
18346
|
-
### Building
|
|
18347
|
-
- **Verify before claiming done.** Hit the endpoint, check the rendered output, confirm the data round-trips. Never say "should work" \u2014 prove it works.
|
|
18348
|
-
- **Preview frontend changes.** After any UI/styling build, provide the localhost URL so the user can visually review. Don't make them ask for it.
|
|
18349
|
-
- **Debug one change at a time.** When fixing issues, make one change, verify it, then move on. Don't stack multiple untested fixes.
|
|
18350
|
-
- **Test the write-read roundtrip.** Every data write path must have a verified read path. If you write to DB, confirm the read query returns what was written. This is the #1 source of silent failures.
|
|
18351
|
-
- **Test after every build.** Run the project's test suite after implementing. Suggest follow-up tasks from learnings when meaningful.
|
|
18352
|
-
- **Build patiently.** Validate each phase against the last. Don't rush through implementation \u2014 test through the UI, not just the API.
|
|
18353
|
-
|
|
18354
|
-
### Security
|
|
18355
|
-
- **Audit before widening access.** Before any build that adds endpoints, modifies auth/RLS, introduces new user types, or changes access controls \u2014 review the security implications first. Fix findings before shipping.
|
|
18356
|
-
- **Flag access-widening changes.** If a build touches auth, RLS policies, API keys, or user-facing access, note "Security surface reviewed" in the build report's \`discovered_issues\` or \`architecture_notes\`.
|
|
18357
|
-
- **Never ship secrets.** Do not commit .env files, API keys, or credentials. Check \`.gitignore\` covers sensitive files before pushing.
|
|
18358
|
-
- **Telemetry opt-out.** PAPI collects anonymous usage data (tool name, duration, project ID). To disable, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
|
|
18294
|
+
var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
|
|
18295
|
+
var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
|
|
18296
|
+
var CLAUDE_MD_STUB = `# {{project_name}}
|
|
18359
18297
|
|
|
18360
|
-
|
|
18361
|
-
- **NEVER run \`plan\` more than once per cycle.** Adjust the cycle with \`board_deprioritise\` or \`idea\` instead.
|
|
18362
|
-
- **NEVER skip cycles.** Complete and release the current cycle before running the next \`plan\`.
|
|
18363
|
-
- **Large plan/handoff outputs:** If the prepare-phase output is too large to pass inline (>50 KB), write it to a file and pass the absolute path via \`llm_response_file\` instead of \`llm_response\`. The \`plan\`, \`strategy_review\`, and \`handoff_generate\` apply modes all accept \`llm_response_file\`. The two parameters are mutually exclusive.
|
|
18364
|
-
- **Only build tasks assigned to the current cycle.** Use \`build_list\` \u2014 it filters to current-cycle tasks with handoffs.
|
|
18365
|
-
- **Don't ask premature questions.** If the project is in early cycles, don't ask about deployment accounts, hosting providers, OAuth setup, or commercial features. Focus on building core functionality first.
|
|
18366
|
-
- **Split large ideas.** If an idea has 3+ concerns, submit it as 2-3 separate ideas so the planner creates properly scoped tasks \u2014 not kitchen-sink handoffs.
|
|
18367
|
-
- **Auto-release completed cycles.** When all cycle tasks are Done and reviews accepted, run \`release\` immediately. Forgetting causes cycle number drift and merge conflicts in the next session.
|
|
18368
|
-
- **Verify cycle readiness before releasing.** Before calling \`release\`, run \`board_view\` to confirm every task in the current cycle has status Done or Cancelled. The review queue is NOT sufficient evidence \u2014 \`review_list\` only shows built-and-pending-review tasks; it does not show Backlog or In Progress tasks. If any task is Backlog or In Progress: (a) build it, (b) move it to the next cycle via \`board_edit({ task_id, cycle: N+1 })\`, or (c) cancel it via \`board_edit\`. Do not call \`release\` until the cycle has no pending work. The \`release\` tool enforces this server-side and will block with a task list if the check fails.
|
|
18298
|
+
This project is managed with **PAPI**. The agent harness \u2014 session workflow, the plan \u2192 build \u2192 review cycle, branching, and conventions \u2014 lives in **\`AGENTS.md\`** (always loaded) plus lazy-loaded phase skills under \`.agents/skills/papi-cycle/\`.
|
|
18369
18299
|
|
|
18370
|
-
|
|
18371
|
-
- **Show task names, not just IDs.** When summarising board state or reconciliation, include task names \u2014 e.g. "task-42: Add supplier form" not just "task-42".
|
|
18372
|
-
- **Surface the next command.** After each step, tell the user what comes next. Commands should be surfaced, not memorised.
|
|
18300
|
+
**At session start: read \`AGENTS.md\`, then run \`orient\`.** Phase mechanics (planning, building, strategy, ideas) load on demand from the skills bundle. Add project-specific notes below this line \u2014 they will not be overwritten.
|
|
18373
18301
|
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
- **Pattern:** Audit access surface \u2192 fix vulnerabilities \u2192 then widen access. Never ship access-widening without a security phase.
|
|
18302
|
+
${CLAUDE_MD_ENRICHMENT_SENTINEL_T1}
|
|
18303
|
+
${CLAUDE_MD_ENRICHMENT_SENTINEL_T2}
|
|
18377
18304
|
`;
|
|
18378
|
-
var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
|
|
18379
|
-
var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
|
|
18380
18305
|
var CLAUDE_MD_TIER_1 = `
|
|
18381
18306
|
${CLAUDE_MD_ENRICHMENT_SENTINEL_T1}
|
|
18382
18307
|
|
|
@@ -18605,7 +18530,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
18605
18530
|
await mkdir(config2.papiDir, { recursive: true });
|
|
18606
18531
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
18607
18532
|
const content = substitute(template, vars);
|
|
18608
|
-
await writeFile2(
|
|
18533
|
+
await writeFile2(join5(config2.papiDir, filename), content, "utf-8");
|
|
18609
18534
|
}
|
|
18610
18535
|
}
|
|
18611
18536
|
} else {
|
|
@@ -18620,18 +18545,18 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
18620
18545
|
} catch {
|
|
18621
18546
|
}
|
|
18622
18547
|
}
|
|
18623
|
-
const commandsDir =
|
|
18624
|
-
const docsDir =
|
|
18548
|
+
const commandsDir = join5(config2.projectRoot, ".claude", "commands");
|
|
18549
|
+
const docsDir = join5(config2.projectRoot, "docs");
|
|
18625
18550
|
await mkdir(commandsDir, { recursive: true });
|
|
18626
18551
|
await mkdir(docsDir, { recursive: true });
|
|
18627
|
-
const claudeMdPath =
|
|
18552
|
+
const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
|
|
18628
18553
|
let claudeMdExists = false;
|
|
18629
18554
|
try {
|
|
18630
18555
|
await access2(claudeMdPath);
|
|
18631
18556
|
claudeMdExists = true;
|
|
18632
18557
|
} catch {
|
|
18633
18558
|
}
|
|
18634
|
-
const docsIndexPath =
|
|
18559
|
+
const docsIndexPath = join5(docsDir, "INDEX.md");
|
|
18635
18560
|
let docsIndexExists = false;
|
|
18636
18561
|
try {
|
|
18637
18562
|
await access2(docsIndexPath);
|
|
@@ -18639,26 +18564,30 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
18639
18564
|
} catch {
|
|
18640
18565
|
}
|
|
18641
18566
|
const scaffoldFiles = {
|
|
18642
|
-
[
|
|
18643
|
-
[
|
|
18644
|
-
[
|
|
18567
|
+
[join5(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
|
|
18568
|
+
[join5(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
|
|
18569
|
+
[join5(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
|
|
18645
18570
|
};
|
|
18646
18571
|
if (!docsIndexExists) {
|
|
18647
18572
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
18648
18573
|
}
|
|
18649
18574
|
if (!claudeMdExists) {
|
|
18650
|
-
scaffoldFiles[claudeMdPath] = substitute(
|
|
18575
|
+
scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_STUB, vars);
|
|
18651
18576
|
} else {
|
|
18652
18577
|
try {
|
|
18653
18578
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
18654
|
-
if (!existing.includes("
|
|
18655
|
-
const
|
|
18656
|
-
scaffoldFiles[claudeMdPath] =
|
|
18579
|
+
if (!existing.includes("AGENTS.md")) {
|
|
18580
|
+
const pointer = "> This project uses PAPI. The agent harness lives in `AGENTS.md` + `.agents/skills/papi-cycle/`. Read `AGENTS.md` at session start, then run `orient`.\n\n";
|
|
18581
|
+
scaffoldFiles[claudeMdPath] = pointer + existing;
|
|
18657
18582
|
}
|
|
18658
18583
|
} catch {
|
|
18659
18584
|
}
|
|
18660
18585
|
}
|
|
18661
|
-
const
|
|
18586
|
+
for (const [dest, content] of Object.entries(planBundleInstall(config2.projectRoot, input.projectName, { skipExisting: true }))) {
|
|
18587
|
+
await mkdir(dirname2(dest), { recursive: true });
|
|
18588
|
+
scaffoldFiles[dest] = content;
|
|
18589
|
+
}
|
|
18590
|
+
const cursorDir = join5(config2.projectRoot, ".cursor");
|
|
18662
18591
|
let cursorDetected = false;
|
|
18663
18592
|
try {
|
|
18664
18593
|
await access2(cursorDir);
|
|
@@ -18666,8 +18595,8 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
18666
18595
|
} catch {
|
|
18667
18596
|
}
|
|
18668
18597
|
if (cursorDetected) {
|
|
18669
|
-
const cursorRulesDir =
|
|
18670
|
-
const cursorRulesPath =
|
|
18598
|
+
const cursorRulesDir = join5(cursorDir, "rules");
|
|
18599
|
+
const cursorRulesPath = join5(cursorRulesDir, "papi.mdc");
|
|
18671
18600
|
await mkdir(cursorRulesDir, { recursive: true });
|
|
18672
18601
|
try {
|
|
18673
18602
|
await access2(cursorRulesPath);
|
|
@@ -18693,7 +18622,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
18693
18622
|
}
|
|
18694
18623
|
var PAPI_PERMISSION = "mcp__papi__*";
|
|
18695
18624
|
async function ensurePapiPermission(projectRoot) {
|
|
18696
|
-
const settingsPath =
|
|
18625
|
+
const settingsPath = join5(projectRoot, ".claude", "settings.json");
|
|
18697
18626
|
try {
|
|
18698
18627
|
let settings = {};
|
|
18699
18628
|
try {
|
|
@@ -18712,7 +18641,7 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
18712
18641
|
if (!allow.includes(PAPI_PERMISSION)) {
|
|
18713
18642
|
allow.push(PAPI_PERMISSION);
|
|
18714
18643
|
}
|
|
18715
|
-
await mkdir(
|
|
18644
|
+
await mkdir(join5(projectRoot, ".claude"), { recursive: true });
|
|
18716
18645
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
18717
18646
|
} catch {
|
|
18718
18647
|
}
|
|
@@ -18720,7 +18649,7 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
18720
18649
|
async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
|
|
18721
18650
|
const warnings = [];
|
|
18722
18651
|
if (config2.adapterType !== "pg") {
|
|
18723
|
-
await writeFile2(
|
|
18652
|
+
await writeFile2(join5(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
|
|
18724
18653
|
}
|
|
18725
18654
|
await adapter2.updateProductBrief(briefText);
|
|
18726
18655
|
const briefPhases = parsePhases(briefText);
|
|
@@ -18785,7 +18714,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
|
|
|
18785
18714
|
}
|
|
18786
18715
|
if (conventionsText?.trim()) {
|
|
18787
18716
|
try {
|
|
18788
|
-
const claudeMdPath =
|
|
18717
|
+
const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
|
|
18789
18718
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
18790
18719
|
await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
|
|
18791
18720
|
} catch {
|
|
@@ -18865,13 +18794,13 @@ async function scanCodebase(projectRoot) {
|
|
|
18865
18794
|
}
|
|
18866
18795
|
let packageJson;
|
|
18867
18796
|
try {
|
|
18868
|
-
const content = await readFile4(
|
|
18797
|
+
const content = await readFile4(join5(projectRoot, "package.json"), "utf-8");
|
|
18869
18798
|
packageJson = JSON.parse(content);
|
|
18870
18799
|
} catch {
|
|
18871
18800
|
}
|
|
18872
18801
|
let readme;
|
|
18873
18802
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
18874
|
-
const content = await safeReadFile(
|
|
18803
|
+
const content = await safeReadFile(join5(projectRoot, name), 5e3);
|
|
18875
18804
|
if (content) {
|
|
18876
18805
|
readme = content;
|
|
18877
18806
|
break;
|
|
@@ -18881,7 +18810,7 @@ async function scanCodebase(projectRoot) {
|
|
|
18881
18810
|
let totalFiles = topLevelFiles.length;
|
|
18882
18811
|
for (const dir of topLevelDirs) {
|
|
18883
18812
|
try {
|
|
18884
|
-
const entries = await readdir(
|
|
18813
|
+
const entries = await readdir(join5(projectRoot, dir), { withFileTypes: true });
|
|
18885
18814
|
const files = entries.filter((e) => e.isFile());
|
|
18886
18815
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
18887
18816
|
totalFiles += files.length;
|
|
@@ -19145,7 +19074,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19145
19074
|
}
|
|
19146
19075
|
}
|
|
19147
19076
|
try {
|
|
19148
|
-
const claudeMdPath =
|
|
19077
|
+
const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
|
|
19149
19078
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
19150
19079
|
if (!existing.includes("Dogfood Logging")) {
|
|
19151
19080
|
const dogfoodSection = [
|
|
@@ -19190,7 +19119,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19190
19119
|
const gitignoreNote = await ensureMcpJsonGitignored(config2.projectRoot);
|
|
19191
19120
|
let cursorScaffolded = false;
|
|
19192
19121
|
try {
|
|
19193
|
-
await access2(
|
|
19122
|
+
await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
19194
19123
|
cursorScaffolded = true;
|
|
19195
19124
|
} catch {
|
|
19196
19125
|
}
|
|
@@ -19208,11 +19137,11 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19208
19137
|
}
|
|
19209
19138
|
async function ensureMcpJsonGitignored(projectRoot) {
|
|
19210
19139
|
try {
|
|
19211
|
-
await access2(
|
|
19140
|
+
await access2(join5(projectRoot, ".git"));
|
|
19212
19141
|
} catch {
|
|
19213
19142
|
return void 0;
|
|
19214
19143
|
}
|
|
19215
|
-
const gitignorePath =
|
|
19144
|
+
const gitignorePath = join5(projectRoot, ".gitignore");
|
|
19216
19145
|
let existing = "";
|
|
19217
19146
|
try {
|
|
19218
19147
|
existing = await readFile4(gitignorePath, "utf-8");
|
|
@@ -19551,8 +19480,8 @@ init_dist2();
|
|
|
19551
19480
|
init_git();
|
|
19552
19481
|
init_git();
|
|
19553
19482
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
19554
|
-
import { readdirSync as
|
|
19555
|
-
import { join as
|
|
19483
|
+
import { readdirSync as readdirSync4, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
19484
|
+
import { join as join6 } from "path";
|
|
19556
19485
|
var buildStartTimes = /* @__PURE__ */ new Map();
|
|
19557
19486
|
var taskBranchMap = /* @__PURE__ */ new Map();
|
|
19558
19487
|
var taskStartShaMap = /* @__PURE__ */ new Map();
|
|
@@ -19953,17 +19882,17 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
19953
19882
|
return { task, branchLines, phaseChanges };
|
|
19954
19883
|
}
|
|
19955
19884
|
function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched) {
|
|
19956
|
-
const papiDir =
|
|
19957
|
-
if (!
|
|
19885
|
+
const papiDir = join6(projectRoot, ".papi");
|
|
19886
|
+
if (!existsSync4(papiDir)) {
|
|
19958
19887
|
mkdirSync(papiDir, { recursive: true });
|
|
19959
19888
|
}
|
|
19960
|
-
const scopePath =
|
|
19889
|
+
const scopePath = join6(papiDir, "active-task-scope.txt");
|
|
19961
19890
|
const lines = [taskId, ...filesLikelyTouched ?? []];
|
|
19962
19891
|
writeFileSync(scopePath, lines.join("\n") + "\n", "utf-8");
|
|
19963
19892
|
}
|
|
19964
19893
|
function clearActiveTaskScope(projectRoot) {
|
|
19965
|
-
const scopePath =
|
|
19966
|
-
if (
|
|
19894
|
+
const scopePath = join6(projectRoot, ".papi", "active-task-scope.txt");
|
|
19895
|
+
if (existsSync4(scopePath)) {
|
|
19967
19896
|
unlinkSync(scopePath);
|
|
19968
19897
|
}
|
|
19969
19898
|
}
|
|
@@ -19977,7 +19906,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
19977
19906
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
19978
19907
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
19979
19908
|
try {
|
|
19980
|
-
const content =
|
|
19909
|
+
const content = readFileSync2(absolutePath, "utf-8").slice(0, 2e3);
|
|
19981
19910
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19982
19911
|
if (fmMatch) {
|
|
19983
19912
|
const fm = fmMatch[1];
|
|
@@ -20283,14 +20212,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
20283
20212
|
let docWarning;
|
|
20284
20213
|
try {
|
|
20285
20214
|
if (adapter2.searchDocs) {
|
|
20286
|
-
const docsDir =
|
|
20287
|
-
if (
|
|
20215
|
+
const docsDir = join6(config2.projectRoot, "docs");
|
|
20216
|
+
if (existsSync4(docsDir)) {
|
|
20288
20217
|
const scanDir = (dir, depth = 0) => {
|
|
20289
20218
|
if (depth > 8) return [];
|
|
20290
|
-
const entries =
|
|
20219
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20291
20220
|
const files = [];
|
|
20292
20221
|
for (const e of entries) {
|
|
20293
|
-
const full =
|
|
20222
|
+
const full = join6(dir, e.name);
|
|
20294
20223
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
20295
20224
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
20296
20225
|
}
|
|
@@ -20305,7 +20234,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
20305
20234
|
const failed = [];
|
|
20306
20235
|
for (const docPath of unregistered) {
|
|
20307
20236
|
try {
|
|
20308
|
-
const meta = extractDocMeta(
|
|
20237
|
+
const meta = extractDocMeta(join6(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
20309
20238
|
await adapter2.registerDoc({
|
|
20310
20239
|
title: meta.title,
|
|
20311
20240
|
type: meta.type,
|
|
@@ -21938,12 +21867,12 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
21938
21867
|
init_git();
|
|
21939
21868
|
|
|
21940
21869
|
// src/services/reconcile.ts
|
|
21941
|
-
import { readFileSync as
|
|
21942
|
-
import { join as
|
|
21870
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
21871
|
+
import { join as join7 } from "path";
|
|
21943
21872
|
function loadDocsIndex(projectRoot) {
|
|
21944
21873
|
try {
|
|
21945
|
-
const indexPath =
|
|
21946
|
-
const raw =
|
|
21874
|
+
const indexPath = join7(projectRoot, "docs", "INDEX.md");
|
|
21875
|
+
const raw = readFileSync3(indexPath, "utf8");
|
|
21947
21876
|
const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
|
|
21948
21877
|
if (rows.length === 0) return "";
|
|
21949
21878
|
const entries = rows.map((row) => {
|
|
@@ -22519,16 +22448,16 @@ Produce your analysis and structured output above. Present Part 1 to the user an
|
|
|
22519
22448
|
|
|
22520
22449
|
// src/services/release.ts
|
|
22521
22450
|
import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
22522
|
-
import { join as
|
|
22451
|
+
import { join as join9 } from "path";
|
|
22523
22452
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
22524
22453
|
|
|
22525
22454
|
// src/lib/install-id.ts
|
|
22526
22455
|
import { randomUUID as randomUUID13 } from "crypto";
|
|
22527
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
22456
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, chmodSync } from "fs";
|
|
22528
22457
|
import { homedir as homedir2 } from "os";
|
|
22529
|
-
import { join as
|
|
22530
|
-
var PAPI_HOME_DIR =
|
|
22531
|
-
var INSTALL_ID_FILE =
|
|
22458
|
+
import { join as join8 } from "path";
|
|
22459
|
+
var PAPI_HOME_DIR = join8(homedir2(), ".papi");
|
|
22460
|
+
var INSTALL_ID_FILE = join8(PAPI_HOME_DIR, "install-id.json");
|
|
22532
22461
|
var cachedInstallId = null;
|
|
22533
22462
|
function isValidUuid(s) {
|
|
22534
22463
|
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);
|
|
@@ -22536,7 +22465,7 @@ function isValidUuid(s) {
|
|
|
22536
22465
|
function getInstallId() {
|
|
22537
22466
|
if (cachedInstallId) return cachedInstallId;
|
|
22538
22467
|
try {
|
|
22539
|
-
const raw =
|
|
22468
|
+
const raw = readFileSync4(INSTALL_ID_FILE, "utf-8");
|
|
22540
22469
|
const parsed = JSON.parse(raw);
|
|
22541
22470
|
if (isValidUuid(parsed.install_id)) {
|
|
22542
22471
|
cachedInstallId = parsed.install_id;
|
|
@@ -22912,7 +22841,7 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
22912
22841
|
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
22913
22842
|
}
|
|
22914
22843
|
const latestTag = getLatestTag(config2.projectRoot);
|
|
22915
|
-
const changelogPath =
|
|
22844
|
+
const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
|
|
22916
22845
|
if (!latestTag) {
|
|
22917
22846
|
const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
22918
22847
|
await writeFile3(changelogPath, initialContent, "utf-8");
|
|
@@ -23092,8 +23021,8 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
23092
23021
|
}
|
|
23093
23022
|
|
|
23094
23023
|
// src/tools/review.ts
|
|
23095
|
-
import { existsSync as
|
|
23096
|
-
import { join as
|
|
23024
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
23025
|
+
import { join as join10 } from "path";
|
|
23097
23026
|
init_git();
|
|
23098
23027
|
|
|
23099
23028
|
// src/services/review.ts
|
|
@@ -23312,6 +23241,57 @@ async function buildSessionGuidance() {
|
|
|
23312
23241
|
}
|
|
23313
23242
|
|
|
23314
23243
|
// src/tools/review.ts
|
|
23244
|
+
var REVIEW_DISPATCH_THRESHOLD = 50 * 1024;
|
|
23245
|
+
var REVIEW_RUBRIC = [
|
|
23246
|
+
"You are reviewing a completed PAPI build for acceptance. Judge:",
|
|
23247
|
+
"- Correctness: does the change do what the build report claims, without obvious bugs?",
|
|
23248
|
+
"- Scope adherence: does the diff match the BUILD HANDOFF scope (no unrelated changes, no skipped acceptance criteria)?",
|
|
23249
|
+
"- Security: any auth/data/secret/injection risk introduced?",
|
|
23250
|
+
"- Quality: tests present and meaningful, no obvious debt or dead code.",
|
|
23251
|
+
"Be specific and cite file/line where you can. Recommend fail only for blocking issues."
|
|
23252
|
+
].join("\n");
|
|
23253
|
+
async function buildReviewDispatch(adapter2, config2, taskId) {
|
|
23254
|
+
const task = await adapter2.getTask(taskId);
|
|
23255
|
+
if (!task) {
|
|
23256
|
+
return { ok: false, error: `Task ${taskId} not found \u2014 cannot assemble review context.` };
|
|
23257
|
+
}
|
|
23258
|
+
const handoff = task.buildHandoff ? `### BUILD HANDOFF (scope to check against)
|
|
23259
|
+
${JSON.stringify(task.buildHandoff, null, 2)}` : "### BUILD HANDOFF\n(none recorded)";
|
|
23260
|
+
const report = task.buildReport ? `### Build Report
|
|
23261
|
+
${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
23262
|
+
const diff = getBranchDiff(config2.projectRoot);
|
|
23263
|
+
const diffBlock = diff ? `### Branch diff vs base
|
|
23264
|
+
\`\`\`diff
|
|
23265
|
+
${diff}
|
|
23266
|
+
\`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
|
|
23267
|
+
let projectContext = "";
|
|
23268
|
+
const ctxPath = join10(config2.projectRoot, ".agents", "papi-context.md");
|
|
23269
|
+
if (existsSync5(ctxPath)) {
|
|
23270
|
+
try {
|
|
23271
|
+
projectContext = `### Project context (.agents/papi-context.md)
|
|
23272
|
+
${readFileSync5(ctxPath, "utf-8")}
|
|
23273
|
+
|
|
23274
|
+
`;
|
|
23275
|
+
} catch {
|
|
23276
|
+
}
|
|
23277
|
+
}
|
|
23278
|
+
const userMessage = `## Task ${task.id}: ${task.title}
|
|
23279
|
+
|
|
23280
|
+
${projectContext}${handoff}
|
|
23281
|
+
|
|
23282
|
+
${report}
|
|
23283
|
+
|
|
23284
|
+
${diffBlock}`;
|
|
23285
|
+
const contextBytes = Buffer.byteLength(userMessage, "utf-8");
|
|
23286
|
+
const prompt2 = buildSubagentDispatchPrompt({
|
|
23287
|
+
tool: "review_submit",
|
|
23288
|
+
taskId,
|
|
23289
|
+
systemPrompt: REVIEW_RUBRIC,
|
|
23290
|
+
userMessage,
|
|
23291
|
+
contextBytes
|
|
23292
|
+
});
|
|
23293
|
+
return { ok: true, prompt: prompt2, contextBytes };
|
|
23294
|
+
}
|
|
23315
23295
|
var reviewListTool = {
|
|
23316
23296
|
name: "review_list",
|
|
23317
23297
|
description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
|
|
@@ -23356,6 +23336,11 @@ var reviewSubmitTool = {
|
|
|
23356
23336
|
type: "string",
|
|
23357
23337
|
description: 'Reviewer name (default: "human").'
|
|
23358
23338
|
},
|
|
23339
|
+
dispatch: {
|
|
23340
|
+
type: "string",
|
|
23341
|
+
enum: ["inline", "subagent"],
|
|
23342
|
+
description: `task-1864: set "subagent" (build-acceptance only) to offload code review to a fresh sub-agent. Returns a Task() invocation prompt that feeds the build report + branch diff to the sub-agent, which returns structured auto_review findings. Verdict is NOT required on this call \u2014 you call review_submit again with the human verdict + the sub-agent's auto_review. Default "inline" (record the verdict directly).`
|
|
23343
|
+
},
|
|
23359
23344
|
reviewer_confirmed: {
|
|
23360
23345
|
type: "boolean",
|
|
23361
23346
|
description: "Set to true to confirm you have reviewed the build (read the build report or the pending list via review_list) before submitting an accept verdict. Required to accept a build-acceptance review unless review_list was called in the same session within the last 15 minutes. Defense-in-depth against SUP-2026-010 (Codex prematurely accepted a task because review_list was missing from its tool surface)."
|
|
@@ -23434,8 +23419,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
23434
23419
|
};
|
|
23435
23420
|
}
|
|
23436
23421
|
const details = [];
|
|
23437
|
-
const papiDir =
|
|
23438
|
-
if (
|
|
23422
|
+
const papiDir = join10(config2.projectRoot, ".papi");
|
|
23423
|
+
if (existsSync5(papiDir)) {
|
|
23439
23424
|
try {
|
|
23440
23425
|
const commitResult = stageDirAndCommit(
|
|
23441
23426
|
config2.projectRoot,
|
|
@@ -23553,6 +23538,19 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23553
23538
|
if (!stage) {
|
|
23554
23539
|
return errorResponse('stage is required. Use "handoff-review" or "build-acceptance".');
|
|
23555
23540
|
}
|
|
23541
|
+
const explicitDispatch = args.dispatch === "subagent";
|
|
23542
|
+
const autoDispatchEligible = !verdict && args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false";
|
|
23543
|
+
if ((explicitDispatch || autoDispatchEligible) && stage === "build-acceptance" && taskId) {
|
|
23544
|
+
const dispatch = await buildReviewDispatch(adapter2, config2, taskId);
|
|
23545
|
+
if (!dispatch.ok) {
|
|
23546
|
+
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
23547
|
+
} else if (explicitDispatch || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23548
|
+
return textResponse(dispatch.prompt);
|
|
23549
|
+
}
|
|
23550
|
+
}
|
|
23551
|
+
if (explicitDispatch && stage !== "build-acceptance") {
|
|
23552
|
+
return errorResponse('dispatch:"subagent" is only supported for stage "build-acceptance".');
|
|
23553
|
+
}
|
|
23556
23554
|
if (!verdict) {
|
|
23557
23555
|
return errorResponse('verdict is required. Use "approve", "accept", "request-changes", or "reject".');
|
|
23558
23556
|
}
|
|
@@ -24567,13 +24565,13 @@ function formatUnblockSection(candidates) {
|
|
|
24567
24565
|
}
|
|
24568
24566
|
|
|
24569
24567
|
// src/lib/skill-detection.ts
|
|
24570
|
-
import { existsSync as
|
|
24571
|
-
import { join as
|
|
24568
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
24569
|
+
import { join as join11 } from "path";
|
|
24572
24570
|
function readPackageJson(projectRoot) {
|
|
24573
|
-
const path7 =
|
|
24574
|
-
if (!
|
|
24571
|
+
const path7 = join11(projectRoot, "package.json");
|
|
24572
|
+
if (!existsSync6(path7)) return null;
|
|
24575
24573
|
try {
|
|
24576
|
-
const raw =
|
|
24574
|
+
const raw = readFileSync6(path7, "utf-8");
|
|
24577
24575
|
return JSON.parse(raw);
|
|
24578
24576
|
} catch {
|
|
24579
24577
|
return null;
|
|
@@ -24590,31 +24588,31 @@ function hasDependencyMatching(deps, pattern) {
|
|
|
24590
24588
|
return false;
|
|
24591
24589
|
}
|
|
24592
24590
|
function hasGitHubWorkflows(projectRoot) {
|
|
24593
|
-
const dir =
|
|
24594
|
-
if (!
|
|
24591
|
+
const dir = join11(projectRoot, ".github", "workflows");
|
|
24592
|
+
if (!existsSync6(dir)) return false;
|
|
24595
24593
|
try {
|
|
24596
|
-
const entries =
|
|
24594
|
+
const entries = readdirSync5(dir);
|
|
24597
24595
|
return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
24598
24596
|
} catch {
|
|
24599
24597
|
return false;
|
|
24600
24598
|
}
|
|
24601
24599
|
}
|
|
24602
24600
|
function envExampleMentionsStaging(projectRoot) {
|
|
24603
|
-
const path7 =
|
|
24604
|
-
if (!
|
|
24601
|
+
const path7 = join11(projectRoot, ".env.example");
|
|
24602
|
+
if (!existsSync6(path7)) return false;
|
|
24605
24603
|
try {
|
|
24606
|
-
const raw =
|
|
24604
|
+
const raw = readFileSync6(path7, "utf-8");
|
|
24607
24605
|
return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
|
|
24608
24606
|
} catch {
|
|
24609
24607
|
return false;
|
|
24610
24608
|
}
|
|
24611
24609
|
}
|
|
24612
24610
|
function hasVercelConfig(projectRoot) {
|
|
24613
|
-
if (
|
|
24614
|
-
const vercelDir =
|
|
24615
|
-
if (!
|
|
24611
|
+
if (existsSync6(join11(projectRoot, "vercel.json"))) return true;
|
|
24612
|
+
const vercelDir = join11(projectRoot, ".vercel");
|
|
24613
|
+
if (!existsSync6(vercelDir)) return false;
|
|
24616
24614
|
try {
|
|
24617
|
-
return
|
|
24615
|
+
return statSync4(vercelDir).isDirectory();
|
|
24618
24616
|
} catch {
|
|
24619
24617
|
return false;
|
|
24620
24618
|
}
|
|
@@ -24672,9 +24670,82 @@ function formatSkillProposals(proposals) {
|
|
|
24672
24670
|
return "\n" + lines.join("\n");
|
|
24673
24671
|
}
|
|
24674
24672
|
|
|
24673
|
+
// src/tools/agent-list.ts
|
|
24674
|
+
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
24675
|
+
import { join as join12 } from "path";
|
|
24676
|
+
var NO_AGENTS_HINT = "No project sub-agents found in `.claude/agents/`. Add a `*.md` file with `name` + `description` frontmatter \u2014 see the 1926-Census marketing sub-agent for a reference implementation.";
|
|
24677
|
+
function parseAgentFrontmatter(content) {
|
|
24678
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
24679
|
+
if (!match) return null;
|
|
24680
|
+
const fm = match[1];
|
|
24681
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
24682
|
+
const descMatch = fm.match(/^description:\s*[>|]?\n?([\s\S]*?)(?=\n\w|\n---|$)/m);
|
|
24683
|
+
const description = descMatch ? descMatch[1].replace(/^\s{2}/gm, "").replace(/\n+$/, "").replace(/\n/g, " ").trim() : "";
|
|
24684
|
+
return { name: nameMatch?.[1].trim(), description };
|
|
24685
|
+
}
|
|
24686
|
+
async function listAgents(projectRoot) {
|
|
24687
|
+
const agentsDir = join12(projectRoot, ".claude", "agents");
|
|
24688
|
+
let files;
|
|
24689
|
+
try {
|
|
24690
|
+
files = await readdir2(agentsDir);
|
|
24691
|
+
} catch {
|
|
24692
|
+
return [];
|
|
24693
|
+
}
|
|
24694
|
+
const agents = [];
|
|
24695
|
+
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
24696
|
+
let content;
|
|
24697
|
+
try {
|
|
24698
|
+
content = await readFile7(join12(agentsDir, file), "utf-8");
|
|
24699
|
+
} catch {
|
|
24700
|
+
continue;
|
|
24701
|
+
}
|
|
24702
|
+
const meta = parseAgentFrontmatter(content);
|
|
24703
|
+
agents.push({
|
|
24704
|
+
name: meta?.name ?? file.replace(/\.md$/, ""),
|
|
24705
|
+
description: meta?.description ?? "",
|
|
24706
|
+
path: join12(".claude", "agents", file)
|
|
24707
|
+
});
|
|
24708
|
+
}
|
|
24709
|
+
agents.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
24710
|
+
return agents;
|
|
24711
|
+
}
|
|
24712
|
+
var agentListTool = {
|
|
24713
|
+
name: "agent_list",
|
|
24714
|
+
description: "List the sub-agents discovered in the project's `.claude/agents/*.md` files (read-only). Returns each agent's name and description so you can see which specialised sub-agents are available before dispatching one via the Task/Agent tool. Discovery only \u2014 does not invoke or manage agents.",
|
|
24715
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
24716
|
+
inputSchema: {
|
|
24717
|
+
type: "object",
|
|
24718
|
+
properties: {
|
|
24719
|
+
include_path: {
|
|
24720
|
+
type: "boolean",
|
|
24721
|
+
description: "Include each agent's project-relative file path in the output. Default false."
|
|
24722
|
+
}
|
|
24723
|
+
},
|
|
24724
|
+
required: []
|
|
24725
|
+
}
|
|
24726
|
+
};
|
|
24727
|
+
async function handleAgentList(config2, args) {
|
|
24728
|
+
const includePath = args.include_path === true;
|
|
24729
|
+
const agents = await listAgents(config2.projectRoot);
|
|
24730
|
+
if (agents.length === 0) {
|
|
24731
|
+
return textResponse(`# Sub-agents
|
|
24732
|
+
|
|
24733
|
+
${NO_AGENTS_HINT}`);
|
|
24734
|
+
}
|
|
24735
|
+
const formatted = agents.map((a) => {
|
|
24736
|
+
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
24737
|
+
const pathSuffix = includePath ? `
|
|
24738
|
+
\`${a.path}\`` : "";
|
|
24739
|
+
return `- **${a.name}**${desc}${pathSuffix}`;
|
|
24740
|
+
}).join("\n");
|
|
24741
|
+
return textResponse(`# Sub-agents (${agents.length})
|
|
24742
|
+
|
|
24743
|
+
${formatted}`);
|
|
24744
|
+
}
|
|
24745
|
+
|
|
24675
24746
|
// src/tools/doc-registry.ts
|
|
24676
|
-
import { readdirSync as
|
|
24677
|
-
import { join as
|
|
24747
|
+
import { readdirSync as readdirSync6, existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
24748
|
+
import { join as join13, relative } from "path";
|
|
24678
24749
|
import { homedir as homedir3 } from "os";
|
|
24679
24750
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
24680
24751
|
var docRegisterTool = {
|
|
@@ -24819,12 +24890,12 @@ ${d.summary}
|
|
|
24819
24890
|
${lines.join("\n---\n\n")}`);
|
|
24820
24891
|
}
|
|
24821
24892
|
function scanMdFiles(dir, rootDir) {
|
|
24822
|
-
if (!
|
|
24893
|
+
if (!existsSync7(dir)) return [];
|
|
24823
24894
|
const files = [];
|
|
24824
24895
|
try {
|
|
24825
|
-
const entries =
|
|
24896
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
24826
24897
|
for (const entry of entries) {
|
|
24827
|
-
const full =
|
|
24898
|
+
const full = join13(dir, entry.name);
|
|
24828
24899
|
if (entry.isDirectory()) {
|
|
24829
24900
|
files.push(...scanMdFiles(full, rootDir));
|
|
24830
24901
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -24837,7 +24908,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
24837
24908
|
}
|
|
24838
24909
|
function extractTitle(filePath) {
|
|
24839
24910
|
try {
|
|
24840
|
-
const content =
|
|
24911
|
+
const content = readFileSync7(filePath, "utf-8").slice(0, 1e3);
|
|
24841
24912
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
24842
24913
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
24843
24914
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -24853,17 +24924,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
24853
24924
|
const includePlans = args.include_plans ?? false;
|
|
24854
24925
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
24855
24926
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
24856
|
-
const docsDir =
|
|
24927
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
24857
24928
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
24858
24929
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
24859
24930
|
let unregisteredPlans = [];
|
|
24860
24931
|
if (includePlans) {
|
|
24861
|
-
const plansDir =
|
|
24862
|
-
if (
|
|
24932
|
+
const plansDir = join13(homedir3(), ".claude", "plans");
|
|
24933
|
+
if (existsSync7(plansDir)) {
|
|
24863
24934
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
24864
24935
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
24865
24936
|
path: f,
|
|
24866
|
-
title: extractTitle(
|
|
24937
|
+
title: extractTitle(join13(plansDir, f.replace("plans/", "")))
|
|
24867
24938
|
}));
|
|
24868
24939
|
}
|
|
24869
24940
|
}
|
|
@@ -24874,7 +24945,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
24874
24945
|
if (unregisteredDocs.length > 0) {
|
|
24875
24946
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
24876
24947
|
for (const f of unregisteredDocs) {
|
|
24877
|
-
const title = extractTitle(
|
|
24948
|
+
const title = extractTitle(join13(config2.projectRoot, f));
|
|
24878
24949
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
24879
24950
|
}
|
|
24880
24951
|
}
|
|
@@ -25038,8 +25109,8 @@ function dedupeEnrichmentBlob(existing, blob) {
|
|
|
25038
25109
|
// src/tools/orient.ts
|
|
25039
25110
|
import { execFile } from "child_process";
|
|
25040
25111
|
import { promisify } from "util";
|
|
25041
|
-
import { readFileSync as
|
|
25042
|
-
import { join as
|
|
25112
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, existsSync as existsSync8 } from "fs";
|
|
25113
|
+
import { join as join14 } from "path";
|
|
25043
25114
|
var execFileAsync = promisify(execFile);
|
|
25044
25115
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
25045
25116
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -25102,7 +25173,7 @@ var orientTool = {
|
|
|
25102
25173
|
required: []
|
|
25103
25174
|
}
|
|
25104
25175
|
};
|
|
25105
|
-
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown") {
|
|
25176
|
+
function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoot, environment = "unknown", subAgents = []) {
|
|
25106
25177
|
const lines = [];
|
|
25107
25178
|
const cycleIsComplete = health.latestCycleStatus === "complete";
|
|
25108
25179
|
const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
|
|
@@ -25159,6 +25230,16 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
25159
25230
|
lines.push("## Board");
|
|
25160
25231
|
lines.push(health.boardSummary);
|
|
25161
25232
|
lines.push("");
|
|
25233
|
+
if (subAgents.length > 0) {
|
|
25234
|
+
lines.push(`**Sub-agents (${subAgents.length}):** ` + subAgents.map((a) => {
|
|
25235
|
+
const desc = a.description ? ` (${a.description.slice(0, 60)})` : "";
|
|
25236
|
+
return `\`${a.name}\`${desc}`;
|
|
25237
|
+
}).join(", "));
|
|
25238
|
+
lines.push("");
|
|
25239
|
+
} else {
|
|
25240
|
+
lines.push(`_${NO_AGENTS_HINT}_`);
|
|
25241
|
+
lines.push("");
|
|
25242
|
+
}
|
|
25162
25243
|
if (buildInfo.cycleTasks.total > 0) {
|
|
25163
25244
|
const parts = [];
|
|
25164
25245
|
if (buildInfo.cycleTasks.inProgress > 0) parts.push(`${buildInfo.cycleTasks.inProgress} in progress`);
|
|
@@ -25281,8 +25362,8 @@ async function getLatestGitTag(projectRoot) {
|
|
|
25281
25362
|
}
|
|
25282
25363
|
async function checkNpmVersionDrift() {
|
|
25283
25364
|
try {
|
|
25284
|
-
const pkgPath =
|
|
25285
|
-
const pkg = JSON.parse(
|
|
25365
|
+
const pkgPath = join14(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
25366
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
25286
25367
|
const localVersion = pkg.version;
|
|
25287
25368
|
const packageName = pkg.name;
|
|
25288
25369
|
const { stdout } = await execFileAsync("npm", ["view", packageName, "version"], {
|
|
@@ -25570,8 +25651,9 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
25570
25651
|
return "\n\n> **Early-cycle tip:** No delivery-shape AD found. Consider whether this project is a *service* (manual delivery, human-in-the-loop) or a *platform* (self-serve). The distinction shapes task priorities. Log it with `idea` to mint an AD.";
|
|
25571
25652
|
}),
|
|
25572
25653
|
// task-1652: deep housekeeping — opt-in sweep (board, unrecorded commits, unregistered docs)
|
|
25654
|
+
// task-1865: also detects stale skill forks vs the @papi-ai/skills registry.
|
|
25573
25655
|
tracked("deep-housekeeping", async () => {
|
|
25574
|
-
if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "" };
|
|
25656
|
+
if (!deepHousekeeping) return { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
25575
25657
|
let reconciliationNote2 = "";
|
|
25576
25658
|
try {
|
|
25577
25659
|
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
@@ -25607,7 +25689,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
25607
25689
|
let unregisteredDocsNote2 = "";
|
|
25608
25690
|
try {
|
|
25609
25691
|
if (adapter2.searchDocs) {
|
|
25610
|
-
const docsDir =
|
|
25692
|
+
const docsDir = join14(config2.projectRoot, "docs");
|
|
25611
25693
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
25612
25694
|
if (docsFiles.length > 0) {
|
|
25613
25695
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -25619,7 +25701,22 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
25619
25701
|
}
|
|
25620
25702
|
} catch {
|
|
25621
25703
|
}
|
|
25622
|
-
|
|
25704
|
+
let staleSkillsNote2 = "";
|
|
25705
|
+
try {
|
|
25706
|
+
const { detectStaleForks, loadManifest } = await import("@papi-ai/skills/manifest");
|
|
25707
|
+
const stale = detectStaleForks(config2.projectRoot);
|
|
25708
|
+
if (stale.length > 0) {
|
|
25709
|
+
const version = loadManifest().packageVersion;
|
|
25710
|
+
const lines = [`
|
|
25711
|
+
|
|
25712
|
+
## Stale Skill Forks (${stale.length})`];
|
|
25713
|
+
lines.push(`These skills differ from the pinned \`@papi-ai/skills@${version}\` registry. Re-run \`npx @papi-ai/skills install .\` to refresh, or move a skill to \`.claude/skills.local/\` to keep your fork. Replacement is offered, never forced.`);
|
|
25714
|
+
for (const f of stale) lines.push(`- \u26A0\uFE0F **${f.name}** \u2014 local copy diverged from registry`);
|
|
25715
|
+
staleSkillsNote2 = lines.join("\n");
|
|
25716
|
+
}
|
|
25717
|
+
} catch {
|
|
25718
|
+
}
|
|
25719
|
+
return { reconciliationNote: reconciliationNote2, unrecordedNote: unrecordedNote2, unregisteredDocsNote: unregisteredDocsNote2, staleSkillsNote: staleSkillsNote2 };
|
|
25623
25720
|
})
|
|
25624
25721
|
]);
|
|
25625
25722
|
const proxyWarning = proxyVersionOutcome.status === "fulfilled" ? proxyVersionOutcome.value : void 0;
|
|
@@ -25640,7 +25737,7 @@ ${versionDrift}` : "";
|
|
|
25640
25737
|
const projectBannerNote = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : "";
|
|
25641
25738
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
25642
25739
|
const deliveryShapeNote = deliveryShapeOutcome.status === "fulfilled" ? deliveryShapeOutcome.value : "";
|
|
25643
|
-
const { reconciliationNote, unrecordedNote, unregisteredDocsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "" };
|
|
25740
|
+
const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
25644
25741
|
let enrichmentNote = "";
|
|
25645
25742
|
try {
|
|
25646
25743
|
enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
|
|
@@ -25679,8 +25776,9 @@ ${section}`;
|
|
|
25679
25776
|
} catch {
|
|
25680
25777
|
}
|
|
25681
25778
|
tracker.mark("format-summary");
|
|
25682
|
-
const
|
|
25683
|
-
|
|
25779
|
+
const subAgents = await listAgents(config2.projectRoot);
|
|
25780
|
+
const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks.*";
|
|
25781
|
+
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint);
|
|
25684
25782
|
} catch (err) {
|
|
25685
25783
|
const message = err instanceof Error ? err.message : String(err);
|
|
25686
25784
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -25698,9 +25796,9 @@ ${section}`;
|
|
|
25698
25796
|
}
|
|
25699
25797
|
}
|
|
25700
25798
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
25701
|
-
const claudeMdPath =
|
|
25702
|
-
if (!
|
|
25703
|
-
const content =
|
|
25799
|
+
const claudeMdPath = join14(projectRoot, "CLAUDE.md");
|
|
25800
|
+
if (!existsSync8(claudeMdPath)) return "";
|
|
25801
|
+
const content = readFileSync8(claudeMdPath, "utf-8");
|
|
25704
25802
|
const additions = [];
|
|
25705
25803
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
25706
25804
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
|
|
@@ -26386,7 +26484,7 @@ ${result.userMessage}
|
|
|
26386
26484
|
|
|
26387
26485
|
// src/services/scope-brief.ts
|
|
26388
26486
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
26389
|
-
import { join as
|
|
26487
|
+
import { join as join15, dirname as dirname3 } from "path";
|
|
26390
26488
|
import Anthropic from "@anthropic-ai/sdk";
|
|
26391
26489
|
var SCOPE_BRIEF_SYSTEM = `You are a technical scoping tool. You receive a brief-class task (too large to build directly) and decompose it into a structured scope document.
|
|
26392
26490
|
|
|
@@ -26441,8 +26539,8 @@ async function runScopeBrief(adapter2, input) {
|
|
|
26441
26539
|
}
|
|
26442
26540
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
26443
26541
|
const relPath = `docs/scopes/${slug}.md`;
|
|
26444
|
-
const absPath =
|
|
26445
|
-
mkdirSync3(
|
|
26542
|
+
const absPath = join15(input.projectRoot, relPath);
|
|
26543
|
+
mkdirSync3(dirname3(absPath), { recursive: true });
|
|
26446
26544
|
writeFileSync4(absPath, addFrontmatter(docContent, task, input.cycleNumber), "utf-8");
|
|
26447
26545
|
const taskCount = countSubTasks(docContent);
|
|
26448
26546
|
const summary = buildSummary(task, taskCount);
|
|
@@ -26818,6 +26916,166 @@ Update PAPI_PROJECT_ID (local) or x-papi-project-id (remote) to \`${result.proje
|
|
|
26818
26916
|
);
|
|
26819
26917
|
}
|
|
26820
26918
|
|
|
26919
|
+
// src/services/harness-inventory.ts
|
|
26920
|
+
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
26921
|
+
import { join as join16 } from "path";
|
|
26922
|
+
import { createHash as createHash3 } from "crypto";
|
|
26923
|
+
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
26924
|
+
async function computeFingerprint(root) {
|
|
26925
|
+
const parts = [];
|
|
26926
|
+
for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
|
|
26927
|
+
const dir = join16(root, sub);
|
|
26928
|
+
try {
|
|
26929
|
+
const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
|
|
26930
|
+
for (const name of names) {
|
|
26931
|
+
let mtime = "";
|
|
26932
|
+
try {
|
|
26933
|
+
mtime = String(Math.floor((await stat3(join16(dir, name))).mtimeMs));
|
|
26934
|
+
} catch {
|
|
26935
|
+
}
|
|
26936
|
+
parts.push(`${sub}/${name}:${mtime}`);
|
|
26937
|
+
}
|
|
26938
|
+
} catch {
|
|
26939
|
+
parts.push(`${sub}:absent`);
|
|
26940
|
+
}
|
|
26941
|
+
}
|
|
26942
|
+
try {
|
|
26943
|
+
const { loadManifest } = await import("@papi-ai/skills/manifest");
|
|
26944
|
+
parts.push(`manifest:${loadManifest().packageVersion}`);
|
|
26945
|
+
} catch {
|
|
26946
|
+
parts.push("manifest:none");
|
|
26947
|
+
}
|
|
26948
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
26949
|
+
}
|
|
26950
|
+
async function readSkillDescription(skillDir) {
|
|
26951
|
+
try {
|
|
26952
|
+
const content = await readFile8(join16(skillDir, "SKILL.md"), "utf-8");
|
|
26953
|
+
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26954
|
+
if (!fm) return void 0;
|
|
26955
|
+
const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
|
|
26956
|
+
if (!desc) return void 0;
|
|
26957
|
+
return desc[1].replace(/^\s+/gm, "").replace(/\n+/g, " ").trim() || void 0;
|
|
26958
|
+
} catch {
|
|
26959
|
+
return void 0;
|
|
26960
|
+
}
|
|
26961
|
+
}
|
|
26962
|
+
async function scanInventory(root, toolDefs) {
|
|
26963
|
+
const entries = [];
|
|
26964
|
+
const stale = /* @__PURE__ */ new Set();
|
|
26965
|
+
let version;
|
|
26966
|
+
try {
|
|
26967
|
+
const { detectStaleForks, loadManifest } = await import("@papi-ai/skills/manifest");
|
|
26968
|
+
for (const fork of detectStaleForks(root)) stale.add(fork.name);
|
|
26969
|
+
version = loadManifest().packageVersion;
|
|
26970
|
+
} catch {
|
|
26971
|
+
}
|
|
26972
|
+
const skillsDir = join16(root, ".claude", "skills");
|
|
26973
|
+
try {
|
|
26974
|
+
const dirents = await readdir3(skillsDir, { withFileTypes: true });
|
|
26975
|
+
for (const d of dirents.filter((e) => e.isDirectory())) {
|
|
26976
|
+
entries.push({
|
|
26977
|
+
kind: "skill",
|
|
26978
|
+
name: d.name,
|
|
26979
|
+
description: await readSkillDescription(join16(skillsDir, d.name)),
|
|
26980
|
+
version,
|
|
26981
|
+
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
26982
|
+
path: join16(".claude", "skills", d.name)
|
|
26983
|
+
});
|
|
26984
|
+
}
|
|
26985
|
+
} catch {
|
|
26986
|
+
}
|
|
26987
|
+
for (const a of await listAgents(root)) {
|
|
26988
|
+
entries.push({
|
|
26989
|
+
kind: "agent",
|
|
26990
|
+
name: a.name,
|
|
26991
|
+
description: a.description || void 0,
|
|
26992
|
+
status: "ok",
|
|
26993
|
+
path: a.path
|
|
26994
|
+
});
|
|
26995
|
+
}
|
|
26996
|
+
const present = /* @__PURE__ */ new Set();
|
|
26997
|
+
try {
|
|
26998
|
+
for (const f of (await readdir3(join16(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
|
|
26999
|
+
present.add(f);
|
|
27000
|
+
entries.push({ kind: "hook", name: f, status: "ok", path: join16(".claude", "hooks", f) });
|
|
27001
|
+
}
|
|
27002
|
+
} catch {
|
|
27003
|
+
}
|
|
27004
|
+
for (const rec of RECOMMENDED_HOOKS) {
|
|
27005
|
+
if (!present.has(rec)) {
|
|
27006
|
+
entries.push({ kind: "hook", name: rec, description: "Recommended hook \u2014 not installed", status: "missing" });
|
|
27007
|
+
}
|
|
27008
|
+
}
|
|
27009
|
+
for (const t of toolDefs ?? []) {
|
|
27010
|
+
entries.push({ kind: "mcp_tool", name: t.name, description: t.description, status: "ok" });
|
|
27011
|
+
}
|
|
27012
|
+
return entries;
|
|
27013
|
+
}
|
|
27014
|
+
async function syncHarnessInventory(adapter2, config2, opts) {
|
|
27015
|
+
if (!adapter2.getHarnessState || !adapter2.setHarnessState || !adapter2.replaceHarnessInventory) {
|
|
27016
|
+
return { changed: false, skipped: true };
|
|
27017
|
+
}
|
|
27018
|
+
const fingerprint = await computeFingerprint(config2.projectRoot);
|
|
27019
|
+
if (!opts?.force) {
|
|
27020
|
+
const state2 = await adapter2.getHarnessState();
|
|
27021
|
+
if (state2 && state2.fingerprint === fingerprint) return { changed: false };
|
|
27022
|
+
}
|
|
27023
|
+
const entries = await scanInventory(config2.projectRoot, opts?.toolDefs);
|
|
27024
|
+
await adapter2.replaceHarnessInventory(entries);
|
|
27025
|
+
await adapter2.setHarnessState(fingerprint);
|
|
27026
|
+
const counts = { skill: 0, agent: 0, hook: 0, mcp_tool: 0 };
|
|
27027
|
+
let staleForks = 0;
|
|
27028
|
+
let missingHooks = 0;
|
|
27029
|
+
for (const e of entries) {
|
|
27030
|
+
counts[e.kind]++;
|
|
27031
|
+
if (e.status === "stale_fork") staleForks++;
|
|
27032
|
+
if (e.status === "missing") missingHooks++;
|
|
27033
|
+
}
|
|
27034
|
+
return { changed: true, counts, staleForks, missingHooks };
|
|
27035
|
+
}
|
|
27036
|
+
|
|
27037
|
+
// src/tools/inventory-sync.ts
|
|
27038
|
+
var inventorySyncTool = {
|
|
27039
|
+
name: "inventory_sync",
|
|
27040
|
+
description: "Sync this project's harness inventory \u2014 skills, sub-agents, hooks, and MCP tools \u2014 to the database so the dashboard can surface it. Gated by a cheap change-fingerprint: a no-op when the harness hasn't changed since the last sync. Set force=true to re-scan and write regardless. Runs automatically at setup and release; use this for an explicit refresh after editing your harness.",
|
|
27041
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27042
|
+
inputSchema: {
|
|
27043
|
+
type: "object",
|
|
27044
|
+
properties: {
|
|
27045
|
+
force: {
|
|
27046
|
+
type: "boolean",
|
|
27047
|
+
description: "Re-scan and write even if the change-fingerprint is unchanged. Default false."
|
|
27048
|
+
}
|
|
27049
|
+
},
|
|
27050
|
+
required: []
|
|
27051
|
+
}
|
|
27052
|
+
};
|
|
27053
|
+
async function handleInventorySync(adapter2, config2, args, toolDefs) {
|
|
27054
|
+
const force = args.force === true;
|
|
27055
|
+
const result = await syncHarnessInventory(adapter2, config2, { force, toolDefs });
|
|
27056
|
+
if (result.skipped) {
|
|
27057
|
+
return textResponse(
|
|
27058
|
+
"# Harness inventory\n\nNo database adapter available \u2014 harness inventory is only persisted when running against the hosted database (pg/proxy adapter). Nothing to sync."
|
|
27059
|
+
);
|
|
27060
|
+
}
|
|
27061
|
+
if (!result.changed) {
|
|
27062
|
+
return textResponse(
|
|
27063
|
+
"# Harness inventory\n\n\u2713 Already up to date \u2014 the harness has not changed since the last sync (no write needed)."
|
|
27064
|
+
);
|
|
27065
|
+
}
|
|
27066
|
+
const c = result.counts;
|
|
27067
|
+
const lines = [
|
|
27068
|
+
"# Harness inventory synced",
|
|
27069
|
+
"",
|
|
27070
|
+
`Wrote **${c.skill}** skills, **${c.agent}** sub-agents, **${c.hook}** hooks, **${c.mcp_tool}** MCP tools to the dashboard.`
|
|
27071
|
+
];
|
|
27072
|
+
if (result.staleForks) lines.push(`
|
|
27073
|
+
\u26A0\uFE0F **${result.staleForks}** skill(s) flagged as stale forks (drifted from the pinned registry).`);
|
|
27074
|
+
if (result.missingHooks) lines.push(`
|
|
27075
|
+
\u26A0\uFE0F **${result.missingHooks}** recommended hook(s) not installed.`);
|
|
27076
|
+
return textResponse(lines.join("\n"));
|
|
27077
|
+
}
|
|
27078
|
+
|
|
26821
27079
|
// src/server.ts
|
|
26822
27080
|
var DEFAULT_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_TOOL_TIMEOUT_MS ?? "30000", 10);
|
|
26823
27081
|
var LONG_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_LONG_TOOL_TIMEOUT_MS ?? "180000", 10);
|
|
@@ -26925,14 +27183,19 @@ var PAPI_TOOLS = [
|
|
|
26925
27183
|
learningActionTool,
|
|
26926
27184
|
projectCreateTool,
|
|
26927
27185
|
projectListTool,
|
|
26928
|
-
projectSwitchTool
|
|
27186
|
+
projectSwitchTool,
|
|
27187
|
+
agentListTool,
|
|
27188
|
+
inventorySyncTool
|
|
26929
27189
|
];
|
|
27190
|
+
function getToolMetadata() {
|
|
27191
|
+
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
27192
|
+
}
|
|
26930
27193
|
function createServer(adapter2, config2) {
|
|
26931
|
-
const __pkgFilename =
|
|
26932
|
-
const __pkgDir =
|
|
27194
|
+
const __pkgFilename = fileURLToPath2(import.meta.url);
|
|
27195
|
+
const __pkgDir = dirname4(__pkgFilename);
|
|
26933
27196
|
let serverVersion = "unknown";
|
|
26934
27197
|
try {
|
|
26935
|
-
const pkg = JSON.parse(
|
|
27198
|
+
const pkg = JSON.parse(readFileSync9(join17(__pkgDir, "..", "package.json"), "utf-8"));
|
|
26936
27199
|
serverVersion = pkg.version ?? "unknown";
|
|
26937
27200
|
} catch {
|
|
26938
27201
|
}
|
|
@@ -26945,9 +27208,9 @@ function createServer(adapter2, config2) {
|
|
|
26945
27208
|
"\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"
|
|
26946
27209
|
);
|
|
26947
27210
|
}
|
|
26948
|
-
const __filename =
|
|
26949
|
-
const __dirname2 =
|
|
26950
|
-
const skillsDir =
|
|
27211
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
27212
|
+
const __dirname2 = dirname4(__filename);
|
|
27213
|
+
const skillsDir = join17(__dirname2, "..", "skills");
|
|
26951
27214
|
function parseSkillFrontmatter(content) {
|
|
26952
27215
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26953
27216
|
if (!match) return null;
|
|
@@ -26961,11 +27224,11 @@ function createServer(adapter2, config2) {
|
|
|
26961
27224
|
}
|
|
26962
27225
|
server2.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
26963
27226
|
try {
|
|
26964
|
-
const files = await
|
|
27227
|
+
const files = await readdir4(skillsDir);
|
|
26965
27228
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
26966
27229
|
const prompts = [];
|
|
26967
27230
|
for (const file of mdFiles) {
|
|
26968
|
-
const content = await
|
|
27231
|
+
const content = await readFile9(join17(skillsDir, file), "utf-8");
|
|
26969
27232
|
const meta = parseSkillFrontmatter(content);
|
|
26970
27233
|
if (meta) {
|
|
26971
27234
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -26979,9 +27242,9 @@ function createServer(adapter2, config2) {
|
|
|
26979
27242
|
server2.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
26980
27243
|
const { name } = request.params;
|
|
26981
27244
|
try {
|
|
26982
|
-
const files = await
|
|
27245
|
+
const files = await readdir4(skillsDir);
|
|
26983
27246
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
26984
|
-
const content = await
|
|
27247
|
+
const content = await readFile9(join17(skillsDir, file), "utf-8");
|
|
26985
27248
|
const meta = parseSkillFrontmatter(content);
|
|
26986
27249
|
if (meta?.name === name) {
|
|
26987
27250
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -27100,6 +27363,10 @@ function createServer(adapter2, config2) {
|
|
|
27100
27363
|
return handleProjectList(adapter2, config2, safeArgs);
|
|
27101
27364
|
case "project_switch":
|
|
27102
27365
|
return handleProjectSwitch(adapter2, config2, safeArgs);
|
|
27366
|
+
case "agent_list":
|
|
27367
|
+
return handleAgentList(config2, safeArgs);
|
|
27368
|
+
case "inventory_sync":
|
|
27369
|
+
return handleInventorySync(adapter2, config2, safeArgs, getToolMetadata());
|
|
27103
27370
|
default:
|
|
27104
27371
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
27105
27372
|
}
|
|
@@ -27153,6 +27420,11 @@ function createServer(adapter2, config2) {
|
|
|
27153
27420
|
}
|
|
27154
27421
|
}
|
|
27155
27422
|
}
|
|
27423
|
+
if (!isError && (name === "setup" || name === "release")) {
|
|
27424
|
+
const force = name === "setup";
|
|
27425
|
+
void syncHarnessInventory(adapter2, config2, { force, toolDefs: getToolMetadata() }).catch(() => {
|
|
27426
|
+
});
|
|
27427
|
+
}
|
|
27156
27428
|
const footer = formatMetricsFooter(elapsed, usage, contextBytes);
|
|
27157
27429
|
result.content.push({ type: "text", text: footer });
|
|
27158
27430
|
return result;
|
|
@@ -27456,10 +27728,10 @@ async function dispatchRequest(args) {
|
|
|
27456
27728
|
}
|
|
27457
27729
|
|
|
27458
27730
|
// src/index.ts
|
|
27459
|
-
var __dirname =
|
|
27731
|
+
var __dirname = dirname5(fileURLToPath3(import.meta.url));
|
|
27460
27732
|
var pkgVersion = "unknown";
|
|
27461
27733
|
try {
|
|
27462
|
-
const pkg = JSON.parse(
|
|
27734
|
+
const pkg = JSON.parse(readFileSync12(join20(__dirname, "..", "package.json"), "utf-8"));
|
|
27463
27735
|
pkgVersion = pkg.version;
|
|
27464
27736
|
} catch {
|
|
27465
27737
|
}
|