@papi-ai/server 0.7.44 → 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/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;
|
|
@@ -1989,7 +1993,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1989
1993
|
}
|
|
1990
1994
|
});
|
|
1991
1995
|
|
|
1992
|
-
//
|
|
1996
|
+
// ../../node_modules/postgres/src/query.js
|
|
1993
1997
|
function cachedError(xs) {
|
|
1994
1998
|
if (originCache.has(xs))
|
|
1995
1999
|
return originCache.get(xs);
|
|
@@ -2001,7 +2005,7 @@ function cachedError(xs) {
|
|
|
2001
2005
|
}
|
|
2002
2006
|
var originCache, originStackCache, originError, CLOSE, Query;
|
|
2003
2007
|
var init_query = __esm({
|
|
2004
|
-
"
|
|
2008
|
+
"../../node_modules/postgres/src/query.js"() {
|
|
2005
2009
|
"use strict";
|
|
2006
2010
|
originCache = /* @__PURE__ */ new Map();
|
|
2007
2011
|
originStackCache = /* @__PURE__ */ new Map();
|
|
@@ -2132,7 +2136,7 @@ var init_query = __esm({
|
|
|
2132
2136
|
}
|
|
2133
2137
|
});
|
|
2134
2138
|
|
|
2135
|
-
//
|
|
2139
|
+
// ../../node_modules/postgres/src/errors.js
|
|
2136
2140
|
function connection(x, options, socket) {
|
|
2137
2141
|
const { host, port } = socket || options;
|
|
2138
2142
|
const error = Object.assign(
|
|
@@ -2170,7 +2174,7 @@ function notSupported(x) {
|
|
|
2170
2174
|
}
|
|
2171
2175
|
var PostgresError, Errors;
|
|
2172
2176
|
var init_errors = __esm({
|
|
2173
|
-
"
|
|
2177
|
+
"../../node_modules/postgres/src/errors.js"() {
|
|
2174
2178
|
"use strict";
|
|
2175
2179
|
PostgresError = class extends Error {
|
|
2176
2180
|
constructor(x) {
|
|
@@ -2188,7 +2192,7 @@ var init_errors = __esm({
|
|
|
2188
2192
|
}
|
|
2189
2193
|
});
|
|
2190
2194
|
|
|
2191
|
-
//
|
|
2195
|
+
// ../../node_modules/postgres/src/types.js
|
|
2192
2196
|
function handleValue(x, parameters, types2, options) {
|
|
2193
2197
|
let value = x instanceof Parameter ? x.value : x;
|
|
2194
2198
|
if (value === void 0) {
|
|
@@ -2303,7 +2307,7 @@ function createJsonTransform(fn) {
|
|
|
2303
2307
|
}
|
|
2304
2308
|
var types, NotTagged, Identifier, Parameter, Builder, defaultHandlers, builders, serializers, parsers, mergeUserTypes, escapeIdentifier, inferType, escapeBackslash, escapeQuote, arraySerializer, arrayParserState, arrayParser, toCamel, toPascal, toKebab, fromCamel, fromPascal, fromKebab, camel, pascal, kebab;
|
|
2305
2309
|
var init_types = __esm({
|
|
2306
|
-
"
|
|
2310
|
+
"../../node_modules/postgres/src/types.js"() {
|
|
2307
2311
|
"use strict";
|
|
2308
2312
|
init_query();
|
|
2309
2313
|
init_errors();
|
|
@@ -2482,10 +2486,10 @@ var init_types = __esm({
|
|
|
2482
2486
|
}
|
|
2483
2487
|
});
|
|
2484
2488
|
|
|
2485
|
-
//
|
|
2489
|
+
// ../../node_modules/postgres/src/result.js
|
|
2486
2490
|
var Result;
|
|
2487
2491
|
var init_result = __esm({
|
|
2488
|
-
"
|
|
2492
|
+
"../../node_modules/postgres/src/result.js"() {
|
|
2489
2493
|
"use strict";
|
|
2490
2494
|
Result = class extends Array {
|
|
2491
2495
|
constructor() {
|
|
@@ -2505,7 +2509,7 @@ var init_result = __esm({
|
|
|
2505
2509
|
}
|
|
2506
2510
|
});
|
|
2507
2511
|
|
|
2508
|
-
//
|
|
2512
|
+
// ../../node_modules/postgres/src/queue.js
|
|
2509
2513
|
function Queue(initial = []) {
|
|
2510
2514
|
let xs = initial.slice();
|
|
2511
2515
|
let index = 0;
|
|
@@ -2532,13 +2536,13 @@ function Queue(initial = []) {
|
|
|
2532
2536
|
}
|
|
2533
2537
|
var queue_default;
|
|
2534
2538
|
var init_queue = __esm({
|
|
2535
|
-
"
|
|
2539
|
+
"../../node_modules/postgres/src/queue.js"() {
|
|
2536
2540
|
"use strict";
|
|
2537
2541
|
queue_default = Queue;
|
|
2538
2542
|
}
|
|
2539
2543
|
});
|
|
2540
2544
|
|
|
2541
|
-
//
|
|
2545
|
+
// ../../node_modules/postgres/src/bytes.js
|
|
2542
2546
|
function fit(x) {
|
|
2543
2547
|
if (buffer.length - b.i < x) {
|
|
2544
2548
|
const prev = buffer, length = prev.length;
|
|
@@ -2552,7 +2556,7 @@ function reset() {
|
|
|
2552
2556
|
}
|
|
2553
2557
|
var size, buffer, messages, b, bytes_default;
|
|
2554
2558
|
var init_bytes = __esm({
|
|
2555
|
-
"
|
|
2559
|
+
"../../node_modules/postgres/src/bytes.js"() {
|
|
2556
2560
|
"use strict";
|
|
2557
2561
|
size = 256;
|
|
2558
2562
|
buffer = Buffer.allocUnsafe(size);
|
|
@@ -2617,7 +2621,7 @@ var init_bytes = __esm({
|
|
|
2617
2621
|
}
|
|
2618
2622
|
});
|
|
2619
2623
|
|
|
2620
|
-
//
|
|
2624
|
+
// ../../node_modules/postgres/src/connection.js
|
|
2621
2625
|
import net from "net";
|
|
2622
2626
|
import tls from "tls";
|
|
2623
2627
|
import crypto2 from "crypto";
|
|
@@ -3411,7 +3415,7 @@ function timer(fn, seconds) {
|
|
|
3411
3415
|
}
|
|
3412
3416
|
var connection_default, uid, Sync, Flush, SSLRequest, ExecuteUnnamed, DescribeUnnamed, noop, retryRoutines, errorFields;
|
|
3413
3417
|
var init_connection = __esm({
|
|
3414
|
-
"
|
|
3418
|
+
"../../node_modules/postgres/src/connection.js"() {
|
|
3415
3419
|
"use strict";
|
|
3416
3420
|
init_types();
|
|
3417
3421
|
init_errors();
|
|
@@ -3474,7 +3478,7 @@ var init_connection = __esm({
|
|
|
3474
3478
|
}
|
|
3475
3479
|
});
|
|
3476
3480
|
|
|
3477
|
-
//
|
|
3481
|
+
// ../../node_modules/postgres/src/subscribe.js
|
|
3478
3482
|
function Subscribe(postgres2, options) {
|
|
3479
3483
|
const subscribers = /* @__PURE__ */ new Map(), slot = "postgresjs_" + Math.random().toString(36).slice(2), state = {};
|
|
3480
3484
|
let connection2, stream, ended = false;
|
|
@@ -3685,14 +3689,14 @@ function parseEvent(x) {
|
|
|
3685
3689
|
}
|
|
3686
3690
|
var noop2;
|
|
3687
3691
|
var init_subscribe = __esm({
|
|
3688
|
-
"
|
|
3692
|
+
"../../node_modules/postgres/src/subscribe.js"() {
|
|
3689
3693
|
"use strict";
|
|
3690
3694
|
noop2 = () => {
|
|
3691
3695
|
};
|
|
3692
3696
|
}
|
|
3693
3697
|
});
|
|
3694
3698
|
|
|
3695
|
-
//
|
|
3699
|
+
// ../../node_modules/postgres/src/large.js
|
|
3696
3700
|
import Stream2 from "stream";
|
|
3697
3701
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3698
3702
|
return new Promise(async (resolve3, reject) => {
|
|
@@ -3758,12 +3762,12 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3758
3762
|
});
|
|
3759
3763
|
}
|
|
3760
3764
|
var init_large = __esm({
|
|
3761
|
-
"
|
|
3765
|
+
"../../node_modules/postgres/src/large.js"() {
|
|
3762
3766
|
"use strict";
|
|
3763
3767
|
}
|
|
3764
3768
|
});
|
|
3765
3769
|
|
|
3766
|
-
//
|
|
3770
|
+
// ../../node_modules/postgres/src/index.js
|
|
3767
3771
|
var src_exports = {};
|
|
3768
3772
|
__export(src_exports, {
|
|
3769
3773
|
default: () => src_default
|
|
@@ -4153,7 +4157,7 @@ function osUsername() {
|
|
|
4153
4157
|
}
|
|
4154
4158
|
var src_default;
|
|
4155
4159
|
var init_src = __esm({
|
|
4156
|
-
"
|
|
4160
|
+
"../../node_modules/postgres/src/index.js"() {
|
|
4157
4161
|
"use strict";
|
|
4158
4162
|
init_types();
|
|
4159
4163
|
init_connection();
|
|
@@ -5043,6 +5047,7 @@ var HELP_FOOTER_MD = `
|
|
|
5043
5047
|
// src/config.ts
|
|
5044
5048
|
var STRATEGY_REVIEW_OFFER_GAP = 5;
|
|
5045
5049
|
var STRATEGY_REVIEW_BLOCK_GAP = 7;
|
|
5050
|
+
var ZOOM_OUT_OFFER_GAP = 25;
|
|
5046
5051
|
function loadConfig() {
|
|
5047
5052
|
const projectArgIdx = process.argv.indexOf("--project");
|
|
5048
5053
|
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
@@ -5060,6 +5065,9 @@ function loadConfig() {
|
|
|
5060
5065
|
const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
|
|
5061
5066
|
const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
|
|
5062
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;
|
|
5063
5071
|
const telemetryEnabled = process.env.PAPI_TELEMETRY !== "off" && process.env.PAPI_TELEMETRY !== "false";
|
|
5064
5072
|
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
5065
5073
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
@@ -5134,7 +5142,10 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5134
5142
|
skipProjectSpecificRules,
|
|
5135
5143
|
userId,
|
|
5136
5144
|
telemetryEnabled,
|
|
5137
|
-
resolutionMethod
|
|
5145
|
+
resolutionMethod,
|
|
5146
|
+
gateCommand,
|
|
5147
|
+
deployCommand,
|
|
5148
|
+
verifyCommand
|
|
5138
5149
|
};
|
|
5139
5150
|
}
|
|
5140
5151
|
|
|
@@ -5142,31 +5153,18 @@ Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env con
|
|
|
5142
5153
|
import path3 from "path";
|
|
5143
5154
|
import { execSync } from "child_process";
|
|
5144
5155
|
|
|
5145
|
-
//
|
|
5156
|
+
// ../adapter-md/dist/index.js
|
|
5146
5157
|
import { readFile, writeFile, access } from "fs/promises";
|
|
5147
5158
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5148
5159
|
import { join } from "path";
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
"Done": [],
|
|
5158
|
-
"Blocked": ["Backlog", "Ready", "In Cycle", "In Progress", "Cancelled"],
|
|
5159
|
-
"Cancelled": [],
|
|
5160
|
-
"Deferred": ["Backlog", "Cancelled"]
|
|
5161
|
-
};
|
|
5162
|
-
var RETIRED_DECISION_OUTCOMES = ["resolved", "abandoned", "superseded"];
|
|
5163
|
-
function isLiveDecision(d) {
|
|
5164
|
-
if (d.superseded === true) return false;
|
|
5165
|
-
if (d.outcome != null && RETIRED_DECISION_OUTCOMES.includes(d.outcome)) return false;
|
|
5166
|
-
return true;
|
|
5167
|
-
}
|
|
5168
|
-
|
|
5169
|
-
// ../../../../../packages/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";
|
|
5170
5168
|
import { randomUUID } from "crypto";
|
|
5171
5169
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5172
5170
|
import yaml from "js-yaml";
|
|
@@ -5175,8 +5173,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
5175
5173
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
5176
5174
|
import yaml2 from "js-yaml";
|
|
5177
5175
|
import yaml3 from "js-yaml";
|
|
5178
|
-
var
|
|
5179
|
-
var
|
|
5176
|
+
var VALID_TRANSITIONS = _VALID_TRANSITIONS;
|
|
5177
|
+
var isLiveDecision = _isLiveDecision;
|
|
5180
5178
|
function extractSection(content, heading) {
|
|
5181
5179
|
const headingPattern = new RegExp(`^## ${heading}\\s*$`, "m");
|
|
5182
5180
|
const start = content.search(headingPattern);
|
|
@@ -6477,6 +6475,7 @@ function toCycle(raw) {
|
|
|
6477
6475
|
};
|
|
6478
6476
|
if (raw.end_date) cycle.endDate = raw.end_date;
|
|
6479
6477
|
if (raw.user_id) cycle.userId = raw.user_id;
|
|
6478
|
+
if (raw.dependency_chain) cycle.dependencyChain = raw.dependency_chain;
|
|
6480
6479
|
return cycle;
|
|
6481
6480
|
}
|
|
6482
6481
|
function fromCycle(cycle) {
|
|
@@ -6491,6 +6490,7 @@ function fromCycle(cycle) {
|
|
|
6491
6490
|
};
|
|
6492
6491
|
if (cycle.endDate) raw.end_date = cycle.endDate;
|
|
6493
6492
|
if (cycle.userId) raw.user_id = cycle.userId;
|
|
6493
|
+
if (cycle.dependencyChain) raw.dependency_chain = cycle.dependencyChain;
|
|
6494
6494
|
return raw;
|
|
6495
6495
|
}
|
|
6496
6496
|
function extractYamlBlock2(content) {
|
|
@@ -6535,7 +6535,7 @@ function prependCycle(cycle, content) {
|
|
|
6535
6535
|
const yamlStr = yaml2.dump({ cycles: [raw] }, { lineWidth: 120, quotingType: '"' });
|
|
6536
6536
|
return header + YAML_START2 + "\n" + yamlStr + YAML_END2 + "\n";
|
|
6537
6537
|
}
|
|
6538
|
-
const existing = parseCycles(content);
|
|
6538
|
+
const existing = parseCycles(content).filter((c) => c.number !== cycle.number);
|
|
6539
6539
|
const merged = [cycle, ...existing];
|
|
6540
6540
|
return serializeCycles(merged, content);
|
|
6541
6541
|
}
|
|
@@ -6632,7 +6632,7 @@ var MdFileAdapter = class {
|
|
|
6632
6632
|
if (!content) return [];
|
|
6633
6633
|
const all = parseActiveDecisions(content);
|
|
6634
6634
|
if (options?.includeRetired) return all;
|
|
6635
|
-
return all.filter(
|
|
6635
|
+
return all.filter(isLiveDecision);
|
|
6636
6636
|
}
|
|
6637
6637
|
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
6638
6638
|
async getCycleLog(limit) {
|
|
@@ -6669,14 +6669,27 @@ var MdFileAdapter = class {
|
|
|
6669
6669
|
notes: review.notes
|
|
6670
6670
|
});
|
|
6671
6671
|
}
|
|
6672
|
-
/**
|
|
6673
|
-
|
|
6672
|
+
/**
|
|
6673
|
+
* Get the cycle number of the last strategy review.
|
|
6674
|
+
* task-2416 (C315): `excludeZoomOut` is accepted for interface parity with the pg
|
|
6675
|
+
* adapter. The md adapter already matches strategy-review titles only (zoom-out logs
|
|
6676
|
+
* carry a "Zoom-Out" title, not a strategy-review one), so the flag is a no-op here —
|
|
6677
|
+
* legacy/best-effort path.
|
|
6678
|
+
*/
|
|
6679
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6680
|
+
async getLastStrategyReviewCycle(_opts) {
|
|
6674
6681
|
const log2 = await this.getCycleLog();
|
|
6675
6682
|
const entry = log2.find(
|
|
6676
6683
|
(e) => /strategy.*review|strategic.*shift/i.test(e.title)
|
|
6677
6684
|
);
|
|
6678
6685
|
return entry?.cycleNumber ?? 0;
|
|
6679
6686
|
}
|
|
6687
|
+
/** task-2416 (C315): cycle of the last zoom-out retrospective. md best-effort — matches the log title. */
|
|
6688
|
+
async getLastZoomOutCycle() {
|
|
6689
|
+
const log2 = await this.getCycleLog();
|
|
6690
|
+
const entry = log2.find((e) => /zoom[-\s]?out/i.test(e.title));
|
|
6691
|
+
return entry?.cycleNumber ?? 0;
|
|
6692
|
+
}
|
|
6680
6693
|
/** Get strategy reviews — md adapter returns empty (reviews live in cycle log). */
|
|
6681
6694
|
async getStrategyReviews(_limit, _includeFullAnalysis) {
|
|
6682
6695
|
return [];
|
|
@@ -6787,7 +6800,7 @@ var MdFileAdapter = class {
|
|
|
6787
6800
|
}
|
|
6788
6801
|
if (updates.status && updates.status !== tasks[idx].status && !options?.force) {
|
|
6789
6802
|
const from = tasks[idx].status;
|
|
6790
|
-
const allowed =
|
|
6803
|
+
const allowed = VALID_TRANSITIONS[from];
|
|
6791
6804
|
if (!allowed.includes(updates.status)) {
|
|
6792
6805
|
console.warn(`[papi] Warning: invalid status transition "${from}" \u2192 "${updates.status}" for task ${id}. Allowed from "${from}": ${allowed.length > 0 ? allowed.join(", ") : "none"}`);
|
|
6793
6806
|
}
|
|
@@ -9112,6 +9125,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
9112
9125
|
if (ctx.board) {
|
|
9113
9126
|
parts.push("### Board", "", ctx.board, "");
|
|
9114
9127
|
}
|
|
9128
|
+
if (ctx.resurfacedBlockers) {
|
|
9129
|
+
parts.push(
|
|
9130
|
+
"### Ready to Unblock (typed blockers now resolved)",
|
|
9131
|
+
"",
|
|
9132
|
+
"These tasks are Blocked but their typed dependency has cleared. Treat them as actionable candidates \u2014 resurface them into the cycle (or confirm they still belong Blocked), do not leave them sitting.",
|
|
9133
|
+
"",
|
|
9134
|
+
ctx.resurfacedBlockers,
|
|
9135
|
+
""
|
|
9136
|
+
);
|
|
9137
|
+
}
|
|
9115
9138
|
if (ctx.candidateTaskFullNotes) {
|
|
9116
9139
|
parts.push("### Full Notes for Candidate Tasks", "", ctx.candidateTaskFullNotes, "");
|
|
9117
9140
|
}
|
|
@@ -9249,6 +9272,14 @@ function buildHandoffsOnlyUserMessage(inputs) {
|
|
|
9249
9272
|
}
|
|
9250
9273
|
return parts.join("\n");
|
|
9251
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
|
+
}
|
|
9252
9283
|
function parseStructuredOutput(raw) {
|
|
9253
9284
|
const marker = "<!-- PAPI_STRUCTURED_OUTPUT -->";
|
|
9254
9285
|
const markerIdx = raw.indexOf(marker);
|
|
@@ -9264,6 +9295,7 @@ function parseStructuredOutput(raw) {
|
|
|
9264
9295
|
try {
|
|
9265
9296
|
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
9266
9297
|
const data = coerceStructuredOutput(parsed);
|
|
9298
|
+
data.dependencyChain = extractDependencyChain(displayText);
|
|
9267
9299
|
return { displayText, data };
|
|
9268
9300
|
} catch {
|
|
9269
9301
|
return { displayText, data: null };
|
|
@@ -9351,6 +9383,7 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
|
|
|
9351
9383
|
- **Product impact first, process second.** The reader wants to know: what got better for users, what's broken, what opportunities exist. Internal machinery (AD wording, taxonomy labels, hierarchy status) is secondary \u2014 handle it in a compact appendix, not the main review.
|
|
9352
9384
|
- **Lead with insight, not data recitation.** Open each section with the strategic takeaway or pattern, THEN support it with cycle data and task references. Bad: "C131 built task-700, C132 built task-710." Good: "The last 5 cycles show a clear shift from infrastructure to user-facing work \u2014 80% of tasks were dashboard or onboarding, up from 30% in the prior review window."
|
|
9353
9385
|
- **Cycle data first, conversation context second.** Base your review on build reports, cycle logs, board state, and ADs \u2014 not on whatever was discussed earlier in the conversation. If recent conversation context conflicts with the data, flag it but trust the data.
|
|
9386
|
+
- **Verify before claiming \u2014 your context is a snapshot, not live state.** The task bodies, doc summaries, prior strategy reviews, and zoom-out framing you were given were written in the cycle they are dated; the product and codebase have moved since. Treat every present-tense claim about current product or code state \u2014 "X is broken", "Y only works in Z", "feature not built", "metric unmeasured", a stalled task "still needs building" \u2014 as a HYPOTHESIS to check, not a fact. If you have tools to inspect the live code or dashboard (grep, file read, running app), verify the claim before asserting it. If you cannot verify, attribute and hedge it explicitly ("per task-NNNN's C303 framing \u2014 unverified against current code") rather than stating it as current fact. A confident claim the reader can falsify in one grep destroys the review's credibility. **Stalled tasks are the highest-risk case:** a task sitting in Backlog for many cycles is frequently already shipped under another task and never closed \u2014 check the code before recommending you build, cancel, or defer it.
|
|
9354
9387
|
- **Be concise and scannable.** Use short paragraphs, bullet points, and clear headings. Avoid walls of text. The review should be readable in 3 minutes, not 15. Format cycle summaries as compact bullet points, not multi-paragraph narratives.
|
|
9355
9388
|
- **Every conditional section earns its place.** If a conditional section has nothing meaningful to say, skip it entirely. Do not write "No issues found" or "No concerns" \u2014 just omit the section.
|
|
9356
9389
|
- **AD housekeeping is an appendix, not the centerpiece.** Just list changes and make them. Don't score every AD individually. Don't ask for approval on wording tweaks \u2014 small changes (confidence bumps, deleting stale ADs, fixing wording) should just happen. Only flag ADs that represent a genuine strategic question.
|
|
@@ -10636,6 +10669,52 @@ function computeCarryForwardStaleness(log2, doneTaskIds) {
|
|
|
10636
10669
|
].join("\n");
|
|
10637
10670
|
}
|
|
10638
10671
|
|
|
10672
|
+
// src/lib/blocker.ts
|
|
10673
|
+
function idMatches(candidate, ref) {
|
|
10674
|
+
if (!candidate) return false;
|
|
10675
|
+
return candidate.toLowerCase() === ref.toLowerCase();
|
|
10676
|
+
}
|
|
10677
|
+
function isBlockerResolved(blocker, ctx) {
|
|
10678
|
+
if (!blocker || !blocker.ref) return false;
|
|
10679
|
+
switch (blocker.type) {
|
|
10680
|
+
case "depends-on": {
|
|
10681
|
+
const dep = ctx.tasks.find(
|
|
10682
|
+
(t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
|
|
10683
|
+
);
|
|
10684
|
+
return dep?.status === "Done";
|
|
10685
|
+
}
|
|
10686
|
+
case "owner-action": {
|
|
10687
|
+
const action = ctx.ownerActions.find((a) => idMatches(a.id, blocker.ref));
|
|
10688
|
+
return action != null && action.completed_at != null;
|
|
10689
|
+
}
|
|
10690
|
+
case "decision-gate": {
|
|
10691
|
+
const decision = ctx.decisions.find(
|
|
10692
|
+
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10693
|
+
);
|
|
10694
|
+
const resolvedOutcomes = /* @__PURE__ */ new Set(["resolved", "confirmed", "validated", "superseded"]);
|
|
10695
|
+
if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
|
|
10696
|
+
const hasRecentEvent = ctx.decisionEvents.some(
|
|
10697
|
+
(e) => idMatches(e.decisionId, blocker.ref) && e.cycle >= blocker.blockedCycle
|
|
10698
|
+
);
|
|
10699
|
+
return hasRecentEvent;
|
|
10700
|
+
}
|
|
10701
|
+
default:
|
|
10702
|
+
return false;
|
|
10703
|
+
}
|
|
10704
|
+
}
|
|
10705
|
+
function formatBlockerWaiting(blocker) {
|
|
10706
|
+
switch (blocker.type) {
|
|
10707
|
+
case "depends-on":
|
|
10708
|
+
return `waiting on ${blocker.ref} \u2014 clears when that task is Done`;
|
|
10709
|
+
case "owner-action":
|
|
10710
|
+
return `waiting on owner action ${blocker.ref} \u2014 clears when you complete it`;
|
|
10711
|
+
case "decision-gate":
|
|
10712
|
+
return `waiting on decision ${blocker.ref} \u2014 clears when that decision is resolved`;
|
|
10713
|
+
default:
|
|
10714
|
+
return `waiting on ${blocker.ref}`;
|
|
10715
|
+
}
|
|
10716
|
+
}
|
|
10717
|
+
|
|
10639
10718
|
// src/lib/visibility-inheritance.ts
|
|
10640
10719
|
var TIER_RANK = {
|
|
10641
10720
|
public: 0,
|
|
@@ -11068,6 +11147,28 @@ function computeFoundationalTasksGuidance(cycleNumber, brief) {
|
|
|
11068
11147
|
"- User-submitted backlog tasks remain in `cycleHandoffs` as usual \u2014 do NOT drop them in favour of foundational tasks."
|
|
11069
11148
|
].join("\n");
|
|
11070
11149
|
}
|
|
11150
|
+
async function computeResurfacedBlockers(adapter2, tasks, decisions, currentCycle) {
|
|
11151
|
+
const blocked = tasks.filter((t) => t.status === "Blocked" && t.blocker);
|
|
11152
|
+
if (blocked.length === 0) return void 0;
|
|
11153
|
+
const minCycle = Math.max(0, currentCycle - 5);
|
|
11154
|
+
const decisionEvents = adapter2.getDecisionEventsSince ? await adapter2.getDecisionEventsSince(minCycle).catch(() => []) : [];
|
|
11155
|
+
let ownerActions = [];
|
|
11156
|
+
if (adapter2.listOwnerActionsForBlockerScan && adapter2.getProjectOwnerUserId) {
|
|
11157
|
+
try {
|
|
11158
|
+
const userId = await adapter2.getProjectOwnerUserId();
|
|
11159
|
+
if (userId) ownerActions = await adapter2.listOwnerActionsForBlockerScan(userId);
|
|
11160
|
+
} catch {
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
const ctx = { tasks, decisions, decisionEvents, ownerActions };
|
|
11164
|
+
const resolvedLines = [];
|
|
11165
|
+
for (const t of blocked) {
|
|
11166
|
+
if (t.blocker && isBlockerResolved(t.blocker, ctx)) {
|
|
11167
|
+
resolvedLines.push(`- **${t.displayId}** (${t.title}) \u2014 ${formatBlockerWaiting(t.blocker)} \u2192 now RESOLVED, ready to unblock`);
|
|
11168
|
+
}
|
|
11169
|
+
}
|
|
11170
|
+
return resolvedLines.length > 0 ? resolvedLines.join("\n") : void 0;
|
|
11171
|
+
}
|
|
11071
11172
|
function detectBoardFlags(tasks) {
|
|
11072
11173
|
let hasBugTasks = false;
|
|
11073
11174
|
let hasResearchTasks = false;
|
|
@@ -11546,6 +11647,7 @@ ${logLines}`);
|
|
|
11546
11647
|
const targetCycle = health.totalCycles + 1;
|
|
11547
11648
|
const preAssigned = plannerTasks.filter((t2) => t2.cycle === targetCycle);
|
|
11548
11649
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
11650
|
+
const resurfacedBlockersText = await computeResurfacedBlockers(adapter2, tasks, decisions, health.totalCycles).catch(() => void 0);
|
|
11549
11651
|
const gapFull = health.cyclesSinceLastStrategyReview;
|
|
11550
11652
|
const lastReviewCycleFull = health.totalCycles - gapFull;
|
|
11551
11653
|
const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < STRATEGY_REVIEW_OFFER_GAP ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + STRATEGY_REVIEW_OFFER_GAP}.` : gapFull < STRATEGY_REVIEW_BLOCK_GAP ? `\u25CB Strategy review available \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Offered now (optional); planning hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Planning is blocked until it runs (or \`force: true\`).`;
|
|
@@ -11578,6 +11680,7 @@ ${logLines}`);
|
|
|
11578
11680
|
doneTasksResultFull.status === "fulfilled" ? new Set(doneTasksResultFull.value.map((t2) => t2.displayId)) : void 0
|
|
11579
11681
|
),
|
|
11580
11682
|
preAssignedTasks: preAssignedText,
|
|
11683
|
+
resurfacedBlockers: resurfacedBlockersText,
|
|
11581
11684
|
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
|
|
11582
11685
|
strategyReviewCadence: strategyReviewCadenceFull,
|
|
11583
11686
|
candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks),
|
|
@@ -11651,8 +11754,27 @@ ${cleanContent}`;
|
|
|
11651
11754
|
contextHashes,
|
|
11652
11755
|
// task-2071 (MU-3): own the cycle by the planning member (pg defaults to the
|
|
11653
11756
|
// project owner when unset, so owner-operated plans stay correct).
|
|
11654
|
-
...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 } : {}
|
|
11655
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
|
+
}
|
|
11656
11778
|
let dedupedNewTasks = data.newTasks ?? [];
|
|
11657
11779
|
if (dedupedNewTasks.length > 0) {
|
|
11658
11780
|
const existingTasks = await adapter2.queryBoard({ compact: true });
|
|
@@ -11745,7 +11867,7 @@ ${cleanContent}`;
|
|
|
11745
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.)`
|
|
11746
11868
|
);
|
|
11747
11869
|
}
|
|
11748
|
-
const allWarnings = [...result.warnings, ...verifyWarnings];
|
|
11870
|
+
const allWarnings = [...result.warnings, ...verifyWarnings, ...earlyCreateWarnings];
|
|
11749
11871
|
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
11750
11872
|
const correctionCount = data.boardCorrections?.length ?? 0;
|
|
11751
11873
|
const newTaskCount = result.newTaskIdMap.size;
|
|
@@ -11785,6 +11907,23 @@ ${cleanContent}`;
|
|
|
11785
11907
|
});
|
|
11786
11908
|
const cycleTaskCount = legacyHandoffs.length;
|
|
11787
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
|
+
}
|
|
11788
11927
|
const cycleLogPromise = adapter2.writeCycleLogEntry({
|
|
11789
11928
|
uuid: randomUUID8(),
|
|
11790
11929
|
cycleNumber: newCycleNumber,
|
|
@@ -12047,7 +12186,10 @@ ${cleanContent}`;
|
|
|
12047
12186
|
taskIds: cycleTaskIds,
|
|
12048
12187
|
contextHashes,
|
|
12049
12188
|
// task-2071 (MU-3): own the cycle by the planning member.
|
|
12050
|
-
...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 } : {}
|
|
12051
12193
|
};
|
|
12052
12194
|
await adapter2.createCycle(cycle);
|
|
12053
12195
|
} catch (err) {
|
|
@@ -12527,12 +12669,58 @@ ${JSON.stringify(payload, null, 2)}`;
|
|
|
12527
12669
|
}
|
|
12528
12670
|
var ProgressTracker = class {
|
|
12529
12671
|
lastStep;
|
|
12672
|
+
persistAdapter;
|
|
12673
|
+
streamCtx;
|
|
12530
12674
|
constructor(initial = "start") {
|
|
12531
12675
|
this.lastStep = initial;
|
|
12532
12676
|
}
|
|
12533
12677
|
mark(step) {
|
|
12534
12678
|
this.lastStep = step;
|
|
12535
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
|
+
}
|
|
12536
12724
|
};
|
|
12537
12725
|
function defaultHint(tool) {
|
|
12538
12726
|
const knownTools = /* @__PURE__ */ new Set([
|
|
@@ -12571,6 +12759,8 @@ function buildSubagentDispatchPrompt(input) {
|
|
|
12571
12759
|
applyNote = `\`plan\` again with mode="apply", llm_response=<sub-agent output>, plan_mode="${applyMode ?? "full"}", cycle_number=${cycleNumber + 1}, strategy_review_warning=${JSON.stringify(strategyReviewWarning)}`;
|
|
12572
12760
|
} else if (tool === "strategy_review") {
|
|
12573
12761
|
applyNote = `\`strategy_review\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber}`;
|
|
12762
|
+
} else if (tool === "zoom_out") {
|
|
12763
|
+
applyNote = `\`zoom_out\` again with mode="apply", llm_response=<sub-agent output>, cycle_number=${cycleNumber}`;
|
|
12574
12764
|
} else {
|
|
12575
12765
|
applyNote = `\`review_submit\` with task_id="${taskId ?? "<task-id>"}", stage="build-acceptance", verdict=<your decision: accept / request-changes / reject>, comments=<your reasoning>, and auto_review set to the sub-agent's JSON object verbatim. The sub-agent's verdict is a RECOMMENDATION \u2014 you make the final call`;
|
|
12576
12766
|
}
|
|
@@ -12991,7 +13181,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
12991
13181
|
const focus = typeof args.focus === "string" ? args.focus : void 0;
|
|
12992
13182
|
const force = args.force === true;
|
|
12993
13183
|
const handoffsOnly = args.handoffs_only === true;
|
|
12994
|
-
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" });
|
|
12995
13185
|
try {
|
|
12996
13186
|
if (toolMode === "apply") {
|
|
12997
13187
|
const resolved = await resolveLlmResponse(
|
|
@@ -13063,6 +13253,10 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13063
13253
|
}
|
|
13064
13254
|
const skipHandoffs = args.skip_handoffs === true;
|
|
13065
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
|
+
}
|
|
13066
13260
|
planPrepareCache.set(callerKey, {
|
|
13067
13261
|
contextHashes: result.contextHashes,
|
|
13068
13262
|
userMessage: result.userMessage,
|
|
@@ -14437,7 +14631,7 @@ function extractDecisionEvidence(ad, eventType, warnings) {
|
|
|
14437
14631
|
async function writeBack2(adapter2, cycleNumber, data, fullAnalysis, warnings) {
|
|
14438
14632
|
const cleanTitle = data.sessionLogTitle.replace(/^(?:Cycle|Session)\s+\d+\s*—\s*/i, "").trim();
|
|
14439
14633
|
const cleanContent = data.sessionLogContent.replace(/^#{1,3}\s+(?:Cycle|Session)\s+\d+\s*—[^\n]*\n*/i, "").trim();
|
|
14440
|
-
const lastReviewCycle = await adapter2.getLastStrategyReviewCycle();
|
|
14634
|
+
const lastReviewCycle = await adapter2.getLastStrategyReviewCycle({ excludeZoomOut: true });
|
|
14441
14635
|
const rangeStart = lastReviewCycle > 0 ? lastReviewCycle + 1 : cycleNumber;
|
|
14442
14636
|
const cycleRange = rangeStart < cycleNumber ? `${rangeStart}-${cycleNumber}` : `${cycleNumber}`;
|
|
14443
14637
|
const logBlock = `### Cycles ${cycleRange} \u2014 ${cleanTitle}
|
|
@@ -15240,7 +15434,7 @@ Confidence: ${input.confidence}. Captured mid-conversation via strategy_change c
|
|
|
15240
15434
|
var reviewPrepareCache = new PerCallerCache();
|
|
15241
15435
|
var strategyReviewTool = {
|
|
15242
15436
|
name: "strategy_review",
|
|
15243
|
-
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles.
|
|
15437
|
+
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run it in your current conversation \u2014 only start a fresh one if you are genuinely under context pressure (your host just compacted, you are near the context limit, or the session is heavy with build context), not just because a review is next. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
|
|
15244
15438
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15245
15439
|
inputSchema: {
|
|
15246
15440
|
type: "object",
|
|
@@ -15826,6 +16020,15 @@ var boardDeprioritiseTool = {
|
|
|
15826
16020
|
type: "string",
|
|
15827
16021
|
description: 'Why this task is being removed. Required for "cancel", recommended for "defer".'
|
|
15828
16022
|
},
|
|
16023
|
+
blocker_type: {
|
|
16024
|
+
type: "string",
|
|
16025
|
+
enum: ["depends-on", "decision-gate", "owner-action"],
|
|
16026
|
+
description: 'Optional (action="block" only). Typed blocker so the plan/orient scan can auto-detect when the block clears. "depends-on" = another task must be Done (blocker_ref = task display-id). "decision-gate" = a decision must resolve (blocker_ref = AD/decision id). "owner-action" = an owner action must complete (blocker_ref = owner_action id). Omit for a reason-only block.'
|
|
16027
|
+
},
|
|
16028
|
+
blocker_ref: {
|
|
16029
|
+
type: "string",
|
|
16030
|
+
description: "Required when blocker_type is set. The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an owner_action id (owner-action)."
|
|
16031
|
+
},
|
|
15829
16032
|
defer: {
|
|
15830
16033
|
type: "boolean",
|
|
15831
16034
|
description: 'DEPRECATED \u2014 use action instead. If true, equivalent to action="defer".'
|
|
@@ -16060,19 +16263,50 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
16060
16263
|
if (!reason) {
|
|
16061
16264
|
return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
|
|
16062
16265
|
}
|
|
16266
|
+
const blockerType = args.blocker_type;
|
|
16267
|
+
const blockerRef = args.blocker_ref;
|
|
16268
|
+
const validTypes = /* @__PURE__ */ new Set(["depends-on", "decision-gate", "owner-action"]);
|
|
16269
|
+
if (blockerType !== void 0 && !validTypes.has(blockerType)) {
|
|
16270
|
+
return errorResponse(`blocker_type must be one of: depends-on, decision-gate, owner-action (got "${blockerType}").`);
|
|
16271
|
+
}
|
|
16272
|
+
if (blockerType !== void 0 && !blockerRef) {
|
|
16273
|
+
return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on.");
|
|
16274
|
+
}
|
|
16063
16275
|
try {
|
|
16064
16276
|
const task = await adapter2.getTask(taskId);
|
|
16065
16277
|
if (!task) return errorResponse(`Task ${taskId} not found.`);
|
|
16066
16278
|
const existingNotes = task.notes ? `${task.notes}
|
|
16067
16279
|
|
|
16068
16280
|
` : "";
|
|
16069
|
-
|
|
16281
|
+
const updates = {
|
|
16070
16282
|
status: "Blocked",
|
|
16071
16283
|
notes: `${existingNotes}BLOCKED: ${reason}`
|
|
16072
|
-
}
|
|
16284
|
+
};
|
|
16285
|
+
let typedSuffix = "";
|
|
16286
|
+
if (blockerType !== void 0 && blockerRef) {
|
|
16287
|
+
const health = await adapter2.getCycleHealth().catch(() => null);
|
|
16288
|
+
const blockedCycle = health?.totalCycles ?? 0;
|
|
16289
|
+
updates.blocker = {
|
|
16290
|
+
type: blockerType,
|
|
16291
|
+
ref: blockerRef,
|
|
16292
|
+
reason,
|
|
16293
|
+
blockedCycle
|
|
16294
|
+
};
|
|
16295
|
+
typedSuffix = `
|
|
16296
|
+
|
|
16297
|
+
Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
|
|
16298
|
+
if (blockerType === "owner-action" && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
|
|
16299
|
+
try {
|
|
16300
|
+
const ownerUserId = await adapter2.getProjectOwnerUserId();
|
|
16301
|
+
if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
|
|
16302
|
+
} catch {
|
|
16303
|
+
}
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16306
|
+
await adapter2.updateTask(taskId, updates);
|
|
16073
16307
|
return textResponse(`Blocked **${taskId}** (${task.title}).
|
|
16074
16308
|
|
|
16075
|
-
Reason: ${reason}
|
|
16309
|
+
Reason: ${reason}${typedSuffix}
|
|
16076
16310
|
|
|
16077
16311
|
Task remains visible on the board but will be skipped by the planner and build_list.`);
|
|
16078
16312
|
} catch (err) {
|
|
@@ -16684,7 +16918,7 @@ This is a one-time check at the start of the cycle, not per-task. It catches sco
|
|
|
16684
16918
|
Every 5 cycles, PAPI offers a strategy review \u2014 a deep analysis of velocity, estimation accuracy, active decisions, and project direction.
|
|
16685
16919
|
|
|
16686
16920
|
- **Don't skip them.** They're where compounding value comes from.
|
|
16687
|
-
-
|
|
16921
|
+
- Run the review in your current conversation \u2014 a fresh one is only warranted if you're genuinely under context pressure (your host just compacted, or you're near the limit), never just because a review is next.
|
|
16688
16922
|
- Reviews produce recommendations that feed into the next plan.
|
|
16689
16923
|
- If the review recommends AD changes, use \`strategy_change\` to apply them.
|
|
16690
16924
|
|
|
@@ -16809,7 +17043,7 @@ var DOCS_INDEX_TEMPLATE = `# {{project_name}} \u2014 Document Index
|
|
|
16809
17043
|
Who or what reads this doc: \`planner\`, \`builder\`, \`strategy-review\`, \`user\`, \`onboarding\`.
|
|
16810
17044
|
`;
|
|
16811
17045
|
var CURSOR_RULES_TEMPLATE = `---
|
|
16812
|
-
description: PAPI \u2014
|
|
17046
|
+
description: PAPI cycle workflow for {{project_name}} \u2014 plan, build, review
|
|
16813
17047
|
globs:
|
|
16814
17048
|
alwaysApply: true
|
|
16815
17049
|
---
|
|
@@ -18185,6 +18419,86 @@ Examples (use whichever provider you have): ${spec.examples}.
|
|
|
18185
18419
|
_Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
|
|
18186
18420
|
}
|
|
18187
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
|
+
|
|
18188
18502
|
// src/services/build.ts
|
|
18189
18503
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
18190
18504
|
import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -18502,40 +18816,62 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
18502
18816
|
if (adapter2) {
|
|
18503
18817
|
const currentCycle = resolvedCycleNum;
|
|
18504
18818
|
if (currentCycle > 0) {
|
|
18819
|
+
let existing;
|
|
18505
18820
|
try {
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
existing = (await adapter2.readCycles()).find((c) => c.number === currentCycle);
|
|
18509
|
-
} catch {
|
|
18510
|
-
}
|
|
18511
|
-
await adapter2.createCycle({
|
|
18512
|
-
id: `cycle-${currentCycle}`,
|
|
18513
|
-
number: currentCycle,
|
|
18514
|
-
status: "complete",
|
|
18515
|
-
startDate: existing?.startDate ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
18516
|
-
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18517
|
-
goals: existing?.goals ?? [],
|
|
18518
|
-
boardHealth: existing?.boardHealth ?? "",
|
|
18519
|
-
taskIds: existing?.taskIds ?? []
|
|
18520
|
-
});
|
|
18521
|
-
} catch (err) {
|
|
18522
|
-
const msg = `createCycle (mark complete) failed for cycle ${currentCycle}: ${err instanceof Error ? err.message : String(err)}`;
|
|
18523
|
-
console.error(`[release] ${msg}`);
|
|
18524
|
-
throw new Error(
|
|
18525
|
-
`Release blocked \u2014 could not mark cycle ${currentCycle} complete in the DB. ${err instanceof Error ? err.message : String(err)}. Resolve the DB error and re-run \`release\`.`
|
|
18526
|
-
);
|
|
18821
|
+
existing = (await adapter2.readCycles()).find((c) => c.number === currentCycle);
|
|
18822
|
+
} catch {
|
|
18527
18823
|
}
|
|
18528
|
-
|
|
18824
|
+
const completedCycle = {
|
|
18825
|
+
id: `cycle-${currentCycle}`,
|
|
18826
|
+
number: currentCycle,
|
|
18827
|
+
status: "complete",
|
|
18828
|
+
startDate: existing?.startDate ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
18829
|
+
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18830
|
+
goals: existing?.goals ?? [],
|
|
18831
|
+
boardHealth: existing?.boardHealth ?? "",
|
|
18832
|
+
taskIds: existing?.taskIds ?? []
|
|
18833
|
+
};
|
|
18834
|
+
let snapshot;
|
|
18835
|
+
if (adapter2.getBuildReportsSince) {
|
|
18529
18836
|
try {
|
|
18530
18837
|
const cycleReports = (await adapter2.getBuildReportsSince(currentCycle)).filter((r) => r.cycle === currentCycle);
|
|
18531
|
-
|
|
18532
|
-
if (snapshot) await adapter2.appendCycleMetrics(snapshot);
|
|
18838
|
+
[snapshot] = computeSnapshotsFromBuildReports(cycleReports);
|
|
18533
18839
|
} catch (err) {
|
|
18534
18840
|
console.error(
|
|
18535
|
-
`[release] cycle-metrics snapshot
|
|
18841
|
+
`[release] cycle-metrics snapshot compute failed for cycle ${currentCycle} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
|
|
18536
18842
|
);
|
|
18537
18843
|
}
|
|
18538
18844
|
}
|
|
18845
|
+
if (typeof adapter2.commitRelease === "function") {
|
|
18846
|
+
try {
|
|
18847
|
+
await adapter2.commitRelease({ cycle: completedCycle, snapshot: snapshot ?? null });
|
|
18848
|
+
} catch (err) {
|
|
18849
|
+
const msg = `commitRelease (mark complete) failed for cycle ${currentCycle}: ${err instanceof Error ? err.message : String(err)}`;
|
|
18850
|
+
console.error(`[release] ${msg}`);
|
|
18851
|
+
throw new Error(
|
|
18852
|
+
`Release blocked \u2014 could not mark cycle ${currentCycle} complete in the DB. ${err instanceof Error ? err.message : String(err)}. Resolve the DB error and re-run \`release\`.`
|
|
18853
|
+
);
|
|
18854
|
+
}
|
|
18855
|
+
} else {
|
|
18856
|
+
try {
|
|
18857
|
+
await adapter2.createCycle(completedCycle);
|
|
18858
|
+
} catch (err) {
|
|
18859
|
+
const msg = `createCycle (mark complete) failed for cycle ${currentCycle}: ${err instanceof Error ? err.message : String(err)}`;
|
|
18860
|
+
console.error(`[release] ${msg}`);
|
|
18861
|
+
throw new Error(
|
|
18862
|
+
`Release blocked \u2014 could not mark cycle ${currentCycle} complete in the DB. ${err instanceof Error ? err.message : String(err)}. Resolve the DB error and re-run \`release\`.`
|
|
18863
|
+
);
|
|
18864
|
+
}
|
|
18865
|
+
if (adapter2.appendCycleMetrics && snapshot) {
|
|
18866
|
+
try {
|
|
18867
|
+
await adapter2.appendCycleMetrics(snapshot);
|
|
18868
|
+
} catch (err) {
|
|
18869
|
+
console.error(
|
|
18870
|
+
`[release] cycle-metrics snapshot write failed for cycle ${currentCycle} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
|
|
18871
|
+
);
|
|
18872
|
+
}
|
|
18873
|
+
}
|
|
18874
|
+
}
|
|
18539
18875
|
}
|
|
18540
18876
|
}
|
|
18541
18877
|
return { resolvedCycleNum, warnings, force, skipVersion };
|
|
@@ -18839,6 +19175,65 @@ async function updatePluginMarketplaces(version) {
|
|
|
18839
19175
|
return [cursor, vscode].filter((r) => r !== null);
|
|
18840
19176
|
}
|
|
18841
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
|
+
|
|
18842
19237
|
// src/tools/release.ts
|
|
18843
19238
|
function parseGithubOwner(input) {
|
|
18844
19239
|
if (!input) return null;
|
|
@@ -18919,6 +19314,11 @@ var releaseTool = {
|
|
|
18919
19314
|
type: "boolean",
|
|
18920
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."
|
|
18921
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
|
+
},
|
|
18922
19322
|
observations: {
|
|
18923
19323
|
type: "array",
|
|
18924
19324
|
description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
|
|
@@ -18949,6 +19349,7 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
18949
19349
|
let version = args.version;
|
|
18950
19350
|
const force = args.force;
|
|
18951
19351
|
const skipVersion = args.skipVersion;
|
|
19352
|
+
const gateResult = args.gate_result;
|
|
18952
19353
|
const rawObservations = args.observations;
|
|
18953
19354
|
if (!branch || !version) {
|
|
18954
19355
|
return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
|
|
@@ -18962,8 +19363,9 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
18962
19363
|
version = `${version}-${suffix}`;
|
|
18963
19364
|
}
|
|
18964
19365
|
}
|
|
18965
|
-
const tracker = new ProgressTracker("validate-args");
|
|
19366
|
+
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
|
|
18966
19367
|
try {
|
|
19368
|
+
await tracker.recordStep("release_starting");
|
|
18967
19369
|
tracker.mark("owner-identity-guard");
|
|
18968
19370
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
18969
19371
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
@@ -19090,12 +19492,41 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19090
19492
|
console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
|
|
19091
19493
|
}
|
|
19092
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
|
+
}
|
|
19093
19517
|
tracker.mark("create-release");
|
|
19094
19518
|
const result = await createRelease(config2, branch, version, adapter2, void 0, {
|
|
19095
19519
|
force: force ?? false,
|
|
19096
19520
|
skipVersion: skipVersion ?? false,
|
|
19097
19521
|
callerUserId: gate.callerUserId
|
|
19098
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
|
+
}
|
|
19099
19530
|
const lines = [
|
|
19100
19531
|
`## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
|
|
19101
19532
|
"",
|
|
@@ -19124,10 +19555,8 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19124
19555
|
const reports = await adapter2.getBuildReportsSince(closedCycle);
|
|
19125
19556
|
const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
|
|
19126
19557
|
const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
lines.push("", "*These issues were logged during builds \u2014 triage them in the next plan.*");
|
|
19130
|
-
}
|
|
19558
|
+
const issuesDirective = buildDiscoveredIssuesDirective(caps, issues);
|
|
19559
|
+
if (issuesDirective) lines.push(issuesDirective);
|
|
19131
19560
|
}
|
|
19132
19561
|
} catch {
|
|
19133
19562
|
}
|
|
@@ -19147,28 +19576,54 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19147
19576
|
lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
|
|
19148
19577
|
}
|
|
19149
19578
|
}
|
|
19150
|
-
const cycleUpdateDirective =
|
|
19579
|
+
const cycleUpdateDirective = buildChangelogDirective(
|
|
19580
|
+
caps,
|
|
19581
|
+
buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
|
|
19582
|
+
);
|
|
19151
19583
|
if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
|
|
19152
|
-
|
|
19153
|
-
|
|
19154
|
-
|
|
19155
|
-
|
|
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
|
+
});
|
|
19156
19598
|
}
|
|
19157
|
-
|
|
19158
|
-
|
|
19159
|
-
|
|
19160
|
-
|
|
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 {
|
|
19161
19605
|
}
|
|
19162
|
-
} catch {
|
|
19163
19606
|
}
|
|
19164
|
-
|
|
19165
|
-
|
|
19166
|
-
|
|
19167
|
-
|
|
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 {
|
|
19168
19621
|
}
|
|
19169
|
-
} catch {
|
|
19170
19622
|
}
|
|
19623
|
+
const verifyDirective = buildVerifyHealthCheckDirective(caps);
|
|
19624
|
+
if (verifyDirective) lines.push(verifyDirective);
|
|
19171
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 } });
|
|
19172
19627
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
19173
19628
|
return textResponse(lines.join("\n") + filesToWriteSection);
|
|
19174
19629
|
} catch (err) {
|
|
@@ -19189,18 +19644,26 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19189
19644
|
}
|
|
19190
19645
|
|
|
19191
19646
|
// src/lib/owns-local-workspace.ts
|
|
19192
|
-
async function
|
|
19193
|
-
|
|
19647
|
+
async function resolveWorkspaceOwners(adapter2, cwd) {
|
|
19648
|
+
const empty = { storedOwner: null, remoteOwner: null, remoteSlug: null };
|
|
19649
|
+
if (!adapter2.getProjectInfo) return empty;
|
|
19194
19650
|
try {
|
|
19195
19651
|
const info = await adapter2.getProjectInfo();
|
|
19196
|
-
const
|
|
19197
|
-
|
|
19198
|
-
|
|
19199
|
-
|
|
19652
|
+
const remoteSlug = getOriginRepoSlug(cwd);
|
|
19653
|
+
return {
|
|
19654
|
+
storedOwner: parseGithubOwner(info?.repo_url ?? null),
|
|
19655
|
+
remoteOwner: parseGithubOwner(remoteSlug),
|
|
19656
|
+
remoteSlug
|
|
19657
|
+
};
|
|
19200
19658
|
} catch {
|
|
19201
|
-
return
|
|
19659
|
+
return empty;
|
|
19202
19660
|
}
|
|
19203
19661
|
}
|
|
19662
|
+
async function ownsLocalWorkspace(adapter2, cwd) {
|
|
19663
|
+
const { storedOwner, remoteOwner } = await resolveWorkspaceOwners(adapter2, cwd);
|
|
19664
|
+
if (!storedOwner || !remoteOwner) return false;
|
|
19665
|
+
return storedOwner === remoteOwner;
|
|
19666
|
+
}
|
|
19204
19667
|
|
|
19205
19668
|
// src/services/build.ts
|
|
19206
19669
|
init_git();
|
|
@@ -19917,13 +20380,19 @@ function assertDeployVerification(config2, input) {
|
|
|
19917
20380
|
if (triggerHits.length === 0) return;
|
|
19918
20381
|
const verification = input.productionVerification;
|
|
19919
20382
|
if (!verification) {
|
|
19920
|
-
|
|
19921
|
-
`Deploy-verification required: this branch's diff touches ${triggerHits.length} trigger-surface file(s):
|
|
19922
|
-
` + triggerHits.map((p) => ` - ${p}`).join("\n") + `
|
|
20383
|
+
const targetLine = config2.verifyCommand ? `
|
|
19923
20384
|
|
|
19924
|
-
|
|
20385
|
+
Verify against your configured target (papi.verify): \`${config2.verifyCommand}\`
|
|
20386
|
+
Run it yourself, then pass production_verification on build_execute complete:
|
|
19925
20387
|
{ urls, curl_command, http_status, response_excerpt, verified_at }
|
|
20388
|
+
` : `
|
|
19926
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 + `
|
|
19927
20396
|
Trigger surface is enforced because changes here have historically shipped to prod undetected (C166, C264-C265, C271).`
|
|
19928
20397
|
);
|
|
19929
20398
|
}
|
|
@@ -19991,7 +20460,24 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
19991
20460
|
} catch {
|
|
19992
20461
|
}
|
|
19993
20462
|
}
|
|
19994
|
-
|
|
20463
|
+
const surpriseNote = input.surprises === "None" ? "" : ` Surprises: ${input.surprises}.`;
|
|
20464
|
+
const issueNote = input.discoveredIssues === "None" ? "" : ` Issues: ${input.discoveredIssues}.`;
|
|
20465
|
+
const iterNote = iterationCount > 1 ? ` Iterations: ${iterationCount} (${iterationCount - 1} pushback${iterationCount > 2 ? "s" : ""}).` : "";
|
|
20466
|
+
const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${iterNote}${surpriseNote}${issueNote}`;
|
|
20467
|
+
const statusChange = input.completed === "yes" ? { from: task.status, to: options.light ? "Done" : "In Review" } : void 0;
|
|
20468
|
+
let atomicCommitDone = false;
|
|
20469
|
+
if (typeof adapter2.commitBuildComplete === "function") {
|
|
20470
|
+
await adapter2.commitBuildComplete({
|
|
20471
|
+
report,
|
|
20472
|
+
taskId,
|
|
20473
|
+
buildReportSummary,
|
|
20474
|
+
statusChange,
|
|
20475
|
+
cycle: cycleNumber
|
|
20476
|
+
});
|
|
20477
|
+
atomicCommitDone = true;
|
|
20478
|
+
} else {
|
|
20479
|
+
await adapter2.appendBuildReport(report);
|
|
20480
|
+
}
|
|
19995
20481
|
let reportWriteVerified;
|
|
19996
20482
|
if (typeof adapter2.getBuildReportCountForTask === "function") {
|
|
19997
20483
|
try {
|
|
@@ -20132,16 +20618,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
20132
20618
|
if (task.cycle == null && cycleNumber > 0) {
|
|
20133
20619
|
await adapter2.updateTask(taskId, { cycle: cycleNumber });
|
|
20134
20620
|
}
|
|
20135
|
-
|
|
20136
|
-
|
|
20137
|
-
|
|
20138
|
-
|
|
20139
|
-
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
|
|
20143
|
-
} else {
|
|
20144
|
-
await adapter2.updateTaskStatus(taskId, "In Review");
|
|
20621
|
+
if (!atomicCommitDone) {
|
|
20622
|
+
await adapter2.updateTask(taskId, { buildReport: buildReportSummary });
|
|
20623
|
+
if (input.completed === "yes") {
|
|
20624
|
+
if (options.light) {
|
|
20625
|
+
await adapter2.updateTaskStatus(taskId, "Done");
|
|
20626
|
+
} else {
|
|
20627
|
+
await adapter2.updateTaskStatus(taskId, "In Review");
|
|
20628
|
+
}
|
|
20145
20629
|
}
|
|
20146
20630
|
}
|
|
20147
20631
|
let dogfoodResolvedCount = 0;
|
|
@@ -20410,7 +20894,7 @@ import { homedir as homedir3 } from "os";
|
|
|
20410
20894
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
20411
20895
|
var docRegisterTool = {
|
|
20412
20896
|
name: "doc_register",
|
|
20413
|
-
description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
|
|
20897
|
+
description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content. Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
|
|
20414
20898
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20415
20899
|
inputSchema: {
|
|
20416
20900
|
type: "object",
|
|
@@ -21122,13 +21606,17 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
21122
21606
|
if (hasReportFields(args)) {
|
|
21123
21607
|
return handleExecuteComplete(adapter2, config2, taskId, args, light, clientName);
|
|
21124
21608
|
}
|
|
21125
|
-
const tracker = new ProgressTracker("start_build");
|
|
21609
|
+
const tracker = new ProgressTracker("start_build").bindStream(adapter2, { stage: "build", taskId });
|
|
21126
21610
|
try {
|
|
21611
|
+
await tracker.recordStep("started");
|
|
21127
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");
|
|
21128
21615
|
tracker.mark("start_decorate_handoff");
|
|
21129
21616
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
21130
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") : "";
|
|
21131
21618
|
const projectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
21619
|
+
const caps = projectInfo?.capabilities ?? {};
|
|
21132
21620
|
const projectBanner = projectInfo ? getProjectConnectionBanner(projectInfo.name, projectInfo.slug) : null;
|
|
21133
21621
|
const projectLine = projectBanner ? `> ${projectBanner}
|
|
21134
21622
|
|
|
@@ -21176,8 +21664,12 @@ ${entries}`;
|
|
|
21176
21664
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
21177
21665
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
21178
21666
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
21179
|
-
const modelNote =
|
|
21180
|
-
|
|
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);
|
|
21181
21673
|
} catch (err) {
|
|
21182
21674
|
if (isNoHandoffError(err)) {
|
|
21183
21675
|
const lines = [
|
|
@@ -21264,7 +21756,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21264
21756
|
if (!parsedEstimatedEffort) {
|
|
21265
21757
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
21266
21758
|
}
|
|
21267
|
-
const tracker = new ProgressTracker("complete_validate");
|
|
21759
|
+
const tracker = new ProgressTracker("complete_validate").bindStream(adapter2, { stage: "build", taskId });
|
|
21268
21760
|
try {
|
|
21269
21761
|
tracker.mark("complete_build");
|
|
21270
21762
|
const result = await completeBuild(adapter2, config2, taskId, {
|
|
@@ -21293,6 +21785,11 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21293
21785
|
preview
|
|
21294
21786
|
}, { light }, clientName);
|
|
21295
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
|
+
});
|
|
21296
21793
|
if (resolvesLearnings && resolvesLearnings.length > 0 && adapter2.updateCycleLearningActionRef) {
|
|
21297
21794
|
for (const learningId of resolvesLearnings) {
|
|
21298
21795
|
try {
|
|
@@ -21301,8 +21798,35 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
21301
21798
|
}
|
|
21302
21799
|
}
|
|
21303
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
|
+
}
|
|
21304
21818
|
const docsNote = await detectUnregisteredDocsNote(adapter2, config2);
|
|
21305
|
-
|
|
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);
|
|
21306
21830
|
} catch (err) {
|
|
21307
21831
|
const message = err instanceof Error ? err.message : String(err);
|
|
21308
21832
|
if (isBuildPushError(err)) {
|
|
@@ -21838,6 +22362,12 @@ init_git();
|
|
|
21838
22362
|
|
|
21839
22363
|
// src/services/ad-hoc.ts
|
|
21840
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
|
+
}
|
|
21841
22371
|
async function recordAdHoc(adapter2, input) {
|
|
21842
22372
|
const [health, phases] = await Promise.all([
|
|
21843
22373
|
adapter2.getCycleHealth(),
|
|
@@ -21845,6 +22375,14 @@ async function recordAdHoc(adapter2, input) {
|
|
|
21845
22375
|
]);
|
|
21846
22376
|
const cycle = health.totalCycles;
|
|
21847
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";
|
|
21848
22386
|
let task;
|
|
21849
22387
|
if (input.taskId) {
|
|
21850
22388
|
const existing = await adapter2.getTask(input.taskId);
|
|
@@ -21866,18 +22404,19 @@ async function recordAdHoc(adapter2, input) {
|
|
|
21866
22404
|
uuid: randomUUID14(),
|
|
21867
22405
|
displayId: "",
|
|
21868
22406
|
title: input.title,
|
|
21869
|
-
status: "Done",
|
|
22407
|
+
status: landInReview ? "In Review" : "Done",
|
|
21870
22408
|
priority: input.priority || "P2 Medium",
|
|
21871
22409
|
complexity: input.effort === "XS" || input.effort === "S" ? "Small" : "Medium",
|
|
21872
22410
|
module: input.module || "Core",
|
|
21873
22411
|
epic: input.epic || "Platform",
|
|
21874
22412
|
phase,
|
|
21875
22413
|
owner: input.owner || "Cathal",
|
|
21876
|
-
reviewed:
|
|
22414
|
+
reviewed: !landInReview,
|
|
21877
22415
|
createdCycle: cycle,
|
|
22416
|
+
...targetCycle !== null ? { cycle: targetCycle } : {},
|
|
21878
22417
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
21879
22418
|
taskType: input.taskType || "task",
|
|
21880
|
-
source: "
|
|
22419
|
+
source: "ad_hoc"
|
|
21881
22420
|
});
|
|
21882
22421
|
}
|
|
21883
22422
|
const report = {
|
|
@@ -21952,6 +22491,22 @@ var adHocTool = {
|
|
|
21952
22491
|
type: "string",
|
|
21953
22492
|
enum: ["task", "bug", "research", "discovery", "spike", "idea"],
|
|
21954
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`."
|
|
21955
22510
|
}
|
|
21956
22511
|
},
|
|
21957
22512
|
required: []
|
|
@@ -21982,6 +22537,12 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
21982
22537
|
rawNotes = rawNotes.slice(0, MAX_NOTES_LENGTH);
|
|
21983
22538
|
notesTruncated = true;
|
|
21984
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;
|
|
21985
22546
|
const result = await recordAdHoc(adapter2, {
|
|
21986
22547
|
title: title || "",
|
|
21987
22548
|
taskId,
|
|
@@ -21992,9 +22553,12 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
21992
22553
|
priority: priorityRaw,
|
|
21993
22554
|
taskType: typeRaw,
|
|
21994
22555
|
// PROJECT-SPECIFIC: owner resolved from config (PAPI_OWNER env var, default 'Cathal')
|
|
21995
|
-
owner: config2.projectOwner
|
|
22556
|
+
owner: config2.projectOwner,
|
|
22557
|
+
cycle: cycleArg,
|
|
22558
|
+
stage: stageArg,
|
|
22559
|
+
hold: holdArg
|
|
21996
22560
|
});
|
|
21997
|
-
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
22561
|
+
if (!holdArg && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
21998
22562
|
try {
|
|
21999
22563
|
stageDirAndCommit(
|
|
22000
22564
|
config2.projectRoot,
|
|
@@ -22007,8 +22571,29 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
22007
22571
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
22008
22572
|
const taskModule = result.task.module || "Core";
|
|
22009
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
|
+
}
|
|
22010
22595
|
return textResponse(
|
|
22011
|
-
`**${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.
|
|
22012
22597
|
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
22013
22598
|
);
|
|
22014
22599
|
}
|
|
@@ -22706,10 +23291,20 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
22706
23291
|
if (input.autoReview) {
|
|
22707
23292
|
review.autoReview = input.autoReview;
|
|
22708
23293
|
}
|
|
22709
|
-
await adapter2.writeReview(review);
|
|
22710
23294
|
const newStatus = resolveStatus(input.stage, input.verdict);
|
|
22711
|
-
|
|
22712
|
-
|
|
23295
|
+
const statusChange = newStatus ? { from: task.status, to: newStatus } : void 0;
|
|
23296
|
+
if (typeof adapter2.commitReviewSubmit === "function") {
|
|
23297
|
+
await adapter2.commitReviewSubmit({
|
|
23298
|
+
review,
|
|
23299
|
+
taskId: input.taskId,
|
|
23300
|
+
statusChange,
|
|
23301
|
+
cycle
|
|
23302
|
+
});
|
|
23303
|
+
} else {
|
|
23304
|
+
await adapter2.writeReview(review);
|
|
23305
|
+
if (newStatus) {
|
|
23306
|
+
await adapter2.updateTaskStatus(input.taskId, newStatus);
|
|
23307
|
+
}
|
|
22713
23308
|
}
|
|
22714
23309
|
const unblockedTasks = [];
|
|
22715
23310
|
if (newStatus === "Done") {
|
|
@@ -23093,13 +23688,22 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23093
23688
|
if (!stage) {
|
|
23094
23689
|
return errorResponse('stage is required. Use "handoff-review" or "build-acceptance".');
|
|
23095
23690
|
}
|
|
23691
|
+
let caps = {};
|
|
23692
|
+
try {
|
|
23693
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
23694
|
+
caps = info?.capabilities ?? {};
|
|
23695
|
+
} catch {
|
|
23696
|
+
caps = {};
|
|
23697
|
+
}
|
|
23096
23698
|
const explicitDispatch = args.dispatch === "subagent";
|
|
23097
|
-
const
|
|
23098
|
-
|
|
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) {
|
|
23099
23703
|
const dispatch = await buildReviewDispatch(adapter2, config2, taskId);
|
|
23100
23704
|
if (!dispatch.ok) {
|
|
23101
23705
|
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
23102
|
-
} else if (explicitDispatch || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23706
|
+
} else if (explicitDispatch || capabilityAutoReviewEligible || dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
23103
23707
|
return textResponse(dispatch.prompt);
|
|
23104
23708
|
}
|
|
23105
23709
|
}
|
|
@@ -23140,7 +23744,8 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23140
23744
|
}
|
|
23141
23745
|
}
|
|
23142
23746
|
}
|
|
23143
|
-
const tracker = new ProgressTracker("validate-args");
|
|
23747
|
+
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "review", taskId });
|
|
23748
|
+
await tracker.recordStep("reviewer_confirmed");
|
|
23144
23749
|
const handoffRegenResponse = args.handoff_regen_response;
|
|
23145
23750
|
if (handoffRegenResponse?.trim()) {
|
|
23146
23751
|
tracker.mark("apply-handoff-regen");
|
|
@@ -23157,6 +23762,14 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
23157
23762
|
adapter2,
|
|
23158
23763
|
{ taskId, stage, verdict, comments, reviewer, autoReview }
|
|
23159
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
|
+
}
|
|
23160
23773
|
const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
|
|
23161
23774
|
const unblockNote = result.unblockedTasks.length > 0 ? `
|
|
23162
23775
|
|
|
@@ -23196,6 +23809,7 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
23196
23809
|
|
|
23197
23810
|
> ${mergeResult.message}`;
|
|
23198
23811
|
}
|
|
23812
|
+
await tracker.recordStep("deferred", { metadata: { reason: "merge-skipped" } });
|
|
23199
23813
|
} else if (!mergeResult.merged) {
|
|
23200
23814
|
mergeFailed = true;
|
|
23201
23815
|
let revertNote = "";
|
|
@@ -23210,11 +23824,13 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
23210
23824
|
\u26A0\uFE0F **PR merge failed \u2014 task NOT marked Done.**${revertNote}
|
|
23211
23825
|
|
|
23212
23826
|
${mergeResult.message}`;
|
|
23827
|
+
await tracker.recordStep("deferred", { status: "failed", metadata: { reason: "merge-failed" } });
|
|
23213
23828
|
} else {
|
|
23214
23829
|
try {
|
|
23215
23830
|
await adapter2.updateTask(taskId, { mergedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23216
23831
|
} catch {
|
|
23217
23832
|
}
|
|
23833
|
+
await tracker.recordStep("merged");
|
|
23218
23834
|
const detailLines = mergeResult.details.map((l) => `> ${l}`).join("\n");
|
|
23219
23835
|
mergeNote = `
|
|
23220
23836
|
|
|
@@ -23290,6 +23906,7 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
23290
23906
|
}
|
|
23291
23907
|
const version = `v0.${result.currentCycle}.0`;
|
|
23292
23908
|
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
23909
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
23293
23910
|
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
23294
23911
|
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
23295
23912
|
autoReleaseNote = `
|
|
@@ -23345,9 +23962,7 @@ ${findingLines}${more}` : "");
|
|
|
23345
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.`;
|
|
23346
23963
|
}
|
|
23347
23964
|
} else if (stage === "build-acceptance" && verdict === "accept") {
|
|
23348
|
-
autoReviewNote =
|
|
23349
|
-
|
|
23350
|
-
**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) ?? "";
|
|
23351
23966
|
}
|
|
23352
23967
|
let nextStepNote = "";
|
|
23353
23968
|
if (!autoReleaseNote && stage === "build-acceptance") {
|
|
@@ -23359,6 +23974,7 @@ ${findingLines}${more}` : "");
|
|
|
23359
23974
|
Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
23360
23975
|
}
|
|
23361
23976
|
}
|
|
23977
|
+
const securityNote = stage === "build-acceptance" && verdict === "accept" ? buildSecurityScanDirective(caps) ?? "" : "";
|
|
23362
23978
|
tracker.mark("format-response");
|
|
23363
23979
|
return textResponse(
|
|
23364
23980
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
@@ -23366,7 +23982,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
23366
23982
|
- **Verdict:** ${result.verdict}
|
|
23367
23983
|
- **Comments:** ${result.comments}
|
|
23368
23984
|
|
|
23369
|
-
${statusNote}${autoReviewNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
23985
|
+
${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
23370
23986
|
);
|
|
23371
23987
|
} catch (err) {
|
|
23372
23988
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -23935,6 +24551,14 @@ ${writeNote}
|
|
|
23935
24551
|
}
|
|
23936
24552
|
|
|
23937
24553
|
// src/services/health.ts
|
|
24554
|
+
function computeZoomOutWarning(cycleNumber, lastZoomOutCycle) {
|
|
24555
|
+
if (cycleNumber <= 0) return "";
|
|
24556
|
+
const baseline = lastZoomOutCycle > 0 ? lastZoomOutCycle : 1;
|
|
24557
|
+
const gap = cycleNumber - baseline;
|
|
24558
|
+
if (gap < ZOOM_OUT_OFFER_GAP) return "";
|
|
24559
|
+
const ran = lastZoomOutCycle > 0 ? `last run Cycle ${lastZoomOutCycle}` : "never run";
|
|
24560
|
+
return `\u25CB Zoom-out available \u2014 ${gap} cycles since last zoom-out (${ran}). QBR/all-hands tier above strategy review; offer-only, no block. Run \`zoom_out\` to read the full project arc.`;
|
|
24561
|
+
}
|
|
23938
24562
|
function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
|
|
23939
24563
|
if (cycleNumber < 3) return null;
|
|
23940
24564
|
const scores = [];
|
|
@@ -24004,6 +24628,12 @@ async function getHealthSummary(adapter2) {
|
|
|
24004
24628
|
const reviewGateBlocking = cyclesSinceReview >= STRATEGY_REVIEW_BLOCK_GAP;
|
|
24005
24629
|
const reviewAvailable = cyclesSinceReview >= STRATEGY_REVIEW_OFFER_GAP && !reviewGateBlocking;
|
|
24006
24630
|
const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : reviewAvailable ? `\u25CB Strategy review available \u2014 ${cyclesSinceReview} cycles since last review. Offered now (optional); \`plan\` hard-blocks at ${STRATEGY_REVIEW_BLOCK_GAP} cycles.` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
|
|
24631
|
+
let zoomOutWarning = "";
|
|
24632
|
+
try {
|
|
24633
|
+
const lastZoomOutCycle = adapter2.getLastZoomOutCycle ? await adapter2.getLastZoomOutCycle() : 0;
|
|
24634
|
+
zoomOutWarning = computeZoomOutWarning(cycleNumber, lastZoomOutCycle);
|
|
24635
|
+
} catch {
|
|
24636
|
+
}
|
|
24007
24637
|
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
24008
24638
|
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
24009
24639
|
const statusCounts = countByStatus(nonDeferredTasks);
|
|
@@ -24157,6 +24787,7 @@ ${lines.join("\n")}`;
|
|
|
24157
24787
|
latestCycleStatus: health.latestCycleStatus,
|
|
24158
24788
|
connectionStatus: getConnectionStatus(),
|
|
24159
24789
|
reviewWarning,
|
|
24790
|
+
zoomOutWarning,
|
|
24160
24791
|
boardSummary,
|
|
24161
24792
|
staleTasks,
|
|
24162
24793
|
inReviewSummary,
|
|
@@ -24234,6 +24865,30 @@ async function findUnblockCandidates(adapter2, currentCycle) {
|
|
|
24234
24865
|
} catch {
|
|
24235
24866
|
return [];
|
|
24236
24867
|
}
|
|
24868
|
+
const blockedTasks = allTasks.filter((t) => t.status === "Blocked");
|
|
24869
|
+
if (blockedTasks.length === 0) return [];
|
|
24870
|
+
const ctx = await buildResolverContext(adapter2, allTasks, currentCycle).catch(
|
|
24871
|
+
() => ({ tasks: allTasks, decisions: [], decisionEvents: [], ownerActions: [] })
|
|
24872
|
+
);
|
|
24873
|
+
const candidates = [];
|
|
24874
|
+
for (const blocked of blockedTasks) {
|
|
24875
|
+
if (candidates.length >= MAX_CANDIDATES) break;
|
|
24876
|
+
const blocker = blocked.blocker;
|
|
24877
|
+
if (!blocker) continue;
|
|
24878
|
+
const resolved = isBlockerResolved(blocker, ctx);
|
|
24879
|
+
candidates.push({
|
|
24880
|
+
blockedId: blocked.displayId ?? blocked.id,
|
|
24881
|
+
blockedTitle: blocked.title,
|
|
24882
|
+
blockerId: blocker.ref,
|
|
24883
|
+
blockerTitle: "",
|
|
24884
|
+
blockerCycle: blocker.blockedCycle,
|
|
24885
|
+
typed: {
|
|
24886
|
+
kind: blocker.type,
|
|
24887
|
+
waiting: formatBlockerWaiting(blocker),
|
|
24888
|
+
resolved
|
|
24889
|
+
}
|
|
24890
|
+
});
|
|
24891
|
+
}
|
|
24237
24892
|
const minCycle = Math.max(0, currentCycle - RECENT_CYCLE_WINDOW + 1);
|
|
24238
24893
|
const recentDoneById = /* @__PURE__ */ new Map();
|
|
24239
24894
|
for (const t of allTasks) {
|
|
@@ -24242,42 +24897,71 @@ async function findUnblockCandidates(adapter2, currentCycle) {
|
|
|
24242
24897
|
if (cycle < minCycle) continue;
|
|
24243
24898
|
if (t.displayId) recentDoneById.set(t.displayId.toLowerCase(), t);
|
|
24244
24899
|
}
|
|
24245
|
-
if (recentDoneById.size
|
|
24246
|
-
|
|
24247
|
-
|
|
24248
|
-
|
|
24249
|
-
|
|
24250
|
-
|
|
24251
|
-
|
|
24252
|
-
|
|
24253
|
-
|
|
24254
|
-
|
|
24255
|
-
|
|
24256
|
-
|
|
24257
|
-
|
|
24258
|
-
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
|
|
24263
|
-
|
|
24264
|
-
|
|
24265
|
-
|
|
24266
|
-
|
|
24267
|
-
|
|
24268
|
-
|
|
24900
|
+
if (recentDoneById.size > 0) {
|
|
24901
|
+
for (const blocked of blockedTasks) {
|
|
24902
|
+
if (candidates.length >= MAX_CANDIDATES) break;
|
|
24903
|
+
if (blocked.blocker) continue;
|
|
24904
|
+
const notes = blocked.notes ?? "";
|
|
24905
|
+
if (!notes) continue;
|
|
24906
|
+
const matches = notes.match(TASK_REF);
|
|
24907
|
+
if (!matches) continue;
|
|
24908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24909
|
+
for (const raw of matches) {
|
|
24910
|
+
const key = raw.toLowerCase();
|
|
24911
|
+
if (seen.has(key)) continue;
|
|
24912
|
+
seen.add(key);
|
|
24913
|
+
if (blocked.displayId && key === blocked.displayId.toLowerCase()) continue;
|
|
24914
|
+
const blocker = recentDoneById.get(key);
|
|
24915
|
+
if (!blocker) continue;
|
|
24916
|
+
candidates.push({
|
|
24917
|
+
blockedId: blocked.displayId ?? blocked.id,
|
|
24918
|
+
blockedTitle: blocked.title,
|
|
24919
|
+
blockerId: blocker.displayId ?? blocker.id,
|
|
24920
|
+
blockerTitle: blocker.title,
|
|
24921
|
+
blockerCycle: blocker.cycle ?? 0
|
|
24922
|
+
});
|
|
24923
|
+
break;
|
|
24924
|
+
}
|
|
24269
24925
|
}
|
|
24270
|
-
if (candidates.length >= MAX_CANDIDATES) break;
|
|
24271
24926
|
}
|
|
24272
24927
|
return candidates;
|
|
24273
24928
|
}
|
|
24929
|
+
async function buildResolverContext(adapter2, tasks, currentCycle) {
|
|
24930
|
+
const decisions = adapter2.getActiveDecisions ? await adapter2.getActiveDecisions().catch(() => []) : [];
|
|
24931
|
+
const minCycle = Math.max(0, currentCycle - RECENT_CYCLE_WINDOW);
|
|
24932
|
+
const decisionEvents = adapter2.getDecisionEventsSince ? await adapter2.getDecisionEventsSince(minCycle).catch(() => []) : [];
|
|
24933
|
+
let ownerActions = [];
|
|
24934
|
+
if (adapter2.listOwnerActionsForBlockerScan && adapter2.getProjectOwnerUserId) {
|
|
24935
|
+
try {
|
|
24936
|
+
const userId = await adapter2.getProjectOwnerUserId();
|
|
24937
|
+
if (userId) ownerActions = await adapter2.listOwnerActionsForBlockerScan(userId);
|
|
24938
|
+
} catch {
|
|
24939
|
+
}
|
|
24940
|
+
}
|
|
24941
|
+
return { tasks, decisions, decisionEvents, ownerActions };
|
|
24942
|
+
}
|
|
24274
24943
|
function formatUnblockSection(candidates) {
|
|
24275
24944
|
if (candidates.length === 0) return "";
|
|
24945
|
+
const resolved = candidates.filter((c) => c.typed?.resolved || !c.typed);
|
|
24946
|
+
const stillWaiting = candidates.filter((c) => c.typed && !c.typed.resolved);
|
|
24276
24947
|
const lines = ["## Unblock Candidates"];
|
|
24277
|
-
|
|
24278
|
-
|
|
24279
|
-
|
|
24280
|
-
|
|
24948
|
+
if (resolved.length > 0) {
|
|
24949
|
+
lines.push(`${resolved.length} blocked task(s) ready to unblock \u2014 run \`board_reconcile\` to promote.`);
|
|
24950
|
+
lines.push("");
|
|
24951
|
+
for (const c of resolved) {
|
|
24952
|
+
if (c.typed) {
|
|
24953
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 ${c.typed.kind}: ${c.blockerId} now resolved \u2705`);
|
|
24954
|
+
} else {
|
|
24955
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 blocker **${c.blockerId}** Done in C${c.blockerCycle}`);
|
|
24956
|
+
}
|
|
24957
|
+
}
|
|
24958
|
+
}
|
|
24959
|
+
if (stillWaiting.length > 0) {
|
|
24960
|
+
if (resolved.length > 0) lines.push("");
|
|
24961
|
+
lines.push(`${stillWaiting.length} still blocked:`);
|
|
24962
|
+
for (const c of stillWaiting) {
|
|
24963
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 ${c.typed.waiting}`);
|
|
24964
|
+
}
|
|
24281
24965
|
}
|
|
24282
24966
|
return lines.join("\n");
|
|
24283
24967
|
}
|
|
@@ -24606,6 +25290,10 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
24606
25290
|
lines.push("");
|
|
24607
25291
|
lines.push(`**Strategy Review:** ${health.reviewWarning}`);
|
|
24608
25292
|
lines.push("");
|
|
25293
|
+
if (health.zoomOutWarning) {
|
|
25294
|
+
lines.push(`**Zoom-Out:** ${health.zoomOutWarning}`);
|
|
25295
|
+
lines.push("");
|
|
25296
|
+
}
|
|
24609
25297
|
if (hierarchy) {
|
|
24610
25298
|
lines.push(`**Position:** ${hierarchy.horizon} \u2192 ${hierarchy.stage}`);
|
|
24611
25299
|
if (hierarchy.activePhases.length > 0) {
|
|
@@ -25310,6 +25998,21 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
25310
25998
|
for (const w of verifyWarnings) buildResult.warnings.push(w);
|
|
25311
25999
|
} catch {
|
|
25312
26000
|
}
|
|
26001
|
+
try {
|
|
26002
|
+
if (hasLocalWorkspace() && adapter2.getProjectInfo) {
|
|
26003
|
+
const { storedOwner, remoteOwner, remoteSlug } = await resolveWorkspaceOwners(adapter2, config2.projectRoot);
|
|
26004
|
+
if (storedOwner && remoteOwner && storedOwner !== remoteOwner) {
|
|
26005
|
+
buildResult.warnings.push(
|
|
26006
|
+
`\u26A0\uFE0F Project mismatch \u2014 this PAPI project is bound to owner '${storedOwner}' but the cwd git remote points to owner '${remoteOwner}'. Run \`project_switch\` to the right project, or update the project's repo_url.`
|
|
26007
|
+
);
|
|
26008
|
+
} else if (!storedOwner && remoteOwner) {
|
|
26009
|
+
buildResult.warnings.push(
|
|
26010
|
+
`\u2139\uFE0F repo_url not set on this project \u2014 cwd git remote is '${remoteSlug}'. Run \`setup\` to capture it so the mismatch guard can fire.`
|
|
26011
|
+
);
|
|
26012
|
+
}
|
|
26013
|
+
}
|
|
26014
|
+
} catch {
|
|
26015
|
+
}
|
|
25313
26016
|
const ttfvNote = ttfvOutcome.status === "fulfilled" ? ttfvOutcome.value : "";
|
|
25314
26017
|
const latestTag = latestTagOutcome.status === "fulfilled" ? latestTagOutcome.value : "";
|
|
25315
26018
|
const versionDrift = versionDriftOutcome.status === "fulfilled" ? versionDriftOutcome.value : void 0;
|
|
@@ -25824,7 +26527,8 @@ async function prepareZoomOut(adapter2, projectRoot) {
|
|
|
25824
26527
|
return {
|
|
25825
26528
|
cycleNumber,
|
|
25826
26529
|
systemPrompt: ZOOM_OUT_SYSTEM_PROMPT,
|
|
25827
|
-
userMessage
|
|
26530
|
+
userMessage,
|
|
26531
|
+
contextBytes: Buffer.byteLength(userMessage, "utf-8")
|
|
25828
26532
|
};
|
|
25829
26533
|
}
|
|
25830
26534
|
async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
|
|
@@ -25832,7 +26536,8 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
|
|
|
25832
26536
|
cycleNumber,
|
|
25833
26537
|
title: `Zoom-Out Retrospective \u2014 Cycle ${cycleNumber}`,
|
|
25834
26538
|
content: llmResponse.slice(0, 500),
|
|
25835
|
-
fullAnalysis: llmResponse
|
|
26539
|
+
fullAnalysis: llmResponse,
|
|
26540
|
+
reviewType: "zoom_out"
|
|
25836
26541
|
});
|
|
25837
26542
|
return {
|
|
25838
26543
|
cycleNumber,
|
|
@@ -25860,6 +26565,11 @@ var zoomOutTool = {
|
|
|
25860
26565
|
cycle_number: {
|
|
25861
26566
|
type: "number",
|
|
25862
26567
|
description: 'The cycle number from prepare phase (mode "apply" only).'
|
|
26568
|
+
},
|
|
26569
|
+
dispatch: {
|
|
26570
|
+
type: "string",
|
|
26571
|
+
enum: ["inline", "subagent"],
|
|
26572
|
+
description: '"inline" (default) returns the retrospective prompt for the calling LLM to execute directly. "subagent" returns a Task() invocation prompt to dispatch the heavyweight reasoning to a fresh sub-agent. The apply-phase contract is unchanged either way. Auto-routes to "subagent" past the same context threshold plan/strategy_review use (zoom_out runs the largest context of any tool).'
|
|
25863
26573
|
}
|
|
25864
26574
|
},
|
|
25865
26575
|
required: []
|
|
@@ -25887,6 +26597,26 @@ Retrospective saved. Use insights to inform your next \`strategy_review\` or \`p
|
|
|
25887
26597
|
}
|
|
25888
26598
|
{
|
|
25889
26599
|
const result = await prepareZoomOut(adapter2, config2.projectRoot);
|
|
26600
|
+
const autoDispatchEnabled = process.env.PAPI_AUTO_DISPATCH !== "false";
|
|
26601
|
+
const autoDispatchThreshold = 50 * 1024;
|
|
26602
|
+
let dispatch;
|
|
26603
|
+
if (args.dispatch === "inline" || args.dispatch === "subagent") {
|
|
26604
|
+
dispatch = args.dispatch;
|
|
26605
|
+
} else if (autoDispatchEnabled && result.contextBytes > autoDispatchThreshold) {
|
|
26606
|
+
dispatch = "subagent";
|
|
26607
|
+
} else {
|
|
26608
|
+
dispatch = "inline";
|
|
26609
|
+
}
|
|
26610
|
+
if (dispatch === "subagent") {
|
|
26611
|
+
const dispatchPrompt = buildSubagentDispatchPrompt({
|
|
26612
|
+
tool: "zoom_out",
|
|
26613
|
+
cycleNumber: result.cycleNumber,
|
|
26614
|
+
systemPrompt: result.systemPrompt,
|
|
26615
|
+
userMessage: result.userMessage,
|
|
26616
|
+
contextBytes: result.contextBytes
|
|
26617
|
+
});
|
|
26618
|
+
return textResponse(dispatchPrompt);
|
|
26619
|
+
}
|
|
25890
26620
|
return textResponse(
|
|
25891
26621
|
`## PAPI Zoom-Out Retrospective \u2014 Prepare Phase (Cycle ${result.cycleNumber})
|
|
25892
26622
|
|
|
@@ -26682,7 +27412,7 @@ var contributorRemoveTool = {
|
|
|
26682
27412
|
};
|
|
26683
27413
|
var contributorListTool = {
|
|
26684
27414
|
name: "contributor_list",
|
|
26685
|
-
description: "List the current project's contributors (
|
|
27415
|
+
description: "List the current project's contributors (any project member). Shows each member's email, display name, role, and join date. Does not call the Anthropic API.",
|
|
26686
27416
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
26687
27417
|
inputSchema: {
|
|
26688
27418
|
type: "object",
|
|
@@ -26695,10 +27425,15 @@ var contributorListTool = {
|
|
|
26695
27425
|
required: []
|
|
26696
27426
|
}
|
|
26697
27427
|
};
|
|
26698
|
-
|
|
27428
|
+
function denyUnlessCohortCapable(adapter2) {
|
|
26699
27429
|
if (typeof adapter2.listContributors !== "function") {
|
|
26700
27430
|
return "Contributor management is not available on this adapter (requires the pg or proxy adapter \u2014 the local md adapter has no cohort).";
|
|
26701
27431
|
}
|
|
27432
|
+
return null;
|
|
27433
|
+
}
|
|
27434
|
+
async function denyUnlessOwner(adapter2, config2) {
|
|
27435
|
+
const capDenied = denyUnlessCohortCapable(adapter2);
|
|
27436
|
+
if (capDenied) return capDenied;
|
|
26702
27437
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
26703
27438
|
if (gate.enforced && !gate.callerIsOwner) {
|
|
26704
27439
|
const note = gate.resolutionError ? ` (Identity resolution failed: ${gate.resolutionError} \u2014 the gate fails closed; retry once connectivity is restored.)` : "";
|
|
@@ -26706,6 +27441,17 @@ async function denyUnlessOwner(adapter2, config2) {
|
|
|
26706
27441
|
}
|
|
26707
27442
|
return null;
|
|
26708
27443
|
}
|
|
27444
|
+
async function denyUnlessMember(adapter2, config2) {
|
|
27445
|
+
const capDenied = denyUnlessCohortCapable(adapter2);
|
|
27446
|
+
if (capDenied) return capDenied;
|
|
27447
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
27448
|
+
if (!gate.enforced) return null;
|
|
27449
|
+
if (gate.callerIsOwner) return null;
|
|
27450
|
+
const callerRole = gate.callerUserId && typeof adapter2.getContributorRole === "function" ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
27451
|
+
if (callerRole) return null;
|
|
27452
|
+
const note = gate.resolutionError ? ` (Identity resolution failed: ${gate.resolutionError} \u2014 the gate fails closed; retry once connectivity is restored.)` : "";
|
|
27453
|
+
return `Listing contributors is restricted to project members. Your identity does not match this project's owner or any contributor.${note}`;
|
|
27454
|
+
}
|
|
26709
27455
|
var EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
26710
27456
|
function requireEmail(args) {
|
|
26711
27457
|
const email = typeof args.email === "string" ? args.email.trim() : "";
|
|
@@ -26745,7 +27491,7 @@ async function handleContributorRemove(adapter2, config2, args) {
|
|
|
26745
27491
|
}
|
|
26746
27492
|
}
|
|
26747
27493
|
async function handleContributorList(adapter2, config2, _args) {
|
|
26748
|
-
const denied = await
|
|
27494
|
+
const denied = await denyUnlessMember(adapter2, config2);
|
|
26749
27495
|
if (denied) return errorResponse(denied);
|
|
26750
27496
|
try {
|
|
26751
27497
|
const entries = await adapter2.listContributors();
|
|
@@ -27360,7 +28106,10 @@ var LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set([
|
|
|
27360
28106
|
"release",
|
|
27361
28107
|
"setup",
|
|
27362
28108
|
"doc_register",
|
|
27363
|
-
"doc_scan"
|
|
28109
|
+
"doc_scan",
|
|
28110
|
+
// task-2416 (C315): zoom_out assembles a 120–150K-char context and, hosted, fans out
|
|
28111
|
+
// ~14 proxied HTTP round-trips — well over the 30s default. See zoom-out-tooling-audit-c313.md #3.
|
|
28112
|
+
"zoom_out"
|
|
27364
28113
|
]);
|
|
27365
28114
|
function toolTimeoutMs(name) {
|
|
27366
28115
|
return LONG_RUNNING_TOOLS.has(name) ? LONG_TOOL_TIMEOUT_MS : DEFAULT_TOOL_TIMEOUT_MS;
|