@papi-ai/server 0.7.80 → 0.7.82
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/backfill-cycle-metrics.js +299 -2
- package/dist/index.js +1250 -139
- package/dist/prompts.js +36 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ __export(git_exports, {
|
|
|
29
29
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
30
30
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
31
31
|
getBranchDiff: () => getBranchDiff,
|
|
32
|
+
getCommitFiles: () => getCommitFiles,
|
|
32
33
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
33
34
|
getCurrentBranch: () => getCurrentBranch,
|
|
34
35
|
getDocPathsTouchedOnBranch: () => getDocPathsTouchedOnBranch,
|
|
@@ -42,11 +43,13 @@ __export(git_exports, {
|
|
|
42
43
|
getModifiedFiles: () => getModifiedFiles,
|
|
43
44
|
getOriginRepoSlug: () => getOriginRepoSlug,
|
|
44
45
|
getOriginUrl: () => getOriginUrl,
|
|
46
|
+
getPathsDifferingFrom: () => getPathsDifferingFrom,
|
|
45
47
|
getPullRequestUrl: () => getPullRequestUrl,
|
|
46
48
|
getRemoteBranchFiles: () => getRemoteBranchFiles,
|
|
47
49
|
getRootCommitHash: () => getRootCommitHash,
|
|
48
50
|
getStagedFiles: () => getStagedFiles,
|
|
49
51
|
getTagTarget: () => getTagTarget,
|
|
52
|
+
getTaskDiff: () => getTaskDiff,
|
|
50
53
|
getTaskIdsOnBranch: () => getTaskIdsOnBranch,
|
|
51
54
|
getTrackedModifiedFiles: () => getTrackedModifiedFiles,
|
|
52
55
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
@@ -61,6 +64,7 @@ __export(git_exports, {
|
|
|
61
64
|
isGhAvailable: () => isGhAvailable,
|
|
62
65
|
isGitAvailable: () => isGitAvailable,
|
|
63
66
|
isGitRepo: () => isGitRepo,
|
|
67
|
+
isPathCommittedAnywhere: () => isPathCommittedAnywhere,
|
|
64
68
|
isPathIgnored: () => isPathIgnored,
|
|
65
69
|
isPathTracked: () => isPathTracked,
|
|
66
70
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
@@ -100,6 +104,22 @@ function isGitRepo(cwd) {
|
|
|
100
104
|
return false;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
107
|
+
function isPathCommittedAnywhere(cwd, path7) {
|
|
108
|
+
const run = (args) => {
|
|
109
|
+
try {
|
|
110
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const commit = run(["log", "--all", "--format=%H", "-1", "--", path7]);
|
|
116
|
+
if (!commit) return false;
|
|
117
|
+
const committedBlob = run(["rev-parse", `${commit}:${path7}`]);
|
|
118
|
+
if (!committedBlob) return false;
|
|
119
|
+
const onDiskBlob = run(["hash-object", "--", path7]);
|
|
120
|
+
if (!onDiskBlob) return false;
|
|
121
|
+
return committedBlob === onDiskBlob;
|
|
122
|
+
}
|
|
103
123
|
function isPathTracked(cwd, path7) {
|
|
104
124
|
try {
|
|
105
125
|
execFileSync("git", ["ls-files", "--error-unmatch", "--", path7], {
|
|
@@ -272,6 +292,50 @@ function getBranchDiff(cwd, base = "origin/main", maxBytes = 2e5) {
|
|
|
272
292
|
}
|
|
273
293
|
return "";
|
|
274
294
|
}
|
|
295
|
+
function getTaskDiff(cwd, taskId, base = "origin/main", maxBytes = 2e5) {
|
|
296
|
+
const truncate2 = (out) => out.length > maxBytes ? `${out.slice(0, maxBytes)}
|
|
297
|
+
|
|
298
|
+
... [diff truncated at ${Math.round(maxBytes / 1024)} KB]` : out;
|
|
299
|
+
try {
|
|
300
|
+
const shas = execFileSync(
|
|
301
|
+
"git",
|
|
302
|
+
["log", "--all", "--format=%H", `--grep=${taskId})`, "--fixed-strings"],
|
|
303
|
+
{ cwd, encoding: "utf-8", maxBuffer: 8 * 1024 * 1024 }
|
|
304
|
+
).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
305
|
+
if (shas.length > 0) {
|
|
306
|
+
const parts = [];
|
|
307
|
+
for (const sha of [...shas].reverse()) {
|
|
308
|
+
try {
|
|
309
|
+
const one = execFileSync("git", ["diff", `${sha}^..${sha}`], {
|
|
310
|
+
cwd,
|
|
311
|
+
encoding: "utf-8",
|
|
312
|
+
maxBuffer: 32 * 1024 * 1024
|
|
313
|
+
});
|
|
314
|
+
if (one) parts.push(one);
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const out = parts.join("\n");
|
|
319
|
+
if (out) {
|
|
320
|
+
const range = shas.length === 1 ? shas[0].slice(0, 8) : `${shas[shas.length - 1].slice(0, 8)}\u2026${shas[0].slice(0, 8)}`;
|
|
321
|
+
return {
|
|
322
|
+
diff: truncate2(out),
|
|
323
|
+
scope: "task-commits",
|
|
324
|
+
detail: `${shas.length} commit(s) for ${taskId} (${range}), each against its own parent`
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
const branch = getCurrentBranch(cwd);
|
|
331
|
+
const diff = getBranchDiff(cwd, base, maxBytes);
|
|
332
|
+
if (!diff) return { diff: "", scope: "none", detail: "no diff resolved" };
|
|
333
|
+
return {
|
|
334
|
+
diff,
|
|
335
|
+
scope: "whole-branch",
|
|
336
|
+
detail: `no commit naming ${taskId} was found, so this is the ENTIRE diff of ${branch ?? "the current branch"} vs ${base} \u2014 it may include other tasks' work, and may not include this task's`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
275
339
|
function getHeadCommitSubject(cwd) {
|
|
276
340
|
try {
|
|
277
341
|
const out = execFileSync("git", ["log", "-1", "--format=%s"], {
|
|
@@ -319,6 +383,28 @@ function branchExists(cwd, branch) {
|
|
|
319
383
|
return false;
|
|
320
384
|
}
|
|
321
385
|
}
|
|
386
|
+
function getPathsDifferingFrom(cwd, target) {
|
|
387
|
+
try {
|
|
388
|
+
const out = execFileSync("git", ["diff", "--name-only", "HEAD", target], {
|
|
389
|
+
cwd,
|
|
390
|
+
encoding: "utf-8"
|
|
391
|
+
});
|
|
392
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function getCommitFiles(cwd, ref = "HEAD") {
|
|
398
|
+
try {
|
|
399
|
+
const out = execFileSync("git", ["show", "--name-only", "--pretty=format:", ref], {
|
|
400
|
+
cwd,
|
|
401
|
+
encoding: "utf-8"
|
|
402
|
+
});
|
|
403
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
322
408
|
function checkoutBranch(cwd, branch) {
|
|
323
409
|
try {
|
|
324
410
|
execFileSync("git", ["checkout", branch], { cwd, encoding: "utf-8" });
|
|
@@ -1273,7 +1359,7 @@ function wrapWithForwarding(instance) {
|
|
|
1273
1359
|
function createProxyAdapter(config2) {
|
|
1274
1360
|
return wrapWithForwarding(new ProxyPapiAdapter(config2));
|
|
1275
1361
|
}
|
|
1276
|
-
var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, NO_FORWARD, ProxyPapiAdapter;
|
|
1362
|
+
var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, HOSTED_SERVED, NO_FORWARD, ProxyPapiAdapter;
|
|
1277
1363
|
var init_proxy_adapter = __esm({
|
|
1278
1364
|
"src/proxy-adapter.ts"() {
|
|
1279
1365
|
"use strict";
|
|
@@ -1301,6 +1387,156 @@ var init_proxy_adapter = __esm({
|
|
|
1301
1387
|
"getRecentReviews",
|
|
1302
1388
|
"getActiveDecisions"
|
|
1303
1389
|
]);
|
|
1390
|
+
HOSTED_SERVED = /* @__PURE__ */ new Set([
|
|
1391
|
+
"actionRecommendation",
|
|
1392
|
+
"addAgendaTopic",
|
|
1393
|
+
"addContributorByEmail",
|
|
1394
|
+
"appendBuildReport",
|
|
1395
|
+
"appendCycleLearnings",
|
|
1396
|
+
"appendCycleMetrics",
|
|
1397
|
+
"appendDecisionEvent",
|
|
1398
|
+
"appendToolMetric",
|
|
1399
|
+
"archiveTasks",
|
|
1400
|
+
"claimTask",
|
|
1401
|
+
"clearPendingReviewResponse",
|
|
1402
|
+
"compressBuildReports",
|
|
1403
|
+
"compressCycleLog",
|
|
1404
|
+
"confirmPendingActiveDecisions",
|
|
1405
|
+
"correctLatestBuildReportEffort",
|
|
1406
|
+
"countDueOwnerActions",
|
|
1407
|
+
"countNudgedOwnerActions",
|
|
1408
|
+
"countOpenOwnerActions",
|
|
1409
|
+
"countOwnerActionsBlockingTasks",
|
|
1410
|
+
"countPlanRunsForCycle",
|
|
1411
|
+
"createConvention",
|
|
1412
|
+
"createCycle",
|
|
1413
|
+
"createHorizon",
|
|
1414
|
+
"createOwnerAction",
|
|
1415
|
+
"createStage",
|
|
1416
|
+
"createTask",
|
|
1417
|
+
"deleteActiveDecision",
|
|
1418
|
+
"deleteConvention",
|
|
1419
|
+
"deleteDoc",
|
|
1420
|
+
"dismissRecommendation",
|
|
1421
|
+
"findPendingDocActionsForTask",
|
|
1422
|
+
"getActiveDecisions",
|
|
1423
|
+
"getActiveStage",
|
|
1424
|
+
"getBuildReportCountForTask",
|
|
1425
|
+
"getBuildReportsSince",
|
|
1426
|
+
"getContextHashes",
|
|
1427
|
+
"getContextUtilisation",
|
|
1428
|
+
"getCostSnapshots",
|
|
1429
|
+
"getCostSummary",
|
|
1430
|
+
"getCurrentNorthStar",
|
|
1431
|
+
"getCycleHealth",
|
|
1432
|
+
"getCycleLearningPatterns",
|
|
1433
|
+
"getCycleLearnings",
|
|
1434
|
+
"getCycleLog",
|
|
1435
|
+
"getCycleLogSince",
|
|
1436
|
+
"getDecisionEvents",
|
|
1437
|
+
"getDecisionEventsSince",
|
|
1438
|
+
"getDecisionScorePatterns",
|
|
1439
|
+
"getDecisionScores",
|
|
1440
|
+
"getDecisionUsage",
|
|
1441
|
+
"getDoc",
|
|
1442
|
+
"getDocBody",
|
|
1443
|
+
"getDocBodyUsage",
|
|
1444
|
+
"getDogfoodLog",
|
|
1445
|
+
"getEstimationCalibration",
|
|
1446
|
+
"getFailedBuildAttemptsForTask",
|
|
1447
|
+
"getHarnessInventory",
|
|
1448
|
+
"getHarnessState",
|
|
1449
|
+
"getLastStrategyReviewCycle",
|
|
1450
|
+
"getLatestDecisionScores",
|
|
1451
|
+
"getModelOutcomeStats",
|
|
1452
|
+
"getModuleEstimationStats",
|
|
1453
|
+
"getNorthStarSetCycle",
|
|
1454
|
+
"getNorthStarStaleness",
|
|
1455
|
+
"getOwnerIdentity",
|
|
1456
|
+
"getPendingAgendaTopics",
|
|
1457
|
+
"getPendingRecommendations",
|
|
1458
|
+
"getPendingReviewResponse",
|
|
1459
|
+
"getPlanContextSummary",
|
|
1460
|
+
"getProjectInfo",
|
|
1461
|
+
"getProjectOwnerUserId",
|
|
1462
|
+
"getRecentBuildReports",
|
|
1463
|
+
"getRecentReviews",
|
|
1464
|
+
"getRecentTaskComments",
|
|
1465
|
+
"getRecommendationEffectiveness",
|
|
1466
|
+
"getStrategyReviews",
|
|
1467
|
+
"getTask",
|
|
1468
|
+
"getTasks",
|
|
1469
|
+
"getToolCallCount",
|
|
1470
|
+
"getUnactionedDogfoodEntries",
|
|
1471
|
+
"getUnnotifiedResolvedFeedback",
|
|
1472
|
+
"hasToolMilestone",
|
|
1473
|
+
"insertPlanRun",
|
|
1474
|
+
"insertToolRun",
|
|
1475
|
+
"linkOwnerActionToTask",
|
|
1476
|
+
"linkPhasesToStage",
|
|
1477
|
+
"listContributors",
|
|
1478
|
+
"listConventions",
|
|
1479
|
+
"listMyBugReports",
|
|
1480
|
+
"listOwnerActionsForBlockerScan",
|
|
1481
|
+
"logEntityReferences",
|
|
1482
|
+
"markAgendaTopicsAddressed",
|
|
1483
|
+
"markCycleLearningResolved",
|
|
1484
|
+
"markFeedbackNotified",
|
|
1485
|
+
"moveTask",
|
|
1486
|
+
"planWriteBack",
|
|
1487
|
+
"projectExists",
|
|
1488
|
+
"queryBoard",
|
|
1489
|
+
"readCycleMetrics",
|
|
1490
|
+
"readCycles",
|
|
1491
|
+
"readCycles",
|
|
1492
|
+
"readDiscoveryCanvas",
|
|
1493
|
+
"readHorizons",
|
|
1494
|
+
"readPhases",
|
|
1495
|
+
"readPlanningLog",
|
|
1496
|
+
"readProductBrief",
|
|
1497
|
+
"readRegistries",
|
|
1498
|
+
"readStages",
|
|
1499
|
+
"readToolMetrics",
|
|
1500
|
+
"recordProgressStep",
|
|
1501
|
+
"recordTransition",
|
|
1502
|
+
"registerDoc",
|
|
1503
|
+
"removeContributorByEmail",
|
|
1504
|
+
"reorderDocs",
|
|
1505
|
+
"replaceHarnessInventory",
|
|
1506
|
+
"resolveLearningsForDoneTasks",
|
|
1507
|
+
"savePendingReviewResponse",
|
|
1508
|
+
"searchDocs",
|
|
1509
|
+
"setCriterionMet",
|
|
1510
|
+
"setCycleHealth",
|
|
1511
|
+
"setHarnessState",
|
|
1512
|
+
"setProjectPapiDir",
|
|
1513
|
+
"storeDocBody",
|
|
1514
|
+
"submitBugReport",
|
|
1515
|
+
"unclaimTask",
|
|
1516
|
+
"updateActiveDecision",
|
|
1517
|
+
"updateCycleLearningActionRef",
|
|
1518
|
+
"updateDiscoveryCanvas",
|
|
1519
|
+
"updateDocAction",
|
|
1520
|
+
"updateDocStatus",
|
|
1521
|
+
"updateDogfoodEntryStatus",
|
|
1522
|
+
"updateHorizonStatus",
|
|
1523
|
+
"updatePhaseStatus",
|
|
1524
|
+
"updateProductBrief",
|
|
1525
|
+
"updateRegistries",
|
|
1526
|
+
"updateStageExitCriteria",
|
|
1527
|
+
"updateStageStatus",
|
|
1528
|
+
"updateTask",
|
|
1529
|
+
"updateTaskStatus",
|
|
1530
|
+
"upsertActiveDecision",
|
|
1531
|
+
"upsertNorthStar",
|
|
1532
|
+
"writeCycleLogEntry",
|
|
1533
|
+
"writeDecisionScore",
|
|
1534
|
+
"writeDogfoodEntries",
|
|
1535
|
+
"writePhases",
|
|
1536
|
+
"writeRecommendation",
|
|
1537
|
+
"writeReview",
|
|
1538
|
+
"writeStrategyReview"
|
|
1539
|
+
]);
|
|
1304
1540
|
NO_FORWARD = /* @__PURE__ */ new Set([
|
|
1305
1541
|
// (1) local-only
|
|
1306
1542
|
"close",
|
|
@@ -1329,7 +1565,7 @@ var init_proxy_adapter = __esm({
|
|
|
1329
1565
|
"listContributorReleasePrs",
|
|
1330
1566
|
"claimReview",
|
|
1331
1567
|
"getSiblingAds",
|
|
1332
|
-
"getSiblingRepoTasks"
|
|
1568
|
+
"getSiblingRepoTasks",
|
|
1333
1569
|
// task-2828 (C339): attributed-intelligence analytics reader — pg-only that cycle.
|
|
1334
1570
|
// task-2864 (C343): WIRED. getModelOutcomeStats now has an edge case handler (raw
|
|
1335
1571
|
// SQL via postgres.js mirroring the pg query + inlined computeModelOutcomes bucketing)
|
|
@@ -1353,6 +1589,28 @@ var init_proxy_adapter = __esm({
|
|
|
1353
1589
|
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
1354
1590
|
// hosted callers and persists a project-scoped cycle_progress_steps row. Removed
|
|
1355
1591
|
// from NO_FORWARD (was the task-2484 pg-only gap) — hosted parity restored.
|
|
1592
|
+
//
|
|
1593
|
+
// (3) pg-only optimisations probed STRUCTURALLY with a local fallback beside them.
|
|
1594
|
+
// These are the dangerous class: the caller asks `typeof adapter.X === 'function'`
|
|
1595
|
+
// to decide whether the fast path exists, and the get-trap answers "yes" for any
|
|
1596
|
+
// name absent from this set. The probe then passes, the call forwards, and the
|
|
1597
|
+
// edge 403s — while a working fallback sits a few lines below, unreachable.
|
|
1598
|
+
// A method belongs here when BOTH are true: it is probed by `typeof` rather than
|
|
1599
|
+
// called unconditionally, and the probe's else-branch is a real fallback.
|
|
1600
|
+
//
|
|
1601
|
+
// task-3290 (C361): allocateActiveDecision + applyActiveDecisionUpdates are pg-only
|
|
1602
|
+
// (atomic id allocation; transactional batch apply). Both are probed in
|
|
1603
|
+
// services/strategy.ts — asDecisionIdAllocator and asDecisionBatchApplier — and both
|
|
1604
|
+
// have read-then-write / sequential fallbacks. Hosted users hit a 403 minting ANY
|
|
1605
|
+
// Active Decision via setup's AD seed or strategy_change until these were listed.
|
|
1606
|
+
"allocateActiveDecision",
|
|
1607
|
+
"applyActiveDecisionUpdates",
|
|
1608
|
+
// task-3290 (C361), same sweep: getLastZoomOutCycle was documented in the parity
|
|
1609
|
+
// ledger as "proxy NO_FORWARD → safe no-op" while NOT being in this set. It is
|
|
1610
|
+
// optional-chain probed in services/health.ts and wrapped in try/catch, so it
|
|
1611
|
+
// degraded rather than crashed — but every hosted `orient` paid a round-trip to
|
|
1612
|
+
// earn a silent 403. Listing it makes the ledger's claim true and skips the trip.
|
|
1613
|
+
"getLastZoomOutCycle"
|
|
1356
1614
|
]);
|
|
1357
1615
|
ProxyPapiAdapter = class _ProxyPapiAdapter {
|
|
1358
1616
|
endpoint;
|
|
@@ -1365,6 +1623,30 @@ var init_proxy_adapter = __esm({
|
|
|
1365
1623
|
this.projectId = config2.projectId ?? "";
|
|
1366
1624
|
this.onAuthRejected = config2.onAuthRejected;
|
|
1367
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Does the hosted path genuinely serve `name`? (task-3022, C362)
|
|
1628
|
+
*
|
|
1629
|
+
* This is the honest answer to the question `typeof adapter.name === 'function'`
|
|
1630
|
+
* USED to ask and could not answer: the get-trap below manufactures a function
|
|
1631
|
+
* for any name absent from NO_FORWARD, so a structural probe reads true for
|
|
1632
|
+
* pg-only methods and the caller then earns a 403 from the edge.
|
|
1633
|
+
*
|
|
1634
|
+
* Answered from NO_FORWARD itself, deliberately — the same set the forwarder
|
|
1635
|
+
* consults — so this can never disagree with what an actual call would do. A
|
|
1636
|
+
* name in NO_FORWARD is either local-only or not yet wired hosted; either way
|
|
1637
|
+
* the caller must take its fallback. The proxy-parity test guarantees every
|
|
1638
|
+
* method is an explicit wrapper, in NO_FORWARD, or in the edge allowlist, so
|
|
1639
|
+
* "not in NO_FORWARD" is a sound proxy for "served hosted".
|
|
1640
|
+
*
|
|
1641
|
+
* A REAL method, not a forwarded one: Reflect.get in the trap returns it before
|
|
1642
|
+
* the forwarding branch is reached, so callers get this implementation rather
|
|
1643
|
+
* than a forwarder that would POST "supportsMethod" to the edge.
|
|
1644
|
+
*/
|
|
1645
|
+
supportsMethod(name) {
|
|
1646
|
+
if (NO_FORWARD.has(name)) return false;
|
|
1647
|
+
if (name in _ProxyPapiAdapter.prototype) return true;
|
|
1648
|
+
return HOSTED_SERVED.has(name);
|
|
1649
|
+
}
|
|
1368
1650
|
/**
|
|
1369
1651
|
* task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
|
|
1370
1652
|
* (no projectId needed), so it answers exactly one question: does the proxy
|
|
@@ -1884,6 +2166,21 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1884
2166
|
updateDogfoodEntryStatus(id, status, linkedTaskId) {
|
|
1885
2167
|
return this.invoke("updateDogfoodEntryStatus", [id, status, linkedTaskId]);
|
|
1886
2168
|
}
|
|
2169
|
+
// --- Project conventions (task-3271) ---
|
|
2170
|
+
//
|
|
2171
|
+
// Wired to the edge handler from the start rather than parked in NO_FORWARD.
|
|
2172
|
+
// The hosted remote connector is the only install path an external user has,
|
|
2173
|
+
// so a local-only conventions store would be a feature nobody in the actual
|
|
2174
|
+
// user base can reach.
|
|
2175
|
+
listConventions() {
|
|
2176
|
+
return this.invoke("listConventions");
|
|
2177
|
+
}
|
|
2178
|
+
createConvention(convention) {
|
|
2179
|
+
return this.invoke("createConvention", [convention]);
|
|
2180
|
+
}
|
|
2181
|
+
deleteConvention(id) {
|
|
2182
|
+
return this.invoke("deleteConvention", [id]);
|
|
2183
|
+
}
|
|
1887
2184
|
// --- Harness inventory (task-1896) ---
|
|
1888
2185
|
getHarnessInventory() {
|
|
1889
2186
|
return this.invoke("getHarnessInventory");
|
|
@@ -6087,7 +6384,78 @@ function validateFindings(findings) {
|
|
|
6087
6384
|
return { ok: violations.length === 0, violations };
|
|
6088
6385
|
}
|
|
6089
6386
|
var WAB_WINDOW_DAYS = 7;
|
|
6387
|
+
var WAB_DEFAULT_WEEKS = 8;
|
|
6090
6388
|
var WAB_WEEK_MS = WAB_WINDOW_DAYS * 24 * 60 * 60 * 1e3;
|
|
6389
|
+
function wabCompletionMs(row) {
|
|
6390
|
+
const ts = row.end_date ?? row.updated_at;
|
|
6391
|
+
if (!ts) return null;
|
|
6392
|
+
const ms = Date.parse(ts);
|
|
6393
|
+
return Number.isNaN(ms) ? null : ms;
|
|
6394
|
+
}
|
|
6395
|
+
function bucketWabSeries(rows, ownerUserId, nowMs, weeks = WAB_DEFAULT_WEEKS) {
|
|
6396
|
+
const buckets = Array.from({ length: weeks }, () => /* @__PURE__ */ new Set());
|
|
6397
|
+
for (const row of rows) {
|
|
6398
|
+
if (!row.user_id) continue;
|
|
6399
|
+
const ms = wabCompletionMs(row);
|
|
6400
|
+
if (ms === null) continue;
|
|
6401
|
+
const idx = Math.floor((nowMs - ms) / WAB_WEEK_MS);
|
|
6402
|
+
if (idx < 0 || idx >= weeks) continue;
|
|
6403
|
+
buckets[idx].add(row.user_id);
|
|
6404
|
+
}
|
|
6405
|
+
const out = [];
|
|
6406
|
+
for (let idx = weeks - 1; idx >= 0; idx--) {
|
|
6407
|
+
const owners = buckets[idx];
|
|
6408
|
+
const external = ownerUserId ? [...owners].filter((id) => id !== ownerUserId).length : owners.size;
|
|
6409
|
+
out.push({
|
|
6410
|
+
weekStart: new Date(nowMs - (idx + 1) * WAB_WEEK_MS).toISOString(),
|
|
6411
|
+
total: owners.size,
|
|
6412
|
+
external
|
|
6413
|
+
});
|
|
6414
|
+
}
|
|
6415
|
+
return out;
|
|
6416
|
+
}
|
|
6417
|
+
var PRODUCT_MARKER = /WHAT SHIPS FOR USERS\b[^\n:]*:/i;
|
|
6418
|
+
var MECHANICS_MARKER = /RELEASE MECHANICS\b[^\n:]*:/i;
|
|
6419
|
+
function splitCarryForward(raw) {
|
|
6420
|
+
const productMatch = PRODUCT_MARKER.exec(raw);
|
|
6421
|
+
const mechanicsMatch = MECHANICS_MARKER.exec(raw);
|
|
6422
|
+
if (!productMatch && !mechanicsMatch) {
|
|
6423
|
+
return { product: raw.trim(), mechanics: "", split: false };
|
|
6424
|
+
}
|
|
6425
|
+
if (!productMatch && mechanicsMatch) {
|
|
6426
|
+
const head = raw.slice(0, mechanicsMatch.index).trim();
|
|
6427
|
+
const tail = raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim();
|
|
6428
|
+
return { product: head, mechanics: tail, split: true };
|
|
6429
|
+
}
|
|
6430
|
+
const productStart = productMatch.index + productMatch[0].length;
|
|
6431
|
+
if (!mechanicsMatch) {
|
|
6432
|
+
return { product: raw.slice(productStart).trim(), mechanics: "", split: true };
|
|
6433
|
+
}
|
|
6434
|
+
if (mechanicsMatch.index < productMatch.index) {
|
|
6435
|
+
return {
|
|
6436
|
+
product: raw.slice(productStart).trim(),
|
|
6437
|
+
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length, productMatch.index).trim(),
|
|
6438
|
+
split: true
|
|
6439
|
+
};
|
|
6440
|
+
}
|
|
6441
|
+
return {
|
|
6442
|
+
product: raw.slice(productStart, mechanicsMatch.index).trim(),
|
|
6443
|
+
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim(),
|
|
6444
|
+
split: true
|
|
6445
|
+
};
|
|
6446
|
+
}
|
|
6447
|
+
function renderCarryForward(raw, decorate = (t) => t) {
|
|
6448
|
+
const { product, mechanics, split } = splitCarryForward(raw);
|
|
6449
|
+
if (!split) return [decorate(product)];
|
|
6450
|
+
const lines = [];
|
|
6451
|
+
if (product) lines.push(decorate(product));
|
|
6452
|
+
if (mechanics) {
|
|
6453
|
+
if (product) lines.push("");
|
|
6454
|
+
lines.push("**Release mechanics** \u2014 needed at release, not now:");
|
|
6455
|
+
lines.push(decorate(mechanics));
|
|
6456
|
+
}
|
|
6457
|
+
return lines;
|
|
6458
|
+
}
|
|
6091
6459
|
var CHECK_VALUE_MAX = 200;
|
|
6092
6460
|
function codeSpanSafe(value) {
|
|
6093
6461
|
const flat = value.replace(/[`\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -7407,6 +7775,24 @@ If a candidate AD body could be invalidated by running a SQL query, refreshing a
|
|
|
7407
7775
|
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
7408
7776
|
|
|
7409
7777
|
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
7778
|
+
var AD_ADMISSION_RULES = `**AD Admission Rule \u2014 PROPOSE a decision when the project takes a real stance.**
|
|
7779
|
+
|
|
7780
|
+
The guard above says what to reject. This says what to propose. Rejecting is not the safe default: a stance that never gets minted is a stance the next cycle cannot see, and re-deciding it every session is the exact cost this project is paying you to remove. When a candidate passes all four tests below, propose it \u2014 do not wait for the next strategy review.
|
|
7781
|
+
|
|
7782
|
+
**The four tests. All four must pass.**
|
|
7783
|
+
(a) **Alternatives were real.** Something else could genuinely have been chosen. If there was only ever one way to do it, it is not a decision.
|
|
7784
|
+
(b) **It constrains future work not yet scoped.** It changes what a task nobody has written yet will do. Not "it describes work we did" \u2014 "it binds work we have not planned".
|
|
7785
|
+
(c) **It is arguable today.** A competent person could argue the other side right now, with the evidence currently available. Not "was once debated" \u2014 live.
|
|
7786
|
+
(d) **Reversing it costs more than making it did.** If undoing it is as cheap as doing it, nothing is being constrained.
|
|
7787
|
+
|
|
7788
|
+
**Routing for a near-miss \u2014 a candidate that fails one test still goes somewhere.**
|
|
7789
|
+
- **Fails (c) only** \u2014 real alternatives, binds future work, expensive to reverse, but nobody is arguing the other side any more: it is a **Convention**, not a Decision. A Convention is a settled answer to a recurring question; it does not need adjudicating, it needs to be *known* by whoever builds next. Record it as a project convention so it rides future build handoffs. Do not mint it as an AD, and do not drop it.
|
|
7790
|
+
- **Fails (b)** \u2014 it does not constrain any future work: it is not durable at all, it is just work. Capture it as a task, a build report note, or a doc. Do not mint it.
|
|
7791
|
+
- **Fails (a) or (d)** \u2014 not a stance. Same routing as (b): capture it, do not mint it.
|
|
7792
|
+
|
|
7793
|
+
**Copy variants are NEVER decisions.** A tagline, a headline, a value proposition, a piece of marketing or product wording: swapping one for another constrains no future work, so it fails test (b) outright. Wording changes as often as the market teaches you something, and routing every edit through decision ceremony is what buries the handful of decisions that genuinely constrain the project. Edit the wording where the wording lives. If two variants are being compared against a measurement, that is an experiment, not a decision.
|
|
7794
|
+
|
|
7795
|
+
**A decision decaying into a convention is the healthy path, not a failure.** When a live AD stops being arguable \u2014 the alternatives are no longer on the table and nobody would re-litigate it \u2014 say so during housekeeping and propose retiring it into a convention. An AD registry where most entries are settled is one nobody reads.`;
|
|
7410
7796
|
var AD_CONFLICT_SURFACING_RULES = `**A contradiction is NOT a veto \u2014 surface it, never silently shelve it.**
|
|
7411
7797
|
|
|
7412
7798
|
Active Decisions are *active*: they can be superseded, modified, or abandoned. You do NOT have authority to kill a piece of work simply because it cuts against one. That is the user's call, and they can only make it if you show it to them.
|
|
@@ -7653,7 +8039,7 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
7653
8039
|
var PLAN_FRAGMENT_DESIGN_BRIEF = `
|
|
7654
8040
|
**Design brief task detection:** When a task's task type is "design-brief", generate a DESIGN BRIEF handoff. Inside SCOPE (DO THIS), use these type-specific subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
7655
8041
|
- AUDIENCE: Who this design is for \u2014 persona and context of use (e.g. "non-technical Owner, first dashboard visit")
|
|
7656
|
-
- BRAND CONSTRAINTS: Palette, typography, tone \u2014 pull from
|
|
8042
|
+
- BRAND CONSTRAINTS: Palette, typography, tone \u2014 pull from \`PRODUCT.md\` (strategic: brand, users, product purpose, design principles) AND \`DESIGN.md\` (visual tokens: palette, typography, elevation, components) if present. If neither exists, state "No brand doc \u2014 Owner should define constraints before starting."
|
|
7657
8043
|
- DELIVERABLE FORMAT: What the output looks like \u2014 design handoff package / annotated mockup / style spec. Be specific so the person doing the work knows what "done" means.
|
|
7658
8044
|
- REVIEW POINTS: What the Owner must approve before the design is considered done (e.g. layout, copy, colour, imagery).
|
|
7659
8045
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION sections as normal.
|
|
@@ -7685,11 +8071,11 @@ var PLAN_FRAGMENT_OPS_BRIEF = `
|
|
|
7685
8071
|
var PLAN_FRAGMENT_UI = `
|
|
7686
8072
|
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
7687
8073
|
When a task IS a UI task (primary scope is visual/frontend):
|
|
7688
|
-
- Add to SCOPE: "Read
|
|
8074
|
+
- Add to SCOPE: "Read \`PRODUCT.md\` for product purpose, users and design principles, and \`DESIGN.md\` for the visual tokens (palette, typography, elevation, components) \u2014 these are the two files the \`impeccable\` skill reads, and every visual decision must align with them. Run \`impeccable init\` to create them if they do not exist. Use the \`frontend-design\` skill for implementation."
|
|
7689
8075
|
- For M/L UI tasks, add to SCOPE: "Use the full impeccable workflow: shape (direction approval) \u2192 craft (design+build via \`impeccable craft\` / frontend-design) \u2192 live (in-browser HMR iteration via \`impeccable live\`) \u2192 detect (slop check). The approved direction is the quality bar; expect 2-3 iterations. Playground is for pre-build direction approval of shareable/static artifacts or non-dashboard explorers; Playwright is for post-build verification, not mid-design iteration."
|
|
7690
8076
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
7691
|
-
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection \u2014 pull from \`
|
|
7692
|
-
The planner's job is scoping, not design direction. Design decisions happen at build time via
|
|
8077
|
+
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection \u2014 pull from \`PRODUCT.md\` and \`DESIGN.md\` for canonical brand identity."
|
|
8078
|
+
The planner's job is scoping, not design direction. Design decisions happen at build time via \`PRODUCT.md\` (product purpose, users, design principles) + \`DESIGN.md\` (visual tokens) and the frontend-design skill \u2014 don't try to write design specs in the handoff.`;
|
|
7693
8079
|
var PLAN_FRAGMENT_PRODUCT_BRIEF = `
|
|
7694
8080
|
12. **Product Brief** \u2014 Check whether the product brief still reflects reality. Update the brief when ANY of these apply:
|
|
7695
8081
|
- A new AD was created or an existing AD was superseded that changes product scope, target user, or positioning
|
|
@@ -7783,6 +8169,8 @@ ${AD_CONFLICT_SURFACING_RULES}
|
|
|
7783
8169
|
|
|
7784
8170
|
${AD_REJECTION_RULES}
|
|
7785
8171
|
|
|
8172
|
+
${AD_ADMISSION_RULES}
|
|
8173
|
+
|
|
7786
8174
|
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
7787
8175
|
|
|
7788
8176
|
### Operational Quality Rules
|
|
@@ -8325,6 +8713,8 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
8325
8713
|
|
|
8326
8714
|
${AD_REJECTION_RULES}
|
|
8327
8715
|
|
|
8716
|
+
${AD_ADMISSION_RULES}
|
|
8717
|
+
|
|
8328
8718
|
**Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
|
|
8329
8719
|
|
|
8330
8720
|
**Doc Action Staleness:** If a "### Doc Action Staleness" section is present, treat it as a research-to-action audit. For each entry:
|
|
@@ -8673,6 +9063,8 @@ The body field must be the COMPLETE replacement text for the AD block (including
|
|
|
8673
9063
|
|
|
8674
9064
|
${AD_REJECTION_RULES}
|
|
8675
9065
|
|
|
9066
|
+
${AD_ADMISSION_RULES}
|
|
9067
|
+
|
|
8676
9068
|
## PHASE UPDATES
|
|
8677
9069
|
|
|
8678
9070
|
If the strategic change affects the project's phase structure, include a phaseUpdates array.
|
|
@@ -8845,10 +9237,15 @@ function buildPreScanInstruction(opts) {
|
|
|
8845
9237
|
const docRouting = opts.hosted ? "paste the relevant content (decision records, roadmap, spec excerpts) directly into `description` \u2014 over a remote connector PAPI cannot read local file paths." : "pass local doc file paths as `sources` (comma-separated), or paste content into `description`.";
|
|
8846
9238
|
return [
|
|
8847
9239
|
`**\u{1F50E} Before you generate anything \u2014 scan the project and gather real context.**`,
|
|
8848
|
-
|
|
9240
|
+
// task-3293: state the division of labour ONCE, up front. A user watching this
|
|
9241
|
+
// session sees PAPI say it cannot read their files and then sees their agent
|
|
9242
|
+
// read their files. That reads as a contradiction, or worse, unless it is named
|
|
9243
|
+
// first: PAPI never touches the filesystem, YOU do, and you send back a summary.
|
|
9244
|
+
`**How this works:** PAPI has no access to your machine and never reads your files. You (the agent running this) do the reading locally and send back a short summary. Nothing is uploaded except what you pass in that summary.`,
|
|
9245
|
+
`A brief and Active Decisions built from the real code and docs are far sharper than ones guessed from a project name.`,
|
|
8849
9246
|
``,
|
|
8850
9247
|
`1. **Confirm the environment.** Check that you are running from the root of this project's dev environment \u2014 the folder that holds its source, git history, and docs. If you are not there, or you are connected over a remote connector with no access to the user's files, say so and ask the user where the project lives before continuing.`,
|
|
8851
|
-
`2. **Scan the code.** List the top-level directories and files, read the package/build manifest and the README (first ~3000 chars), and collect recent commit subjects. Pass this back as \`codebase_scan\` on the next \`setup\` call.`,
|
|
9248
|
+
`2. **Scan the code.** List the top-level directories and files, read the package/build manifest and the README (first ~3000 chars), and collect recent commit subjects. Pass this back as \`codebase_scan\` on the next \`setup\` call. **Never include secrets** \u2014 skip \`.env\` and any credential, key or certificate file, and do not paste tokens, passwords or connection strings into the summary. PAPI also strips secret-shaped values server-side, but do not rely on that: leave them out.`,
|
|
8852
9249
|
`3. **Scan the docs.** Look through \`docs/\`, design notes, ADRs/decision records, and any roadmap or planning files \u2014 not just code.`,
|
|
8853
9250
|
`4. **Check for sibling / separate repos.** Many projects span more than one repo (a separate frontend, backend, infra, or mobile repo, or other packages in a monorepo). Ask the user whether any related repos or directories exist, and scan those too.`,
|
|
8854
9251
|
`5. **Ask the user to point you at extra context.** Prompt them: "Is there anything else I should read before setting this up \u2014 a PRD or spec, decision records, a roadmap, or links to related repos or docs?" Fold whatever they share into the scan.`,
|
|
@@ -9725,6 +10122,13 @@ function formatBlockerWaiting(blocker) {
|
|
|
9725
10122
|
}
|
|
9726
10123
|
}
|
|
9727
10124
|
|
|
10125
|
+
// src/lib/adapter-capability.ts
|
|
10126
|
+
function adapterSupports(adapter2, method) {
|
|
10127
|
+
const declared = adapter2.supportsMethod;
|
|
10128
|
+
if (typeof declared === "function") return declared.call(adapter2, method);
|
|
10129
|
+
return typeof adapter2[method] === "function";
|
|
10130
|
+
}
|
|
10131
|
+
|
|
9728
10132
|
// src/lib/tool-telemetry.ts
|
|
9729
10133
|
var INSTRUMENTED_TOOLS = /* @__PURE__ */ new Set([
|
|
9730
10134
|
"plan",
|
|
@@ -9747,7 +10151,7 @@ function measureResultBytes(content) {
|
|
|
9747
10151
|
}
|
|
9748
10152
|
function recordToolRun(adapter2, sample) {
|
|
9749
10153
|
if (sample.toolName !== PLAN_PREPARE_TOOL_NAME && !WRAPPER_INSTRUMENTED_TOOLS.has(sample.toolName)) return;
|
|
9750
|
-
if (
|
|
10154
|
+
if (!adapterSupports(adapter2, "insertToolRun")) return;
|
|
9751
10155
|
try {
|
|
9752
10156
|
const promise = adapter2.insertToolRun({
|
|
9753
10157
|
toolName: sample.toolName,
|
|
@@ -9821,7 +10225,7 @@ function isProjectOwner(callerUserId, ownerUserId) {
|
|
|
9821
10225
|
return caller === owner;
|
|
9822
10226
|
}
|
|
9823
10227
|
async function resolveOwnerGate(adapter2, config2) {
|
|
9824
|
-
if (
|
|
10228
|
+
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
9825
10229
|
try {
|
|
9826
10230
|
const identity = await adapter2.getOwnerIdentity();
|
|
9827
10231
|
const callerUserId = identity.callerUserId ?? config2.userId ?? null;
|
|
@@ -9843,7 +10247,7 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
9843
10247
|
};
|
|
9844
10248
|
}
|
|
9845
10249
|
}
|
|
9846
|
-
if (
|
|
10250
|
+
if (adapterSupports(adapter2, "getProjectOwnerUserId")) {
|
|
9847
10251
|
let ownerUserId = null;
|
|
9848
10252
|
let resolutionError;
|
|
9849
10253
|
try {
|
|
@@ -12345,7 +12749,9 @@ async function buildSessionGuidance(callerKey) {
|
|
|
12345
12749
|
|
|
12346
12750
|
// src/services/onboarding-coaching.ts
|
|
12347
12751
|
var ONBOARDING_EARLY_CYCLE_MAX = 2;
|
|
12752
|
+
var ONBOARDING_FIRST_CYCLE_MAX = 1;
|
|
12348
12753
|
var MAX_COACHING_LINES = 4;
|
|
12754
|
+
var COACH_EXPLAIN_VALUE = "First cycle: before you run each step, tell the user in one line what it produces and why it is worth the wait, then run it. Say what it means for their project, not what the tool does.";
|
|
12349
12755
|
var COACH_CONNECT_REPO = "No repository is linked to this project yet. Link your repo from the dashboard Settings (or capture it during `setup`) so builds, reviews, and releases attach to the right codebase.";
|
|
12350
12756
|
var COACH_ROOT_DIR = "Before building, confirm this session is running from your project root directory, so commits and builds land against the right files.";
|
|
12351
12757
|
var COACH_CLICKABLE_TASKS = "Task cards on your dashboard expand on click. Open one to read its full build handoff, comments, and history.";
|
|
@@ -12357,10 +12763,14 @@ var ONBOARDING_COACHING_HEADING = "## Getting Started";
|
|
|
12357
12763
|
function buildOnboardingCoaching(state) {
|
|
12358
12764
|
const lines = [];
|
|
12359
12765
|
const isEarly = state.cycleNumber <= ONBOARDING_EARLY_CYCLE_MAX;
|
|
12766
|
+
const isFirstRun = state.cycleNumber <= ONBOARDING_FIRST_CYCLE_MAX;
|
|
12360
12767
|
const { surface } = state;
|
|
12361
12768
|
if (state.repoConnected === false && (surface === "orient" || surface === "plan")) {
|
|
12362
12769
|
lines.push(COACH_CONNECT_REPO);
|
|
12363
12770
|
}
|
|
12771
|
+
if (isFirstRun) {
|
|
12772
|
+
lines.push(COACH_EXPLAIN_VALUE);
|
|
12773
|
+
}
|
|
12364
12774
|
if (state.hasLocalWorkspace && (surface === "setup" || surface === "orient" && isEarly)) {
|
|
12365
12775
|
lines.push(COACH_ROOT_DIR);
|
|
12366
12776
|
}
|
|
@@ -12500,7 +12910,7 @@ function savePrepareContextFile(projectId, callerKey, content) {
|
|
|
12500
12910
|
var planPrepareCache = new PerCallerCache();
|
|
12501
12911
|
var planTool = {
|
|
12502
12912
|
name: "plan",
|
|
12503
|
-
description: '
|
|
12913
|
+
description: 'Turn a backlog into one scoped cycle of work, with a written spec for every task in it. plan reads the whole board, the decisions this project has already taken, and how much it actually delivered in recent cycles, then prioritises what to do next and writes a per-task BUILD HANDOFF: scope, what is deliberately out of scope, acceptance criteria, files likely touched, security notes, and the shared branch each task belongs on. It also reports board health, so stale, blocked and drifting work is visible instead of accumulating. This is not the same as planning inside one session: the cycle, the specs and the reasoning are stored, so the next session, the next tool, or the next person picks up where this one stopped. Run once per cycle, after setup the first time, or after completing all builds AND running release for the previous cycle. NEVER call when unbuilt cycle tasks exist: build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs, and generate handoffs separately via `handoff_generate`.',
|
|
12504
12914
|
annotations: { title: "Plan Cycle", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
12505
12915
|
inputSchema: {
|
|
12506
12916
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -17395,6 +17805,33 @@ async function ensureDesignHookRegistered(projectRoot) {
|
|
|
17395
17805
|
var TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
17396
17806
|
var CONVENTIONS_SENTINEL = "<!-- PAPI_CONVENTIONS -->";
|
|
17397
17807
|
var CONVENTIONS_HEADING = "## Code Style Conventions";
|
|
17808
|
+
function describeSeedWriteFailure(err) {
|
|
17809
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
17810
|
+
const CLASSES = [
|
|
17811
|
+
[
|
|
17812
|
+
/\b(?:401|403)\b|permission|unauthor|forbidden/i,
|
|
17813
|
+
"the storage backend refused the write (permission denied)."
|
|
17814
|
+
],
|
|
17815
|
+
[
|
|
17816
|
+
/\b(?:5\d\d)\b|internal server error|bad gateway|unavailable/i,
|
|
17817
|
+
"the storage backend returned a server error."
|
|
17818
|
+
],
|
|
17819
|
+
[
|
|
17820
|
+
/timeout|timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|fetch failed|network/i,
|
|
17821
|
+
"PAPI could not reach the storage backend."
|
|
17822
|
+
],
|
|
17823
|
+
[
|
|
17824
|
+
/duplicate key|unique constraint|23505/i,
|
|
17825
|
+
"a decision with that id already exists."
|
|
17826
|
+
],
|
|
17827
|
+
[
|
|
17828
|
+
/violates|constraint|invalid input syntax|22P02|23\d{3}/i,
|
|
17829
|
+
"the storage backend rejected the decision record."
|
|
17830
|
+
]
|
|
17831
|
+
];
|
|
17832
|
+
const matched = CLASSES.find(([pattern]) => pattern.test(raw));
|
|
17833
|
+
return matched ? `could not be saved \u2014 ${matched[1]}` : "could not be saved \u2014 the write to the storage backend failed.";
|
|
17834
|
+
}
|
|
17398
17835
|
async function applySetupOutputs(adapter2, config2, input, collector, briefText, adSeedText, conventionsText) {
|
|
17399
17836
|
const warnings = [];
|
|
17400
17837
|
await adapter2.updateProductBrief(briefText);
|
|
@@ -17434,9 +17871,19 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
17434
17871
|
let seededAds = 0;
|
|
17435
17872
|
let skippedAds = 0;
|
|
17436
17873
|
if (adSeedText) {
|
|
17874
|
+
let ads;
|
|
17875
|
+
let adSeedFailed = false;
|
|
17437
17876
|
try {
|
|
17438
17877
|
const cleaned = adSeedText.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
17439
|
-
|
|
17878
|
+
ads = JSON.parse(cleaned);
|
|
17879
|
+
} catch (err) {
|
|
17880
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17881
|
+
warnings.push(
|
|
17882
|
+
`AD seeding failed \u2014 active decisions were not created. Check that your ad_seed_response is valid JSON. Error: ${msg}`
|
|
17883
|
+
);
|
|
17884
|
+
adSeedFailed = true;
|
|
17885
|
+
}
|
|
17886
|
+
try {
|
|
17440
17887
|
if (Array.isArray(ads)) {
|
|
17441
17888
|
const existingAdIds = adapter2.getActiveDecisions ? new Set(
|
|
17442
17889
|
(await adapter2.getActiveDecisions({ includeRetired: true }).catch(() => [])).map((a) => a.displayId)
|
|
@@ -17460,18 +17907,17 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
17460
17907
|
}
|
|
17461
17908
|
}
|
|
17462
17909
|
} catch (err) {
|
|
17463
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
17464
17910
|
warnings.push(
|
|
17465
|
-
`
|
|
17911
|
+
`Active Decisions: ${describeSeedWriteFailure(err)}` + (seededAds > 0 ? ` ${seededAds} decision(s) were created before this and are saved \u2014 re-running \`setup\` will not duplicate them.` : " No decisions were created.") + " This is a PAPI-side failure, not a problem with your response."
|
|
17466
17912
|
);
|
|
17467
|
-
|
|
17913
|
+
adSeedFailed = true;
|
|
17468
17914
|
}
|
|
17469
17915
|
if (skippedAds > 0) {
|
|
17470
17916
|
warnings.push(
|
|
17471
17917
|
`Active Decisions: detected ${skippedAds} existing AD(s) and left them untouched${seededAds > 0 ? `; created ${seededAds} new one(s)` : " (none new to create)"}.`
|
|
17472
17918
|
);
|
|
17473
17919
|
} else if (seededAds === 0 && adSeedText) {
|
|
17474
|
-
if (!
|
|
17920
|
+
if (!adSeedFailed) {
|
|
17475
17921
|
warnings.push(
|
|
17476
17922
|
"AD seeding produced 0 active decisions \u2014 the JSON may be valid but empty or missing required `id` and `body` fields."
|
|
17477
17923
|
);
|
|
@@ -17532,6 +17978,61 @@ function isSecretFile(name) {
|
|
|
17532
17978
|
const lower = name.toLowerCase();
|
|
17533
17979
|
return SECRET_PATTERNS.some((p) => lower.includes(p));
|
|
17534
17980
|
}
|
|
17981
|
+
var SECRET_VALUE_PATTERNS = [
|
|
17982
|
+
// KEY=value / KEY: value where the KEY names a credential. Value must be
|
|
17983
|
+
// non-trivial so `API_KEY=` or `TOKEN=changeme` placeholders in docs are hit too.
|
|
17984
|
+
/\b([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_KEY|API_KEY|APIKEY|AUTH)[A-Z0-9_]*)\s*[:=]\s*\S+/gi,
|
|
17985
|
+
// Provider-issued key formats.
|
|
17986
|
+
/\bsk-[A-Za-z0-9_-]{16,}/g,
|
|
17987
|
+
// OpenAI / Anthropic style
|
|
17988
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
|
|
17989
|
+
// GitHub tokens
|
|
17990
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
17991
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
17992
|
+
// AWS access key id
|
|
17993
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
17994
|
+
// Slack
|
|
17995
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
17996
|
+
// JWT
|
|
17997
|
+
// Credentials embedded in a connection string.
|
|
17998
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s@]+@/gi,
|
|
17999
|
+
// Whole PEM blocks.
|
|
18000
|
+
/-----BEGIN[^-]*PRIVATE KEY-----[\s\S]*?-----END[^-]*PRIVATE KEY-----/g,
|
|
18001
|
+
/\bBearer\s+[A-Za-z0-9._-]{20,}/g
|
|
18002
|
+
];
|
|
18003
|
+
var REDACTED = "[redacted by PAPI]";
|
|
18004
|
+
function redactPackageJson(pkg) {
|
|
18005
|
+
if (!pkg) return pkg;
|
|
18006
|
+
const out = { ...pkg };
|
|
18007
|
+
for (const field of ["description", "name"]) {
|
|
18008
|
+
if (typeof out[field] === "string") out[field] = redactSecretValues(out[field]);
|
|
18009
|
+
}
|
|
18010
|
+
return out;
|
|
18011
|
+
}
|
|
18012
|
+
function normaliseClientScan(raw) {
|
|
18013
|
+
return {
|
|
18014
|
+
topLevelDirs: raw.topLevelDirs ?? [],
|
|
18015
|
+
topLevelFiles: (raw.topLevelFiles ?? []).filter((f) => !isSecretFile(f)),
|
|
18016
|
+
packageJson: redactPackageJson(raw.packageJson),
|
|
18017
|
+
readme: redactSecretValues(raw.readme),
|
|
18018
|
+
configFiles: (raw.configFiles ?? []).filter((f) => !isSecretFile(f)),
|
|
18019
|
+
sourceFiles: raw.sourceFiles ?? [],
|
|
18020
|
+
totalFiles: raw.totalFiles ?? 0,
|
|
18021
|
+
sourcePaths: (raw.sourcePaths ?? []).filter((p) => !isSecretFile(p)),
|
|
18022
|
+
commitHistory: raw.commitHistory?.map((c) => redactSecretValues(c))
|
|
18023
|
+
};
|
|
18024
|
+
}
|
|
18025
|
+
function redactSecretValues(text) {
|
|
18026
|
+
if (!text) return text;
|
|
18027
|
+
let out = text;
|
|
18028
|
+
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
18029
|
+
out = out.replace(pattern, (match) => {
|
|
18030
|
+
const kv = match.match(/^([A-Z0-9_]+)\s*[:=]/i);
|
|
18031
|
+
return kv ? `${kv[1]}=${REDACTED}` : REDACTED;
|
|
18032
|
+
});
|
|
18033
|
+
}
|
|
18034
|
+
return out;
|
|
18035
|
+
}
|
|
17535
18036
|
async function safeReadFile(filePath, maxBytes = 1e4) {
|
|
17536
18037
|
if (isSecretFile(basename(filePath))) return null;
|
|
17537
18038
|
try {
|
|
@@ -17721,18 +18222,7 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
17721
18222
|
const hasUserSignalForScan = Boolean(input.description?.trim()) || Boolean(input.targetUsers?.trim());
|
|
17722
18223
|
const shouldScan = isExistingProject || !hasUserSignalForScan;
|
|
17723
18224
|
if (input.codebaseScan) {
|
|
17724
|
-
const
|
|
17725
|
-
const clientScan = {
|
|
17726
|
-
topLevelDirs: raw.topLevelDirs ?? [],
|
|
17727
|
-
topLevelFiles: raw.topLevelFiles ?? [],
|
|
17728
|
-
packageJson: raw.packageJson,
|
|
17729
|
-
readme: raw.readme,
|
|
17730
|
-
configFiles: raw.configFiles ?? [],
|
|
17731
|
-
sourceFiles: raw.sourceFiles ?? [],
|
|
17732
|
-
totalFiles: raw.totalFiles ?? 0,
|
|
17733
|
-
sourcePaths: raw.sourcePaths ?? [],
|
|
17734
|
-
commitHistory: raw.commitHistory
|
|
17735
|
-
};
|
|
18225
|
+
const clientScan = normaliseClientScan(input.codebaseScan);
|
|
17736
18226
|
const hasScanSignal = Boolean(clientScan.readme) || Boolean(clientScan.packageJson) || Boolean(clientScan.commitHistory && clientScan.commitHistory.length > 0) || clientScan.topLevelDirs.length > 0 || clientScan.topLevelFiles.length > 0;
|
|
17737
18227
|
if (hasScanSignal) {
|
|
17738
18228
|
codebaseSummary = formatCodebaseSummary(clientScan, sourceContents);
|
|
@@ -17747,7 +18237,10 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
17747
18237
|
}
|
|
17748
18238
|
} else if (shouldScan && !canScanFilesystem && input.existingProject === true) {
|
|
17749
18239
|
warnings.push(
|
|
17750
|
-
|
|
18240
|
+
// task-3293: this used to end at "cannot read your local files", which read as
|
|
18241
|
+
// a flat contradiction to the very next thing the user saw — their own agent
|
|
18242
|
+
// reading those files. Name the division of labour instead of just the limit.
|
|
18243
|
+
"Codebase scan skipped \u2014 PAPI has no access to your machine and never reads your files directly. Your AI client can: ask it to scan the project and pass the summary back as `codebase_scan` (leaving out `.env` and anything holding keys, tokens or passwords). Or provide `description` and `target_users` directly and the brief generator will use those instead."
|
|
17751
18244
|
);
|
|
17752
18245
|
}
|
|
17753
18246
|
const hasCodebaseSignal = Boolean(codebaseSummary && codebaseSummary.trim().length > 0);
|
|
@@ -18077,7 +18570,7 @@ async function ensureMcpJsonGitignored(projectRoot) {
|
|
|
18077
18570
|
// src/tools/setup.ts
|
|
18078
18571
|
var setupTool = {
|
|
18079
18572
|
name: "setup",
|
|
18080
|
-
description: '
|
|
18573
|
+
description: 'Give a project a memory, so every later session starts with its context instead of a blank slate. Setup reads the project, writes a Product Brief that says what it is for and who it serves, records the first Active Decisions and build conventions so settled answers outlive the session that found them, and installs the workflow instructions the coding assistant follows from then on. Everything after this (planning, building, reviewing) reads from what setup writes. It takes a few minutes and runs in two passes: the first returns prompts for you to execute, the second saves the results. Tell the user what each pass is doing while it runs. Run after configuring your MCP credentials (via `init` or manually from getpapi.ai). Only project_name is required \u2014 description and target_users are derived from README, package.json, and commit history when omitted. Set existing_project: true to adopt an existing codebase. ADOPTING AN EXISTING PROJECT OVER A REMOTE/HOSTED CONNECTOR (no local stdio install): PAPI cannot read your filesystem, so YOU (the client) must gather a `codebase_scan` and pass it in \u2014 list top-level dirs/files, the package manifest, the README (first ~3000 chars), and recent commit subjects. Without it, adoption falls back to asking for description/target_users. On a local stdio install PAPI scans the tree itself, so `codebase_scan` is optional there. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run `plan` to start your first cycle.',
|
|
18081
18574
|
annotations: { title: "Set Up Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
18082
18575
|
inputSchema: {
|
|
18083
18576
|
type: "object",
|
|
@@ -18261,6 +18754,36 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
|
|
|
18261
18754
|
Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
|
|
18262
18755
|
);
|
|
18263
18756
|
}
|
|
18757
|
+
var SETUP_NARRATION_HEADING = "**What happens now, and how long it takes**";
|
|
18758
|
+
function formatSetupNarration(result) {
|
|
18759
|
+
const steps = [];
|
|
18760
|
+
if (result.preScanInstruction || result.newProjectInstruction) {
|
|
18761
|
+
steps.push("Gather the context below, so what follows describes this project rather than a generic one.");
|
|
18762
|
+
}
|
|
18763
|
+
if (result.briefPrompt) {
|
|
18764
|
+
steps.push("Write the Product Brief: what this project is, who it is for, and what it has to get right. Every later plan reads it.");
|
|
18765
|
+
}
|
|
18766
|
+
if (result.adSeedPrompt) {
|
|
18767
|
+
steps.push("Record the first Active Decisions, so the choices already made stop being re-argued in later sessions.");
|
|
18768
|
+
}
|
|
18769
|
+
if (result.conventionsPrompt) {
|
|
18770
|
+
steps.push("Capture the build conventions this project expects, so they reach every future build without being restated.");
|
|
18771
|
+
}
|
|
18772
|
+
if (result.northStarPrompt) {
|
|
18773
|
+
steps.push("Agree a North Star with the user, so progress can be judged against something.");
|
|
18774
|
+
}
|
|
18775
|
+
if (result.initialTasksPrompt) {
|
|
18776
|
+
steps.push("Seed the backlog, so the first planning cycle has real work to choose from.");
|
|
18777
|
+
}
|
|
18778
|
+
steps.push('Call `setup` again with `mode: "apply"` to save all of it.');
|
|
18779
|
+
return [
|
|
18780
|
+
SETUP_NARRATION_HEADING,
|
|
18781
|
+
"",
|
|
18782
|
+
"Expect a few minutes end to end. Nothing is saved until the final step, so do not stop partway.",
|
|
18783
|
+
"Say what you are doing as you go, in one line per step, so the wait is visible to the user:",
|
|
18784
|
+
...steps.map((s, i) => `${i + 1}. ${s}`)
|
|
18785
|
+
];
|
|
18786
|
+
}
|
|
18264
18787
|
async function handleSetup(adapter2, config2, args, clientName) {
|
|
18265
18788
|
const toolMode = args.mode;
|
|
18266
18789
|
const REQUIRED_FIELDS = ["project_name"];
|
|
@@ -18304,6 +18827,7 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
18304
18827
|
result.createdProject ? `Project "${result.projectName}" scaffolded \u2014 database tables created.
|
|
18305
18828
|
` : ""
|
|
18306
18829
|
];
|
|
18830
|
+
sections.push(...formatSetupNarration(result), "");
|
|
18307
18831
|
if (result.autoDetected) {
|
|
18308
18832
|
sections.push(
|
|
18309
18833
|
`**Codebase detected:** Existing codebase found \u2014 running in adoption mode. If this is wrong, re-run setup with \`existing_project: false\`.`,
|
|
@@ -18642,7 +19166,7 @@ function isDecisionPending(task) {
|
|
|
18642
19166
|
return d != null && d.answer == null;
|
|
18643
19167
|
}
|
|
18644
19168
|
async function countFailedAttempts(adapter2, taskId) {
|
|
18645
|
-
if (
|
|
19169
|
+
if (!adapterSupports(adapter2, "getFailedBuildAttemptsForTask")) return null;
|
|
18646
19170
|
try {
|
|
18647
19171
|
return await adapter2.getFailedBuildAttemptsForTask(taskId);
|
|
18648
19172
|
} catch {
|
|
@@ -19055,7 +19579,7 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
19055
19579
|
boardHealth: existing?.boardHealth ?? "",
|
|
19056
19580
|
taskIds: existing?.taskIds ?? []
|
|
19057
19581
|
};
|
|
19058
|
-
if (
|
|
19582
|
+
if (adapterSupports(adapter2, "commitRelease")) {
|
|
19059
19583
|
try {
|
|
19060
19584
|
await adapter2.commitRelease({ cycle: completedCycle, snapshot: snapshot ?? null });
|
|
19061
19585
|
} catch (err) {
|
|
@@ -20260,16 +20784,20 @@ function triggerSurfaceHitsOnBranch(projectRoot, baseRef = "origin/main") {
|
|
|
20260
20784
|
}
|
|
20261
20785
|
|
|
20262
20786
|
// src/services/build.ts
|
|
20263
|
-
function selectAutostashPaths(modified, untracked) {
|
|
20787
|
+
function selectAutostashPaths(modified, untracked, conflictingPaths) {
|
|
20264
20788
|
const untrackedSet = new Set(untracked);
|
|
20265
|
-
const isDocsPath = (p) => p === "docs" || p.startsWith("docs/");
|
|
20266
20789
|
const isUntrackedDirEntry = (p) => p.endsWith("/") && untracked.some((u) => u.startsWith(p));
|
|
20267
20790
|
const trackedDirty = modified.filter(
|
|
20268
20791
|
(p) => !untrackedSet.has(p) && !isUntrackedDirEntry(p)
|
|
20269
20792
|
);
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20793
|
+
if (conflictingPaths == null) {
|
|
20794
|
+
return { toStash: trackedDirty, preservedDocs: untracked };
|
|
20795
|
+
}
|
|
20796
|
+
const conflicting = new Set(conflictingPaths);
|
|
20797
|
+
return {
|
|
20798
|
+
toStash: trackedDirty.filter((p) => conflicting.has(p)),
|
|
20799
|
+
preservedDocs: untracked
|
|
20800
|
+
};
|
|
20273
20801
|
}
|
|
20274
20802
|
function resolveRelatedDecisions(provided, buildHandoff) {
|
|
20275
20803
|
const fromInput = (provided ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -20391,11 +20919,18 @@ function matchedPredictedEntry(changedPath, predicted) {
|
|
|
20391
20919
|
function isPathInPredictedScope(changedPath, predicted) {
|
|
20392
20920
|
return matchedPredictedEntry(changedPath, predicted) !== null;
|
|
20393
20921
|
}
|
|
20922
|
+
function verifyCommitContents(cwd, intended) {
|
|
20923
|
+
if (intended.length === 0) return [];
|
|
20924
|
+
const actual = getCommitFiles(cwd);
|
|
20925
|
+
if (actual === null) return [];
|
|
20926
|
+
const actualSet = new Set(actual);
|
|
20927
|
+
return intended.filter((p) => !actualSet.has(p));
|
|
20928
|
+
}
|
|
20394
20929
|
function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
20395
20930
|
const cwd = config2.projectRoot;
|
|
20396
20931
|
const message = `feat(${taskId}): ${taskTitle}`;
|
|
20397
|
-
if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
|
|
20398
|
-
if (!isGitRepo(cwd)) return "Auto-commit: skipped (not a git repository).";
|
|
20932
|
+
if (!isGitAvailable()) return { line: "Auto-commit: skipped (git not found).", missing: [] };
|
|
20933
|
+
if (!isGitRepo(cwd)) return { line: "Auto-commit: skipped (not a git repository).", missing: [] };
|
|
20399
20934
|
const safeRun = (fn) => {
|
|
20400
20935
|
try {
|
|
20401
20936
|
const r = fn();
|
|
@@ -20409,26 +20944,27 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
20409
20944
|
`^(feat|fix|chore|refactor|docs|test|style|build|perf|ci)\\(${taskId}\\):`
|
|
20410
20945
|
);
|
|
20411
20946
|
if (headSubject && taskCommitRe.test(headSubject)) {
|
|
20412
|
-
return `Auto-commit: skipped \u2014 HEAD already has a fresh ${taskId} commit (${headSubject})
|
|
20947
|
+
return { line: `Auto-commit: skipped \u2014 HEAD already has a fresh ${taskId} commit (${headSubject}).`, missing: [] };
|
|
20413
20948
|
}
|
|
20414
20949
|
const staged = getStagedFiles(cwd);
|
|
20415
20950
|
if (staged.length > 0) {
|
|
20416
|
-
|
|
20951
|
+
const line = safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
20952
|
+
return { line, missing: verifyCommitContents(cwd, staged) };
|
|
20417
20953
|
}
|
|
20418
20954
|
const checkpoint = readBuildCheckpointIfLocal({ cwd, taskId });
|
|
20419
20955
|
const headSha = getHeadCommitSha(cwd);
|
|
20420
20956
|
if (checkpoint?.lastCommitSha && headSha && checkpoint.lastCommitSha !== headSha) {
|
|
20421
20957
|
const leftover = getModifiedFiles(cwd);
|
|
20422
20958
|
if (leftover.length === 0) {
|
|
20423
|
-
return "Auto-commit: skipped (builder already committed; working tree clean).";
|
|
20959
|
+
return { line: "Auto-commit: skipped (builder already committed; working tree clean).", missing: [] };
|
|
20424
20960
|
}
|
|
20425
20961
|
const sample = leftover.slice(0, 10).join(", ");
|
|
20426
20962
|
const more = leftover.length > 10 ? ` (+${leftover.length - 10} more)` : "";
|
|
20427
|
-
return `Auto-commit: skipped \u2014 you already committed during this build, and ${leftover.length} file(s) are still modified. They were NOT committed, because at this point PAPI cannot tell your deliberately-excluded work from a concurrent session's files (task-3054). Left uncommitted: ${sample}${more}. If any belong to ${taskId}, \`git add\` them and re-run build_execute complete
|
|
20963
|
+
return { line: `Auto-commit: skipped \u2014 you already committed during this build, and ${leftover.length} file(s) are still modified. They were NOT committed, because at this point PAPI cannot tell your deliberately-excluded work from a concurrent session's files (task-3054). Left uncommitted: ${sample}${more}. If any belong to ${taskId}, \`git add\` them and re-run build_execute complete.`, missing: [] };
|
|
20428
20964
|
}
|
|
20429
20965
|
const modified = getModifiedFiles(cwd);
|
|
20430
20966
|
if (modified.length === 0) {
|
|
20431
|
-
return "Auto-commit: skipped (no working-tree changes).";
|
|
20967
|
+
return { line: "Auto-commit: skipped (no working-tree changes).", missing: [] };
|
|
20432
20968
|
}
|
|
20433
20969
|
const commitResult = safeRun(() => stageAllAndCommit(cwd, message));
|
|
20434
20970
|
if (predictedFiles && predictedFiles.length > 0) {
|
|
@@ -20437,13 +20973,22 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
20437
20973
|
if (outOfScope.length > 0) {
|
|
20438
20974
|
const sample = outOfScope.slice(0, 10).join(", ");
|
|
20439
20975
|
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
20440
|
-
return
|
|
20976
|
+
return {
|
|
20977
|
+
line: `${commitResult} (staged all ${modified.length} changed file(s)). \u2139\uFE0F Scope drift: ${outOfScope.length} committed file(s) were outside the handoff's FILES LIKELY TOUCHED \u2014 handoff under-predicted: ${sample}${more}.`,
|
|
20978
|
+
missing: verifyCommitContents(cwd, modified)
|
|
20979
|
+
};
|
|
20441
20980
|
}
|
|
20442
20981
|
const matches = modified.slice(0, 5).map((p) => `${p} \u2190 ${matchedPredictedEntry(p, cleanedPredicted) ?? "?"}`).join(", ");
|
|
20443
20982
|
const extra = modified.length > 5 ? ` (+${modified.length - 5} more)` : "";
|
|
20444
|
-
return
|
|
20983
|
+
return {
|
|
20984
|
+
line: `${commitResult} (staged all ${modified.length} changed file(s), each matched to FILES LIKELY TOUCHED: ${matches}${extra}).`,
|
|
20985
|
+
missing: verifyCommitContents(cwd, modified)
|
|
20986
|
+
};
|
|
20445
20987
|
}
|
|
20446
|
-
return
|
|
20988
|
+
return {
|
|
20989
|
+
line: `${commitResult} (staged all ${modified.length} changed file(s)).`,
|
|
20990
|
+
missing: verifyCommitContents(cwd, modified)
|
|
20991
|
+
};
|
|
20447
20992
|
}
|
|
20448
20993
|
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
20449
20994
|
const lines = [];
|
|
@@ -20592,7 +21137,7 @@ async function listBuilds(adapter2, config2) {
|
|
|
20592
21137
|
const [{ result: tasks, warnings }, health] = await Promise.all([
|
|
20593
21138
|
withBaseBranchSync(
|
|
20594
21139
|
{ projectRoot: config2.projectRoot, baseBranch: config2.baseBranch },
|
|
20595
|
-
() => adapter2.queryBoard()
|
|
21140
|
+
() => adapter2.queryBoard({ compact: true })
|
|
20596
21141
|
),
|
|
20597
21142
|
adapter2.getCycleHealth().catch(() => null)
|
|
20598
21143
|
]);
|
|
@@ -20600,14 +21145,16 @@ async function listBuilds(adapter2, config2) {
|
|
|
20600
21145
|
if (tasks.length === 0) {
|
|
20601
21146
|
return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: true, noHandoffs: false, currentCycle, totalTasks: tasks.length };
|
|
20602
21147
|
}
|
|
20603
|
-
const
|
|
20604
|
-
if (!t.buildHandoff) return false;
|
|
21148
|
+
const candidates = tasks.filter((t) => {
|
|
20605
21149
|
if (t.status === "Done" || t.status === "Cancelled" || t.status === "Blocked" || t.status === "Backlog" || t.status === "Deferred" || t.status === "Archived") {
|
|
20606
21150
|
return false;
|
|
20607
21151
|
}
|
|
20608
21152
|
if (currentCycle > 0 && t.cycle !== currentCycle) return false;
|
|
20609
21153
|
return true;
|
|
20610
21154
|
});
|
|
21155
|
+
const hydrated = candidates.length > 0 ? await adapter2.getTasks(candidates.map((t) => t.id)) : [];
|
|
21156
|
+
const byId = new Map(hydrated.map((t) => [t.id, t]));
|
|
21157
|
+
const withHandoff = candidates.map((t) => byId.get(t.id)).filter((t) => Boolean(t?.buildHandoff));
|
|
20611
21158
|
if (withHandoff.length === 0) {
|
|
20612
21159
|
return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: false, noHandoffs: true, currentCycle, totalTasks: tasks.length };
|
|
20613
21160
|
}
|
|
@@ -20859,20 +21406,23 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20859
21406
|
);
|
|
20860
21407
|
}
|
|
20861
21408
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
21409
|
+
const stashTarget = branchExists(config2.projectRoot, featureBranch) ? featureBranch : resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
21410
|
+
const conflictingPaths = getPathsDifferingFrom(config2.projectRoot, stashTarget);
|
|
20862
21411
|
const { toStash, preservedDocs } = selectAutostashPaths(
|
|
20863
21412
|
getModifiedFiles(config2.projectRoot),
|
|
20864
|
-
getUntrackedFiles(config2.projectRoot)
|
|
21413
|
+
getUntrackedFiles(config2.projectRoot),
|
|
21414
|
+
conflictingPaths
|
|
20865
21415
|
);
|
|
20866
|
-
const preservedNote = preservedDocs.length > 0 ? ` ${preservedDocs.length} untracked
|
|
21416
|
+
const preservedNote = preservedDocs.length > 0 ? ` ${preservedDocs.length} untracked file(s) left in place, not stashed (task-3282 \u2014 an untracked file never blocks a branch switch, and a sub-agent's brand-new file is untracked by definition).` : "";
|
|
20867
21417
|
if (toStash.length === 0) {
|
|
20868
21418
|
if (preservedDocs.length > 0) {
|
|
20869
|
-
branchLines.push(`Auto-stash skipped \u2014
|
|
21419
|
+
branchLines.push(`Auto-stash skipped \u2014 nothing tracked is dirty.${preservedNote}`);
|
|
20870
21420
|
}
|
|
20871
21421
|
} else {
|
|
20872
21422
|
const stashLabel = `papi-autostash/${taskId}-${Math.floor(Date.now() / 1e3)}`;
|
|
20873
21423
|
try {
|
|
20874
21424
|
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
20875
|
-
execFileSync7("git", ["stash", "push", "-
|
|
21425
|
+
execFileSync7("git", ["stash", "push", "-m", stashLabel, "--", ...toStash], {
|
|
20876
21426
|
cwd: config2.projectRoot,
|
|
20877
21427
|
encoding: "utf-8"
|
|
20878
21428
|
});
|
|
@@ -21102,7 +21652,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21102
21652
|
assertDeployVerification(config2, input, { deployingNow: options.light === true });
|
|
21103
21653
|
const [healthResult, priorCount] = await Promise.all([
|
|
21104
21654
|
adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
|
|
21105
|
-
|
|
21655
|
+
adapterSupports(adapter2, "getBuildReportCountForTask") ? adapter2.getBuildReportCountForTask(taskId).catch(() => 0) : Promise.resolve(0)
|
|
21106
21656
|
]);
|
|
21107
21657
|
const cycleNumber = healthResult.totalCycles;
|
|
21108
21658
|
const iterationCount = priorCount + 1;
|
|
@@ -21146,7 +21696,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21146
21696
|
console.error(
|
|
21147
21697
|
`[build] task ${taskId} related_decisions: ${relatedAdIds.length} AD(s) (${relatedAdIds.length === 0 ? "none" : relatedInferred ? "inferred from handoff" : "builder-provided"}).`
|
|
21148
21698
|
);
|
|
21149
|
-
if (report.startedAt && report.completedAt &&
|
|
21699
|
+
if (report.startedAt && report.completedAt && adapterSupports(adapter2, "getToolCallCount")) {
|
|
21150
21700
|
try {
|
|
21151
21701
|
const count = await adapter2.getToolCallCount(report.startedAt, report.completedAt);
|
|
21152
21702
|
if (count > 0) report.toolCallCount = count;
|
|
@@ -21159,7 +21709,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21159
21709
|
const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${iterNote}${surpriseNote}${issueNote}`;
|
|
21160
21710
|
const statusChange = input.completed === "yes" ? { from: task.status, to: options.light ? "Done" : "In Review" } : void 0;
|
|
21161
21711
|
let atomicCommitDone = false;
|
|
21162
|
-
if (
|
|
21712
|
+
if (adapterSupports(adapter2, "commitBuildComplete")) {
|
|
21163
21713
|
await adapter2.commitBuildComplete({
|
|
21164
21714
|
report,
|
|
21165
21715
|
taskId,
|
|
@@ -21172,7 +21722,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21172
21722
|
await adapter2.appendBuildReport(report);
|
|
21173
21723
|
}
|
|
21174
21724
|
let reportWriteVerified;
|
|
21175
|
-
if (
|
|
21725
|
+
if (adapterSupports(adapter2, "getBuildReportCountForTask")) {
|
|
21176
21726
|
try {
|
|
21177
21727
|
const postWriteCount = await adapter2.getBuildReportCountForTask(taskId);
|
|
21178
21728
|
reportWriteVerified = postWriteCount >= iterationCount;
|
|
@@ -21290,7 +21840,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21290
21840
|
if (row.findingKey) insertedByKey.set(row.findingKey, row.inserted);
|
|
21291
21841
|
}
|
|
21292
21842
|
const haveInsertSignal = appendedRows.length > 0;
|
|
21293
|
-
if (findingRows.length > 0 &&
|
|
21843
|
+
if (findingRows.length > 0 && adapterSupports(adapter2, "createTask")) {
|
|
21294
21844
|
for (const { finding, learning } of findingRows) {
|
|
21295
21845
|
if (learning.category !== "issue") continue;
|
|
21296
21846
|
if (finding.disposition !== "filed") continue;
|
|
@@ -21433,7 +21983,20 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21433
21983
|
} else if (dbOnlyMode) {
|
|
21434
21984
|
commitLine = DB_ONLY_COMPLETE_NOTICE;
|
|
21435
21985
|
} else {
|
|
21436
|
-
|
|
21986
|
+
const outcome = autoCommit(config2, taskId, task.title, task.buildHandoff?.filesLikelyTouched);
|
|
21987
|
+
commitLine = outcome.line;
|
|
21988
|
+
if (outcome.missing.length > 0) {
|
|
21989
|
+
const sample = outcome.missing.slice(0, 10).join(", ");
|
|
21990
|
+
const more = outcome.missing.length > 10 ? ` (+${outcome.missing.length - 10} more)` : "";
|
|
21991
|
+
if (input.completed === "yes" && !atomicCommitDone) {
|
|
21992
|
+
try {
|
|
21993
|
+
await adapter2.updateTaskStatus(taskId, task.status);
|
|
21994
|
+
} catch {
|
|
21995
|
+
}
|
|
21996
|
+
}
|
|
21997
|
+
commitLine = `\u26A0\uFE0F AUTO-COMMIT VERIFICATION FAILED \u2014 the commit does NOT contain ${outcome.missing.length} file(s) that were staged for it. Missing: ${sample}${more}. This is the task-3029 failure mode: a pre-commit hook reverted modified files while staged adds/renames survived, so the commit is PARTIAL and will not build. ${taskId} was NOT advanced \u2014 it stays ${task.status}. Fix: inspect \`git show --stat HEAD\`, restore the missing changes, \`git add\` them, and re-run build_execute complete.
|
|
21998
|
+
(Original commit note: ${outcome.line})`;
|
|
21999
|
+
}
|
|
21437
22000
|
}
|
|
21438
22001
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
21439
22002
|
const sha = getHeadCommitSha(config2.projectRoot);
|
|
@@ -21603,6 +22166,216 @@ async function cancelBuild(adapter2, taskId, reason, projectRoot) {
|
|
|
21603
22166
|
return { task, reason };
|
|
21604
22167
|
}
|
|
21605
22168
|
|
|
22169
|
+
// src/tools/conventions.ts
|
|
22170
|
+
var MAX_CONVENTION_RULE_LENGTH = 2e3;
|
|
22171
|
+
var MAX_CONVENTION_MODULE_LENGTH = 120;
|
|
22172
|
+
var MAX_INJECTED_CONVENTIONS = 25;
|
|
22173
|
+
var MAX_INJECTED_CONVENTION_CHARS = 8e3;
|
|
22174
|
+
var CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
22175
|
+
function sanitiseConventionRule(raw) {
|
|
22176
|
+
if (typeof raw !== "string") return null;
|
|
22177
|
+
const stripped = raw.replace(CONTROL_CHARS, "").trim();
|
|
22178
|
+
if (!stripped) return null;
|
|
22179
|
+
return stripped.slice(0, MAX_CONVENTION_RULE_LENGTH);
|
|
22180
|
+
}
|
|
22181
|
+
function sanitiseConventionModule(raw) {
|
|
22182
|
+
if (typeof raw !== "string") return void 0;
|
|
22183
|
+
const stripped = raw.replace(CONTROL_CHARS, "").trim();
|
|
22184
|
+
if (!stripped) return void 0;
|
|
22185
|
+
return stripped.slice(0, MAX_CONVENTION_MODULE_LENGTH);
|
|
22186
|
+
}
|
|
22187
|
+
function selectConventionsForModule(conventions, module) {
|
|
22188
|
+
return conventions.filter((c) => !c.module || module !== void 0 && c.module === module);
|
|
22189
|
+
}
|
|
22190
|
+
var CONVENTIONS_HEADING2 = "**PROJECT CONVENTIONS \u2014 settled rules for this project**";
|
|
22191
|
+
function formatConventionsSection(conventions, module) {
|
|
22192
|
+
const applicable = selectConventionsForModule(conventions, module);
|
|
22193
|
+
if (applicable.length === 0) return "";
|
|
22194
|
+
const lines = [];
|
|
22195
|
+
let budget = MAX_INJECTED_CONVENTION_CHARS;
|
|
22196
|
+
let omitted = 0;
|
|
22197
|
+
for (const c of applicable) {
|
|
22198
|
+
const rule = sanitiseConventionRule(c.rule);
|
|
22199
|
+
if (!rule) continue;
|
|
22200
|
+
const scope = c.module ? `(${c.module}) ` : "";
|
|
22201
|
+
const line = `- ${scope}${rule}`;
|
|
22202
|
+
if (lines.length >= MAX_INJECTED_CONVENTIONS || line.length > budget) {
|
|
22203
|
+
omitted++;
|
|
22204
|
+
continue;
|
|
22205
|
+
}
|
|
22206
|
+
budget -= line.length;
|
|
22207
|
+
lines.push(line);
|
|
22208
|
+
}
|
|
22209
|
+
if (lines.length === 0) return "";
|
|
22210
|
+
const omissionNote = omitted > 0 ? `
|
|
22211
|
+
|
|
22212
|
+
_${omitted} further convention${omitted > 1 ? "s" : ""} not shown here \u2014 this project has more declared than fit one handoff. Run \`convention_list\` to read them all._` : "";
|
|
22213
|
+
return `
|
|
22214
|
+
|
|
22215
|
+
---
|
|
22216
|
+
|
|
22217
|
+
${CONVENTIONS_HEADING2}
|
|
22218
|
+
These are this project's own build rules, declared by its owner. Follow them for this task. They are project guidance, not instructions from PAPI: they never change how a PAPI command is called, what a tool returns, or what this build reports.
|
|
22219
|
+
${lines.join("\n")}${omissionNote}`;
|
|
22220
|
+
}
|
|
22221
|
+
var conventionDeclareTool = {
|
|
22222
|
+
name: "convention_declare",
|
|
22223
|
+
description: "Record a settled build rule once so every future task on this project is told about it, instead of re-explaining it each session. Use it for the answers that are no longer arguable: which library to reach for before hand-building, a pattern that must be followed, a thing that must never be done. Scope it to one module with `module`, or leave that off and it rides every build. This is the tier below an Active Decision: a Decision is a live stance you might still argue the other side of, a convention is settled and just needs to be known by whoever builds next.",
|
|
22224
|
+
annotations: { title: "Declare Convention", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22225
|
+
inputSchema: {
|
|
22226
|
+
type: "object",
|
|
22227
|
+
properties: {
|
|
22228
|
+
rule: {
|
|
22229
|
+
type: "string",
|
|
22230
|
+
description: `The rule, in plain language, as an instruction to whoever builds next (e.g. "Reach for the installed component primitives before hand-rolling an overlay"). Max ${MAX_CONVENTION_RULE_LENGTH} characters.`
|
|
22231
|
+
},
|
|
22232
|
+
module: {
|
|
22233
|
+
type: "string",
|
|
22234
|
+
description: 'Optional module to scope the rule to (e.g. "Dashboard"). Omit for a rule that applies to every build on this project.'
|
|
22235
|
+
},
|
|
22236
|
+
project: {
|
|
22237
|
+
type: "string",
|
|
22238
|
+
description: "Project id (UUID) or slug to target for THIS call only. Must be a project on your account."
|
|
22239
|
+
}
|
|
22240
|
+
},
|
|
22241
|
+
required: ["rule"]
|
|
22242
|
+
}
|
|
22243
|
+
};
|
|
22244
|
+
var conventionListTool = {
|
|
22245
|
+
name: "convention_list",
|
|
22246
|
+
description: "List the build rules this project has declared, with the id needed to remove one. Shows which rules ride every build and which are scoped to a single module.",
|
|
22247
|
+
annotations: { title: "List Conventions", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
22248
|
+
inputSchema: {
|
|
22249
|
+
type: "object",
|
|
22250
|
+
properties: {
|
|
22251
|
+
module: {
|
|
22252
|
+
type: "string",
|
|
22253
|
+
description: "Optional: show only the rules that would apply to a build in this module (project-wide rules plus that module's own)."
|
|
22254
|
+
},
|
|
22255
|
+
project: {
|
|
22256
|
+
type: "string",
|
|
22257
|
+
description: "Project id (UUID) or slug to target for THIS call only. Must be a project on your account."
|
|
22258
|
+
}
|
|
22259
|
+
},
|
|
22260
|
+
required: []
|
|
22261
|
+
}
|
|
22262
|
+
};
|
|
22263
|
+
var conventionRemoveTool = {
|
|
22264
|
+
name: "convention_remove",
|
|
22265
|
+
description: "Remove a declared build rule so it stops appearing in future builds. Takes the id from `convention_list`. Existing handoffs already generated are unaffected; the next build no longer carries it.",
|
|
22266
|
+
annotations: { title: "Remove Convention", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
22267
|
+
inputSchema: {
|
|
22268
|
+
type: "object",
|
|
22269
|
+
properties: {
|
|
22270
|
+
id: {
|
|
22271
|
+
type: "string",
|
|
22272
|
+
description: "The convention id, from `convention_list`."
|
|
22273
|
+
},
|
|
22274
|
+
project: {
|
|
22275
|
+
type: "string",
|
|
22276
|
+
description: "Project id (UUID) or slug to target for THIS call only. Must be a project on your account."
|
|
22277
|
+
}
|
|
22278
|
+
},
|
|
22279
|
+
required: ["id"]
|
|
22280
|
+
}
|
|
22281
|
+
};
|
|
22282
|
+
var UNSUPPORTED = "Conventions are not available on this connection. The store lives in the PAPI database, so a project running on a file-backed adapter has nowhere to record one.";
|
|
22283
|
+
async function handleConventionDeclare(adapter2, config2, args) {
|
|
22284
|
+
const tracker = new ProgressTracker("validate");
|
|
22285
|
+
try {
|
|
22286
|
+
if (!adapter2.createConvention) return errorResponse(UNSUPPORTED);
|
|
22287
|
+
const rule = sanitiseConventionRule(args.rule);
|
|
22288
|
+
if (!rule) {
|
|
22289
|
+
return errorResponse(
|
|
22290
|
+
"A convention needs a rule. Pass `rule` with the instruction you want every future build on this project to follow."
|
|
22291
|
+
);
|
|
22292
|
+
}
|
|
22293
|
+
const wasTruncated = typeof args.rule === "string" && args.rule.trim().length > MAX_CONVENTION_RULE_LENGTH;
|
|
22294
|
+
const moduleScope = sanitiseConventionModule(args.module);
|
|
22295
|
+
tracker.mark("write");
|
|
22296
|
+
const created = await adapter2.createConvention({ rule, ...moduleScope ? { module: moduleScope } : {} });
|
|
22297
|
+
const scopeNote = moduleScope ? `It rides every build in the **${moduleScope}** module.` : "It rides every build on this project.";
|
|
22298
|
+
const truncationNote = wasTruncated ? `
|
|
22299
|
+
|
|
22300
|
+
\u26A0\uFE0F The rule was longer than ${MAX_CONVENTION_RULE_LENGTH} characters and was shortened to fit. Re-declare it shorter if the trimmed version reads wrong.` : "";
|
|
22301
|
+
return textResponse(
|
|
22302
|
+
`Convention recorded. ${scopeNote}
|
|
22303
|
+
|
|
22304
|
+
> ${rule}
|
|
22305
|
+
|
|
22306
|
+
Id: \`${created.id ?? "(unknown)"}\` \u2014 remove it with \`convention_remove\`.${truncationNote}`
|
|
22307
|
+
);
|
|
22308
|
+
} catch (err) {
|
|
22309
|
+
return errorResponse(formatStructuredError({
|
|
22310
|
+
tool: "convention_declare",
|
|
22311
|
+
mode: "declare",
|
|
22312
|
+
adapter: config2.adapterType,
|
|
22313
|
+
lastStep: tracker.lastStep,
|
|
22314
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22315
|
+
hint: defaultHint("convention_declare")
|
|
22316
|
+
}));
|
|
22317
|
+
}
|
|
22318
|
+
}
|
|
22319
|
+
async function handleConventionList(adapter2, config2, args) {
|
|
22320
|
+
const tracker = new ProgressTracker("read");
|
|
22321
|
+
try {
|
|
22322
|
+
if (!adapter2.listConventions) return errorResponse(UNSUPPORTED);
|
|
22323
|
+
const all = await adapter2.listConventions();
|
|
22324
|
+
const moduleFilter = sanitiseConventionModule(args.module);
|
|
22325
|
+
const rows = moduleFilter ? selectConventionsForModule(all, moduleFilter) : all;
|
|
22326
|
+
if (rows.length === 0) {
|
|
22327
|
+
return textResponse(
|
|
22328
|
+
moduleFilter ? `No conventions apply to the **${moduleFilter}** module yet. Declare one with \`convention_declare\` and every future build in it will carry the rule.` : "No conventions declared yet. When you settle a question you do not want to answer again, record it with `convention_declare` and every future build will carry it."
|
|
22329
|
+
);
|
|
22330
|
+
}
|
|
22331
|
+
const lines = rows.map((c) => {
|
|
22332
|
+
const scope = c.module ? `**${c.module}**` : "every build";
|
|
22333
|
+
return `- ${scope} \u2014 ${sanitiseConventionRule(c.rule) ?? c.rule}
|
|
22334
|
+
\`${c.id ?? "(no id)"}\``;
|
|
22335
|
+
});
|
|
22336
|
+
const header = moduleFilter ? `${rows.length} convention${rows.length > 1 ? "s" : ""} apply to a build in **${moduleFilter}** (project-wide rules included):` : `${rows.length} convention${rows.length > 1 ? "s" : ""} declared on this project:`;
|
|
22337
|
+
return textResponse(`${header}
|
|
22338
|
+
|
|
22339
|
+
${lines.join("\n")}`);
|
|
22340
|
+
} catch (err) {
|
|
22341
|
+
return errorResponse(formatStructuredError({
|
|
22342
|
+
tool: "convention_list",
|
|
22343
|
+
mode: "list",
|
|
22344
|
+
adapter: config2.adapterType,
|
|
22345
|
+
lastStep: tracker.lastStep,
|
|
22346
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22347
|
+
hint: defaultHint("convention_list")
|
|
22348
|
+
}));
|
|
22349
|
+
}
|
|
22350
|
+
}
|
|
22351
|
+
async function handleConventionRemove(adapter2, config2, args) {
|
|
22352
|
+
const tracker = new ProgressTracker("validate");
|
|
22353
|
+
try {
|
|
22354
|
+
if (!adapter2.deleteConvention) return errorResponse(UNSUPPORTED);
|
|
22355
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
22356
|
+
if (!id) {
|
|
22357
|
+
return errorResponse("Pass the `id` of the convention to remove. `convention_list` prints the id under each rule.");
|
|
22358
|
+
}
|
|
22359
|
+
tracker.mark("delete");
|
|
22360
|
+
const removed = await adapter2.deleteConvention(id);
|
|
22361
|
+
if (!removed) {
|
|
22362
|
+
return errorResponse(
|
|
22363
|
+
`No convention with id \`${id}\` on this project. Run \`convention_list\` to see what is declared.`
|
|
22364
|
+
);
|
|
22365
|
+
}
|
|
22366
|
+
return textResponse(`Convention removed. Future builds no longer carry it.`);
|
|
22367
|
+
} catch (err) {
|
|
22368
|
+
return errorResponse(formatStructuredError({
|
|
22369
|
+
tool: "convention_remove",
|
|
22370
|
+
mode: "remove",
|
|
22371
|
+
adapter: config2.adapterType,
|
|
22372
|
+
lastStep: tracker.lastStep,
|
|
22373
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22374
|
+
hint: defaultHint("convention_remove")
|
|
22375
|
+
}));
|
|
22376
|
+
}
|
|
22377
|
+
}
|
|
22378
|
+
|
|
21606
22379
|
// src/tools/module-instructions.ts
|
|
21607
22380
|
var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
|
|
21608
22381
|
var MODULE_INSTRUCTIONS = {
|
|
@@ -21681,6 +22454,194 @@ function getModuleInstructions(module) {
|
|
|
21681
22454
|
|
|
21682
22455
|
${instructions}`;
|
|
21683
22456
|
}
|
|
22457
|
+
function getBuilderInstructions(module, conventions) {
|
|
22458
|
+
const conventionsSection = formatConventionsSection(conventions, module);
|
|
22459
|
+
const hasModuleScopedOverride = Boolean(
|
|
22460
|
+
module && conventions.some((c) => c.module === module)
|
|
22461
|
+
);
|
|
22462
|
+
const builtIn = hasModuleScopedOverride ? "" : getModuleInstructions(module);
|
|
22463
|
+
return builtIn + conventionsSection;
|
|
22464
|
+
}
|
|
22465
|
+
|
|
22466
|
+
// src/services/build-proposal.ts
|
|
22467
|
+
var MAX_PROPOSAL_TITLE = 200;
|
|
22468
|
+
var MAX_PROPOSAL_BODY = 2e3;
|
|
22469
|
+
function validateProposal(raw) {
|
|
22470
|
+
if (typeof raw !== "object" || raw === null) {
|
|
22471
|
+
return { error: "proposal must be an object." };
|
|
22472
|
+
}
|
|
22473
|
+
const p = raw;
|
|
22474
|
+
if (typeof p.title === "string" && p.title.trim().length > MAX_PROPOSAL_TITLE) {
|
|
22475
|
+
return { error: `proposal.title exceeds ${MAX_PROPOSAL_TITLE} characters.` };
|
|
22476
|
+
}
|
|
22477
|
+
if (typeof p.body === "string" && p.body.trim().length > MAX_PROPOSAL_BODY) {
|
|
22478
|
+
return { error: `proposal.body exceeds ${MAX_PROPOSAL_BODY} characters.` };
|
|
22479
|
+
}
|
|
22480
|
+
const title = sanitiseConventionRule(p.title) ?? "";
|
|
22481
|
+
if (!title) return { error: "proposal.title is required \u2014 one line stating the stance or rule." };
|
|
22482
|
+
const body = sanitiseConventionRule(p.body) ?? "";
|
|
22483
|
+
if (!body) return { error: "proposal.body is required \u2014 what was decided or settled, and why." };
|
|
22484
|
+
const t = p.tests;
|
|
22485
|
+
if (typeof t !== "object" || t === null) {
|
|
22486
|
+
return { error: "proposal.tests is required \u2014 apply the four admission tests and report each as a boolean." };
|
|
22487
|
+
}
|
|
22488
|
+
const tt = t;
|
|
22489
|
+
const keys = [
|
|
22490
|
+
"alternativesWereReal",
|
|
22491
|
+
"constrainsFutureWork",
|
|
22492
|
+
"arguableToday",
|
|
22493
|
+
"reversalCostsMore"
|
|
22494
|
+
];
|
|
22495
|
+
for (const k of keys) {
|
|
22496
|
+
if (typeof tt[k] !== "boolean") {
|
|
22497
|
+
return { error: `proposal.tests.${k} must be true or false. All four admission tests must be answered explicitly \u2014 an omitted test is not a failed test.` };
|
|
22498
|
+
}
|
|
22499
|
+
}
|
|
22500
|
+
const moduleScope = sanitiseConventionModule(p.module);
|
|
22501
|
+
return {
|
|
22502
|
+
proposal: {
|
|
22503
|
+
title,
|
|
22504
|
+
body,
|
|
22505
|
+
...moduleScope ? { module: moduleScope } : {},
|
|
22506
|
+
tests: {
|
|
22507
|
+
alternativesWereReal: tt.alternativesWereReal,
|
|
22508
|
+
constrainsFutureWork: tt.constrainsFutureWork,
|
|
22509
|
+
arguableToday: tt.arguableToday,
|
|
22510
|
+
reversalCostsMore: tt.reversalCostsMore
|
|
22511
|
+
}
|
|
22512
|
+
}
|
|
22513
|
+
};
|
|
22514
|
+
}
|
|
22515
|
+
function routeProposal(proposal) {
|
|
22516
|
+
const { alternativesWereReal, constrainsFutureWork, arguableToday, reversalCostsMore } = proposal.tests;
|
|
22517
|
+
if (!constrainsFutureWork) {
|
|
22518
|
+
return {
|
|
22519
|
+
outcome: "rejected",
|
|
22520
|
+
failedTest: "b",
|
|
22521
|
+
reason: "It does not constrain future work, so it is not durable \u2014 it is just work. Capture it as a task, a build report note, or a doc."
|
|
22522
|
+
};
|
|
22523
|
+
}
|
|
22524
|
+
if (!alternativesWereReal) {
|
|
22525
|
+
return {
|
|
22526
|
+
outcome: "rejected",
|
|
22527
|
+
failedTest: "a",
|
|
22528
|
+
reason: "There were no real alternatives, so no stance was taken. If there was only ever one way to do it, it is not a decision. Capture it as a task, a note, or a doc."
|
|
22529
|
+
};
|
|
22530
|
+
}
|
|
22531
|
+
if (!reversalCostsMore) {
|
|
22532
|
+
return {
|
|
22533
|
+
outcome: "rejected",
|
|
22534
|
+
failedTest: "d",
|
|
22535
|
+
reason: "Reversing it is as cheap as doing it, so nothing is being constrained. Capture it as a task, a note, or a doc."
|
|
22536
|
+
};
|
|
22537
|
+
}
|
|
22538
|
+
if (!arguableToday) {
|
|
22539
|
+
return {
|
|
22540
|
+
outcome: "recorded-convention",
|
|
22541
|
+
failedTest: "c",
|
|
22542
|
+
reason: "Real alternatives, binds future work, expensive to reverse, but nobody is arguing the other side any more. That is a Convention, not a Decision \u2014 it does not need adjudicating, it needs to be known by whoever builds next."
|
|
22543
|
+
};
|
|
22544
|
+
}
|
|
22545
|
+
return {
|
|
22546
|
+
outcome: "queued-decision",
|
|
22547
|
+
reason: "All four admission tests pass, so this is a live stance. Queued for your decision \u2014 nothing has been minted."
|
|
22548
|
+
};
|
|
22549
|
+
}
|
|
22550
|
+
var PROPOSED_DECISION_REF = "proposed-decision";
|
|
22551
|
+
async function applyProposal(adapter2, routing, proposal, ctx) {
|
|
22552
|
+
try {
|
|
22553
|
+
if (routing.outcome === "rejected") return {};
|
|
22554
|
+
if (routing.outcome === "recorded-convention") {
|
|
22555
|
+
if (!adapter2.createConvention) {
|
|
22556
|
+
return { error: "This connection has nowhere to record a convention." };
|
|
22557
|
+
}
|
|
22558
|
+
const rule = `${proposal.title}. ${proposal.body}`.slice(0, 2e3);
|
|
22559
|
+
const convention = await adapter2.createConvention({
|
|
22560
|
+
rule,
|
|
22561
|
+
...proposal.module ? { module: proposal.module } : {}
|
|
22562
|
+
});
|
|
22563
|
+
return { convention };
|
|
22564
|
+
}
|
|
22565
|
+
if (!adapter2.createTask || !adapter2.updateTask) {
|
|
22566
|
+
return { error: "This connection cannot queue a decision for confirmation." };
|
|
22567
|
+
}
|
|
22568
|
+
const provenance = ctx.sourceTaskId ? `Proposed while building ${ctx.sourceTaskId}.` : "Proposed during a build.";
|
|
22569
|
+
const task = await adapter2.createTask({
|
|
22570
|
+
displayId: "",
|
|
22571
|
+
title: `Decision proposed: ${proposal.title}`,
|
|
22572
|
+
status: "Backlog",
|
|
22573
|
+
priority: "P1 High",
|
|
22574
|
+
complexity: "XS",
|
|
22575
|
+
module: proposal.module || "Core",
|
|
22576
|
+
epic: "Platform",
|
|
22577
|
+
phase: "Unscoped",
|
|
22578
|
+
owner: "TBD",
|
|
22579
|
+
reviewed: false,
|
|
22580
|
+
createdCycle: ctx.cycleNumber,
|
|
22581
|
+
taskType: "task",
|
|
22582
|
+
notes: `${provenance} All four admission tests passed, so this is a live stance rather than a settled rule.
|
|
22583
|
+
|
|
22584
|
+
${proposal.body}
|
|
22585
|
+
|
|
22586
|
+
Accept it by minting the decision with \`strategy_change\`, or dismiss it by cancelling this task. Nothing has been written as an Active Decision.`
|
|
22587
|
+
});
|
|
22588
|
+
await adapter2.updateTask(task.id, {
|
|
22589
|
+
status: "Blocked",
|
|
22590
|
+
blocker: {
|
|
22591
|
+
type: "decision-gate",
|
|
22592
|
+
ref: PROPOSED_DECISION_REF,
|
|
22593
|
+
reason: "A build proposed this decision. Awaiting the owner's call \u2014 it does not auto-clear, because only a person can rule on it.",
|
|
22594
|
+
blockedCycle: ctx.cycleNumber
|
|
22595
|
+
}
|
|
22596
|
+
});
|
|
22597
|
+
return { taskId: task.id };
|
|
22598
|
+
} catch (err) {
|
|
22599
|
+
return { error: err instanceof Error ? err.message : "Unknown error." };
|
|
22600
|
+
}
|
|
22601
|
+
}
|
|
22602
|
+
function formatProposalOutcome(routing, proposal, detail) {
|
|
22603
|
+
if (detail.error) {
|
|
22604
|
+
return `
|
|
22605
|
+
|
|
22606
|
+
---
|
|
22607
|
+
|
|
22608
|
+
**Proposal not recorded.** ${detail.error}
|
|
22609
|
+
|
|
22610
|
+
The build itself is unaffected \u2014 only the proposal failed.`;
|
|
22611
|
+
}
|
|
22612
|
+
switch (routing.outcome) {
|
|
22613
|
+
case "queued-decision":
|
|
22614
|
+
return `
|
|
22615
|
+
|
|
22616
|
+
---
|
|
22617
|
+
|
|
22618
|
+
**Decision proposed \u2014 waiting on you.**
|
|
22619
|
+
|
|
22620
|
+
> ${proposal.title}
|
|
22621
|
+
|
|
22622
|
+
${routing.reason}` + (detail.taskId ? ` It is parked as ${detail.taskId}, blocked on your call, and shows up in \`orient\` as a decision waiting on you. Nothing was written as an Active Decision.` : " Nothing was written as an Active Decision.");
|
|
22623
|
+
case "recorded-convention":
|
|
22624
|
+
return `
|
|
22625
|
+
|
|
22626
|
+
---
|
|
22627
|
+
|
|
22628
|
+
**Recorded as a convention.**
|
|
22629
|
+
|
|
22630
|
+
> ${proposal.title}
|
|
22631
|
+
|
|
22632
|
+
${routing.reason}` + (proposal.module ? ` It rides every future build in the **${proposal.module}** module.` : " It rides every future build on this project.") + (detail.convention?.id ? ` Remove it with \`convention_remove\` (id \`${detail.convention.id}\`) if that is not what you wanted.` : "");
|
|
22633
|
+
case "rejected":
|
|
22634
|
+
return `
|
|
22635
|
+
|
|
22636
|
+
---
|
|
22637
|
+
|
|
22638
|
+
**Proposal not minted, and here is why.**
|
|
22639
|
+
|
|
22640
|
+
> ${proposal.title}
|
|
22641
|
+
|
|
22642
|
+
Failed test (${routing.failedTest}): ${routing.reason}`;
|
|
22643
|
+
}
|
|
22644
|
+
}
|
|
21684
22645
|
|
|
21685
22646
|
// src/tools/doc-registry.ts
|
|
21686
22647
|
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
@@ -21700,7 +22661,7 @@ var MAX_DOC_BODY_BYTES = 2 * 1024 * 1024;
|
|
|
21700
22661
|
var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
|
|
21701
22662
|
var PRICING_URL = "https://getpapi.ai/pricing";
|
|
21702
22663
|
async function resolveTier(adapter2) {
|
|
21703
|
-
if (
|
|
22664
|
+
if (!adapterSupports(adapter2, "getMeteredUsage")) return null;
|
|
21704
22665
|
try {
|
|
21705
22666
|
const usage = await adapter2.getMeteredUsage();
|
|
21706
22667
|
return usage?.tier ?? null;
|
|
@@ -21715,7 +22676,7 @@ async function enforceProjectCap(adapter2, target) {
|
|
|
21715
22676
|
if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
|
|
21716
22677
|
const tier = await resolveTier(adapter2);
|
|
21717
22678
|
if (tier === null || isPaidTier(tier)) return null;
|
|
21718
|
-
if (
|
|
22679
|
+
if (!adapterSupports(adapter2, "listUserProjects")) return null;
|
|
21719
22680
|
let projects;
|
|
21720
22681
|
try {
|
|
21721
22682
|
projects = await adapter2.listUserProjects();
|
|
@@ -21758,7 +22719,7 @@ The registry entry was saved and everything else is unaffected. Split the docume
|
|
|
21758
22719
|
const tier = await resolveTier(adapter2);
|
|
21759
22720
|
if (tier === null) return { storeBody: true };
|
|
21760
22721
|
const ceiling = DOC_STORAGE_CEILING_BY_TIER[tier] ?? DOC_STORAGE_CEILING_BY_TIER.free;
|
|
21761
|
-
if (
|
|
22722
|
+
if (!adapterSupports(adapter2, "getDocBodyUsage")) return { storeBody: true };
|
|
21762
22723
|
let usage;
|
|
21763
22724
|
try {
|
|
21764
22725
|
usage = await adapter2.getDocBodyUsage();
|
|
@@ -21935,6 +22896,7 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
|
|
|
21935
22896
|
);
|
|
21936
22897
|
}
|
|
21937
22898
|
if (isPathTracked(projectRoot, path7)) return "";
|
|
22899
|
+
if (isPathCommittedAnywhere(projectRoot, path7)) return "";
|
|
21938
22900
|
if (isPathIgnored(projectRoot, path7)) {
|
|
21939
22901
|
return warn(
|
|
21940
22902
|
`\`${path7}\` is excluded by .gitignore, so it cannot be committed (docs/private/ is ignored by design).`,
|
|
@@ -22136,7 +23098,7 @@ function resolveRestoreTarget(projectRoot, docPath) {
|
|
|
22136
23098
|
return { abs };
|
|
22137
23099
|
}
|
|
22138
23100
|
async function handleDocRead(adapter2, config2, args) {
|
|
22139
|
-
if (!adapter2.getDoc ||
|
|
23101
|
+
if (!adapter2.getDoc || !adapterSupports(adapter2, "getDocBody")) {
|
|
22140
23102
|
return errorResponse(
|
|
22141
23103
|
"Doc bodies are not available on this adapter \u2014 requires the pg/proxy adapter. Nothing is blocked; the file on disk (if any) is still the copy you have."
|
|
22142
23104
|
);
|
|
@@ -22516,7 +23478,7 @@ var buildDescribeTool = {
|
|
|
22516
23478
|
};
|
|
22517
23479
|
var buildExecuteTool = {
|
|
22518
23480
|
name: "build_execute",
|
|
22519
|
-
description: "
|
|
23481
|
+
description: "Build one task with its spec, its branch and its record kept for you. Starting a build hands back that task's BUILD HANDOFF (scope, what is out of scope, acceptance criteria, the decisions that constrain it, and what recent builds in the same area already learned or ruled out), puts the work on the right branch, and marks the task In Progress so a second session can see it is taken. Completing it records what actually happened: effort against estimate, surprises, dead ends, and bugs found outside the scope. That record is what later cycles are sized and planned from, so nothing has to be remembered. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
22520
23482
|
annotations: { title: "Run Build", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22521
23483
|
inputSchema: {
|
|
22522
23484
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -22555,6 +23517,27 @@ var buildExecuteTool = {
|
|
|
22555
23517
|
},
|
|
22556
23518
|
required: ["answer"]
|
|
22557
23519
|
},
|
|
23520
|
+
proposal: {
|
|
23521
|
+
type: "object",
|
|
23522
|
+
description: 'task-3273, OPTIONAL: propose a Decision or a Convention you settled while building this. PAPI never mints one for you \u2014 a proposal that passes all four admission tests is QUEUED for the owner behind a decision gate, and nothing is written as an Active Decision. YOU apply the four tests (they are stated in the planning prompts); the server routes on your answers and makes no judgement about the content. Fails "arguable today" only and it is recorded as a Convention instead, riding every future build. Fails "constrains future work" and it is returned to you with the reason, unrecorded.',
|
|
23523
|
+
properties: {
|
|
23524
|
+
title: { type: "string", description: "One line stating the stance or rule." },
|
|
23525
|
+
body: { type: "string", description: "What was decided or settled, and why." },
|
|
23526
|
+
module: { type: "string", description: "Optional module scope. Only used when this lands as a Convention." },
|
|
23527
|
+
tests: {
|
|
23528
|
+
type: "object",
|
|
23529
|
+
description: "The four admission tests, each answered explicitly. An omitted test is NOT a failed test \u2014 the proposal is refused rather than routed.",
|
|
23530
|
+
properties: {
|
|
23531
|
+
alternativesWereReal: { type: "boolean", description: "(a) Something else could genuinely have been chosen." },
|
|
23532
|
+
constrainsFutureWork: { type: "boolean", description: "(b) It changes what a task nobody has written yet will do." },
|
|
23533
|
+
arguableToday: { type: "boolean", description: "(c) A competent person could argue the other side right now." },
|
|
23534
|
+
reversalCostsMore: { type: "boolean", description: "(d) Reversing it costs more than making it did." }
|
|
23535
|
+
},
|
|
23536
|
+
required: ["alternativesWereReal", "constrainsFutureWork", "arguableToday", "reversalCostsMore"]
|
|
23537
|
+
}
|
|
23538
|
+
},
|
|
23539
|
+
required: ["title", "body", "tests"]
|
|
23540
|
+
},
|
|
22558
23541
|
completed: {
|
|
22559
23542
|
type: "string",
|
|
22560
23543
|
enum: ["yes", "no", "partial"],
|
|
@@ -23049,7 +24032,12 @@ These approaches were tried in this module and failed. If one looks right, read
|
|
|
23049
24032
|
}
|
|
23050
24033
|
} catch {
|
|
23051
24034
|
}
|
|
23052
|
-
|
|
24035
|
+
let conventions = [];
|
|
24036
|
+
try {
|
|
24037
|
+
if (adapter2.listConventions) conventions = await adapter2.listConventions();
|
|
24038
|
+
} catch {
|
|
24039
|
+
}
|
|
24040
|
+
const moduleInstructions = getBuilderInstructions(result.task.module, conventions);
|
|
23053
24041
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
23054
24042
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
23055
24043
|
const modelNote = buildModelRecommendationDirective(
|
|
@@ -23269,7 +24257,7 @@ ${checklist}
|
|
|
23269
24257
|
});
|
|
23270
24258
|
}
|
|
23271
24259
|
let fixedResolvedCount = 0;
|
|
23272
|
-
if (fixedIssues && fixedIssues.length > 0 &&
|
|
24260
|
+
if (fixedIssues && fixedIssues.length > 0 && adapterSupports(adapter2, "markCycleLearningResolved")) {
|
|
23273
24261
|
const resolvedBy = `build:${result.task.displayId ?? taskId}`;
|
|
23274
24262
|
for (const learningId of fixedIssues) {
|
|
23275
24263
|
try {
|
|
@@ -23329,7 +24317,27 @@ If any are now fixed, re-run complete with their UUIDs in \`fixed_issues\` \u201
|
|
|
23329
24317
|
fixedNote = "\n\n\u2139\uFE0F This build filed discovered issues but passed no `fixed_issues`. When a future build fixes one, pass its UUID in `fixed_issues` so it counts as FIXED (not just auto-cleared) on the hub ledger.";
|
|
23330
24318
|
}
|
|
23331
24319
|
}
|
|
23332
|
-
|
|
24320
|
+
let proposalNote = "";
|
|
24321
|
+
if (args.proposal !== void 0) {
|
|
24322
|
+
const validated = validateProposal(args.proposal);
|
|
24323
|
+
if ("error" in validated) {
|
|
24324
|
+
proposalNote = `
|
|
24325
|
+
|
|
24326
|
+
---
|
|
24327
|
+
|
|
24328
|
+
**Proposal not recorded.** ${validated.error}
|
|
24329
|
+
|
|
24330
|
+
The build itself is unaffected \u2014 only the proposal failed.`;
|
|
24331
|
+
} else {
|
|
24332
|
+
const routing = routeProposal(validated.proposal);
|
|
24333
|
+
const applied = await applyProposal(adapter2, routing, validated.proposal, {
|
|
24334
|
+
cycleNumber: result.cycleNumber,
|
|
24335
|
+
sourceTaskId: result.task?.id
|
|
24336
|
+
});
|
|
24337
|
+
proposalNote = formatProposalOutcome(routing, validated.proposal, applied);
|
|
24338
|
+
}
|
|
24339
|
+
}
|
|
24340
|
+
return textResponse(formatCompleteResult(result) + fixedNote + docsNote + batchRollupNote + proposalNote);
|
|
23333
24341
|
} catch (err) {
|
|
23334
24342
|
const message = err instanceof Error ? err.message : String(err);
|
|
23335
24343
|
if (isBuildPushError(err)) {
|
|
@@ -24519,6 +25527,27 @@ var adHocTool = {
|
|
|
24519
25527
|
inputSchema: {
|
|
24520
25528
|
type: "object",
|
|
24521
25529
|
properties: {
|
|
25530
|
+
proposal: {
|
|
25531
|
+
type: "object",
|
|
25532
|
+
description: "task-3273, OPTIONAL: propose a Decision or a Convention you settled while doing this work. PAPI never mints one for you \u2014 a proposal passing all four admission tests is QUEUED for the owner behind a decision gate, and nothing is written as an Active Decision. YOU apply the four tests; the server routes on your answers and makes no judgement about the content.",
|
|
25533
|
+
properties: {
|
|
25534
|
+
title: { type: "string", description: "One line stating the stance or rule." },
|
|
25535
|
+
body: { type: "string", description: "What was decided or settled, and why." },
|
|
25536
|
+
module: { type: "string", description: "Optional module scope. Only used when this lands as a Convention." },
|
|
25537
|
+
tests: {
|
|
25538
|
+
type: "object",
|
|
25539
|
+
description: "The four admission tests, each answered explicitly. An omitted test is NOT a failed test.",
|
|
25540
|
+
properties: {
|
|
25541
|
+
alternativesWereReal: { type: "boolean" },
|
|
25542
|
+
constrainsFutureWork: { type: "boolean" },
|
|
25543
|
+
arguableToday: { type: "boolean" },
|
|
25544
|
+
reversalCostsMore: { type: "boolean" }
|
|
25545
|
+
},
|
|
25546
|
+
required: ["alternativesWereReal", "constrainsFutureWork", "arguableToday", "reversalCostsMore"]
|
|
25547
|
+
}
|
|
25548
|
+
},
|
|
25549
|
+
required: ["title", "body", "tests"]
|
|
25550
|
+
},
|
|
24522
25551
|
title: {
|
|
24523
25552
|
type: "string",
|
|
24524
25553
|
description: "What was done \u2014 becomes the task title (required when creating new task, optional when completing existing via task_id)."
|
|
@@ -24578,6 +25607,22 @@ var adHocTool = {
|
|
|
24578
25607
|
required: []
|
|
24579
25608
|
}
|
|
24580
25609
|
};
|
|
25610
|
+
async function recordAdHocProposal(adapter2, args, ctx) {
|
|
25611
|
+
if (args.proposal === void 0) return "";
|
|
25612
|
+
const validated = validateProposal(args.proposal);
|
|
25613
|
+
if ("error" in validated) {
|
|
25614
|
+
return `
|
|
25615
|
+
|
|
25616
|
+
---
|
|
25617
|
+
|
|
25618
|
+
**Proposal not recorded.** ${validated.error}
|
|
25619
|
+
|
|
25620
|
+
The ad-hoc record itself is unaffected \u2014 only the proposal failed.`;
|
|
25621
|
+
}
|
|
25622
|
+
const routing = routeProposal(validated.proposal);
|
|
25623
|
+
const applied = await applyProposal(adapter2, routing, validated.proposal, ctx);
|
|
25624
|
+
return formatProposalOutcome(routing, validated.proposal, applied);
|
|
25625
|
+
}
|
|
24581
25626
|
async function handleAdHoc(adapter2, config2, args) {
|
|
24582
25627
|
const taskId = args.task_id?.trim();
|
|
24583
25628
|
const title = args.title?.trim();
|
|
@@ -24693,12 +25738,12 @@ The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**,
|
|
|
24693
25738
|
\`\`\`
|
|
24694
25739
|
2. Leave the branch **unmerged** \u2014 it is picked up by the next cycle's \`release\`.
|
|
24695
25740
|
|
|
24696
|
-
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25741
|
+
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id })
|
|
24697
25742
|
);
|
|
24698
25743
|
}
|
|
24699
25744
|
return textResponse(
|
|
24700
25745
|
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
24701
|
-
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25746
|
+
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id })
|
|
24702
25747
|
);
|
|
24703
25748
|
}
|
|
24704
25749
|
|
|
@@ -25421,7 +26466,7 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
25421
26466
|
}
|
|
25422
26467
|
const newStatus = resolveStatus(input.stage, input.verdict);
|
|
25423
26468
|
const statusChange = newStatus ? { from: task.status, to: newStatus } : void 0;
|
|
25424
|
-
if (
|
|
26469
|
+
if (adapterSupports(adapter2, "commitReviewSubmit")) {
|
|
25425
26470
|
await adapter2.commitReviewSubmit({
|
|
25426
26471
|
review,
|
|
25427
26472
|
taskId: input.taskId,
|
|
@@ -25452,7 +26497,7 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
25452
26497
|
}
|
|
25453
26498
|
}
|
|
25454
26499
|
const closedDocActions = [];
|
|
25455
|
-
if (newStatus === "Done" &&
|
|
26500
|
+
if (newStatus === "Done" && adapterSupports(adapter2, "findPendingDocActionsForTask") && adapterSupports(adapter2, "updateDocAction")) {
|
|
25456
26501
|
try {
|
|
25457
26502
|
const pending = await adapter2.findPendingDocActionsForTask(input.taskId);
|
|
25458
26503
|
for (const match of pending) {
|
|
@@ -25463,7 +26508,7 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
25463
26508
|
console.error(`[doc-closure] failed to resolve doc action ${match.docId}#${match.actionIndex}: ${err instanceof Error ? err.message : String(err)}`);
|
|
25464
26509
|
}
|
|
25465
26510
|
}
|
|
25466
|
-
if (closedDocActions.length > 0 &&
|
|
26511
|
+
if (closedDocActions.length > 0 && adapterSupports(adapter2, "appendCycleLearnings")) {
|
|
25467
26512
|
try {
|
|
25468
26513
|
await adapter2.appendCycleLearnings([{
|
|
25469
26514
|
taskId: input.taskId,
|
|
@@ -25533,11 +26578,13 @@ async function assembleReviewContext(adapter2, config2, taskId) {
|
|
|
25533
26578
|
${JSON.stringify(task.buildHandoff, null, 2)}` : "### BUILD HANDOFF\n(none recorded)";
|
|
25534
26579
|
const report = task.buildReport ? `### Build Report
|
|
25535
26580
|
${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
25536
|
-
const diff =
|
|
25537
|
-
const
|
|
26581
|
+
const { diff, scope, detail } = getTaskDiff(config2.projectRoot, taskId);
|
|
26582
|
+
const diffHeading = scope === "task-commits" ? `### Diff for ${taskId} (${detail})` : `### \u26A0\uFE0F Diff NOT scoped to ${taskId} (${detail})`;
|
|
26583
|
+
const diffBlock = diff ? `${diffHeading}
|
|
25538
26584
|
\`\`\`diff
|
|
25539
26585
|
${diff}
|
|
25540
|
-
\`\`\`` :
|
|
26586
|
+
\`\`\`` : `### Diff for ${taskId}
|
|
26587
|
+
(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)`;
|
|
25541
26588
|
let projectContext = "";
|
|
25542
26589
|
const ctxPath = join15(config2.projectRoot, ".agents", "papi-context.md");
|
|
25543
26590
|
if (existsSync10(ctxPath)) {
|
|
@@ -25722,6 +26769,19 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
25722
26769
|
};
|
|
25723
26770
|
}
|
|
25724
26771
|
const details = [];
|
|
26772
|
+
if (isBranchMergedInto(config2.projectRoot, featureBranch, baseBranch)) {
|
|
26773
|
+
details.push(`Branch '${featureBranch}' is already merged into '${baseBranch}'.`);
|
|
26774
|
+
return { merged: true, skipped: false, message: `Already merged: \`${featureBranch}\` \u2192 \`${baseBranch}\`.`, details };
|
|
26775
|
+
}
|
|
26776
|
+
if (isBranchContentAlreadyInBase(config2.projectRoot, featureBranch, baseBranch)) {
|
|
26777
|
+
details.push(`Branch '${featureBranch}' adds nothing to '${baseBranch}' \u2014 its commits already landed there.`);
|
|
26778
|
+
return {
|
|
26779
|
+
merged: true,
|
|
26780
|
+
skipped: false,
|
|
26781
|
+
message: `Already on \`${baseBranch}\` \u2014 no PR merge needed (these commits were folded into another branch and landed with it).`,
|
|
26782
|
+
details
|
|
26783
|
+
};
|
|
26784
|
+
}
|
|
25725
26785
|
const papiDir = join15(config2.projectRoot, ".papi");
|
|
25726
26786
|
if (existsSync10(papiDir)) {
|
|
25727
26787
|
try {
|
|
@@ -26066,7 +27126,7 @@ ${overlap}`;
|
|
|
26066
27126
|
\u2705 Verdict recorded. All cycle tasks are Done, but **auto-release is owner-only** \u2014 your identity does not match this project's owner, so no release was cut. Push your branch and open a PR for the owner to run \`release\`.${resolutionNote}`;
|
|
26067
27127
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
26068
27128
|
let planRunCount = null;
|
|
26069
|
-
if (
|
|
27129
|
+
if (adapterSupports(adapter2, "countPlanRunsForCycle")) {
|
|
26070
27130
|
try {
|
|
26071
27131
|
planRunCount = await adapter2.countPlanRunsForCycle(result.currentCycle);
|
|
26072
27132
|
} catch {
|
|
@@ -26269,7 +27329,7 @@ async function resolveReviewerIdentity(adapter2, config2) {
|
|
|
26269
27329
|
const note = gate.resolutionError ? ` (${gate.resolutionError})` : "";
|
|
26270
27330
|
return { error: "Reviewing requires a resolvable user identity, but none was found" + note + ". Set PAPI_USER_ID to your account UUID (local) \u2014 hosted sessions derive it from the bearer token." };
|
|
26271
27331
|
}
|
|
26272
|
-
if (!gate.callerIsOwner &&
|
|
27332
|
+
if (!gate.callerIsOwner && adapterSupports(adapter2, "listContributors")) {
|
|
26273
27333
|
try {
|
|
26274
27334
|
const members = await adapter2.listContributors();
|
|
26275
27335
|
if (!members.some((m) => m.userId === callerUserId)) {
|
|
@@ -26284,7 +27344,7 @@ async function resolveReviewerIdentity(adapter2, config2) {
|
|
|
26284
27344
|
async function handleReviewClaim(adapter2, config2, args) {
|
|
26285
27345
|
const taskId = typeof args.task_id === "string" ? args.task_id.trim() : "";
|
|
26286
27346
|
if (!taskId) return errorResponse('A task_id is required. Example: review_claim task_id="task-2072"');
|
|
26287
|
-
if (
|
|
27347
|
+
if (!adapterSupports(adapter2, "claimReview")) {
|
|
26288
27348
|
return errorResponse("The shared review queue is not available on this adapter.");
|
|
26289
27349
|
}
|
|
26290
27350
|
const identity = await resolveReviewerIdentity(adapter2, config2);
|
|
@@ -27233,50 +28293,6 @@ function formatUnblockSection(candidates) {
|
|
|
27233
28293
|
return lines.join("\n");
|
|
27234
28294
|
}
|
|
27235
28295
|
|
|
27236
|
-
// src/lib/carry-forward-shape.ts
|
|
27237
|
-
var PRODUCT_MARKER = /WHAT SHIPS FOR USERS\b[^\n:]*:/i;
|
|
27238
|
-
var MECHANICS_MARKER = /RELEASE MECHANICS\b[^\n:]*:/i;
|
|
27239
|
-
function splitCarryForward(raw) {
|
|
27240
|
-
const productMatch = PRODUCT_MARKER.exec(raw);
|
|
27241
|
-
const mechanicsMatch = MECHANICS_MARKER.exec(raw);
|
|
27242
|
-
if (!productMatch && !mechanicsMatch) {
|
|
27243
|
-
return { product: raw.trim(), mechanics: "", split: false };
|
|
27244
|
-
}
|
|
27245
|
-
if (!productMatch && mechanicsMatch) {
|
|
27246
|
-
const head = raw.slice(0, mechanicsMatch.index).trim();
|
|
27247
|
-
const tail = raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim();
|
|
27248
|
-
return { product: head, mechanics: tail, split: true };
|
|
27249
|
-
}
|
|
27250
|
-
const productStart = productMatch.index + productMatch[0].length;
|
|
27251
|
-
if (!mechanicsMatch) {
|
|
27252
|
-
return { product: raw.slice(productStart).trim(), mechanics: "", split: true };
|
|
27253
|
-
}
|
|
27254
|
-
if (mechanicsMatch.index < productMatch.index) {
|
|
27255
|
-
return {
|
|
27256
|
-
product: raw.slice(productStart).trim(),
|
|
27257
|
-
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length, productMatch.index).trim(),
|
|
27258
|
-
split: true
|
|
27259
|
-
};
|
|
27260
|
-
}
|
|
27261
|
-
return {
|
|
27262
|
-
product: raw.slice(productStart, mechanicsMatch.index).trim(),
|
|
27263
|
-
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim(),
|
|
27264
|
-
split: true
|
|
27265
|
-
};
|
|
27266
|
-
}
|
|
27267
|
-
function renderCarryForward(raw, decorate = (t) => t) {
|
|
27268
|
-
const { product, mechanics, split } = splitCarryForward(raw);
|
|
27269
|
-
if (!split) return [decorate(product)];
|
|
27270
|
-
const lines = [];
|
|
27271
|
-
if (product) lines.push(decorate(product));
|
|
27272
|
-
if (mechanics) {
|
|
27273
|
-
if (product) lines.push("");
|
|
27274
|
-
lines.push("**Release mechanics** \u2014 needed at release, not now:");
|
|
27275
|
-
lines.push(decorate(mechanics));
|
|
27276
|
-
}
|
|
27277
|
-
return lines;
|
|
27278
|
-
}
|
|
27279
|
-
|
|
27280
28296
|
// src/lib/deferred-gate.ts
|
|
27281
28297
|
var GATE_PHRASES = [
|
|
27282
28298
|
"depends on",
|
|
@@ -27597,12 +28613,39 @@ var evaluateSelfServeActivation = async (adapter2, projectId) => {
|
|
|
27597
28613
|
unevaluatedReason: `mechanically satisfied (${users} non-owner accounts planned in their own project) \u2014 but "ZERO owner intervention" is a human judgement, so this needs an owner tick via set_criterion_met`
|
|
27598
28614
|
} : { met: false, evidence: `0 non-owner accounts with a planned cycle. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.` };
|
|
27599
28615
|
};
|
|
28616
|
+
var evaluateWeeklyActiveBuilders = async (adapter2, projectId) => {
|
|
28617
|
+
const sql = sqlOf(adapter2);
|
|
28618
|
+
if (!sql) return { met: null, unevaluatedReason: "no direct SQL access on this adapter (hosted path)" };
|
|
28619
|
+
const owner = await sql`
|
|
28620
|
+
SELECT user_id FROM projects WHERE id = ${projectId}
|
|
28621
|
+
`;
|
|
28622
|
+
const ownerUserId = owner[0]?.user_id ?? null;
|
|
28623
|
+
const rows = await sql`
|
|
28624
|
+
SELECT c.user_id, c.end_date, c.updated_at
|
|
28625
|
+
FROM cycles c
|
|
28626
|
+
JOIN projects p ON p.id = c.project_id
|
|
28627
|
+
JOIN auth.users u ON u.id = p.user_id
|
|
28628
|
+
WHERE c.status = 'complete'
|
|
28629
|
+
AND c.user_id IS NOT NULL
|
|
28630
|
+
AND u.email NOT LIKE ${TEST_ACCOUNT_PATTERN}
|
|
28631
|
+
`;
|
|
28632
|
+
const series = bucketWabSeries(rows, ownerUserId, Date.now(), WAB_DEFAULT_WEEKS);
|
|
28633
|
+
const peak = series.reduce((max, w) => Math.max(max, w.external), 0);
|
|
28634
|
+
let consecutive = 0;
|
|
28635
|
+
let bestRun = 0;
|
|
28636
|
+
for (const week of series) {
|
|
28637
|
+
consecutive = week.external >= 3 ? consecutive + 1 : 0;
|
|
28638
|
+
bestRun = Math.max(bestRun, consecutive);
|
|
28639
|
+
}
|
|
28640
|
+
const counted = `Peak ${peak} external builder(s) in any trailing week; longest run at >=3 is ${bestRun} consecutive week(s) over the last ${WAB_DEFAULT_WEEKS}. Owner and ${TEST_ACCOUNT_PATTERN} excluded. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.`;
|
|
28641
|
+
return bestRun >= 2 ? { met: true, evidence: counted } : { met: false, evidence: counted };
|
|
28642
|
+
};
|
|
27600
28643
|
var EVALUATORS = {
|
|
27601
28644
|
"c4a11d42-b222-45af-a470-08970e1bd6a9": evaluateSelfServeActivation,
|
|
27602
|
-
"59751bcb-f719-4cf6-a4d4-608770999a22": evaluateUnaidedFullLoop
|
|
28645
|
+
"59751bcb-f719-4cf6-a4d4-608770999a22": evaluateUnaidedFullLoop,
|
|
28646
|
+
"e9d984f4-1e75-4486-8605-5dfd07412915": evaluateWeeklyActiveBuilders
|
|
27603
28647
|
};
|
|
27604
28648
|
var UNEVALUATABLE = {
|
|
27605
|
-
"e9d984f4-1e75-4486-8605-5dfd07412915": "no evaluator here \u2014 the canonical completion-keyed definition lives in lib/weekly-active-builders.ts (Next app) and must not be re-derived in the server",
|
|
27606
28649
|
"c596a082-9ad3-4089-b497-46cd189e892b": "threshold is provisional pending an owner ruling \u2014 no settled bar to evaluate against"
|
|
27607
28650
|
};
|
|
27608
28651
|
async function evaluateExitCriteria(adapter2, projectId, criteria) {
|
|
@@ -27979,13 +29022,43 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
27979
29022
|
}
|
|
27980
29023
|
return lines.join("\n").trimEnd();
|
|
27981
29024
|
}
|
|
29025
|
+
var NOT_SET_UP_MESSAGE = "Setup required \u2014 this project has no PAPI state yet. That is the normal starting point, not an error. Run `setup` to generate your Product Brief and scaffold the workflow, then `plan` to create your first cycle.";
|
|
29026
|
+
function emptyHealthSummary() {
|
|
29027
|
+
return {
|
|
29028
|
+
cycleNumber: 0,
|
|
29029
|
+
// 'degraded' is exactly what happened: the adapter answered some reads and
|
|
29030
|
+
// not others. 'offline' would overstate it and 'connected' would hide it.
|
|
29031
|
+
connectionStatus: "degraded",
|
|
29032
|
+
reviewWarning: "",
|
|
29033
|
+
zoomOutWarning: "",
|
|
29034
|
+
boardSummary: "",
|
|
29035
|
+
staleTasks: "",
|
|
29036
|
+
inReviewSummary: "",
|
|
29037
|
+
carryForward: "",
|
|
29038
|
+
recommendedMode: "",
|
|
29039
|
+
metricsSection: "",
|
|
29040
|
+
derivedMetricsSection: "",
|
|
29041
|
+
costSection: "",
|
|
29042
|
+
decisionUsageSection: "",
|
|
29043
|
+
decisionLifecycleSection: "",
|
|
29044
|
+
decisionScoresSection: "",
|
|
29045
|
+
contextUtilisationSection: "",
|
|
29046
|
+
northStarSection: "",
|
|
29047
|
+
healthScore: null,
|
|
29048
|
+
healthStatus: null,
|
|
29049
|
+
healthReason: null
|
|
29050
|
+
};
|
|
29051
|
+
}
|
|
27982
29052
|
async function getHierarchyPosition(adapter2, projectId) {
|
|
27983
29053
|
try {
|
|
27984
29054
|
const [horizons, stages, phases, allTasks] = await Promise.all([
|
|
27985
29055
|
adapter2.readHorizons?.() ?? [],
|
|
27986
29056
|
adapter2.readStages?.() ?? [],
|
|
27987
29057
|
adapter2.readPhases(),
|
|
27988
|
-
|
|
29058
|
+
// Compact: the only fields read below are `phase` and `status`, for the
|
|
29059
|
+
// per-phase done/total tally. A non-compact pull dragged every handoff and
|
|
29060
|
+
// build report across the wire for two columns.
|
|
29061
|
+
adapter2.queryBoard({ compact: true })
|
|
27989
29062
|
]);
|
|
27990
29063
|
if (horizons.length === 0) return void 0;
|
|
27991
29064
|
const isInProgress = (s) => s.status === "In Progress";
|
|
@@ -28105,7 +29178,7 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
28105
29178
|
return { alertsNote, unactionedIssuesNote };
|
|
28106
29179
|
}
|
|
28107
29180
|
async function computeTeamSummary(adapter2, contributorsInput) {
|
|
28108
|
-
if (
|
|
29181
|
+
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
28109
29182
|
let members;
|
|
28110
29183
|
try {
|
|
28111
29184
|
members = (await (contributorsInput ?? adapter2.listContributors())).length;
|
|
@@ -28126,7 +29199,7 @@ async function computeTeamSummary(adapter2, contributorsInput) {
|
|
|
28126
29199
|
return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
|
|
28127
29200
|
}
|
|
28128
29201
|
async function computeReleaseHistory(adapter2, contributorsInput) {
|
|
28129
|
-
if (
|
|
29202
|
+
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
28130
29203
|
let contributors;
|
|
28131
29204
|
try {
|
|
28132
29205
|
contributors = await (contributorsInput ?? adapter2.listContributors());
|
|
@@ -28166,7 +29239,29 @@ function formatResolvedFeedback(items) {
|
|
|
28166
29239
|
lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
|
|
28167
29240
|
return lines.join("\n");
|
|
28168
29241
|
}
|
|
28169
|
-
|
|
29242
|
+
function withBoardMemo(adapter2) {
|
|
29243
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
29244
|
+
const queryBoard = (options) => {
|
|
29245
|
+
const key = JSON.stringify(options ?? null);
|
|
29246
|
+
const hit = inflight.get(key);
|
|
29247
|
+
if (hit) return hit;
|
|
29248
|
+
const pending = adapter2.queryBoard(options).catch((err) => {
|
|
29249
|
+
inflight.delete(key);
|
|
29250
|
+
throw err;
|
|
29251
|
+
});
|
|
29252
|
+
inflight.set(key, pending);
|
|
29253
|
+
return pending;
|
|
29254
|
+
};
|
|
29255
|
+
return new Proxy(adapter2, {
|
|
29256
|
+
get(target, prop, receiver) {
|
|
29257
|
+
if (prop === "queryBoard") return queryBoard;
|
|
29258
|
+
const value = Reflect.get(target, prop, receiver);
|
|
29259
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
29260
|
+
}
|
|
29261
|
+
});
|
|
29262
|
+
}
|
|
29263
|
+
async function handleOrient(rawAdapter, config2, args = {}, clientName) {
|
|
29264
|
+
const adapter2 = withBoardMemo(rawAdapter);
|
|
28170
29265
|
const environment = normaliseEnvironment(args.environment);
|
|
28171
29266
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
28172
29267
|
const fullEnrichment = args.full === true || deepHousekeeping;
|
|
@@ -28178,11 +29273,18 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28178
29273
|
} catch {
|
|
28179
29274
|
}
|
|
28180
29275
|
tracker.mark("fetch-build-health-hierarchy");
|
|
28181
|
-
const [
|
|
29276
|
+
const [buildSettled, healthSettled, hierarchySettled] = await Promise.allSettled([
|
|
28182
29277
|
tracked("listBuilds", () => listBuilds(adapter2, config2))(),
|
|
28183
29278
|
tracked("getHealthSummary", () => getHealthSummary(adapter2))(),
|
|
28184
29279
|
tracked("getHierarchyPosition", () => getHierarchyPosition(adapter2, config2.projectId))()
|
|
28185
29280
|
]);
|
|
29281
|
+
if (buildSettled.status === "rejected" && healthSettled.status === "rejected") {
|
|
29282
|
+
throw new Error(NOT_SET_UP_MESSAGE);
|
|
29283
|
+
}
|
|
29284
|
+
if (buildSettled.status === "rejected") throw buildSettled.reason;
|
|
29285
|
+
const buildResult = buildSettled.value;
|
|
29286
|
+
const healthResult = healthSettled.status === "fulfilled" ? healthSettled.value : emptyHealthSummary();
|
|
29287
|
+
const hierarchy = hierarchySettled.status === "fulfilled" ? hierarchySettled.value : void 0;
|
|
28186
29288
|
const currentCycle = buildResult.currentCycle;
|
|
28187
29289
|
const cycleIsComplete = healthResult.latestCycleStatus === "complete";
|
|
28188
29290
|
const allTasks = buildResult.sorted;
|
|
@@ -28655,7 +29757,7 @@ ${versionDrift}` : "";
|
|
|
28655
29757
|
}
|
|
28656
29758
|
}
|
|
28657
29759
|
tracker.mark("parallel-tail");
|
|
28658
|
-
const sharedContributorsPromise =
|
|
29760
|
+
const sharedContributorsPromise = adapterSupports(adapter2, "listContributors") ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
28659
29761
|
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
|
|
28660
29762
|
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
|
|
28661
29763
|
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
@@ -30262,7 +31364,7 @@ var contributorListTool = {
|
|
|
30262
31364
|
}
|
|
30263
31365
|
};
|
|
30264
31366
|
function denyUnlessCohortCapable(adapter2) {
|
|
30265
|
-
if (
|
|
31367
|
+
if (!adapterSupports(adapter2, "listContributors")) {
|
|
30266
31368
|
return "Contributor management is not available on this adapter (requires the pg or proxy adapter \u2014 the local md adapter has no cohort).";
|
|
30267
31369
|
}
|
|
30268
31370
|
return null;
|
|
@@ -30283,7 +31385,7 @@ async function denyUnlessMember(adapter2, config2) {
|
|
|
30283
31385
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
30284
31386
|
if (!gate.enforced) return null;
|
|
30285
31387
|
if (gate.callerIsOwner) return null;
|
|
30286
|
-
const callerRole = gate.callerUserId &&
|
|
31388
|
+
const callerRole = gate.callerUserId && adapterSupports(adapter2, "getContributorRole") ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
30287
31389
|
if (callerRole) return null;
|
|
30288
31390
|
const note = gate.resolutionError ? ` (Identity resolution failed: ${gate.resolutionError} \u2014 the gate fails closed; retry once connectivity is restored.)` : "";
|
|
30289
31391
|
return `Listing contributors is restricted to project members. Your identity does not match this project's owner or any contributor.${note}`;
|
|
@@ -30384,7 +31486,7 @@ function requireTaskId(args) {
|
|
|
30384
31486
|
return id.length > 0 ? id : null;
|
|
30385
31487
|
}
|
|
30386
31488
|
async function resolveClaimerIdentity(adapter2, config2) {
|
|
30387
|
-
if (
|
|
31489
|
+
if (!adapterSupports(adapter2, "claimTask")) {
|
|
30388
31490
|
return { error: "Claiming is not available on this adapter." };
|
|
30389
31491
|
}
|
|
30390
31492
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
@@ -30395,7 +31497,7 @@ async function resolveClaimerIdentity(adapter2, config2) {
|
|
|
30395
31497
|
error: "Claiming requires a resolvable user identity, but none was found" + note + ". Set PAPI_USER_ID to your account UUID (local) \u2014 hosted sessions derive it from your bearer token."
|
|
30396
31498
|
};
|
|
30397
31499
|
}
|
|
30398
|
-
if (!gate.callerIsOwner &&
|
|
31500
|
+
if (!gate.callerIsOwner && adapterSupports(adapter2, "listContributors")) {
|
|
30399
31501
|
try {
|
|
30400
31502
|
const members = await adapter2.listContributors();
|
|
30401
31503
|
if (!members.some((m) => m.userId === callerUserId)) {
|
|
@@ -30481,7 +31583,7 @@ ${claimedList}`
|
|
|
30481
31583
|
async function handleTaskUnclaim(adapter2, config2, args) {
|
|
30482
31584
|
const taskId = requireTaskId(args);
|
|
30483
31585
|
if (!taskId) return errorResponse('A task_id is required. Example: task_unclaim task_id="task-2071"');
|
|
30484
|
-
if (
|
|
31586
|
+
if (!adapterSupports(adapter2, "unclaimTask")) {
|
|
30485
31587
|
return errorResponse("Unclaiming is not available on this adapter.");
|
|
30486
31588
|
}
|
|
30487
31589
|
const identity = await resolveClaimerIdentity(adapter2, config2);
|
|
@@ -30532,7 +31634,7 @@ async function handleTaskMove(adapter2, config2, args) {
|
|
|
30532
31634
|
if (!taskId) return errorResponse('A task_id is required. Example: task_move task_id="task-42" target_project="other-project" confirm=true');
|
|
30533
31635
|
const targetProject = requireStr(args.target_project);
|
|
30534
31636
|
if (!targetProject) return errorResponse("A target_project (slug or UUID) is required \u2014 the project you want to move the task into.");
|
|
30535
|
-
if (
|
|
31637
|
+
if (!adapterSupports(adapter2, "moveTask")) {
|
|
30536
31638
|
return errorResponse("Cross-project move is not available on this adapter.");
|
|
30537
31639
|
}
|
|
30538
31640
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
@@ -31087,7 +32189,10 @@ var PAPI_TOOLS = [
|
|
|
31087
32189
|
taskClaimTool,
|
|
31088
32190
|
taskUnclaimTool,
|
|
31089
32191
|
taskMoveTool,
|
|
31090
|
-
inventorySyncTool
|
|
32192
|
+
inventorySyncTool,
|
|
32193
|
+
conventionDeclareTool,
|
|
32194
|
+
conventionListTool,
|
|
32195
|
+
conventionRemoveTool
|
|
31091
32196
|
];
|
|
31092
32197
|
function getToolMetadata() {
|
|
31093
32198
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
@@ -31292,6 +32397,12 @@ function createServer(adapter2, config2) {
|
|
|
31292
32397
|
return handleTaskMove(adapter2, config2, safeArgs);
|
|
31293
32398
|
case "task_unclaim":
|
|
31294
32399
|
return handleTaskUnclaim(adapter2, config2, safeArgs);
|
|
32400
|
+
case "convention_declare":
|
|
32401
|
+
return handleConventionDeclare(adapter2, config2, safeArgs);
|
|
32402
|
+
case "convention_list":
|
|
32403
|
+
return handleConventionList(adapter2, config2, safeArgs);
|
|
32404
|
+
case "convention_remove":
|
|
32405
|
+
return handleConventionRemove(adapter2, config2, safeArgs);
|
|
31295
32406
|
case "inventory_sync":
|
|
31296
32407
|
return handleInventorySync(adapter2, config2, safeArgs, getToolMetadata());
|
|
31297
32408
|
default:
|