@papi-ai/server 0.7.45 → 0.7.47
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 +68 -40
- package/dist/index.js +577 -99
- package/dist/prompts.js +10 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1280,6 +1280,10 @@ var init_proxy_adapter = __esm({
|
|
|
1280
1280
|
"claimReview",
|
|
1281
1281
|
"getSiblingAds",
|
|
1282
1282
|
"getSiblingRepoTasks"
|
|
1283
|
+
// task-2489 (C320): recordProgressStep is now wired to the edge data-proxy
|
|
1284
|
+
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
1285
|
+
// hosted callers and persists a project-scoped cycle_progress_steps row. Removed
|
|
1286
|
+
// from NO_FORWARD (was the task-2484 pg-only gap) — hosted parity restored.
|
|
1283
1287
|
]);
|
|
1284
1288
|
ProxyPapiAdapter = class _ProxyPapiAdapter {
|
|
1285
1289
|
endpoint;
|
|
@@ -5061,6 +5065,9 @@ function loadConfig() {
|
|
|
5061
5065
|
const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
|
|
5062
5066
|
const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
|
|
5063
5067
|
const userId = process.env.PAPI_USER_ID || void 0;
|
|
5068
|
+
const gateCommand = process.env.PAPI_GATE?.trim() || void 0;
|
|
5069
|
+
const deployCommand = process.env.PAPI_DEPLOY?.trim() || void 0;
|
|
5070
|
+
const verifyCommand = process.env.PAPI_VERIFY?.trim() || void 0;
|
|
5064
5071
|
const telemetryEnabled = process.env.PAPI_TELEMETRY !== "off" && process.env.PAPI_TELEMETRY !== "false";
|
|
5065
5072
|
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
5066
5073
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
@@ -5135,7 +5142,10 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5135
5142
|
skipProjectSpecificRules,
|
|
5136
5143
|
userId,
|
|
5137
5144
|
telemetryEnabled,
|
|
5138
|
-
resolutionMethod
|
|
5145
|
+
resolutionMethod,
|
|
5146
|
+
gateCommand,
|
|
5147
|
+
deployCommand,
|
|
5148
|
+
verifyCommand
|
|
5139
5149
|
};
|
|
5140
5150
|
}
|
|
5141
5151
|
|
|
@@ -5147,27 +5157,14 @@ import { execSync } from "child_process";
|
|
|
5147
5157
|
import { readFile, writeFile, access } from "fs/promises";
|
|
5148
5158
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5149
5159
|
import { join } from "path";
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
"Done": [],
|
|
5159
|
-
"Blocked": ["Backlog", "Ready", "In Cycle", "In Progress", "Cancelled"],
|
|
5160
|
-
"Cancelled": [],
|
|
5161
|
-
"Deferred": ["Backlog", "Cancelled"]
|
|
5162
|
-
};
|
|
5163
|
-
var RETIRED_DECISION_OUTCOMES = ["resolved", "abandoned", "superseded"];
|
|
5164
|
-
function isLiveDecision(d) {
|
|
5165
|
-
if (d.superseded === true) return false;
|
|
5166
|
-
if (d.outcome != null && RETIRED_DECISION_OUTCOMES.includes(d.outcome)) return false;
|
|
5167
|
-
return true;
|
|
5168
|
-
}
|
|
5169
|
-
|
|
5170
|
-
// ../adapter-md/dist/index.js
|
|
5160
|
+
import {
|
|
5161
|
+
VALID_TRANSITIONS as _VALID_TRANSITIONS,
|
|
5162
|
+
isValidTransition as _isValidTransition,
|
|
5163
|
+
validateTransition as _validateTransition,
|
|
5164
|
+
isValidStatus as _isValidStatus,
|
|
5165
|
+
isLiveDecision as _isLiveDecision,
|
|
5166
|
+
RETIRED_DECISION_OUTCOMES as _RETIRED_DECISION_OUTCOMES
|
|
5167
|
+
} from "@papi-ai/shared";
|
|
5171
5168
|
import { randomUUID } from "crypto";
|
|
5172
5169
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5173
5170
|
import yaml from "js-yaml";
|
|
@@ -5176,8 +5173,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
5176
5173
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
5177
5174
|
import yaml2 from "js-yaml";
|
|
5178
5175
|
import yaml3 from "js-yaml";
|
|
5179
|
-
var
|
|
5180
|
-
var
|
|
5176
|
+
var VALID_TRANSITIONS = _VALID_TRANSITIONS;
|
|
5177
|
+
var isLiveDecision = _isLiveDecision;
|
|
5181
5178
|
function extractSection(content, heading) {
|
|
5182
5179
|
const headingPattern = new RegExp(`^## ${heading}\\s*$`, "m");
|
|
5183
5180
|
const start = content.search(headingPattern);
|
|
@@ -6478,6 +6475,7 @@ function toCycle(raw) {
|
|
|
6478
6475
|
};
|
|
6479
6476
|
if (raw.end_date) cycle.endDate = raw.end_date;
|
|
6480
6477
|
if (raw.user_id) cycle.userId = raw.user_id;
|
|
6478
|
+
if (raw.dependency_chain) cycle.dependencyChain = raw.dependency_chain;
|
|
6481
6479
|
return cycle;
|
|
6482
6480
|
}
|
|
6483
6481
|
function fromCycle(cycle) {
|
|
@@ -6492,6 +6490,7 @@ function fromCycle(cycle) {
|
|
|
6492
6490
|
};
|
|
6493
6491
|
if (cycle.endDate) raw.end_date = cycle.endDate;
|
|
6494
6492
|
if (cycle.userId) raw.user_id = cycle.userId;
|
|
6493
|
+
if (cycle.dependencyChain) raw.dependency_chain = cycle.dependencyChain;
|
|
6495
6494
|
return raw;
|
|
6496
6495
|
}
|
|
6497
6496
|
function extractYamlBlock2(content) {
|
|
@@ -6536,7 +6535,7 @@ function prependCycle(cycle, content) {
|
|
|
6536
6535
|
const yamlStr = yaml2.dump({ cycles: [raw] }, { lineWidth: 120, quotingType: '"' });
|
|
6537
6536
|
return header + YAML_START2 + "\n" + yamlStr + YAML_END2 + "\n";
|
|
6538
6537
|
}
|
|
6539
|
-
const existing = parseCycles(content);
|
|
6538
|
+
const existing = parseCycles(content).filter((c) => c.number !== cycle.number);
|
|
6540
6539
|
const merged = [cycle, ...existing];
|
|
6541
6540
|
return serializeCycles(merged, content);
|
|
6542
6541
|
}
|
|
@@ -6633,7 +6632,7 @@ var MdFileAdapter = class {
|
|
|
6633
6632
|
if (!content) return [];
|
|
6634
6633
|
const all = parseActiveDecisions(content);
|
|
6635
6634
|
if (options?.includeRetired) return all;
|
|
6636
|
-
return all.filter(
|
|
6635
|
+
return all.filter(isLiveDecision);
|
|
6637
6636
|
}
|
|
6638
6637
|
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
6639
6638
|
async getCycleLog(limit) {
|
|
@@ -6801,7 +6800,7 @@ var MdFileAdapter = class {
|
|
|
6801
6800
|
}
|
|
6802
6801
|
if (updates.status && updates.status !== tasks[idx].status && !options?.force) {
|
|
6803
6802
|
const from = tasks[idx].status;
|
|
6804
|
-
const allowed =
|
|
6803
|
+
const allowed = VALID_TRANSITIONS[from];
|
|
6805
6804
|
if (!allowed.includes(updates.status)) {
|
|
6806
6805
|
console.warn(`[papi] Warning: invalid status transition "${from}" \u2192 "${updates.status}" for task ${id}. Allowed from "${from}": ${allowed.length > 0 ? allowed.join(", ") : "none"}`);
|
|
6807
6806
|
}
|
|
@@ -8307,31 +8306,66 @@ function formatBoardForReview(tasks) {
|
|
|
8307
8306
|
Notes: ${t.notes}` : ""}`
|
|
8308
8307
|
).join("\n\n");
|
|
8309
8308
|
}
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8309
|
+
function effortWeight(size2) {
|
|
8310
|
+
switch ((size2 || "").toUpperCase()) {
|
|
8311
|
+
case "XS":
|
|
8312
|
+
return 1;
|
|
8313
|
+
case "S":
|
|
8314
|
+
case "SMALL":
|
|
8315
|
+
return 2;
|
|
8316
|
+
case "M":
|
|
8317
|
+
case "MEDIUM":
|
|
8318
|
+
return 3;
|
|
8319
|
+
case "L":
|
|
8320
|
+
case "LARGE":
|
|
8321
|
+
return 5;
|
|
8322
|
+
case "XL":
|
|
8323
|
+
return 8;
|
|
8324
|
+
default:
|
|
8325
|
+
return 3;
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
8329
|
+
const reportsByCycle = /* @__PURE__ */ new Map();
|
|
8314
8330
|
for (const r of reports) {
|
|
8315
|
-
const existing =
|
|
8331
|
+
const existing = reportsByCycle.get(r.cycle) ?? [];
|
|
8316
8332
|
existing.push(r);
|
|
8317
|
-
|
|
8333
|
+
reportsByCycle.set(r.cycle, existing);
|
|
8334
|
+
}
|
|
8335
|
+
const tasksByCycle = /* @__PURE__ */ new Map();
|
|
8336
|
+
for (const t of tasks ?? []) {
|
|
8337
|
+
if (t.cycle == null) continue;
|
|
8338
|
+
const existing = tasksByCycle.get(t.cycle) ?? [];
|
|
8339
|
+
existing.push(t);
|
|
8340
|
+
tasksByCycle.set(t.cycle, existing);
|
|
8318
8341
|
}
|
|
8342
|
+
const cycleNumbers = /* @__PURE__ */ new Set([...reportsByCycle.keys(), ...tasksByCycle.keys()]);
|
|
8343
|
+
if (cycleNumbers.size === 0) return [];
|
|
8319
8344
|
const snapshots = [];
|
|
8320
|
-
for (const
|
|
8321
|
-
const
|
|
8322
|
-
const
|
|
8345
|
+
for (const sn of cycleNumbers) {
|
|
8346
|
+
const cycleReports = reportsByCycle.get(sn) ?? [];
|
|
8347
|
+
const cycleTaskRows = tasksByCycle.get(sn);
|
|
8323
8348
|
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
8324
8349
|
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
8325
8350
|
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
8326
|
-
let
|
|
8327
|
-
|
|
8328
|
-
|
|
8351
|
+
let completed;
|
|
8352
|
+
let total;
|
|
8353
|
+
let effortPoints;
|
|
8354
|
+
if (cycleTaskRows && cycleTaskRows.length > 0) {
|
|
8355
|
+
const done = cycleTaskRows.filter((t) => t.status === "Done");
|
|
8356
|
+
completed = done.length;
|
|
8357
|
+
total = cycleTaskRows.length;
|
|
8358
|
+
effortPoints = done.reduce((s, t) => s + effortWeight(t.complexity), 0);
|
|
8359
|
+
} else {
|
|
8360
|
+
completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
8361
|
+
total = cycleReports.length;
|
|
8362
|
+
effortPoints = cycleReports.reduce((s, r) => s + effortWeight(r.actualEffort), 0);
|
|
8329
8363
|
}
|
|
8330
8364
|
snapshots.push({
|
|
8331
8365
|
cycle: sn,
|
|
8332
8366
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8333
|
-
accuracy: [{ cycle: sn, reports:
|
|
8334
|
-
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
8367
|
+
accuracy: [{ cycle: sn, reports: cycleReports.length, matchRate, mae: 0, bias: 0 }],
|
|
8368
|
+
velocity: [{ cycle: sn, completed, partial: 0, failed: Math.max(0, total - completed), effortPoints }]
|
|
8335
8369
|
});
|
|
8336
8370
|
}
|
|
8337
8371
|
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
@@ -9273,6 +9307,14 @@ function buildHandoffsOnlyUserMessage(inputs) {
|
|
|
9273
9307
|
}
|
|
9274
9308
|
return parts.join("\n");
|
|
9275
9309
|
}
|
|
9310
|
+
function extractDependencyChain(displayText) {
|
|
9311
|
+
const match = displayText.match(
|
|
9312
|
+
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|\n-{3,}\s*(?:\n|$)|\n\s*BUILD HANDOFF\b|\n<!--\s*PAPI_STRUCTURED_OUTPUT|$(?![\s\S]))/m
|
|
9313
|
+
);
|
|
9314
|
+
if (!match) return void 0;
|
|
9315
|
+
const body = match[1].trim();
|
|
9316
|
+
return body.length > 0 ? body : void 0;
|
|
9317
|
+
}
|
|
9276
9318
|
function parseStructuredOutput(raw) {
|
|
9277
9319
|
const marker = "<!-- PAPI_STRUCTURED_OUTPUT -->";
|
|
9278
9320
|
const markerIdx = raw.indexOf(marker);
|
|
@@ -9288,6 +9330,7 @@ function parseStructuredOutput(raw) {
|
|
|
9288
9330
|
try {
|
|
9289
9331
|
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
9290
9332
|
const data = coerceStructuredOutput(parsed);
|
|
9333
|
+
data.dependencyChain = extractDependencyChain(displayText);
|
|
9291
9334
|
return { displayText, data };
|
|
9292
9335
|
} catch {
|
|
9293
9336
|
return { displayText, data: null };
|
|
@@ -11746,8 +11789,27 @@ ${cleanContent}`;
|
|
|
11746
11789
|
contextHashes,
|
|
11747
11790
|
// task-2071 (MU-3): own the cycle by the planning member (pg defaults to the
|
|
11748
11791
|
// project owner when unset, so owner-operated plans stay correct).
|
|
11749
|
-
...options.ownerUserId ? { userId: options.ownerUserId } : {}
|
|
11792
|
+
...options.ownerUserId ? { userId: options.ownerUserId } : {},
|
|
11793
|
+
// task-2483 (C319): persist the plan's Dependency Chain build-order markdown.
|
|
11794
|
+
...data.dependencyChain ? { dependencyChain: data.dependencyChain } : {}
|
|
11750
11795
|
};
|
|
11796
|
+
const earlyCreateWarnings = [];
|
|
11797
|
+
try {
|
|
11798
|
+
await adapter2.createCycle({
|
|
11799
|
+
id: `cycle-${newCycleNumber}`,
|
|
11800
|
+
number: newCycleNumber,
|
|
11801
|
+
status: "active",
|
|
11802
|
+
startDate: cycle.startDate,
|
|
11803
|
+
goals: cycle.goals,
|
|
11804
|
+
boardHealth: "",
|
|
11805
|
+
taskIds: [],
|
|
11806
|
+
...cycle.userId ? { userId: cycle.userId } : {}
|
|
11807
|
+
});
|
|
11808
|
+
} catch (err) {
|
|
11809
|
+
const msg = `early createCycle failed for cycle ${newCycleNumber}: ${err instanceof Error ? err.message : String(err)}`;
|
|
11810
|
+
console.error(`[plan] ${msg}`);
|
|
11811
|
+
earlyCreateWarnings.push(msg);
|
|
11812
|
+
}
|
|
11751
11813
|
let dedupedNewTasks = data.newTasks ?? [];
|
|
11752
11814
|
if (dedupedNewTasks.length > 0) {
|
|
11753
11815
|
const existingTasks = await adapter2.queryBoard({ compact: true });
|
|
@@ -11840,7 +11902,7 @@ ${cleanContent}`;
|
|
|
11840
11902
|
`Plan write-back created ${actualNewTasks} of ${expectedNewTasks} new tasks the transaction was asked to create \u2014 investigate the planWriteBack handler. (This count is returned by the committed transaction, not a board re-read, so it is not a hosted read-lag artifact.)`
|
|
11841
11903
|
);
|
|
11842
11904
|
}
|
|
11843
|
-
const allWarnings = [...result.warnings, ...verifyWarnings];
|
|
11905
|
+
const allWarnings = [...result.warnings, ...verifyWarnings, ...earlyCreateWarnings];
|
|
11844
11906
|
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
11845
11907
|
const correctionCount = data.boardCorrections?.length ?? 0;
|
|
11846
11908
|
const newTaskCount = result.newTaskIdMap.size;
|
|
@@ -11880,6 +11942,23 @@ ${cleanContent}`;
|
|
|
11880
11942
|
});
|
|
11881
11943
|
const cycleTaskCount = legacyHandoffs.length;
|
|
11882
11944
|
const cycleEffortPoints = legacyHandoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
|
|
11945
|
+
try {
|
|
11946
|
+
await adapter2.createCycle({
|
|
11947
|
+
id: `cycle-${newCycleNumber}`,
|
|
11948
|
+
number: newCycleNumber,
|
|
11949
|
+
status: "active",
|
|
11950
|
+
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11951
|
+
goals: data.cycleLogTitle ? [data.cycleLogTitle] : [],
|
|
11952
|
+
boardHealth: "",
|
|
11953
|
+
taskIds: [],
|
|
11954
|
+
contextHashes,
|
|
11955
|
+
...options.ownerUserId ? { userId: options.ownerUserId } : {}
|
|
11956
|
+
});
|
|
11957
|
+
} catch (err) {
|
|
11958
|
+
const msg = `early createCycle failed for cycle ${newCycleNumber}: ${err instanceof Error ? err.message : String(err)}`;
|
|
11959
|
+
console.error(`[plan] ${msg}`);
|
|
11960
|
+
warnings.push(msg);
|
|
11961
|
+
}
|
|
11883
11962
|
const cycleLogPromise = adapter2.writeCycleLogEntry({
|
|
11884
11963
|
uuid: randomUUID8(),
|
|
11885
11964
|
cycleNumber: newCycleNumber,
|
|
@@ -12142,7 +12221,10 @@ ${cleanContent}`;
|
|
|
12142
12221
|
taskIds: cycleTaskIds,
|
|
12143
12222
|
contextHashes,
|
|
12144
12223
|
// task-2071 (MU-3): own the cycle by the planning member.
|
|
12145
|
-
...options.ownerUserId ? { userId: options.ownerUserId } : {}
|
|
12224
|
+
...options.ownerUserId ? { userId: options.ownerUserId } : {},
|
|
12225
|
+
// task-2483 (C319): persist the plan's Dependency Chain build-order markdown.
|
|
12226
|
+
// This full write patches the early minimal row created in Phase 1.
|
|
12227
|
+
...data.dependencyChain ? { dependencyChain: data.dependencyChain } : {}
|
|
12146
12228
|
};
|
|
12147
12229
|
await adapter2.createCycle(cycle);
|
|
12148
12230
|
} catch (err) {
|
|
@@ -12468,6 +12550,13 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12468
12550
|
contextHashes
|
|
12469
12551
|
};
|
|
12470
12552
|
}
|
|
12553
|
+
var PLAN_STAGE_STEPS = ["health-check", "inbox-triage", "board-integrity", "maturity-gate", "recommendation", "dependency-chain"];
|
|
12554
|
+
async function streamPlanStageSteps(tracker, newCycleNumber) {
|
|
12555
|
+
tracker.setStreamScope({ cycle: newCycleNumber });
|
|
12556
|
+
for (const step of PLAN_STAGE_STEPS) {
|
|
12557
|
+
await tracker.recordStep(step);
|
|
12558
|
+
}
|
|
12559
|
+
}
|
|
12471
12560
|
async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes, planRunMeta, tracker) {
|
|
12472
12561
|
const applyTimer = startTimer();
|
|
12473
12562
|
console.error(`[plan-perf] applyPlan: start (llm_response=${rawLlmOutput.length} chars)`);
|
|
@@ -12480,6 +12569,7 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
|
|
|
12480
12569
|
if (config2.adapterType === "pg") {
|
|
12481
12570
|
tracker?.mark("process_llm_output_pg");
|
|
12482
12571
|
const result = await processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes, planRunMeta);
|
|
12572
|
+
if (tracker) await streamPlanStageSteps(tracker, cycleNumber + 1);
|
|
12483
12573
|
const applyMs2 = applyTimer();
|
|
12484
12574
|
console.error(`[plan-perf] applyPlan: total=${applyMs2}ms (pg, no git sync)`);
|
|
12485
12575
|
return { ...result, pullNote: "", strategyReviewWarning };
|
|
@@ -12622,12 +12712,58 @@ ${JSON.stringify(payload, null, 2)}`;
|
|
|
12622
12712
|
}
|
|
12623
12713
|
var ProgressTracker = class {
|
|
12624
12714
|
lastStep;
|
|
12715
|
+
persistAdapter;
|
|
12716
|
+
streamCtx;
|
|
12625
12717
|
constructor(initial = "start") {
|
|
12626
12718
|
this.lastStep = initial;
|
|
12627
12719
|
}
|
|
12628
12720
|
mark(step) {
|
|
12629
12721
|
this.lastStep = step;
|
|
12630
12722
|
}
|
|
12723
|
+
/**
|
|
12724
|
+
* Arm progress-stream persistence for this tool call. Safe to call with any
|
|
12725
|
+
* adapter — persistence stays disarmed unless the adapter exposes BOTH
|
|
12726
|
+
* `getProjectId` and `recordProgressStep` (i.e. the pg/DB-backed path). Returns
|
|
12727
|
+
* `this` for chaining at the tracker construction site.
|
|
12728
|
+
*/
|
|
12729
|
+
bindStream(adapter2, ctx) {
|
|
12730
|
+
if (adapter2?.getProjectId && adapter2.recordProgressStep) {
|
|
12731
|
+
this.persistAdapter = adapter2;
|
|
12732
|
+
}
|
|
12733
|
+
this.streamCtx = ctx;
|
|
12734
|
+
return this;
|
|
12735
|
+
}
|
|
12736
|
+
/** Update the bound scope mid-call (e.g. once the cycle number is resolved). */
|
|
12737
|
+
setStreamScope(scope) {
|
|
12738
|
+
if (!this.streamCtx) return;
|
|
12739
|
+
if (scope.cycle !== void 0) this.streamCtx.cycle = scope.cycle;
|
|
12740
|
+
if (scope.taskId !== void 0) this.streamCtx.taskId = scope.taskId;
|
|
12741
|
+
}
|
|
12742
|
+
/**
|
|
12743
|
+
* Record one step of the progress stream. Also advances `lastStep` so the
|
|
12744
|
+
* structured-error diagnostic still points at the most recent named step.
|
|
12745
|
+
* Fire-and-forget and fully guarded — the returned promise always resolves.
|
|
12746
|
+
*/
|
|
12747
|
+
recordStep(step, opts = {}) {
|
|
12748
|
+
this.lastStep = step;
|
|
12749
|
+
const adapter2 = this.persistAdapter;
|
|
12750
|
+
const ctx = this.streamCtx;
|
|
12751
|
+
if (!adapter2?.recordProgressStep || !ctx) return Promise.resolve();
|
|
12752
|
+
const row = {
|
|
12753
|
+
stage: opts.stage ?? ctx.stage,
|
|
12754
|
+
step,
|
|
12755
|
+
status: opts.status ?? "complete",
|
|
12756
|
+
cycle: opts.cycle !== void 0 ? opts.cycle : ctx.cycle ?? null,
|
|
12757
|
+
taskId: opts.taskId !== void 0 ? opts.taskId : ctx.taskId ?? null,
|
|
12758
|
+
capabilityKey: opts.capabilityKey ?? null,
|
|
12759
|
+
capabilityEnabled: opts.capabilityEnabled ?? null,
|
|
12760
|
+
metadata: opts.metadata
|
|
12761
|
+
};
|
|
12762
|
+
return adapter2.recordProgressStep(row).catch((err) => {
|
|
12763
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12764
|
+
console.error(`[progress-stream] failed to persist ${ctx.stage}/${step} (${row.status}): ${message}`);
|
|
12765
|
+
});
|
|
12766
|
+
}
|
|
12631
12767
|
};
|
|
12632
12768
|
function defaultHint(tool) {
|
|
12633
12769
|
const knownTools = /* @__PURE__ */ new Set([
|
|
@@ -13088,7 +13224,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13088
13224
|
const focus = typeof args.focus === "string" ? args.focus : void 0;
|
|
13089
13225
|
const force = args.force === true;
|
|
13090
13226
|
const handoffsOnly = args.handoffs_only === true;
|
|
13091
|
-
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate");
|
|
13227
|
+
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate").bindStream(adapter2, { stage: "plan" });
|
|
13092
13228
|
try {
|
|
13093
13229
|
if (toolMode === "apply") {
|
|
13094
13230
|
const resolved = await resolveLlmResponse(
|
|
@@ -18322,6 +18458,86 @@ Examples (use whichever provider you have): ${spec.examples}.
|
|
|
18322
18458
|
_Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
|
|
18323
18459
|
}
|
|
18324
18460
|
|
|
18461
|
+
// src/lib/capabilities.ts
|
|
18462
|
+
import {
|
|
18463
|
+
CAPABILITY_KEYS,
|
|
18464
|
+
CAPABILITY_REGISTRY,
|
|
18465
|
+
isCapabilityKey,
|
|
18466
|
+
isCapabilityEnabled
|
|
18467
|
+
} from "@papi-ai/shared";
|
|
18468
|
+
|
|
18469
|
+
// src/lib/directive-builders.ts
|
|
18470
|
+
function buildPrReviewerDirective(caps) {
|
|
18471
|
+
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
18472
|
+
return `
|
|
18473
|
+
|
|
18474
|
+
**Quality Gate:** no auto-review attached. PAPI's standard pre-accept step is a code review of the branch diff \u2014 run \`review_submit\` with \`dispatch:"subagent"\` to auto-review, or attach \`auto_review\` findings. Risk-tier work (auth, data, migrations, CI) should always carry one.`;
|
|
18475
|
+
}
|
|
18476
|
+
function buildChangelogDirective(caps, inner) {
|
|
18477
|
+
if (!isCapabilityEnabled(caps, "changelog")) return null;
|
|
18478
|
+
return inner;
|
|
18479
|
+
}
|
|
18480
|
+
function buildDiscoveredIssuesDirective(caps, issueLines) {
|
|
18481
|
+
if (!isCapabilityEnabled(caps, "discoveredIssues")) return null;
|
|
18482
|
+
if (issueLines.length === 0) return null;
|
|
18483
|
+
return [
|
|
18484
|
+
"",
|
|
18485
|
+
"---",
|
|
18486
|
+
"",
|
|
18487
|
+
`## Discovered Issues (${issueLines.length})`,
|
|
18488
|
+
"",
|
|
18489
|
+
...issueLines,
|
|
18490
|
+
"",
|
|
18491
|
+
"*These issues were logged during builds \u2014 triage them in the next plan.*"
|
|
18492
|
+
].join("\n");
|
|
18493
|
+
}
|
|
18494
|
+
function buildModelRecommendationDirective(caps, tierBlock) {
|
|
18495
|
+
if (!isCapabilityEnabled(caps, "modelRecommendation")) return null;
|
|
18496
|
+
return tierBlock;
|
|
18497
|
+
}
|
|
18498
|
+
function buildVerifyHealthCheckDirective(caps) {
|
|
18499
|
+
if (!isCapabilityEnabled(caps, "verifyHealthCheck")) return null;
|
|
18500
|
+
return `
|
|
18501
|
+
|
|
18502
|
+
**Health check:** before you consider the cycle shipped, verify cycle state (plan validity, review coverage, branch hygiene) \u2014 run the \`papi-verify\` skill or a quick \`board_view\` pass.`;
|
|
18503
|
+
}
|
|
18504
|
+
function buildGestaltPreBuildDirective(caps) {
|
|
18505
|
+
if (!isCapabilityEnabled(caps, "gestaltPreBuild")) return null;
|
|
18506
|
+
return `
|
|
18507
|
+
|
|
18508
|
+
**Gestalt pre-build:** on the first task of a multi-task cycle, read every task's BUILD HANDOFF together (shared files, sequencing, module split) before building \u2014 one-time check at cycle start.`;
|
|
18509
|
+
}
|
|
18510
|
+
function buildBatchBuildRollupDirective(caps) {
|
|
18511
|
+
if (!isCapabilityEnabled(caps, "batchBuildRollup")) return null;
|
|
18512
|
+
return `
|
|
18513
|
+
|
|
18514
|
+
**Batch rollup:** after the final task of a batch, emit a cycle-level rollup \u2014 build summary table + discovered issues grouped by severity + next action.`;
|
|
18515
|
+
}
|
|
18516
|
+
function buildSecurityScanDirective(caps) {
|
|
18517
|
+
if (!isCapabilityEnabled(caps, "securityScan")) return null;
|
|
18518
|
+
return `
|
|
18519
|
+
|
|
18520
|
+
**Security scan:** risk-tier changes (auth, data, migrations, secrets, endpoints) should carry a security pass \u2014 OWASP top-10 + secret-exposure check on the changed surface before merge.`;
|
|
18521
|
+
}
|
|
18522
|
+
function buildDeployHookDirective(caps, deployCommand) {
|
|
18523
|
+
if (!isCapabilityEnabled(caps, "deployHook")) return null;
|
|
18524
|
+
const command = deployCommand?.trim() || void 0;
|
|
18525
|
+
if (!command) return null;
|
|
18526
|
+
return `
|
|
18527
|
+
|
|
18528
|
+
---
|
|
18529
|
+
|
|
18530
|
+
## Deploy \u2014 run after this release
|
|
18531
|
+
|
|
18532
|
+
The post-release deploy hook is on and a deploy command is configured, so ship the merged release now.
|
|
18533
|
+
|
|
18534
|
+
Run your deploy command yourself:
|
|
18535
|
+
\`\`\`
|
|
18536
|
+
${command}
|
|
18537
|
+
\`\`\`
|
|
18538
|
+
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment. Turn the "Post-release deploy" capability off in the dashboard, or unset PAPI_DEPLOY, to stop this reminder.`;
|
|
18539
|
+
}
|
|
18540
|
+
|
|
18325
18541
|
// src/services/build.ts
|
|
18326
18542
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
18327
18543
|
import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -18658,7 +18874,8 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
18658
18874
|
if (adapter2.getBuildReportsSince) {
|
|
18659
18875
|
try {
|
|
18660
18876
|
const cycleReports = (await adapter2.getBuildReportsSince(currentCycle)).filter((r) => r.cycle === currentCycle);
|
|
18661
|
-
|
|
18877
|
+
const cycleTasks = (await adapter2.queryBoard({ cycleSince: currentCycle, compact: true })).filter((t) => t.cycle === currentCycle);
|
|
18878
|
+
[snapshot] = computeSnapshotsFromBuildReports(cycleReports, cycleTasks);
|
|
18662
18879
|
} catch (err) {
|
|
18663
18880
|
console.error(
|
|
18664
18881
|
`[release] cycle-metrics snapshot compute failed for cycle ${currentCycle} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -18998,6 +19215,65 @@ async function updatePluginMarketplaces(version) {
|
|
|
18998
19215
|
return [cursor, vscode].filter((r) => r !== null);
|
|
18999
19216
|
}
|
|
19000
19217
|
|
|
19218
|
+
// src/lib/release-gate.ts
|
|
19219
|
+
function evaluateReleaseGate(caps, gateCommand, gateResult) {
|
|
19220
|
+
const command = gateCommand?.trim() || void 0;
|
|
19221
|
+
if (!isCapabilityEnabled(caps, "releaseGate")) {
|
|
19222
|
+
return {
|
|
19223
|
+
action: "proceed",
|
|
19224
|
+
stepStatus: "complete",
|
|
19225
|
+
metadata: { skipped: true, reason: "capability_off" },
|
|
19226
|
+
message: ""
|
|
19227
|
+
};
|
|
19228
|
+
}
|
|
19229
|
+
if (!command) {
|
|
19230
|
+
return {
|
|
19231
|
+
action: "proceed",
|
|
19232
|
+
stepStatus: "complete",
|
|
19233
|
+
metadata: { note: "no gate configured" },
|
|
19234
|
+
message: ""
|
|
19235
|
+
};
|
|
19236
|
+
}
|
|
19237
|
+
if (gateResult === "fail") {
|
|
19238
|
+
return {
|
|
19239
|
+
action: "block",
|
|
19240
|
+
stepStatus: "failed",
|
|
19241
|
+
metadata: { command },
|
|
19242
|
+
message: `Release blocked \u2014 quality gate failed.
|
|
19243
|
+
|
|
19244
|
+
The gate command \`${command}\` reported a failure, so PAPI did NOT tag, merge, or close the cycle.
|
|
19245
|
+
|
|
19246
|
+
Fix the failing tests/build, re-run \`${command}\`, and call \`release\` again with \`gate_result: "pass"\` once it is green. To ship past a failing gate anyway, turn the "Release quality gate" capability off in the dashboard, or unset PAPI_GATE.`
|
|
19247
|
+
};
|
|
19248
|
+
}
|
|
19249
|
+
if (gateResult === "pass") {
|
|
19250
|
+
return {
|
|
19251
|
+
action: "proceed",
|
|
19252
|
+
stepStatus: "complete",
|
|
19253
|
+
metadata: { command },
|
|
19254
|
+
message: ""
|
|
19255
|
+
};
|
|
19256
|
+
}
|
|
19257
|
+
return {
|
|
19258
|
+
action: "directive",
|
|
19259
|
+
stepStatus: "active",
|
|
19260
|
+
metadata: { command },
|
|
19261
|
+
message: `## Quality gate \u2014 run before this release ships
|
|
19262
|
+
|
|
19263
|
+
PAPI's release quality gate is on and a gate command is configured, so the release is HELD until the gate passes. Nothing has been tagged, merged, or closed yet.
|
|
19264
|
+
|
|
19265
|
+
1. Run the gate command yourself:
|
|
19266
|
+
\`\`\`
|
|
19267
|
+
${command}
|
|
19268
|
+
\`\`\`
|
|
19269
|
+
2. Re-call \`release\` with the same \`branch\` and \`version\`, adding:
|
|
19270
|
+
- \`gate_result: "pass"\` if it succeeded \u2014 the release then proceeds normally.
|
|
19271
|
+
- \`gate_result: "fail"\` if it failed \u2014 the release stays blocked so you can fix it first.
|
|
19272
|
+
|
|
19273
|
+
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment and report the result.`
|
|
19274
|
+
};
|
|
19275
|
+
}
|
|
19276
|
+
|
|
19001
19277
|
// src/tools/release.ts
|
|
19002
19278
|
function parseGithubOwner(input) {
|
|
19003
19279
|
if (!input) return null;
|
|
@@ -19078,6 +19354,11 @@ var releaseTool = {
|
|
|
19078
19354
|
type: "boolean",
|
|
19079
19355
|
description: "Update CHANGELOG.md and mark the cycle complete, but skip creating a git tag and pushing it. Use when you want cycle closure and changelog tracking without burning a version number. The version parameter is still used for the CHANGELOG entry heading."
|
|
19080
19356
|
},
|
|
19357
|
+
gate_result: {
|
|
19358
|
+
type: "string",
|
|
19359
|
+
enum: ["pass", "fail"],
|
|
19360
|
+
description: 'task-2482 (release quality gate): the result of running the configured gate command (papi.gate / PAPI_GATE). Set this on the SECOND release call AFTER you have run the gate command yourself \u2014 "pass" lets the release proceed, "fail" keeps it blocked. Leave it UNSET on the first call: if the release-quality-gate capability is on and a gate command is configured, release returns a directive telling you to run the command, then re-call with the result. PAPI never runs the command itself (AD-58).'
|
|
19361
|
+
},
|
|
19081
19362
|
observations: {
|
|
19082
19363
|
type: "array",
|
|
19083
19364
|
description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
|
|
@@ -19108,6 +19389,7 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19108
19389
|
let version = args.version;
|
|
19109
19390
|
const force = args.force;
|
|
19110
19391
|
const skipVersion = args.skipVersion;
|
|
19392
|
+
const gateResult = args.gate_result;
|
|
19111
19393
|
const rawObservations = args.observations;
|
|
19112
19394
|
if (!branch || !version) {
|
|
19113
19395
|
return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
|
|
@@ -19121,8 +19403,9 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19121
19403
|
version = `${version}-${suffix}`;
|
|
19122
19404
|
}
|
|
19123
19405
|
}
|
|
19124
|
-
const tracker = new ProgressTracker("validate-args");
|
|
19406
|
+
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
|
|
19125
19407
|
try {
|
|
19408
|
+
await tracker.recordStep("release_starting");
|
|
19126
19409
|
tracker.mark("owner-identity-guard");
|
|
19127
19410
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
19128
19411
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
@@ -19249,12 +19532,41 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19249
19532
|
console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
|
|
19250
19533
|
}
|
|
19251
19534
|
}
|
|
19535
|
+
await tracker.recordStep("readiness_verified");
|
|
19536
|
+
let caps = {};
|
|
19537
|
+
try {
|
|
19538
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
19539
|
+
caps = info?.capabilities ?? {};
|
|
19540
|
+
} catch {
|
|
19541
|
+
caps = {};
|
|
19542
|
+
}
|
|
19543
|
+
tracker.mark("quality-gate");
|
|
19544
|
+
const gateDecision = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
|
|
19545
|
+
await tracker.recordStep("quality_gate", {
|
|
19546
|
+
status: gateDecision.stepStatus,
|
|
19547
|
+
capabilityKey: "releaseGate",
|
|
19548
|
+
capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
|
|
19549
|
+
metadata: gateDecision.metadata
|
|
19550
|
+
});
|
|
19551
|
+
if (gateDecision.action === "directive") {
|
|
19552
|
+
return textResponse(gateDecision.message);
|
|
19553
|
+
}
|
|
19554
|
+
if (gateDecision.action === "block") {
|
|
19555
|
+
return errorResponse(gateDecision.message);
|
|
19556
|
+
}
|
|
19252
19557
|
tracker.mark("create-release");
|
|
19253
19558
|
const result = await createRelease(config2, branch, version, adapter2, void 0, {
|
|
19254
19559
|
force: force ?? false,
|
|
19255
19560
|
skipVersion: skipVersion ?? false,
|
|
19256
19561
|
callerUserId: gate.callerUserId
|
|
19257
19562
|
});
|
|
19563
|
+
tracker.setStreamScope({ cycle: result.cycleClosed ?? null });
|
|
19564
|
+
await tracker.recordStep("cycle_complete", { metadata: { version: result.version } });
|
|
19565
|
+
for (const m of result.groupedBranchMerges ?? []) {
|
|
19566
|
+
await tracker.recordStep("branch_merged", {
|
|
19567
|
+
metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
|
|
19568
|
+
});
|
|
19569
|
+
}
|
|
19258
19570
|
const lines = [
|
|
19259
19571
|
`## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
|
|
19260
19572
|
"",
|
|
@@ -19283,10 +19595,8 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19283
19595
|
const reports = await adapter2.getBuildReportsSince(closedCycle);
|
|
19284
19596
|
const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
|
|
19285
19597
|
const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
lines.push("", "*These issues were logged during builds \u2014 triage them in the next plan.*");
|
|
19289
|
-
}
|
|
19598
|
+
const issuesDirective = buildDiscoveredIssuesDirective(caps, issues);
|
|
19599
|
+
if (issuesDirective) lines.push(issuesDirective);
|
|
19290
19600
|
}
|
|
19291
19601
|
} catch {
|
|
19292
19602
|
}
|
|
@@ -19306,28 +19616,54 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19306
19616
|
lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
|
|
19307
19617
|
}
|
|
19308
19618
|
}
|
|
19309
|
-
const cycleUpdateDirective =
|
|
19619
|
+
const cycleUpdateDirective = buildChangelogDirective(
|
|
19620
|
+
caps,
|
|
19621
|
+
buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
|
|
19622
|
+
);
|
|
19310
19623
|
if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19624
|
+
await tracker.recordStep("changelog", {
|
|
19625
|
+
capabilityKey: "changelog",
|
|
19626
|
+
capabilityEnabled: isCapabilityEnabled(caps, "changelog"),
|
|
19627
|
+
status: cycleUpdateDirective ? "complete" : "active"
|
|
19628
|
+
});
|
|
19629
|
+
const deployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
19630
|
+
if (deployDirective) {
|
|
19631
|
+
lines.push(deployDirective);
|
|
19632
|
+
await tracker.recordStep("deploy_hook", {
|
|
19633
|
+
capabilityKey: "deployHook",
|
|
19634
|
+
capabilityEnabled: isCapabilityEnabled(caps, "deployHook"),
|
|
19635
|
+
status: "complete",
|
|
19636
|
+
metadata: { configured: true }
|
|
19637
|
+
});
|
|
19315
19638
|
}
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19639
|
+
const publishEnabled = isCapabilityEnabled(caps, "publishDirective");
|
|
19640
|
+
if (publishEnabled) {
|
|
19641
|
+
try {
|
|
19642
|
+
const xPosted = await postReleaseToX(result.version, result.cycleClosed ?? 0);
|
|
19643
|
+
if (xPosted) lines.push("", "Posted release announcement to X.");
|
|
19644
|
+
} catch {
|
|
19320
19645
|
}
|
|
19321
|
-
} catch {
|
|
19322
19646
|
}
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19647
|
+
if (publishEnabled) {
|
|
19648
|
+
try {
|
|
19649
|
+
const registryResults = await updateRegistryListings(result.version);
|
|
19650
|
+
for (const r of registryResults) {
|
|
19651
|
+
lines.push("", `Registry [${r.registry}]: ${r.message}`);
|
|
19652
|
+
}
|
|
19653
|
+
} catch {
|
|
19654
|
+
}
|
|
19655
|
+
try {
|
|
19656
|
+
const marketplaceResults = await updatePluginMarketplaces(result.version);
|
|
19657
|
+
for (const r of marketplaceResults) {
|
|
19658
|
+
lines.push("", `Marketplace [${r.registry}]: ${r.message}`);
|
|
19659
|
+
}
|
|
19660
|
+
} catch {
|
|
19327
19661
|
}
|
|
19328
|
-
} catch {
|
|
19329
19662
|
}
|
|
19663
|
+
const verifyDirective = buildVerifyHealthCheckDirective(caps);
|
|
19664
|
+
if (verifyDirective) lines.push(verifyDirective);
|
|
19330
19665
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`);
|
|
19666
|
+
await tracker.recordStep("released", { metadata: { version: result.version } });
|
|
19331
19667
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
19332
19668
|
return textResponse(lines.join("\n") + filesToWriteSection);
|
|
19333
19669
|
} catch (err) {
|
|
@@ -19775,6 +20111,9 @@ async function describeTask(adapter2, taskId) {
|
|
|
19775
20111
|
}
|
|
19776
20112
|
return { task };
|
|
19777
20113
|
}
|
|
20114
|
+
function shouldBlockDirtyBranchSwitch(opts) {
|
|
20115
|
+
return opts.trackedDirtyCount > 0 && opts.currentBranch !== opts.baseBranch && opts.currentBranch !== opts.featureBranch;
|
|
20116
|
+
}
|
|
19778
20117
|
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
19779
20118
|
const task = await adapter2.getTask(taskId);
|
|
19780
20119
|
if (!task) {
|
|
@@ -19896,11 +20235,11 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
19896
20235
|
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
19897
20236
|
}
|
|
19898
20237
|
const trackedDirty = getTrackedModifiedFiles(config2.projectRoot);
|
|
19899
|
-
if (trackedDirty.length
|
|
20238
|
+
if (shouldBlockDirtyBranchSwitch({ trackedDirtyCount: trackedDirty.length, currentBranch, baseBranch, featureBranch })) {
|
|
19900
20239
|
const shown = trackedDirty.slice(0, 5).join(", ");
|
|
19901
20240
|
const more = trackedDirty.length > 5 ? ` (+${trackedDirty.length - 5} more)` : "";
|
|
19902
20241
|
throw new Error(
|
|
19903
|
-
`build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first,
|
|
20242
|
+
`build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first, run build_execute from '${baseBranch}', or check out '${featureBranch}' if this is your own in-progress work. Dirty: ${shown}${more}.`
|
|
19904
20243
|
);
|
|
19905
20244
|
}
|
|
19906
20245
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
@@ -20073,7 +20412,8 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
20073
20412
|
}
|
|
20074
20413
|
return { title, type, cycle, summary, tags: [] };
|
|
20075
20414
|
}
|
|
20076
|
-
function assertDeployVerification(config2, input) {
|
|
20415
|
+
function assertDeployVerification(config2, input, opts = { deployingNow: true }) {
|
|
20416
|
+
if (!opts.deployingNow) return;
|
|
20077
20417
|
if (config2.adapterType !== "pg" && config2.adapterType !== "proxy") return;
|
|
20078
20418
|
let triggerHits = [];
|
|
20079
20419
|
try {
|
|
@@ -20082,32 +20422,43 @@ function assertDeployVerification(config2, input) {
|
|
|
20082
20422
|
return;
|
|
20083
20423
|
}
|
|
20084
20424
|
if (triggerHits.length === 0) return;
|
|
20425
|
+
const mergeDeployHits = triggerHits.filter((p) => !isDeployAtReleaseSurface(p));
|
|
20426
|
+
if (mergeDeployHits.length === 0) return;
|
|
20085
20427
|
const verification = input.productionVerification;
|
|
20086
20428
|
if (!verification) {
|
|
20087
|
-
|
|
20088
|
-
`Deploy-verification required: this branch's diff touches ${triggerHits.length} trigger-surface file(s):
|
|
20089
|
-
` + triggerHits.map((p) => ` - ${p}`).join("\n") + `
|
|
20429
|
+
const targetLine = config2.verifyCommand ? `
|
|
20090
20430
|
|
|
20091
|
-
|
|
20431
|
+
Verify against your configured target (papi.verify): \`${config2.verifyCommand}\`
|
|
20432
|
+
Run it yourself, then pass production_verification on build_execute complete:
|
|
20092
20433
|
{ urls, curl_command, http_status, response_excerpt, verified_at }
|
|
20434
|
+
` : `
|
|
20093
20435
|
|
|
20094
|
-
|
|
20436
|
+
Run a curl against the live deploy and pass production_verification on build_execute complete:
|
|
20437
|
+
{ urls, curl_command, http_status, response_excerpt, verified_at }
|
|
20438
|
+
`;
|
|
20439
|
+
throw new Error(
|
|
20440
|
+
`Deploy-verification required: this branch's diff touches ${mergeDeployHits.length} deploy-on-merge trigger-surface file(s):
|
|
20441
|
+
` + mergeDeployHits.map((p) => ` - ${p}`).join("\n") + targetLine + `
|
|
20442
|
+
Trigger surface is enforced because changes here have historically shipped to prod undetected (C166, C264-C265, C271). Edge/data-proxy diffs (supabase/functions/**) are verified at release instead.`
|
|
20095
20443
|
);
|
|
20096
20444
|
}
|
|
20097
20445
|
if (verification.http_status < 200 || verification.http_status >= 300) {
|
|
20098
20446
|
throw new Error(
|
|
20099
20447
|
`Deploy-verification failed: production_verification reports http_status=${verification.http_status} on a trigger-surface task.
|
|
20100
|
-
Trigger files touched: ${
|
|
20448
|
+
Trigger files touched: ${mergeDeployHits.join(", ")}
|
|
20101
20449
|
Fix the deploy first, then re-run build_execute complete with a 2xx verification.`
|
|
20102
20450
|
);
|
|
20103
20451
|
}
|
|
20104
20452
|
}
|
|
20453
|
+
function isDeployAtReleaseSurface(path7) {
|
|
20454
|
+
return path7.startsWith("supabase/functions/");
|
|
20455
|
+
}
|
|
20105
20456
|
async function completeBuild(adapter2, config2, taskId, input, options = {}, clientName) {
|
|
20106
20457
|
const task = await adapter2.getTask(taskId);
|
|
20107
20458
|
if (!task) {
|
|
20108
20459
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
20109
20460
|
}
|
|
20110
|
-
assertDeployVerification(config2, input);
|
|
20461
|
+
assertDeployVerification(config2, input, { deployingNow: options.light === true });
|
|
20111
20462
|
const [healthResult, priorCount] = await Promise.all([
|
|
20112
20463
|
adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
|
|
20113
20464
|
typeof adapter2.getBuildReportCountForTask === "function" ? adapter2.getBuildReportCountForTask(taskId).catch(() => 0) : Promise.resolve(0)
|
|
@@ -21304,13 +21655,17 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
21304
21655
|
if (hasReportFields(args)) {
|
|
21305
21656
|
return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
|
|
21306
21657
|
}
|
|
21307
|
-
const tracker = new ProgressTracker("start_build");
|
|
21658
|
+
const tracker = new ProgressTracker("start_build").bindStream(adapter2, { stage: "build", taskId });
|
|
21308
21659
|
try {
|
|
21660
|
+
await tracker.recordStep("started");
|
|
21309
21661
|
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
21662
|
+
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
21663
|
+
await tracker.recordStep("branch_ready");
|
|
21310
21664
|
tracker.mark("start_decorate_handoff");
|
|
21311
21665
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
21312
21666
|
const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
|
|
21313
21667
|
const projectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
21668
|
+
const caps = projectInfo?.capabilities ?? {};
|
|
21314
21669
|
const projectBanner = projectInfo ? getProjectConnectionBanner(projectInfo.name, projectInfo.slug) : null;
|
|
21315
21670
|
const projectLine = projectBanner ? `> ${projectBanner}
|
|
21316
21671
|
|
|
@@ -21358,8 +21713,12 @@ ${entries}`;
|
|
|
21358
21713
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
21359
21714
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
21360
21715
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
21361
|
-
const modelNote =
|
|
21362
|
-
|
|
21716
|
+
const modelNote = buildModelRecommendationDirective(
|
|
21717
|
+
caps,
|
|
21718
|
+
formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity)
|
|
21719
|
+
) ?? "";
|
|
21720
|
+
const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
|
|
21721
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
|
|
21363
21722
|
} catch (err) {
|
|
21364
21723
|
if (isNoHandoffError(err)) {
|
|
21365
21724
|
const lines = [
|
|
@@ -21446,7 +21805,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21446
21805
|
if (!parsedEstimatedEffort) {
|
|
21447
21806
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
21448
21807
|
}
|
|
21449
|
-
const tracker = new ProgressTracker("complete_validate");
|
|
21808
|
+
const tracker = new ProgressTracker("complete_validate").bindStream(adapter2, { stage: "build", taskId });
|
|
21450
21809
|
try {
|
|
21451
21810
|
tracker.mark("complete_build");
|
|
21452
21811
|
const result = await completeBuild(adapter2, config2, taskId, {
|
|
@@ -21475,6 +21834,11 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21475
21834
|
preview
|
|
21476
21835
|
}, { light }, clientName);
|
|
21477
21836
|
tracker.mark("complete_format");
|
|
21837
|
+
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
21838
|
+
await tracker.recordStep("report_written");
|
|
21839
|
+
await tracker.recordStep("issues_triaged", {
|
|
21840
|
+
metadata: { autoTriagedCount: result.autoTriagedCount ?? 0 }
|
|
21841
|
+
});
|
|
21478
21842
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
21479
21843
|
for (const learningId of resolvesLearnings) {
|
|
21480
21844
|
try {
|
|
@@ -21483,8 +21847,35 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21483
21847
|
}
|
|
21484
21848
|
}
|
|
21485
21849
|
}
|
|
21850
|
+
await tracker.recordStep("learnings", {
|
|
21851
|
+
metadata: { learningsLinkedCount: result.learningsLinkedCount ?? 0 }
|
|
21852
|
+
});
|
|
21853
|
+
await tracker.recordStep("moving-to-review", {
|
|
21854
|
+
metadata: { status: result.task.status }
|
|
21855
|
+
});
|
|
21856
|
+
if (productionVerification) {
|
|
21857
|
+
await tracker.recordStep("verify", {
|
|
21858
|
+
stage: "release",
|
|
21859
|
+
status: "complete",
|
|
21860
|
+
metadata: {
|
|
21861
|
+
httpStatus: productionVerification.http_status,
|
|
21862
|
+
urlCount: productionVerification.urls.length,
|
|
21863
|
+
targetConfigured: Boolean(config2.verifyCommand)
|
|
21864
|
+
}
|
|
21865
|
+
});
|
|
21866
|
+
}
|
|
21486
21867
|
const docsNote = await detectUnregisteredDocsNote(adapter2, config2);
|
|
21487
|
-
|
|
21868
|
+
let batchRollupNote = "";
|
|
21869
|
+
if (result.cycleProgress && result.cycleProgress.completed >= result.cycleProgress.total) {
|
|
21870
|
+
try {
|
|
21871
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
21872
|
+
const caps = info?.capabilities ?? {};
|
|
21873
|
+
batchRollupNote = buildBatchBuildRollupDirective(caps) ?? "";
|
|
21874
|
+
} catch {
|
|
21875
|
+
batchRollupNote = "";
|
|
21876
|
+
}
|
|
21877
|
+
}
|
|
21878
|
+
return textResponse(formatCompleteResult(result) + docsNote + batchRollupNote);
|
|
21488
21879
|
} catch (err) {
|
|
21489
21880
|
const message = err instanceof Error ? err.message : String(err);
|
|
21490
21881
|
if (isBuildPushError(err)) {
|
|
@@ -21853,7 +22244,7 @@ function collectDiagnostics(config2) {
|
|
|
21853
22244
|
}
|
|
21854
22245
|
var bugTool = {
|
|
21855
22246
|
name: "bug",
|
|
21856
|
-
description:
|
|
22247
|
+
description: `Report a bug OR submit an idea. Routing: a bug about PAPI itself (a PAPI tool/MCP error, the connector, a handoff/cycle problem) auto-submits UPSTREAM to PAPI maintainers with diagnostics \u2014 you do NOT need to set report=true for these. A bug in the user's OWN project auto-files as a Backlog task on their board. IMPORTANT: report=true is ONLY for a genuine PAPI-product defect \u2014 it is NOT the catch-all for "not about my app". A bug in the user's harness/editor/OS/git/other tooling (e.g. Claude Code, Codex, VS Code, a shell command) is NOT a PAPI bug: file it on the user's own board (report=false / default) or that tool's own tracker, never upstream. Override the routing explicitly: report=true forces upstream (PAPI-product only), report=false forces the user's own board. Set \`type\` ("bug"/"idea") and optional notify-when-fixed / contact-ok consent for upstream submissions. Does not call the Anthropic API.`,
|
|
21857
22248
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21858
22249
|
inputSchema: {
|
|
21859
22250
|
type: "object",
|
|
@@ -21885,7 +22276,7 @@ var bugTool = {
|
|
|
21885
22276
|
},
|
|
21886
22277
|
report: {
|
|
21887
22278
|
type: "boolean",
|
|
21888
|
-
description: "Routing override. Leave UNSET to auto-route: PAPI-product bugs go upstream to maintainers, project-domain bugs go to the user's board. Set true to
|
|
22279
|
+
description: "Routing override. Leave UNSET to auto-route: PAPI-product bugs go upstream to maintainers, project-domain bugs go to the user's board. Set true ONLY for a genuine PAPI-product defect (PAPI tool/MCP/connector/handoff) \u2014 a bug in the user's harness/editor/OS/other tooling is NOT a PAPI bug and must go to their own board (report=false or default), not upstream. Set false to force a task on the user's own board (use false when an auto-detected PAPI bug is actually about the user's project, harness, or environment)."
|
|
21889
22280
|
},
|
|
21890
22281
|
type: {
|
|
21891
22282
|
type: "string",
|
|
@@ -21902,7 +22293,7 @@ var bugTool = {
|
|
|
21902
22293
|
},
|
|
21903
22294
|
project: {
|
|
21904
22295
|
type: "string",
|
|
21905
|
-
description: "Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default."
|
|
22296
|
+
description: "BOARD MODE ONLY. Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. It CANNOT redirect an upstream (report=true) submission \u2014 those always go to PAPI maintainers. Use project_switch to change the session default."
|
|
21906
22297
|
}
|
|
21907
22298
|
},
|
|
21908
22299
|
required: ["text"]
|
|
@@ -22020,6 +22411,12 @@ init_git();
|
|
|
22020
22411
|
|
|
22021
22412
|
// src/services/ad-hoc.ts
|
|
22022
22413
|
import { randomUUID as randomUUID14 } from "crypto";
|
|
22414
|
+
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
22415
|
+
if (cycle === void 0) return null;
|
|
22416
|
+
if (typeof cycle === "number") return cycle;
|
|
22417
|
+
if (cycle === "current") return latest;
|
|
22418
|
+
return latestComplete ? latest + 1 : latest;
|
|
22419
|
+
}
|
|
22023
22420
|
async function recordAdHoc(adapter2, input) {
|
|
22024
22421
|
const [health, phases] = await Promise.all([
|
|
22025
22422
|
adapter2.getCycleHealth(),
|
|
@@ -22027,6 +22424,14 @@ async function recordAdHoc(adapter2, input) {
|
|
|
22027
22424
|
]);
|
|
22028
22425
|
const cycle = health.totalCycles;
|
|
22029
22426
|
const now = /* @__PURE__ */ new Date();
|
|
22427
|
+
const held = input.hold === true;
|
|
22428
|
+
const targetCycle = held ? cycle + 1 : resolveAdHocCycle(
|
|
22429
|
+
input.cycle,
|
|
22430
|
+
health.totalCycles,
|
|
22431
|
+
health.latestCycleStatus === "complete"
|
|
22432
|
+
);
|
|
22433
|
+
const promoted = targetCycle !== null;
|
|
22434
|
+
const landInReview = held || promoted && input.stage === "release";
|
|
22030
22435
|
let task;
|
|
22031
22436
|
if (input.taskId) {
|
|
22032
22437
|
const existing = await adapter2.getTask(input.taskId);
|
|
@@ -22048,18 +22453,19 @@ async function recordAdHoc(adapter2, input) {
|
|
|
22048
22453
|
uuid: randomUUID14(),
|
|
22049
22454
|
displayId: "",
|
|
22050
22455
|
title: input.title,
|
|
22051
|
-
status: "Done",
|
|
22456
|
+
status: landInReview ? "In Review" : "Done",
|
|
22052
22457
|
priority: input.priority || "P2 Medium",
|
|
22053
22458
|
complexity: input.effort === "XS" || input.effort === "S" ? "Small" : "Medium",
|
|
22054
22459
|
module: input.module || "Core",
|
|
22055
22460
|
epic: input.epic || "Platform",
|
|
22056
22461
|
phase,
|
|
22057
22462
|
owner: input.owner || "Cathal",
|
|
22058
|
-
reviewed:
|
|
22463
|
+
reviewed: !landInReview,
|
|
22059
22464
|
createdCycle: cycle,
|
|
22465
|
+
...targetCycle !== null ? { cycle: targetCycle } : {},
|
|
22060
22466
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
22061
22467
|
taskType: input.taskType || "task",
|
|
22062
|
-
source: "
|
|
22468
|
+
source: "ad_hoc"
|
|
22063
22469
|
});
|
|
22064
22470
|
}
|
|
22065
22471
|
const report = {
|
|
@@ -22134,6 +22540,22 @@ var adHocTool = {
|
|
|
22134
22540
|
type: "string",
|
|
22135
22541
|
enum: ["task", "bug", "research", "discovery", "spike", "idea"],
|
|
22136
22542
|
description: 'Task type (default: inferred from description \u2014 "fix"/"bug" \u2192 bug, "research"/"investigate" \u2192 research, otherwise task).'
|
|
22543
|
+
},
|
|
22544
|
+
cycle: {
|
|
22545
|
+
description: 'task-2352: promote this ad-hoc work into a cycle so it shows on the hub and is counted as INJECTED work (distinct from planned). Omit for the default behaviour (an immediate Done task with no cycle). "current" = the active cycle; "next-if-plan-not-run" = the next cycle if the latest already shipped (so between-cycle work is not lost), else current; or an explicit cycle number.',
|
|
22546
|
+
oneOf: [
|
|
22547
|
+
{ type: "string", enum: ["current", "next-if-plan-not-run"] },
|
|
22548
|
+
{ type: "integer" }
|
|
22549
|
+
]
|
|
22550
|
+
},
|
|
22551
|
+
stage: {
|
|
22552
|
+
type: "string",
|
|
22553
|
+
enum: ["done", "release"],
|
|
22554
|
+
description: 'task-2352: where promoted work lands. "done" (default) records it as Done + reviewed. "release" lands it In Review so it is reviewed and released WITH the cycle (fold-in) instead of being force-completed. Only meaningful with `cycle`.'
|
|
22555
|
+
},
|
|
22556
|
+
hold: {
|
|
22557
|
+
type: "boolean",
|
|
22558
|
+
description: "task-2477: held-adhoc. When true, do NOT force-complete or commit to main \u2014 record the task In Review pinned to the NEXT cycle (current + 1) so the planner won't re-plan it, and return a branch/PR directive (commit on feat/<task-id>, never main, leave unmerged) so it rides the next cycle's review \u2192 release bundled with planned work. One-call replacement for the two-call ad_hoc + board_edit stopgap. Takes precedence over `cycle`/`stage`."
|
|
22137
22559
|
}
|
|
22138
22560
|
},
|
|
22139
22561
|
required: []
|
|
@@ -22164,6 +22586,12 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22164
22586
|
rawNotes = rawNotes.slice(0, MAX_NOTES_LENGTH);
|
|
22165
22587
|
notesTruncated = true;
|
|
22166
22588
|
}
|
|
22589
|
+
const rawCycle = args.cycle;
|
|
22590
|
+
let cycleArg;
|
|
22591
|
+
if (typeof rawCycle === "number") cycleArg = rawCycle;
|
|
22592
|
+
else if (rawCycle === "current" || rawCycle === "next-if-plan-not-run") cycleArg = rawCycle;
|
|
22593
|
+
const stageArg = args.stage === "release" ? "release" : void 0;
|
|
22594
|
+
const holdArg = args.hold === true;
|
|
22167
22595
|
const result = await recordAdHoc(adapter2, {
|
|
22168
22596
|
title: title || "",
|
|
22169
22597
|
taskId,
|
|
@@ -22174,9 +22602,12 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22174
22602
|
priority: priorityRaw,
|
|
22175
22603
|
taskType: typeRaw,
|
|
22176
22604
|
// PROJECT-SPECIFIC: owner resolved from config (PAPI_OWNER env var, default 'Cathal')
|
|
22177
|
-
owner: config2.projectOwner
|
|
22605
|
+
owner: config2.projectOwner,
|
|
22606
|
+
cycle: cycleArg,
|
|
22607
|
+
stage: stageArg,
|
|
22608
|
+
hold: holdArg
|
|
22178
22609
|
});
|
|
22179
|
-
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
22610
|
+
if (!holdArg && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
22180
22611
|
try {
|
|
22181
22612
|
stageDirAndCommit(
|
|
22182
22613
|
config2.projectRoot,
|
|
@@ -22189,8 +22620,29 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22189
22620
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
22190
22621
|
const taskModule = result.task.module || "Core";
|
|
22191
22622
|
const typeLabel = result.task.taskType || typeRaw;
|
|
22623
|
+
const promoNote = result.task.cycle != null ? ` Promoted into Cycle ${result.task.cycle} as injected work${result.task.status === "In Review" ? " (In Review \u2014 will release with the cycle)" : ""}.` : "";
|
|
22624
|
+
if (holdArg) {
|
|
22625
|
+
const branch = `feat/${result.task.id}`;
|
|
22626
|
+
return textResponse(
|
|
22627
|
+
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
22628
|
+
|
|
22629
|
+
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
22630
|
+
The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**, so the planner won't re-plan it and it rides that cycle's review \u2192 release bundled with planned work.
|
|
22631
|
+
|
|
22632
|
+
1. Create a branch and commit your code there (never \`main\`):
|
|
22633
|
+
\`\`\`
|
|
22634
|
+
git switch -c ${branch}
|
|
22635
|
+
git add -- <your changed files>
|
|
22636
|
+
git commit -m "feat(${result.task.id}): ${result.task.title}"
|
|
22637
|
+
git push -u origin ${branch}
|
|
22638
|
+
\`\`\`
|
|
22639
|
+
2. Leave the branch **unmerged** \u2014 it is picked up by the next cycle's \`release\`.
|
|
22640
|
+
|
|
22641
|
+
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
22642
|
+
);
|
|
22643
|
+
}
|
|
22192
22644
|
return textResponse(
|
|
22193
|
-
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning} Build report attached.
|
|
22645
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
22194
22646
|
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
22195
22647
|
);
|
|
22196
22648
|
}
|
|
@@ -23285,13 +23737,22 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23285
23737
|
if (!stage) {
|
|
23286
23738
|
return errorResponse('stage is required. Use "handoff-review" or "build-acceptance".');
|
|
23287
23739
|
}
|
|
23740
|
+
let caps = {};
|
|
23741
|
+
try {
|
|
23742
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
23743
|
+
caps = info?.capabilities ?? {};
|
|
23744
|
+
} catch {
|
|
23745
|
+
caps = {};
|
|
23746
|
+
}
|
|
23288
23747
|
const explicitDispatch = args.dispatch === "subagent";
|
|
23289
|
-
const
|
|
23290
|
-
|
|
23748
|
+
const autoDispatchOptIn = args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false" && isCapabilityEnabled(caps, "prReviewer");
|
|
23749
|
+
const autoDispatchEligible = !verdict && autoDispatchOptIn;
|
|
23750
|
+
const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
|
|
23751
|
+
if ((explicitDispatch || autoDispatchEligible || capabilityAutoReviewEligible) && stage === "build-acceptance" && taskId) {
|
|
23291
23752
|
const dispatch = await buildReviewDispatch(adapter2, config2, taskId);
|
|
23292
23753
|
if (!dispatch.ok) {
|
|
23293
23754
|
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
23294
|
-
} else if (explicitDispatch || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23755
|
+
} else if (explicitDispatch || capabilityAutoReviewEligible || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23295
23756
|
return textResponse(dispatch.prompt);
|
|
23296
23757
|
}
|
|
23297
23758
|
}
|
|
@@ -23332,7 +23793,8 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23332
23793
|
}
|
|
23333
23794
|
}
|
|
23334
23795
|
}
|
|
23335
|
-
const tracker = new ProgressTracker("validate-args");
|
|
23796
|
+
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "review", taskId });
|
|
23797
|
+
await tracker.recordStep("reviewer_confirmed");
|
|
23336
23798
|
const handoffRegenResponse = args.handoff_regen_response;
|
|
23337
23799
|
if (handoffRegenResponse?.trim()) {
|
|
23338
23800
|
tracker.mark("apply-handoff-regen");
|
|
@@ -23349,6 +23811,14 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23349
23811
|
adapter2,
|
|
23350
23812
|
{ taskId, stage, verdict, comments, reviewer, autoReview }
|
|
23351
23813
|
);
|
|
23814
|
+
tracker.setStreamScope({ cycle: result.currentCycle > 0 ? result.currentCycle : null });
|
|
23815
|
+
await tracker.recordStep("verdict_recorded", { metadata: { verdict, stage } });
|
|
23816
|
+
if (result.unblockedTasks.length > 0) {
|
|
23817
|
+
await tracker.recordStep("dependents_unblocked", { metadata: { count: result.unblockedTasks.length } });
|
|
23818
|
+
}
|
|
23819
|
+
if (result.closedDocActions.length > 0) {
|
|
23820
|
+
await tracker.recordStep("docs_closed", { metadata: { count: result.closedDocActions.length } });
|
|
23821
|
+
}
|
|
23352
23822
|
const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
|
|
23353
23823
|
const unblockNote = result.unblockedTasks.length > 0 ? `
|
|
23354
23824
|
|
|
@@ -23388,6 +23858,7 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
23388
23858
|
|
|
23389
23859
|
> ${mergeResult.message}`;
|
|
23390
23860
|
}
|
|
23861
|
+
await tracker.recordStep("deferred", { metadata: { reason: "merge-skipped" } });
|
|
23391
23862
|
} else if (!mergeResult.merged) {
|
|
23392
23863
|
mergeFailed = true;
|
|
23393
23864
|
let revertNote = "";
|
|
@@ -23402,11 +23873,13 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
23402
23873
|
\u26A0\uFE0F **PR merge failed \u2014 task NOT marked Done.**${revertNote}
|
|
23403
23874
|
|
|
23404
23875
|
${mergeResult.message}`;
|
|
23876
|
+
await tracker.recordStep("deferred", { status: "failed", metadata: { reason: "merge-failed" } });
|
|
23405
23877
|
} else {
|
|
23406
23878
|
try {
|
|
23407
23879
|
await adapter2.updateTask(taskId, { mergedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23408
23880
|
} catch {
|
|
23409
23881
|
}
|
|
23882
|
+
await tracker.recordStep("merged");
|
|
23410
23883
|
const detailLines = mergeResult.details.map((l) => `> ${l}`).join("\n");
|
|
23411
23884
|
mergeNote = `
|
|
23412
23885
|
|
|
@@ -23482,6 +23955,7 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
23482
23955
|
}
|
|
23483
23956
|
const version = `v0.${result.currentCycle}.0`;
|
|
23484
23957
|
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
23958
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
23485
23959
|
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
23486
23960
|
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
23487
23961
|
autoReleaseNote = `
|
|
@@ -23537,9 +24011,7 @@ ${findingLines}${more}` : "");
|
|
|
23537
24011
|
\u26A0\uFE0F **Override recorded:** ${reviewer} accepted past a ${autoReview.verdict} quality gate (${autoReview.findings.length} finding${autoReview.findings.length === 1 ? "" : "s"}). The verdict + findings are stored on this review for the audit trail.`;
|
|
23538
24012
|
}
|
|
23539
24013
|
} else if (stage === "build-acceptance" && verdict === "accept") {
|
|
23540
|
-
autoReviewNote =
|
|
23541
|
-
|
|
23542
|
-
**Quality Gate:** no auto-review attached. PAPI's standard pre-accept step is a code review of the branch diff \u2014 run \`review_submit\` with \`dispatch:"subagent"\` to auto-review, or attach \`auto_review\` findings. Risk-tier work (auth, data, migrations, CI) should always carry one.`;
|
|
24014
|
+
autoReviewNote = buildPrReviewerDirective(caps) ?? "";
|
|
23543
24015
|
}
|
|
23544
24016
|
let nextStepNote = "";
|
|
23545
24017
|
if (!autoReleaseNote && stage === "build-acceptance") {
|
|
@@ -23551,6 +24023,7 @@ ${findingLines}${more}` : "");
|
|
|
23551
24023
|
Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
23552
24024
|
}
|
|
23553
24025
|
}
|
|
24026
|
+
const securityNote = stage === "build-acceptance" && verdict === "accept" ? buildSecurityScanDirective(caps) ?? "" : "";
|
|
23554
24027
|
tracker.mark("format-response");
|
|
23555
24028
|
return textResponse(
|
|
23556
24029
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
@@ -23558,7 +24031,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
23558
24031
|
- **Verdict:** ${result.verdict}
|
|
23559
24032
|
- **Comments:** ${result.comments}
|
|
23560
24033
|
|
|
23561
|
-
${statusNote}${autoReviewNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
24034
|
+
${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
23562
24035
|
);
|
|
23563
24036
|
} catch (err) {
|
|
23564
24037
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24274,7 +24747,12 @@ async function getHealthSummary(adapter2) {
|
|
|
24274
24747
|
try {
|
|
24275
24748
|
try {
|
|
24276
24749
|
buildReports = await adapter2.getRecentBuildReports(50);
|
|
24277
|
-
|
|
24750
|
+
let metricsTasks = [];
|
|
24751
|
+
try {
|
|
24752
|
+
metricsTasks = await adapter2.queryBoard({ cycleSince: Math.max(1, cycleNumber - 6), compact: true });
|
|
24753
|
+
} catch {
|
|
24754
|
+
}
|
|
24755
|
+
snapshots = computeSnapshotsFromBuildReports(buildReports, metricsTasks);
|
|
24278
24756
|
} catch {
|
|
24279
24757
|
}
|
|
24280
24758
|
metricsSection = formatCycleMetrics(snapshots);
|