@papi-ai/server 0.7.45 → 0.7.46
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 +19 -26
- package/dist/index.js +498 -74
- package/dist/prompts.js +10 -0
- package/package.json +3 -2
|
@@ -1163,6 +1163,10 @@ var init_proxy_adapter = __esm({
|
|
|
1163
1163
|
"claimReview",
|
|
1164
1164
|
"getSiblingAds",
|
|
1165
1165
|
"getSiblingRepoTasks"
|
|
1166
|
+
// task-2489 (C320): recordProgressStep is now wired to the edge data-proxy
|
|
1167
|
+
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
1168
|
+
// hosted callers and persists a project-scoped cycle_progress_steps row. Removed
|
|
1169
|
+
// from NO_FORWARD (was the task-2484 pg-only gap) — hosted parity restored.
|
|
1166
1170
|
]);
|
|
1167
1171
|
ProxyPapiAdapter = class _ProxyPapiAdapter {
|
|
1168
1172
|
endpoint;
|
|
@@ -1883,27 +1887,14 @@ import { execSync } from "child_process";
|
|
|
1883
1887
|
import { readFile, writeFile, access } from "fs/promises";
|
|
1884
1888
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
1885
1889
|
import { join } from "path";
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
"Done": [],
|
|
1895
|
-
"Blocked": ["Backlog", "Ready", "In Cycle", "In Progress", "Cancelled"],
|
|
1896
|
-
"Cancelled": [],
|
|
1897
|
-
"Deferred": ["Backlog", "Cancelled"]
|
|
1898
|
-
};
|
|
1899
|
-
var RETIRED_DECISION_OUTCOMES = ["resolved", "abandoned", "superseded"];
|
|
1900
|
-
function isLiveDecision(d) {
|
|
1901
|
-
if (d.superseded === true) return false;
|
|
1902
|
-
if (d.outcome != null && RETIRED_DECISION_OUTCOMES.includes(d.outcome)) return false;
|
|
1903
|
-
return true;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
// ../adapter-md/dist/index.js
|
|
1890
|
+
import {
|
|
1891
|
+
VALID_TRANSITIONS as _VALID_TRANSITIONS,
|
|
1892
|
+
isValidTransition as _isValidTransition,
|
|
1893
|
+
validateTransition as _validateTransition,
|
|
1894
|
+
isValidStatus as _isValidStatus,
|
|
1895
|
+
isLiveDecision as _isLiveDecision,
|
|
1896
|
+
RETIRED_DECISION_OUTCOMES as _RETIRED_DECISION_OUTCOMES
|
|
1897
|
+
} from "@papi-ai/shared";
|
|
1907
1898
|
import { randomUUID } from "crypto";
|
|
1908
1899
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1909
1900
|
import yaml from "js-yaml";
|
|
@@ -1912,8 +1903,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
1912
1903
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
1913
1904
|
import yaml2 from "js-yaml";
|
|
1914
1905
|
import yaml3 from "js-yaml";
|
|
1915
|
-
var
|
|
1916
|
-
var
|
|
1906
|
+
var VALID_TRANSITIONS = _VALID_TRANSITIONS;
|
|
1907
|
+
var isLiveDecision = _isLiveDecision;
|
|
1917
1908
|
function extractSection(content, heading) {
|
|
1918
1909
|
const headingPattern = new RegExp(`^## ${heading}\\s*$`, "m");
|
|
1919
1910
|
const start = content.search(headingPattern);
|
|
@@ -3106,6 +3097,7 @@ function toCycle(raw) {
|
|
|
3106
3097
|
};
|
|
3107
3098
|
if (raw.end_date) cycle.endDate = raw.end_date;
|
|
3108
3099
|
if (raw.user_id) cycle.userId = raw.user_id;
|
|
3100
|
+
if (raw.dependency_chain) cycle.dependencyChain = raw.dependency_chain;
|
|
3109
3101
|
return cycle;
|
|
3110
3102
|
}
|
|
3111
3103
|
function fromCycle(cycle) {
|
|
@@ -3120,6 +3112,7 @@ function fromCycle(cycle) {
|
|
|
3120
3112
|
};
|
|
3121
3113
|
if (cycle.endDate) raw.end_date = cycle.endDate;
|
|
3122
3114
|
if (cycle.userId) raw.user_id = cycle.userId;
|
|
3115
|
+
if (cycle.dependencyChain) raw.dependency_chain = cycle.dependencyChain;
|
|
3123
3116
|
return raw;
|
|
3124
3117
|
}
|
|
3125
3118
|
function extractYamlBlock2(content) {
|
|
@@ -3164,7 +3157,7 @@ function prependCycle(cycle, content) {
|
|
|
3164
3157
|
const yamlStr = yaml2.dump({ cycles: [raw] }, { lineWidth: 120, quotingType: '"' });
|
|
3165
3158
|
return header + YAML_START2 + "\n" + yamlStr + YAML_END2 + "\n";
|
|
3166
3159
|
}
|
|
3167
|
-
const existing = parseCycles(content);
|
|
3160
|
+
const existing = parseCycles(content).filter((c) => c.number !== cycle.number);
|
|
3168
3161
|
const merged = [cycle, ...existing];
|
|
3169
3162
|
return serializeCycles(merged, content);
|
|
3170
3163
|
}
|
|
@@ -3261,7 +3254,7 @@ var MdFileAdapter = class {
|
|
|
3261
3254
|
if (!content) return [];
|
|
3262
3255
|
const all = parseActiveDecisions(content);
|
|
3263
3256
|
if (options?.includeRetired) return all;
|
|
3264
|
-
return all.filter(
|
|
3257
|
+
return all.filter(isLiveDecision);
|
|
3265
3258
|
}
|
|
3266
3259
|
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
3267
3260
|
async getCycleLog(limit) {
|
|
@@ -3429,7 +3422,7 @@ var MdFileAdapter = class {
|
|
|
3429
3422
|
}
|
|
3430
3423
|
if (updates.status && updates.status !== tasks[idx].status && !options?.force) {
|
|
3431
3424
|
const from = tasks[idx].status;
|
|
3432
|
-
const allowed =
|
|
3425
|
+
const allowed = VALID_TRANSITIONS[from];
|
|
3433
3426
|
if (!allowed.includes(updates.status)) {
|
|
3434
3427
|
console.warn(`[papi] Warning: invalid status transition "${from}" \u2192 "${updates.status}" for task ${id}. Allowed from "${from}": ${allowed.length > 0 ? allowed.join(", ") : "none"}`);
|
|
3435
3428
|
}
|
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
|
}
|
|
@@ -9273,6 +9272,14 @@ function buildHandoffsOnlyUserMessage(inputs) {
|
|
|
9273
9272
|
}
|
|
9274
9273
|
return parts.join("\n");
|
|
9275
9274
|
}
|
|
9275
|
+
function extractDependencyChain(displayText) {
|
|
9276
|
+
const match = displayText.match(
|
|
9277
|
+
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|$(?![\s\S]))/m
|
|
9278
|
+
);
|
|
9279
|
+
if (!match) return void 0;
|
|
9280
|
+
const body = match[1].trim();
|
|
9281
|
+
return body.length > 0 ? body : void 0;
|
|
9282
|
+
}
|
|
9276
9283
|
function parseStructuredOutput(raw) {
|
|
9277
9284
|
const marker = "<!-- PAPI_STRUCTURED_OUTPUT -->";
|
|
9278
9285
|
const markerIdx = raw.indexOf(marker);
|
|
@@ -9288,6 +9295,7 @@ function parseStructuredOutput(raw) {
|
|
|
9288
9295
|
try {
|
|
9289
9296
|
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
9290
9297
|
const data = coerceStructuredOutput(parsed);
|
|
9298
|
+
data.dependencyChain = extractDependencyChain(displayText);
|
|
9291
9299
|
return { displayText, data };
|
|
9292
9300
|
} catch {
|
|
9293
9301
|
return { displayText, data: null };
|
|
@@ -11746,8 +11754,27 @@ ${cleanContent}`;
|
|
|
11746
11754
|
contextHashes,
|
|
11747
11755
|
// task-2071 (MU-3): own the cycle by the planning member (pg defaults to the
|
|
11748
11756
|
// project owner when unset, so owner-operated plans stay correct).
|
|
11749
|
-
...options.ownerUserId ? { userId: options.ownerUserId } : {}
|
|
11757
|
+
...options.ownerUserId ? { userId: options.ownerUserId } : {},
|
|
11758
|
+
// task-2483 (C319): persist the plan's Dependency Chain build-order markdown.
|
|
11759
|
+
...data.dependencyChain ? { dependencyChain: data.dependencyChain } : {}
|
|
11750
11760
|
};
|
|
11761
|
+
const earlyCreateWarnings = [];
|
|
11762
|
+
try {
|
|
11763
|
+
await adapter2.createCycle({
|
|
11764
|
+
id: `cycle-${newCycleNumber}`,
|
|
11765
|
+
number: newCycleNumber,
|
|
11766
|
+
status: "active",
|
|
11767
|
+
startDate: cycle.startDate,
|
|
11768
|
+
goals: cycle.goals,
|
|
11769
|
+
boardHealth: "",
|
|
11770
|
+
taskIds: [],
|
|
11771
|
+
...cycle.userId ? { userId: cycle.userId } : {}
|
|
11772
|
+
});
|
|
11773
|
+
} catch (err) {
|
|
11774
|
+
const msg = `early createCycle failed for cycle ${newCycleNumber}: ${err instanceof Error ? err.message : String(err)}`;
|
|
11775
|
+
console.error(`[plan] ${msg}`);
|
|
11776
|
+
earlyCreateWarnings.push(msg);
|
|
11777
|
+
}
|
|
11751
11778
|
let dedupedNewTasks = data.newTasks ?? [];
|
|
11752
11779
|
if (dedupedNewTasks.length > 0) {
|
|
11753
11780
|
const existingTasks = await adapter2.queryBoard({ compact: true });
|
|
@@ -11840,7 +11867,7 @@ ${cleanContent}`;
|
|
|
11840
11867
|
`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
11868
|
);
|
|
11842
11869
|
}
|
|
11843
|
-
const allWarnings = [...result.warnings, ...verifyWarnings];
|
|
11870
|
+
const allWarnings = [...result.warnings, ...verifyWarnings, ...earlyCreateWarnings];
|
|
11844
11871
|
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
11845
11872
|
const correctionCount = data.boardCorrections?.length ?? 0;
|
|
11846
11873
|
const newTaskCount = result.newTaskIdMap.size;
|
|
@@ -11880,6 +11907,23 @@ ${cleanContent}`;
|
|
|
11880
11907
|
});
|
|
11881
11908
|
const cycleTaskCount = legacyHandoffs.length;
|
|
11882
11909
|
const cycleEffortPoints = legacyHandoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
|
|
11910
|
+
try {
|
|
11911
|
+
await adapter2.createCycle({
|
|
11912
|
+
id: `cycle-${newCycleNumber}`,
|
|
11913
|
+
number: newCycleNumber,
|
|
11914
|
+
status: "active",
|
|
11915
|
+
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11916
|
+
goals: data.cycleLogTitle ? [data.cycleLogTitle] : [],
|
|
11917
|
+
boardHealth: "",
|
|
11918
|
+
taskIds: [],
|
|
11919
|
+
contextHashes,
|
|
11920
|
+
...options.ownerUserId ? { userId: options.ownerUserId } : {}
|
|
11921
|
+
});
|
|
11922
|
+
} catch (err) {
|
|
11923
|
+
const msg = `early createCycle failed for cycle ${newCycleNumber}: ${err instanceof Error ? err.message : String(err)}`;
|
|
11924
|
+
console.error(`[plan] ${msg}`);
|
|
11925
|
+
warnings.push(msg);
|
|
11926
|
+
}
|
|
11883
11927
|
const cycleLogPromise = adapter2.writeCycleLogEntry({
|
|
11884
11928
|
uuid: randomUUID8(),
|
|
11885
11929
|
cycleNumber: newCycleNumber,
|
|
@@ -12142,7 +12186,10 @@ ${cleanContent}`;
|
|
|
12142
12186
|
taskIds: cycleTaskIds,
|
|
12143
12187
|
contextHashes,
|
|
12144
12188
|
// task-2071 (MU-3): own the cycle by the planning member.
|
|
12145
|
-
...options.ownerUserId ? { userId: options.ownerUserId } : {}
|
|
12189
|
+
...options.ownerUserId ? { userId: options.ownerUserId } : {},
|
|
12190
|
+
// task-2483 (C319): persist the plan's Dependency Chain build-order markdown.
|
|
12191
|
+
// This full write patches the early minimal row created in Phase 1.
|
|
12192
|
+
...data.dependencyChain ? { dependencyChain: data.dependencyChain } : {}
|
|
12146
12193
|
};
|
|
12147
12194
|
await adapter2.createCycle(cycle);
|
|
12148
12195
|
} catch (err) {
|
|
@@ -12622,12 +12669,58 @@ ${JSON.stringify(payload, null, 2)}`;
|
|
|
12622
12669
|
}
|
|
12623
12670
|
var ProgressTracker = class {
|
|
12624
12671
|
lastStep;
|
|
12672
|
+
persistAdapter;
|
|
12673
|
+
streamCtx;
|
|
12625
12674
|
constructor(initial = "start") {
|
|
12626
12675
|
this.lastStep = initial;
|
|
12627
12676
|
}
|
|
12628
12677
|
mark(step) {
|
|
12629
12678
|
this.lastStep = step;
|
|
12630
12679
|
}
|
|
12680
|
+
/**
|
|
12681
|
+
* Arm progress-stream persistence for this tool call. Safe to call with any
|
|
12682
|
+
* adapter — persistence stays disarmed unless the adapter exposes BOTH
|
|
12683
|
+
* `getProjectId` and `recordProgressStep` (i.e. the pg/DB-backed path). Returns
|
|
12684
|
+
* `this` for chaining at the tracker construction site.
|
|
12685
|
+
*/
|
|
12686
|
+
bindStream(adapter2, ctx) {
|
|
12687
|
+
if (adapter2?.getProjectId && adapter2.recordProgressStep) {
|
|
12688
|
+
this.persistAdapter = adapter2;
|
|
12689
|
+
}
|
|
12690
|
+
this.streamCtx = ctx;
|
|
12691
|
+
return this;
|
|
12692
|
+
}
|
|
12693
|
+
/** Update the bound scope mid-call (e.g. once the cycle number is resolved). */
|
|
12694
|
+
setStreamScope(scope) {
|
|
12695
|
+
if (!this.streamCtx) return;
|
|
12696
|
+
if (scope.cycle !== void 0) this.streamCtx.cycle = scope.cycle;
|
|
12697
|
+
if (scope.taskId !== void 0) this.streamCtx.taskId = scope.taskId;
|
|
12698
|
+
}
|
|
12699
|
+
/**
|
|
12700
|
+
* Record one step of the progress stream. Also advances `lastStep` so the
|
|
12701
|
+
* structured-error diagnostic still points at the most recent named step.
|
|
12702
|
+
* Fire-and-forget and fully guarded — the returned promise always resolves.
|
|
12703
|
+
*/
|
|
12704
|
+
recordStep(step, opts = {}) {
|
|
12705
|
+
this.lastStep = step;
|
|
12706
|
+
const adapter2 = this.persistAdapter;
|
|
12707
|
+
const ctx = this.streamCtx;
|
|
12708
|
+
if (!adapter2?.recordProgressStep || !ctx) return Promise.resolve();
|
|
12709
|
+
const row = {
|
|
12710
|
+
stage: opts.stage ?? ctx.stage,
|
|
12711
|
+
step,
|
|
12712
|
+
status: opts.status ?? "complete",
|
|
12713
|
+
cycle: opts.cycle !== void 0 ? opts.cycle : ctx.cycle ?? null,
|
|
12714
|
+
taskId: opts.taskId !== void 0 ? opts.taskId : ctx.taskId ?? null,
|
|
12715
|
+
capabilityKey: opts.capabilityKey ?? null,
|
|
12716
|
+
capabilityEnabled: opts.capabilityEnabled ?? null,
|
|
12717
|
+
metadata: opts.metadata
|
|
12718
|
+
};
|
|
12719
|
+
return adapter2.recordProgressStep(row).catch((err) => {
|
|
12720
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12721
|
+
console.error(`[progress-stream] failed to persist ${ctx.stage}/${step} (${row.status}): ${message}`);
|
|
12722
|
+
});
|
|
12723
|
+
}
|
|
12631
12724
|
};
|
|
12632
12725
|
function defaultHint(tool) {
|
|
12633
12726
|
const knownTools = /* @__PURE__ */ new Set([
|
|
@@ -13088,7 +13181,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13088
13181
|
const focus = typeof args.focus === "string" ? args.focus : void 0;
|
|
13089
13182
|
const force = args.force === true;
|
|
13090
13183
|
const handoffsOnly = args.handoffs_only === true;
|
|
13091
|
-
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate");
|
|
13184
|
+
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate").bindStream(adapter2, { stage: "plan" });
|
|
13092
13185
|
try {
|
|
13093
13186
|
if (toolMode === "apply") {
|
|
13094
13187
|
const resolved = await resolveLlmResponse(
|
|
@@ -13160,6 +13253,10 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13160
13253
|
}
|
|
13161
13254
|
const skipHandoffs = args.skip_handoffs === true;
|
|
13162
13255
|
const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker);
|
|
13256
|
+
tracker.setStreamScope({ cycle: result.cycleNumber + 1 });
|
|
13257
|
+
for (const step of ["health-check", "inbox-triage", "board-integrity", "maturity-gate", "recommendation", "dependency-chain"]) {
|
|
13258
|
+
await tracker.recordStep(step);
|
|
13259
|
+
}
|
|
13163
13260
|
planPrepareCache.set(callerKey, {
|
|
13164
13261
|
contextHashes: result.contextHashes,
|
|
13165
13262
|
userMessage: result.userMessage,
|
|
@@ -18322,6 +18419,86 @@ Examples (use whichever provider you have): ${spec.examples}.
|
|
|
18322
18419
|
_Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
|
|
18323
18420
|
}
|
|
18324
18421
|
|
|
18422
|
+
// src/lib/capabilities.ts
|
|
18423
|
+
import {
|
|
18424
|
+
CAPABILITY_KEYS,
|
|
18425
|
+
CAPABILITY_REGISTRY,
|
|
18426
|
+
isCapabilityKey,
|
|
18427
|
+
isCapabilityEnabled
|
|
18428
|
+
} from "@papi-ai/shared";
|
|
18429
|
+
|
|
18430
|
+
// src/lib/directive-builders.ts
|
|
18431
|
+
function buildPrReviewerDirective(caps) {
|
|
18432
|
+
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
18433
|
+
return `
|
|
18434
|
+
|
|
18435
|
+
**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.`;
|
|
18436
|
+
}
|
|
18437
|
+
function buildChangelogDirective(caps, inner) {
|
|
18438
|
+
if (!isCapabilityEnabled(caps, "changelog")) return null;
|
|
18439
|
+
return inner;
|
|
18440
|
+
}
|
|
18441
|
+
function buildDiscoveredIssuesDirective(caps, issueLines) {
|
|
18442
|
+
if (!isCapabilityEnabled(caps, "discoveredIssues")) return null;
|
|
18443
|
+
if (issueLines.length === 0) return null;
|
|
18444
|
+
return [
|
|
18445
|
+
"",
|
|
18446
|
+
"---",
|
|
18447
|
+
"",
|
|
18448
|
+
`## Discovered Issues (${issueLines.length})`,
|
|
18449
|
+
"",
|
|
18450
|
+
...issueLines,
|
|
18451
|
+
"",
|
|
18452
|
+
"*These issues were logged during builds \u2014 triage them in the next plan.*"
|
|
18453
|
+
].join("\n");
|
|
18454
|
+
}
|
|
18455
|
+
function buildModelRecommendationDirective(caps, tierBlock) {
|
|
18456
|
+
if (!isCapabilityEnabled(caps, "modelRecommendation")) return null;
|
|
18457
|
+
return tierBlock;
|
|
18458
|
+
}
|
|
18459
|
+
function buildVerifyHealthCheckDirective(caps) {
|
|
18460
|
+
if (!isCapabilityEnabled(caps, "verifyHealthCheck")) return null;
|
|
18461
|
+
return `
|
|
18462
|
+
|
|
18463
|
+
**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.`;
|
|
18464
|
+
}
|
|
18465
|
+
function buildGestaltPreBuildDirective(caps) {
|
|
18466
|
+
if (!isCapabilityEnabled(caps, "gestaltPreBuild")) return null;
|
|
18467
|
+
return `
|
|
18468
|
+
|
|
18469
|
+
**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.`;
|
|
18470
|
+
}
|
|
18471
|
+
function buildBatchBuildRollupDirective(caps) {
|
|
18472
|
+
if (!isCapabilityEnabled(caps, "batchBuildRollup")) return null;
|
|
18473
|
+
return `
|
|
18474
|
+
|
|
18475
|
+
**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.`;
|
|
18476
|
+
}
|
|
18477
|
+
function buildSecurityScanDirective(caps) {
|
|
18478
|
+
if (!isCapabilityEnabled(caps, "securityScan")) return null;
|
|
18479
|
+
return `
|
|
18480
|
+
|
|
18481
|
+
**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.`;
|
|
18482
|
+
}
|
|
18483
|
+
function buildDeployHookDirective(caps, deployCommand) {
|
|
18484
|
+
if (!isCapabilityEnabled(caps, "deployHook")) return null;
|
|
18485
|
+
const command = deployCommand?.trim() || void 0;
|
|
18486
|
+
if (!command) return null;
|
|
18487
|
+
return `
|
|
18488
|
+
|
|
18489
|
+
---
|
|
18490
|
+
|
|
18491
|
+
## Deploy \u2014 run after this release
|
|
18492
|
+
|
|
18493
|
+
The post-release deploy hook is on and a deploy command is configured, so ship the merged release now.
|
|
18494
|
+
|
|
18495
|
+
Run your deploy command yourself:
|
|
18496
|
+
\`\`\`
|
|
18497
|
+
${command}
|
|
18498
|
+
\`\`\`
|
|
18499
|
+
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.`;
|
|
18500
|
+
}
|
|
18501
|
+
|
|
18325
18502
|
// src/services/build.ts
|
|
18326
18503
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
18327
18504
|
import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -18998,6 +19175,65 @@ async function updatePluginMarketplaces(version) {
|
|
|
18998
19175
|
return [cursor, vscode].filter((r) => r !== null);
|
|
18999
19176
|
}
|
|
19000
19177
|
|
|
19178
|
+
// src/lib/release-gate.ts
|
|
19179
|
+
function evaluateReleaseGate(caps, gateCommand, gateResult) {
|
|
19180
|
+
const command = gateCommand?.trim() || void 0;
|
|
19181
|
+
if (!isCapabilityEnabled(caps, "releaseGate")) {
|
|
19182
|
+
return {
|
|
19183
|
+
action: "proceed",
|
|
19184
|
+
stepStatus: "complete",
|
|
19185
|
+
metadata: { skipped: true, reason: "capability_off" },
|
|
19186
|
+
message: ""
|
|
19187
|
+
};
|
|
19188
|
+
}
|
|
19189
|
+
if (!command) {
|
|
19190
|
+
return {
|
|
19191
|
+
action: "proceed",
|
|
19192
|
+
stepStatus: "complete",
|
|
19193
|
+
metadata: { note: "no gate configured" },
|
|
19194
|
+
message: ""
|
|
19195
|
+
};
|
|
19196
|
+
}
|
|
19197
|
+
if (gateResult === "fail") {
|
|
19198
|
+
return {
|
|
19199
|
+
action: "block",
|
|
19200
|
+
stepStatus: "failed",
|
|
19201
|
+
metadata: { command },
|
|
19202
|
+
message: `Release blocked \u2014 quality gate failed.
|
|
19203
|
+
|
|
19204
|
+
The gate command \`${command}\` reported a failure, so PAPI did NOT tag, merge, or close the cycle.
|
|
19205
|
+
|
|
19206
|
+
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.`
|
|
19207
|
+
};
|
|
19208
|
+
}
|
|
19209
|
+
if (gateResult === "pass") {
|
|
19210
|
+
return {
|
|
19211
|
+
action: "proceed",
|
|
19212
|
+
stepStatus: "complete",
|
|
19213
|
+
metadata: { command },
|
|
19214
|
+
message: ""
|
|
19215
|
+
};
|
|
19216
|
+
}
|
|
19217
|
+
return {
|
|
19218
|
+
action: "directive",
|
|
19219
|
+
stepStatus: "active",
|
|
19220
|
+
metadata: { command },
|
|
19221
|
+
message: `## Quality gate \u2014 run before this release ships
|
|
19222
|
+
|
|
19223
|
+
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.
|
|
19224
|
+
|
|
19225
|
+
1. Run the gate command yourself:
|
|
19226
|
+
\`\`\`
|
|
19227
|
+
${command}
|
|
19228
|
+
\`\`\`
|
|
19229
|
+
2. Re-call \`release\` with the same \`branch\` and \`version\`, adding:
|
|
19230
|
+
- \`gate_result: "pass"\` if it succeeded \u2014 the release then proceeds normally.
|
|
19231
|
+
- \`gate_result: "fail"\` if it failed \u2014 the release stays blocked so you can fix it first.
|
|
19232
|
+
|
|
19233
|
+
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment and report the result.`
|
|
19234
|
+
};
|
|
19235
|
+
}
|
|
19236
|
+
|
|
19001
19237
|
// src/tools/release.ts
|
|
19002
19238
|
function parseGithubOwner(input) {
|
|
19003
19239
|
if (!input) return null;
|
|
@@ -19078,6 +19314,11 @@ var releaseTool = {
|
|
|
19078
19314
|
type: "boolean",
|
|
19079
19315
|
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
19316
|
},
|
|
19317
|
+
gate_result: {
|
|
19318
|
+
type: "string",
|
|
19319
|
+
enum: ["pass", "fail"],
|
|
19320
|
+
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).'
|
|
19321
|
+
},
|
|
19081
19322
|
observations: {
|
|
19082
19323
|
type: "array",
|
|
19083
19324
|
description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
|
|
@@ -19108,6 +19349,7 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19108
19349
|
let version = args.version;
|
|
19109
19350
|
const force = args.force;
|
|
19110
19351
|
const skipVersion = args.skipVersion;
|
|
19352
|
+
const gateResult = args.gate_result;
|
|
19111
19353
|
const rawObservations = args.observations;
|
|
19112
19354
|
if (!branch || !version) {
|
|
19113
19355
|
return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
|
|
@@ -19121,8 +19363,9 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19121
19363
|
version = `${version}-${suffix}`;
|
|
19122
19364
|
}
|
|
19123
19365
|
}
|
|
19124
|
-
const tracker = new ProgressTracker("validate-args");
|
|
19366
|
+
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
|
|
19125
19367
|
try {
|
|
19368
|
+
await tracker.recordStep("release_starting");
|
|
19126
19369
|
tracker.mark("owner-identity-guard");
|
|
19127
19370
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
19128
19371
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
@@ -19249,12 +19492,41 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19249
19492
|
console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
|
|
19250
19493
|
}
|
|
19251
19494
|
}
|
|
19495
|
+
await tracker.recordStep("readiness_verified");
|
|
19496
|
+
let caps = {};
|
|
19497
|
+
try {
|
|
19498
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
19499
|
+
caps = info?.capabilities ?? {};
|
|
19500
|
+
} catch {
|
|
19501
|
+
caps = {};
|
|
19502
|
+
}
|
|
19503
|
+
tracker.mark("quality-gate");
|
|
19504
|
+
const gateDecision = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
|
|
19505
|
+
await tracker.recordStep("quality_gate", {
|
|
19506
|
+
status: gateDecision.stepStatus,
|
|
19507
|
+
capabilityKey: "releaseGate",
|
|
19508
|
+
capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
|
|
19509
|
+
metadata: gateDecision.metadata
|
|
19510
|
+
});
|
|
19511
|
+
if (gateDecision.action === "directive") {
|
|
19512
|
+
return textResponse(gateDecision.message);
|
|
19513
|
+
}
|
|
19514
|
+
if (gateDecision.action === "block") {
|
|
19515
|
+
return errorResponse(gateDecision.message);
|
|
19516
|
+
}
|
|
19252
19517
|
tracker.mark("create-release");
|
|
19253
19518
|
const result = await createRelease(config2, branch, version, adapter2, void 0, {
|
|
19254
19519
|
force: force ?? false,
|
|
19255
19520
|
skipVersion: skipVersion ?? false,
|
|
19256
19521
|
callerUserId: gate.callerUserId
|
|
19257
19522
|
});
|
|
19523
|
+
tracker.setStreamScope({ cycle: result.cycleClosed ?? null });
|
|
19524
|
+
await tracker.recordStep("cycle_complete", { metadata: { version: result.version } });
|
|
19525
|
+
for (const m of result.groupedBranchMerges ?? []) {
|
|
19526
|
+
await tracker.recordStep("branch_merged", {
|
|
19527
|
+
metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
|
|
19528
|
+
});
|
|
19529
|
+
}
|
|
19258
19530
|
const lines = [
|
|
19259
19531
|
`## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
|
|
19260
19532
|
"",
|
|
@@ -19283,10 +19555,8 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19283
19555
|
const reports = await adapter2.getBuildReportsSince(closedCycle);
|
|
19284
19556
|
const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
|
|
19285
19557
|
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
|
-
}
|
|
19558
|
+
const issuesDirective = buildDiscoveredIssuesDirective(caps, issues);
|
|
19559
|
+
if (issuesDirective) lines.push(issuesDirective);
|
|
19290
19560
|
}
|
|
19291
19561
|
} catch {
|
|
19292
19562
|
}
|
|
@@ -19306,28 +19576,54 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19306
19576
|
lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
|
|
19307
19577
|
}
|
|
19308
19578
|
}
|
|
19309
|
-
const cycleUpdateDirective =
|
|
19579
|
+
const cycleUpdateDirective = buildChangelogDirective(
|
|
19580
|
+
caps,
|
|
19581
|
+
buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
|
|
19582
|
+
);
|
|
19310
19583
|
if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19584
|
+
await tracker.recordStep("changelog", {
|
|
19585
|
+
capabilityKey: "changelog",
|
|
19586
|
+
capabilityEnabled: isCapabilityEnabled(caps, "changelog"),
|
|
19587
|
+
status: cycleUpdateDirective ? "complete" : "active"
|
|
19588
|
+
});
|
|
19589
|
+
const deployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
19590
|
+
if (deployDirective) {
|
|
19591
|
+
lines.push(deployDirective);
|
|
19592
|
+
await tracker.recordStep("deploy_hook", {
|
|
19593
|
+
capabilityKey: "deployHook",
|
|
19594
|
+
capabilityEnabled: isCapabilityEnabled(caps, "deployHook"),
|
|
19595
|
+
status: "complete",
|
|
19596
|
+
metadata: { configured: true }
|
|
19597
|
+
});
|
|
19315
19598
|
}
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19599
|
+
const publishEnabled = isCapabilityEnabled(caps, "publishDirective");
|
|
19600
|
+
if (publishEnabled) {
|
|
19601
|
+
try {
|
|
19602
|
+
const xPosted = await postReleaseToX(result.version, result.cycleClosed ?? 0);
|
|
19603
|
+
if (xPosted) lines.push("", "Posted release announcement to X.");
|
|
19604
|
+
} catch {
|
|
19320
19605
|
}
|
|
19321
|
-
} catch {
|
|
19322
19606
|
}
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19607
|
+
if (publishEnabled) {
|
|
19608
|
+
try {
|
|
19609
|
+
const registryResults = await updateRegistryListings(result.version);
|
|
19610
|
+
for (const r of registryResults) {
|
|
19611
|
+
lines.push("", `Registry [${r.registry}]: ${r.message}`);
|
|
19612
|
+
}
|
|
19613
|
+
} catch {
|
|
19614
|
+
}
|
|
19615
|
+
try {
|
|
19616
|
+
const marketplaceResults = await updatePluginMarketplaces(result.version);
|
|
19617
|
+
for (const r of marketplaceResults) {
|
|
19618
|
+
lines.push("", `Marketplace [${r.registry}]: ${r.message}`);
|
|
19619
|
+
}
|
|
19620
|
+
} catch {
|
|
19327
19621
|
}
|
|
19328
|
-
} catch {
|
|
19329
19622
|
}
|
|
19623
|
+
const verifyDirective = buildVerifyHealthCheckDirective(caps);
|
|
19624
|
+
if (verifyDirective) lines.push(verifyDirective);
|
|
19330
19625
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`);
|
|
19626
|
+
await tracker.recordStep("released", { metadata: { version: result.version } });
|
|
19331
19627
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
19332
19628
|
return textResponse(lines.join("\n") + filesToWriteSection);
|
|
19333
19629
|
} catch (err) {
|
|
@@ -20084,13 +20380,19 @@ function assertDeployVerification(config2, input) {
|
|
|
20084
20380
|
if (triggerHits.length === 0) return;
|
|
20085
20381
|
const verification = input.productionVerification;
|
|
20086
20382
|
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") + `
|
|
20383
|
+
const targetLine = config2.verifyCommand ? `
|
|
20090
20384
|
|
|
20091
|
-
|
|
20385
|
+
Verify against your configured target (papi.verify): \`${config2.verifyCommand}\`
|
|
20386
|
+
Run it yourself, then pass production_verification on build_execute complete:
|
|
20092
20387
|
{ urls, curl_command, http_status, response_excerpt, verified_at }
|
|
20388
|
+
` : `
|
|
20093
20389
|
|
|
20390
|
+
Run a curl against the live deploy and pass production_verification on build_execute complete:
|
|
20391
|
+
{ urls, curl_command, http_status, response_excerpt, verified_at }
|
|
20392
|
+
`;
|
|
20393
|
+
throw new Error(
|
|
20394
|
+
`Deploy-verification required: this branch's diff touches ${triggerHits.length} trigger-surface file(s):
|
|
20395
|
+
` + triggerHits.map((p) => ` - ${p}`).join("\n") + targetLine + `
|
|
20094
20396
|
Trigger surface is enforced because changes here have historically shipped to prod undetected (C166, C264-C265, C271).`
|
|
20095
20397
|
);
|
|
20096
20398
|
}
|
|
@@ -21304,13 +21606,17 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
21304
21606
|
if (hasReportFields(args)) {
|
|
21305
21607
|
return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
|
|
21306
21608
|
}
|
|
21307
|
-
const tracker = new ProgressTracker("start_build");
|
|
21609
|
+
const tracker = new ProgressTracker("start_build").bindStream(adapter2, { stage: "build", taskId });
|
|
21308
21610
|
try {
|
|
21611
|
+
await tracker.recordStep("started");
|
|
21309
21612
|
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
21613
|
+
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
21614
|
+
await tracker.recordStep("branch_ready");
|
|
21310
21615
|
tracker.mark("start_decorate_handoff");
|
|
21311
21616
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
21312
21617
|
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
21618
|
const projectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
21619
|
+
const caps = projectInfo?.capabilities ?? {};
|
|
21314
21620
|
const projectBanner = projectInfo ? getProjectConnectionBanner(projectInfo.name, projectInfo.slug) : null;
|
|
21315
21621
|
const projectLine = projectBanner ? `> ${projectBanner}
|
|
21316
21622
|
|
|
@@ -21358,8 +21664,12 @@ ${entries}`;
|
|
|
21358
21664
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
21359
21665
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
21360
21666
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
21361
|
-
const modelNote =
|
|
21362
|
-
|
|
21667
|
+
const modelNote = buildModelRecommendationDirective(
|
|
21668
|
+
caps,
|
|
21669
|
+
formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity)
|
|
21670
|
+
) ?? "";
|
|
21671
|
+
const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
|
|
21672
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
|
|
21363
21673
|
} catch (err) {
|
|
21364
21674
|
if (isNoHandoffError(err)) {
|
|
21365
21675
|
const lines = [
|
|
@@ -21446,7 +21756,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21446
21756
|
if (!parsedEstimatedEffort) {
|
|
21447
21757
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
21448
21758
|
}
|
|
21449
|
-
const tracker = new ProgressTracker("complete_validate");
|
|
21759
|
+
const tracker = new ProgressTracker("complete_validate").bindStream(adapter2, { stage: "build", taskId });
|
|
21450
21760
|
try {
|
|
21451
21761
|
tracker.mark("complete_build");
|
|
21452
21762
|
const result = await completeBuild(adapter2, config2, taskId, {
|
|
@@ -21475,6 +21785,11 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21475
21785
|
preview
|
|
21476
21786
|
}, { light }, clientName);
|
|
21477
21787
|
tracker.mark("complete_format");
|
|
21788
|
+
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
21789
|
+
await tracker.recordStep("report_written");
|
|
21790
|
+
await tracker.recordStep("issues_triaged", {
|
|
21791
|
+
metadata: { autoTriagedCount: result.autoTriagedCount ?? 0 }
|
|
21792
|
+
});
|
|
21478
21793
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
21479
21794
|
for (const learningId of resolvesLearnings) {
|
|
21480
21795
|
try {
|
|
@@ -21483,8 +21798,35 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21483
21798
|
}
|
|
21484
21799
|
}
|
|
21485
21800
|
}
|
|
21801
|
+
await tracker.recordStep("learnings", {
|
|
21802
|
+
metadata: { learningsLinkedCount: result.learningsLinkedCount ?? 0 }
|
|
21803
|
+
});
|
|
21804
|
+
await tracker.recordStep("moving-to-review", {
|
|
21805
|
+
metadata: { status: result.task.status }
|
|
21806
|
+
});
|
|
21807
|
+
if (productionVerification) {
|
|
21808
|
+
await tracker.recordStep("verify", {
|
|
21809
|
+
stage: "release",
|
|
21810
|
+
status: "complete",
|
|
21811
|
+
metadata: {
|
|
21812
|
+
httpStatus: productionVerification.http_status,
|
|
21813
|
+
urlCount: productionVerification.urls.length,
|
|
21814
|
+
targetConfigured: Boolean(config2.verifyCommand)
|
|
21815
|
+
}
|
|
21816
|
+
});
|
|
21817
|
+
}
|
|
21486
21818
|
const docsNote = await detectUnregisteredDocsNote(adapter2, config2);
|
|
21487
|
-
|
|
21819
|
+
let batchRollupNote = "";
|
|
21820
|
+
if (result.cycleProgress && result.cycleProgress.completed >= result.cycleProgress.total) {
|
|
21821
|
+
try {
|
|
21822
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
21823
|
+
const caps = info?.capabilities ?? {};
|
|
21824
|
+
batchRollupNote = buildBatchBuildRollupDirective(caps) ?? "";
|
|
21825
|
+
} catch {
|
|
21826
|
+
batchRollupNote = "";
|
|
21827
|
+
}
|
|
21828
|
+
}
|
|
21829
|
+
return textResponse(formatCompleteResult(result) + docsNote + batchRollupNote);
|
|
21488
21830
|
} catch (err) {
|
|
21489
21831
|
const message = err instanceof Error ? err.message : String(err);
|
|
21490
21832
|
if (isBuildPushError(err)) {
|
|
@@ -22020,6 +22362,12 @@ init_git();
|
|
|
22020
22362
|
|
|
22021
22363
|
// src/services/ad-hoc.ts
|
|
22022
22364
|
import { randomUUID as randomUUID14 } from "crypto";
|
|
22365
|
+
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
22366
|
+
if (cycle === void 0) return null;
|
|
22367
|
+
if (typeof cycle === "number") return cycle;
|
|
22368
|
+
if (cycle === "current") return latest;
|
|
22369
|
+
return latestComplete ? latest + 1 : latest;
|
|
22370
|
+
}
|
|
22023
22371
|
async function recordAdHoc(adapter2, input) {
|
|
22024
22372
|
const [health, phases] = await Promise.all([
|
|
22025
22373
|
adapter2.getCycleHealth(),
|
|
@@ -22027,6 +22375,14 @@ async function recordAdHoc(adapter2, input) {
|
|
|
22027
22375
|
]);
|
|
22028
22376
|
const cycle = health.totalCycles;
|
|
22029
22377
|
const now = /* @__PURE__ */ new Date();
|
|
22378
|
+
const held = input.hold === true;
|
|
22379
|
+
const targetCycle = held ? cycle + 1 : resolveAdHocCycle(
|
|
22380
|
+
input.cycle,
|
|
22381
|
+
health.totalCycles,
|
|
22382
|
+
health.latestCycleStatus === "complete"
|
|
22383
|
+
);
|
|
22384
|
+
const promoted = targetCycle !== null;
|
|
22385
|
+
const landInReview = held || promoted && input.stage === "release";
|
|
22030
22386
|
let task;
|
|
22031
22387
|
if (input.taskId) {
|
|
22032
22388
|
const existing = await adapter2.getTask(input.taskId);
|
|
@@ -22048,18 +22404,19 @@ async function recordAdHoc(adapter2, input) {
|
|
|
22048
22404
|
uuid: randomUUID14(),
|
|
22049
22405
|
displayId: "",
|
|
22050
22406
|
title: input.title,
|
|
22051
|
-
status: "Done",
|
|
22407
|
+
status: landInReview ? "In Review" : "Done",
|
|
22052
22408
|
priority: input.priority || "P2 Medium",
|
|
22053
22409
|
complexity: input.effort === "XS" || input.effort === "S" ? "Small" : "Medium",
|
|
22054
22410
|
module: input.module || "Core",
|
|
22055
22411
|
epic: input.epic || "Platform",
|
|
22056
22412
|
phase,
|
|
22057
22413
|
owner: input.owner || "Cathal",
|
|
22058
|
-
reviewed:
|
|
22414
|
+
reviewed: !landInReview,
|
|
22059
22415
|
createdCycle: cycle,
|
|
22416
|
+
...targetCycle !== null ? { cycle: targetCycle } : {},
|
|
22060
22417
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
22061
22418
|
taskType: input.taskType || "task",
|
|
22062
|
-
source: "
|
|
22419
|
+
source: "ad_hoc"
|
|
22063
22420
|
});
|
|
22064
22421
|
}
|
|
22065
22422
|
const report = {
|
|
@@ -22134,6 +22491,22 @@ var adHocTool = {
|
|
|
22134
22491
|
type: "string",
|
|
22135
22492
|
enum: ["task", "bug", "research", "discovery", "spike", "idea"],
|
|
22136
22493
|
description: 'Task type (default: inferred from description \u2014 "fix"/"bug" \u2192 bug, "research"/"investigate" \u2192 research, otherwise task).'
|
|
22494
|
+
},
|
|
22495
|
+
cycle: {
|
|
22496
|
+
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.',
|
|
22497
|
+
oneOf: [
|
|
22498
|
+
{ type: "string", enum: ["current", "next-if-plan-not-run"] },
|
|
22499
|
+
{ type: "integer" }
|
|
22500
|
+
]
|
|
22501
|
+
},
|
|
22502
|
+
stage: {
|
|
22503
|
+
type: "string",
|
|
22504
|
+
enum: ["done", "release"],
|
|
22505
|
+
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`.'
|
|
22506
|
+
},
|
|
22507
|
+
hold: {
|
|
22508
|
+
type: "boolean",
|
|
22509
|
+
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
22510
|
}
|
|
22138
22511
|
},
|
|
22139
22512
|
required: []
|
|
@@ -22164,6 +22537,12 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22164
22537
|
rawNotes = rawNotes.slice(0, MAX_NOTES_LENGTH);
|
|
22165
22538
|
notesTruncated = true;
|
|
22166
22539
|
}
|
|
22540
|
+
const rawCycle = args.cycle;
|
|
22541
|
+
let cycleArg;
|
|
22542
|
+
if (typeof rawCycle === "number") cycleArg = rawCycle;
|
|
22543
|
+
else if (rawCycle === "current" || rawCycle === "next-if-plan-not-run") cycleArg = rawCycle;
|
|
22544
|
+
const stageArg = args.stage === "release" ? "release" : void 0;
|
|
22545
|
+
const holdArg = args.hold === true;
|
|
22167
22546
|
const result = await recordAdHoc(adapter2, {
|
|
22168
22547
|
title: title || "",
|
|
22169
22548
|
taskId,
|
|
@@ -22174,9 +22553,12 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22174
22553
|
priority: priorityRaw,
|
|
22175
22554
|
taskType: typeRaw,
|
|
22176
22555
|
// PROJECT-SPECIFIC: owner resolved from config (PAPI_OWNER env var, default 'Cathal')
|
|
22177
|
-
owner: config2.projectOwner
|
|
22556
|
+
owner: config2.projectOwner,
|
|
22557
|
+
cycle: cycleArg,
|
|
22558
|
+
stage: stageArg,
|
|
22559
|
+
hold: holdArg
|
|
22178
22560
|
});
|
|
22179
|
-
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
22561
|
+
if (!holdArg && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
22180
22562
|
try {
|
|
22181
22563
|
stageDirAndCommit(
|
|
22182
22564
|
config2.projectRoot,
|
|
@@ -22189,8 +22571,29 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22189
22571
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
22190
22572
|
const taskModule = result.task.module || "Core";
|
|
22191
22573
|
const typeLabel = result.task.taskType || typeRaw;
|
|
22574
|
+
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)" : ""}.` : "";
|
|
22575
|
+
if (holdArg) {
|
|
22576
|
+
const branch = `feat/${result.task.id}`;
|
|
22577
|
+
return textResponse(
|
|
22578
|
+
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
22579
|
+
|
|
22580
|
+
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
22581
|
+
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.
|
|
22582
|
+
|
|
22583
|
+
1. Create a branch and commit your code there (never \`main\`):
|
|
22584
|
+
\`\`\`
|
|
22585
|
+
git switch -c ${branch}
|
|
22586
|
+
git add -- <your changed files>
|
|
22587
|
+
git commit -m "feat(${result.task.id}): ${result.task.title}"
|
|
22588
|
+
git push -u origin ${branch}
|
|
22589
|
+
\`\`\`
|
|
22590
|
+
2. Leave the branch **unmerged** \u2014 it is picked up by the next cycle's \`release\`.
|
|
22591
|
+
|
|
22592
|
+
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
22593
|
+
);
|
|
22594
|
+
}
|
|
22192
22595
|
return textResponse(
|
|
22193
|
-
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning} Build report attached.
|
|
22596
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
22194
22597
|
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
22195
22598
|
);
|
|
22196
22599
|
}
|
|
@@ -23285,13 +23688,22 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23285
23688
|
if (!stage) {
|
|
23286
23689
|
return errorResponse('stage is required. Use "handoff-review" or "build-acceptance".');
|
|
23287
23690
|
}
|
|
23691
|
+
let caps = {};
|
|
23692
|
+
try {
|
|
23693
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
23694
|
+
caps = info?.capabilities ?? {};
|
|
23695
|
+
} catch {
|
|
23696
|
+
caps = {};
|
|
23697
|
+
}
|
|
23288
23698
|
const explicitDispatch = args.dispatch === "subagent";
|
|
23289
|
-
const
|
|
23290
|
-
|
|
23699
|
+
const autoDispatchOptIn = args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false" && isCapabilityEnabled(caps, "prReviewer");
|
|
23700
|
+
const autoDispatchEligible = !verdict && autoDispatchOptIn;
|
|
23701
|
+
const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
|
|
23702
|
+
if ((explicitDispatch || autoDispatchEligible || capabilityAutoReviewEligible) && stage === "build-acceptance" && taskId) {
|
|
23291
23703
|
const dispatch = await buildReviewDispatch(adapter2, config2, taskId);
|
|
23292
23704
|
if (!dispatch.ok) {
|
|
23293
23705
|
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
23294
|
-
} else if (explicitDispatch || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23706
|
+
} else if (explicitDispatch || capabilityAutoReviewEligible || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23295
23707
|
return textResponse(dispatch.prompt);
|
|
23296
23708
|
}
|
|
23297
23709
|
}
|
|
@@ -23332,7 +23744,8 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23332
23744
|
}
|
|
23333
23745
|
}
|
|
23334
23746
|
}
|
|
23335
|
-
const tracker = new ProgressTracker("validate-args");
|
|
23747
|
+
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "review", taskId });
|
|
23748
|
+
await tracker.recordStep("reviewer_confirmed");
|
|
23336
23749
|
const handoffRegenResponse = args.handoff_regen_response;
|
|
23337
23750
|
if (handoffRegenResponse?.trim()) {
|
|
23338
23751
|
tracker.mark("apply-handoff-regen");
|
|
@@ -23349,6 +23762,14 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23349
23762
|
adapter2,
|
|
23350
23763
|
{ taskId, stage, verdict, comments, reviewer, autoReview }
|
|
23351
23764
|
);
|
|
23765
|
+
tracker.setStreamScope({ cycle: result.currentCycle > 0 ? result.currentCycle : null });
|
|
23766
|
+
await tracker.recordStep("verdict_recorded", { metadata: { verdict, stage } });
|
|
23767
|
+
if (result.unblockedTasks.length > 0) {
|
|
23768
|
+
await tracker.recordStep("dependents_unblocked", { metadata: { count: result.unblockedTasks.length } });
|
|
23769
|
+
}
|
|
23770
|
+
if (result.closedDocActions.length > 0) {
|
|
23771
|
+
await tracker.recordStep("docs_closed", { metadata: { count: result.closedDocActions.length } });
|
|
23772
|
+
}
|
|
23352
23773
|
const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
|
|
23353
23774
|
const unblockNote = result.unblockedTasks.length > 0 ? `
|
|
23354
23775
|
|
|
@@ -23388,6 +23809,7 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
23388
23809
|
|
|
23389
23810
|
> ${mergeResult.message}`;
|
|
23390
23811
|
}
|
|
23812
|
+
await tracker.recordStep("deferred", { metadata: { reason: "merge-skipped" } });
|
|
23391
23813
|
} else if (!mergeResult.merged) {
|
|
23392
23814
|
mergeFailed = true;
|
|
23393
23815
|
let revertNote = "";
|
|
@@ -23402,11 +23824,13 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
23402
23824
|
\u26A0\uFE0F **PR merge failed \u2014 task NOT marked Done.**${revertNote}
|
|
23403
23825
|
|
|
23404
23826
|
${mergeResult.message}`;
|
|
23827
|
+
await tracker.recordStep("deferred", { status: "failed", metadata: { reason: "merge-failed" } });
|
|
23405
23828
|
} else {
|
|
23406
23829
|
try {
|
|
23407
23830
|
await adapter2.updateTask(taskId, { mergedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23408
23831
|
} catch {
|
|
23409
23832
|
}
|
|
23833
|
+
await tracker.recordStep("merged");
|
|
23410
23834
|
const detailLines = mergeResult.details.map((l) => `> ${l}`).join("\n");
|
|
23411
23835
|
mergeNote = `
|
|
23412
23836
|
|
|
@@ -23482,6 +23906,7 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
23482
23906
|
}
|
|
23483
23907
|
const version = `v0.${result.currentCycle}.0`;
|
|
23484
23908
|
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
23909
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
23485
23910
|
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
23486
23911
|
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
23487
23912
|
autoReleaseNote = `
|
|
@@ -23537,9 +23962,7 @@ ${findingLines}${more}` : "");
|
|
|
23537
23962
|
\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
23963
|
}
|
|
23539
23964
|
} 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.`;
|
|
23965
|
+
autoReviewNote = buildPrReviewerDirective(caps) ?? "";
|
|
23543
23966
|
}
|
|
23544
23967
|
let nextStepNote = "";
|
|
23545
23968
|
if (!autoReleaseNote && stage === "build-acceptance") {
|
|
@@ -23551,6 +23974,7 @@ ${findingLines}${more}` : "");
|
|
|
23551
23974
|
Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
23552
23975
|
}
|
|
23553
23976
|
}
|
|
23977
|
+
const securityNote = stage === "build-acceptance" && verdict === "accept" ? buildSecurityScanDirective(caps) ?? "" : "";
|
|
23554
23978
|
tracker.mark("format-response");
|
|
23555
23979
|
return textResponse(
|
|
23556
23980
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
@@ -23558,7 +23982,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
23558
23982
|
- **Verdict:** ${result.verdict}
|
|
23559
23983
|
- **Comments:** ${result.comments}
|
|
23560
23984
|
|
|
23561
|
-
${statusNote}${autoReviewNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
23985
|
+
${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
23562
23986
|
);
|
|
23563
23987
|
} catch (err) {
|
|
23564
23988
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/prompts.js
CHANGED
|
@@ -659,6 +659,14 @@ function buildHandoffsOnlyUserMessage(inputs) {
|
|
|
659
659
|
}
|
|
660
660
|
return parts.join("\n");
|
|
661
661
|
}
|
|
662
|
+
function extractDependencyChain(displayText) {
|
|
663
|
+
const match = displayText.match(
|
|
664
|
+
/^##\s+Dependency Chain[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|$(?![\s\S]))/m
|
|
665
|
+
);
|
|
666
|
+
if (!match) return void 0;
|
|
667
|
+
const body = match[1].trim();
|
|
668
|
+
return body.length > 0 ? body : void 0;
|
|
669
|
+
}
|
|
662
670
|
function parseStructuredOutput(raw) {
|
|
663
671
|
const marker = "<!-- PAPI_STRUCTURED_OUTPUT -->";
|
|
664
672
|
const markerIdx = raw.indexOf(marker);
|
|
@@ -674,6 +682,7 @@ function parseStructuredOutput(raw) {
|
|
|
674
682
|
try {
|
|
675
683
|
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
676
684
|
const data = coerceStructuredOutput(parsed);
|
|
685
|
+
data.dependencyChain = extractDependencyChain(displayText);
|
|
677
686
|
return { displayText, data };
|
|
678
687
|
} catch {
|
|
679
688
|
return { displayText, data: null };
|
|
@@ -1502,6 +1511,7 @@ export {
|
|
|
1502
1511
|
buildReviewSystemPrompt,
|
|
1503
1512
|
buildReviewUserMessage,
|
|
1504
1513
|
buildVisionTasksPrompt,
|
|
1514
|
+
extractDependencyChain,
|
|
1505
1515
|
parseReviewStructuredOutput,
|
|
1506
1516
|
parseStrategyChangeOutput,
|
|
1507
1517
|
parseStructuredOutput
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "PAPI MCP server
|
|
3
|
+
"version": "0.7.46",
|
|
4
|
+
"description": "PAPI MCP server \u2014 AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.getpapi/papi",
|
|
7
7
|
"type": "module",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
54
54
|
"@papi-ai/adapter-md": "^0.2.0",
|
|
55
55
|
"@papi-ai/adapter-pg": "^0.2.0",
|
|
56
|
+
"@papi-ai/shared": "^0.1.0",
|
|
56
57
|
"@papi-ai/skills": "^0.1.0",
|
|
57
58
|
"js-yaml": "^4.1.0"
|
|
58
59
|
},
|