@papi-ai/server 0.7.69 → 0.7.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backfill-cycle-metrics.js +6 -3
- package/dist/index.js +606 -327
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1982,9 +1982,12 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1982
1982
|
return this.invoke("getOwnerIdentity", []);
|
|
1983
1983
|
}
|
|
1984
1984
|
// --- Contributor cohort (task-2029, C288) ---
|
|
1985
|
-
// Owner-only enforcement is BOTH tool-layer (resolveOwnerGate) and
|
|
1986
|
-
//
|
|
1987
|
-
//
|
|
1985
|
+
// Owner-only enforcement is BOTH tool-layer (resolveOwnerGate) and server-side
|
|
1986
|
+
// in the edge function (auth-derived caller vs project owner). task-2442 (C351)
|
|
1987
|
+
// added addContributorByEmail/removeContributorByEmail to the proxy WRITE_METHODS
|
|
1988
|
+
// set too, so a viewer is now rejected at the role-gate wall before the handler —
|
|
1989
|
+
// three layers of defence, since this client runs on the user's machine.
|
|
1990
|
+
// listContributors stays a member-open read (task-2377).
|
|
1988
1991
|
async listContributors() {
|
|
1989
1992
|
return this.invoke("listContributors", []);
|
|
1990
1993
|
}
|
|
@@ -10729,6 +10732,14 @@ function validateHandoffScope(handoff) {
|
|
|
10729
10732
|
if (!isMeaningful(handoff.scopeBoundary)) invalid.push("scopeBoundary");
|
|
10730
10733
|
return invalid;
|
|
10731
10734
|
}
|
|
10735
|
+
function assertHandoffApplyPayloadNonEmpty(data, cycleNumber) {
|
|
10736
|
+
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
10737
|
+
if (handoffCount === 0) {
|
|
10738
|
+
throw new Error(
|
|
10739
|
+
`Handoff apply rejected: trimmed/empty apply payload \u2014 nothing persisted. The parsed output for Cycle ${cycleNumber} carries NO handoffs (cycleHandoffs is empty), but handoff prepare only runs when at least one task needs a handoff \u2014 so the handoffs were dropped. The most likely cause is a truncated apply JSON: the output overflowed the client tool-result ceiling (see task-2905/2906). Re-run handoff_generate and resend the COMPLETE structured output \u2014 ensure the JSON after the <!-- PAPI_STRUCTURED_OUTPUT --> marker includes the full cycleHandoffs array.`
|
|
10740
|
+
);
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10732
10743
|
async function prepareHandoffs(adapter2, _config, taskIds, force = false) {
|
|
10733
10744
|
const timer2 = startTimer();
|
|
10734
10745
|
const cycles = await adapter2.readCycles();
|
|
@@ -10785,10 +10796,8 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10785
10796
|
if (!data) {
|
|
10786
10797
|
throw new Error("Could not parse structured output. Ensure your output includes <!-- PAPI_STRUCTURED_OUTPUT --> with valid JSON.");
|
|
10787
10798
|
}
|
|
10799
|
+
assertHandoffApplyPayloadNonEmpty(data, cycleNumber);
|
|
10788
10800
|
const handoffs = data.cycleHandoffs ?? [];
|
|
10789
|
-
if (handoffs.length === 0) {
|
|
10790
|
-
throw new Error("No cycleHandoffs found in structured output. Ensure your output includes handoffs in the cycleHandoffs array.");
|
|
10791
|
-
}
|
|
10792
10801
|
const taskIdsToWrite = handoffs.map((h) => h.taskId);
|
|
10793
10802
|
const existingHandoffSet = /* @__PURE__ */ new Set();
|
|
10794
10803
|
try {
|
|
@@ -13662,6 +13671,58 @@ async function buildSessionGuidance(callerKey) {
|
|
|
13662
13671
|
return signals.slice(0, 3);
|
|
13663
13672
|
}
|
|
13664
13673
|
|
|
13674
|
+
// src/services/onboarding-coaching.ts
|
|
13675
|
+
var ONBOARDING_EARLY_CYCLE_MAX = 2;
|
|
13676
|
+
var MAX_COACHING_LINES = 4;
|
|
13677
|
+
var COACH_CONNECT_REPO = "No repository is linked to this project yet. Link your repo from the dashboard Settings (or capture it during `setup`) so builds, reviews, and releases attach to the right codebase.";
|
|
13678
|
+
var COACH_ROOT_DIR = "Before building, confirm this session is running from your project root directory, so commits and builds land against the right files.";
|
|
13679
|
+
var COACH_CLICKABLE_TASKS = "Task cards on your dashboard expand on click. Open one to read its full build handoff, comments, and history.";
|
|
13680
|
+
var COACH_OFF_CYCLE = 'When a request falls outside the current cycle, keep it in the loop: `idea "<what you want>"` parks it in the backlog, or promote it into the cycle and generate a build handoff before you build.';
|
|
13681
|
+
var COACH_SESSION_START = 'Start of a session: skim your Active Decisions (the AD summary from `orient`, or `strategy_review`) to reload your project direction, and capture anything new with `idea "<what you want>"`.';
|
|
13682
|
+
var COACH_NO_DECISIONS = 'No Active Decisions recorded yet. Capture your first with `idea "<a direction or constraint>"` so your project starts steering itself.';
|
|
13683
|
+
var COACH_BACKLOG_IMPORT = "Already tracking a backlog elsewhere (Linear, a CSV, a markdown checklist)? Bring it over in one pass with `backlog_import` instead of retyping it.";
|
|
13684
|
+
var ONBOARDING_COACHING_HEADING = "## Getting Started";
|
|
13685
|
+
function buildOnboardingCoaching(state) {
|
|
13686
|
+
const lines = [];
|
|
13687
|
+
const isEarly = state.cycleNumber <= ONBOARDING_EARLY_CYCLE_MAX;
|
|
13688
|
+
const { surface } = state;
|
|
13689
|
+
if (state.repoConnected === false && (surface === "orient" || surface === "plan")) {
|
|
13690
|
+
lines.push(COACH_CONNECT_REPO);
|
|
13691
|
+
}
|
|
13692
|
+
if (state.hasLocalWorkspace && (surface === "setup" || surface === "orient" && isEarly)) {
|
|
13693
|
+
lines.push(COACH_ROOT_DIR);
|
|
13694
|
+
}
|
|
13695
|
+
if ((surface === "orient" || surface === "setup") && isEarly) {
|
|
13696
|
+
lines.push(COACH_CLICKABLE_TASKS);
|
|
13697
|
+
}
|
|
13698
|
+
if ((surface === "orient" || surface === "plan") && state.hasActiveCycle && isEarly) {
|
|
13699
|
+
lines.push(COACH_OFF_CYCLE);
|
|
13700
|
+
}
|
|
13701
|
+
if (surface === "orient" && isEarly) {
|
|
13702
|
+
lines.push(state.hasActiveDecisions === false ? COACH_NO_DECISIONS : COACH_SESSION_START);
|
|
13703
|
+
}
|
|
13704
|
+
if (surface === "plan" && state.bootstrapPlan === true) {
|
|
13705
|
+
lines.push(COACH_BACKLOG_IMPORT);
|
|
13706
|
+
}
|
|
13707
|
+
return lines.slice(0, MAX_COACHING_LINES);
|
|
13708
|
+
}
|
|
13709
|
+
function formatOnboardingCoachingBlock(state) {
|
|
13710
|
+
const lines = buildOnboardingCoaching(state);
|
|
13711
|
+
if (lines.length === 0) return "";
|
|
13712
|
+
return `
|
|
13713
|
+
|
|
13714
|
+
${ONBOARDING_COACHING_HEADING}
|
|
13715
|
+
${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
13716
|
+
}
|
|
13717
|
+
|
|
13718
|
+
// src/lib/hosted-mode.ts
|
|
13719
|
+
function isHostedTransport() {
|
|
13720
|
+
return Boolean(process.env.PORT || process.env.PAPI_HTTP_PORT);
|
|
13721
|
+
}
|
|
13722
|
+
function hasLocalWorkspace() {
|
|
13723
|
+
return !isHostedTransport();
|
|
13724
|
+
}
|
|
13725
|
+
|
|
13665
13726
|
// src/lib/per-caller-cache.ts
|
|
13666
13727
|
var DEFAULT_CALLER_KEY2 = "__default__";
|
|
13667
13728
|
var MAX_PER_CALLER_ENTRIES = 1e3;
|
|
@@ -13905,6 +13966,10 @@ function formatPlanResult(result) {
|
|
|
13905
13966
|
} else {
|
|
13906
13967
|
lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
|
|
13907
13968
|
}
|
|
13969
|
+
if (result.onboardingCoaching && result.onboardingCoaching.length > 0) {
|
|
13970
|
+
lines.push("", ONBOARDING_COACHING_HEADING);
|
|
13971
|
+
for (const c of result.onboardingCoaching) lines.push(`- ${c}`);
|
|
13972
|
+
}
|
|
13908
13973
|
if (result.contextBytes !== void 0) {
|
|
13909
13974
|
const kb = (result.contextBytes / 1024).toFixed(1);
|
|
13910
13975
|
lines.push(`---`, `Context: ${kb}KB`);
|
|
@@ -13986,7 +14051,15 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13986
14051
|
}, tracker);
|
|
13987
14052
|
const planProjectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
13988
14053
|
const projectBanner = planProjectInfo ? getProjectConnectionBanner(planProjectInfo.name, planProjectInfo.slug) ?? void 0 : void 0;
|
|
13989
|
-
const
|
|
14054
|
+
const onboardingCoaching = buildOnboardingCoaching({
|
|
14055
|
+
surface: "plan",
|
|
14056
|
+
repoConnected: planProjectInfo ? !!planProjectInfo.repo_url : void 0,
|
|
14057
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
14058
|
+
cycleNumber: result.cycleNumber + 1,
|
|
14059
|
+
hasActiveCycle: true,
|
|
14060
|
+
bootstrapPlan: result.mode === "bootstrap"
|
|
14061
|
+
});
|
|
14062
|
+
const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs, projectBanner, onboardingCoaching });
|
|
13990
14063
|
return {
|
|
13991
14064
|
...response,
|
|
13992
14065
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -14138,14 +14211,6 @@ import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "f
|
|
|
14138
14211
|
import { join as join4 } from "path";
|
|
14139
14212
|
import { homedir as homedir2 } from "os";
|
|
14140
14213
|
|
|
14141
|
-
// src/lib/hosted-mode.ts
|
|
14142
|
-
function isHostedTransport() {
|
|
14143
|
-
return Boolean(process.env.PORT || process.env.PAPI_HTTP_PORT);
|
|
14144
|
-
}
|
|
14145
|
-
function hasLocalWorkspace() {
|
|
14146
|
-
return !isHostedTransport();
|
|
14147
|
-
}
|
|
14148
|
-
|
|
14149
14214
|
// src/services/idea.ts
|
|
14150
14215
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
14151
14216
|
var OWNER_ACTION_PATTERNS = [
|
|
@@ -15602,6 +15667,13 @@ ${cleanContent}`;
|
|
|
15602
15667
|
} catch {
|
|
15603
15668
|
}
|
|
15604
15669
|
}
|
|
15670
|
+
if (data.hierarchyUpdates && data.hierarchyUpdates.length > 0) {
|
|
15671
|
+
try {
|
|
15672
|
+
await applyHierarchyUpdates(adapter2, data.hierarchyUpdates);
|
|
15673
|
+
} catch (err) {
|
|
15674
|
+
console.error("[strategy] applyHierarchyUpdates failed:", err instanceof Error ? err.message : String(err));
|
|
15675
|
+
}
|
|
15676
|
+
}
|
|
15605
15677
|
const compressionThreshold = cycleNumber - 5;
|
|
15606
15678
|
if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
|
|
15607
15679
|
await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
|
|
@@ -15879,6 +15951,79 @@ function buildPhaseLabel(phase) {
|
|
|
15879
15951
|
if (!numMatch) return phase.label;
|
|
15880
15952
|
return `Phase ${numMatch[1]}: ${phase.label}`;
|
|
15881
15953
|
}
|
|
15954
|
+
function nextHierarchySort(existing) {
|
|
15955
|
+
return existing.length === 0 ? 10 : Math.max(...existing.map((e) => e.sortOrder)) + 10;
|
|
15956
|
+
}
|
|
15957
|
+
async function applyHierarchyUpdates(adapter2, updates) {
|
|
15958
|
+
if (!adapter2.readStages || !adapter2.readHorizons) return;
|
|
15959
|
+
const findHorizon = async (ref) => (await adapter2.readHorizons()).find(
|
|
15960
|
+
(h) => h.slug === ref || h.id === ref || h.label.toLowerCase() === ref.toLowerCase()
|
|
15961
|
+
);
|
|
15962
|
+
const findStage = async (ref) => (await adapter2.readStages()).find(
|
|
15963
|
+
(s) => s.slug === ref || s.id === ref || s.label.toLowerCase() === ref.toLowerCase()
|
|
15964
|
+
);
|
|
15965
|
+
for (const u of updates) {
|
|
15966
|
+
if (u.level === "horizon") {
|
|
15967
|
+
if (u.action === "create") {
|
|
15968
|
+
if (!await findHorizon(u.slug) && adapter2.createHorizon) {
|
|
15969
|
+
const horizons = await adapter2.readHorizons();
|
|
15970
|
+
await adapter2.createHorizon({
|
|
15971
|
+
slug: u.slug,
|
|
15972
|
+
label: u.label ?? u.slug,
|
|
15973
|
+
status: u.status ?? "In Progress",
|
|
15974
|
+
sortOrder: u.sortOrder ?? nextHierarchySort(horizons)
|
|
15975
|
+
});
|
|
15976
|
+
}
|
|
15977
|
+
} else if (u.action === "update_status" && u.status && adapter2.updateHorizonStatus) {
|
|
15978
|
+
const h = await findHorizon(u.slug);
|
|
15979
|
+
if (h) await adapter2.updateHorizonStatus(h.id, u.status);
|
|
15980
|
+
}
|
|
15981
|
+
continue;
|
|
15982
|
+
}
|
|
15983
|
+
if (u.action === "update_criterion" && u.criterionId && typeof u.met === "boolean" && adapter2.setCriterionMet) {
|
|
15984
|
+
const s = await findStage(u.slug);
|
|
15985
|
+
if (s) await adapter2.setCriterionMet(s.id, u.criterionId, u.met, u.evidence ?? null);
|
|
15986
|
+
} else if (u.action === "update_status" && u.status && adapter2.updateStageStatus) {
|
|
15987
|
+
const s = await findStage(u.slug);
|
|
15988
|
+
if (s) await adapter2.updateStageStatus(s.id, u.status);
|
|
15989
|
+
} else if (u.action === "create") {
|
|
15990
|
+
await createStageFromUpdate(adapter2, u, findStage, findHorizon);
|
|
15991
|
+
} else if (u.action === "advance" && adapter2.updateStageStatus) {
|
|
15992
|
+
const current = await findStage(u.slug);
|
|
15993
|
+
if (current) await adapter2.updateStageStatus(current.id, "Done");
|
|
15994
|
+
const newRef = u.newStageSlug ?? u.slug;
|
|
15995
|
+
const existingNext = await findStage(newRef);
|
|
15996
|
+
if (existingNext) {
|
|
15997
|
+
await adapter2.updateStageStatus(existingNext.id, "In Progress");
|
|
15998
|
+
} else if (u.newStageLabel) {
|
|
15999
|
+
await createStageFromUpdate(
|
|
16000
|
+
adapter2,
|
|
16001
|
+
{ ...u, slug: newRef, label: u.newStageLabel, status: "In Progress", horizon: u.horizon ?? current?.horizonId },
|
|
16002
|
+
findStage,
|
|
16003
|
+
findHorizon
|
|
16004
|
+
);
|
|
16005
|
+
}
|
|
16006
|
+
}
|
|
16007
|
+
}
|
|
16008
|
+
}
|
|
16009
|
+
async function createStageFromUpdate(adapter2, u, findStage, findHorizon) {
|
|
16010
|
+
if (!adapter2.createStage || !adapter2.readStages || !adapter2.readHorizons) return;
|
|
16011
|
+
if (await findStage(u.slug)) return;
|
|
16012
|
+
const horizons = await adapter2.readHorizons();
|
|
16013
|
+
const parent = u.horizon ? await findHorizon(u.horizon) : horizons.length === 1 ? horizons[0] : horizons.find((h) => h.status === "In Progress");
|
|
16014
|
+
if (!parent) return;
|
|
16015
|
+
const stagesInHorizon = (await adapter2.readStages()).filter((s) => s.horizonId === parent.id);
|
|
16016
|
+
const id = await adapter2.createStage({
|
|
16017
|
+
slug: u.slug,
|
|
16018
|
+
label: u.label ?? u.slug,
|
|
16019
|
+
status: u.status ?? "Not Started",
|
|
16020
|
+
sortOrder: u.sortOrder ?? nextHierarchySort(stagesInHorizon),
|
|
16021
|
+
horizonId: parent.id
|
|
16022
|
+
});
|
|
16023
|
+
if (id && u.exitCriteria?.length && adapter2.updateStageExitCriteria) {
|
|
16024
|
+
await adapter2.updateStageExitCriteria(id, u.exitCriteria);
|
|
16025
|
+
}
|
|
16026
|
+
}
|
|
15882
16027
|
async function applyPhaseUpdates(adapter2, currentPhases, updates) {
|
|
15883
16028
|
const phasesById = new Map(currentPhases.map((p) => [p.id, p]));
|
|
15884
16029
|
const labelMigrations = [];
|
|
@@ -19111,8 +19256,14 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
|
19111
19256
|
**Important:** This is a remote PAPI connection, so the server could not write to your project directory. Setup prepared your files (${harnessFiles}, .claude/settings.json, docs/) and returned them in the scaffolding section below \u2014 **write each one to disk, then commit** before running \`build_execute\` (it requires a clean working directory).` : `
|
|
19112
19257
|
|
|
19113
19258
|
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.`;
|
|
19259
|
+
const coachingNote = formatOnboardingCoachingBlock({
|
|
19260
|
+
surface: "setup",
|
|
19261
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
19262
|
+
cycleNumber: 0,
|
|
19263
|
+
hasActiveCycle: false
|
|
19264
|
+
});
|
|
19114
19265
|
return textResponse(
|
|
19115
|
-
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}
|
|
19266
|
+
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}${coachingNote}
|
|
19116
19267
|
|
|
19117
19268
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
19118
19269
|
|
|
@@ -19483,8 +19634,8 @@ function buildPapiMetaFramingDirective(caps, inner) {
|
|
|
19483
19634
|
|
|
19484
19635
|
// src/services/build.ts
|
|
19485
19636
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
19486
|
-
import { readdirSync as readdirSync5, existsSync as
|
|
19487
|
-
import { join as
|
|
19637
|
+
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
19638
|
+
import { join as join12 } from "path";
|
|
19488
19639
|
|
|
19489
19640
|
// src/lib/db-only-notices.ts
|
|
19490
19641
|
var DB_ONLY_START_NOTICE = "No git repo detected \u2014 running this cycle in your project database only. Your work stays in the working tree; run `git init` (and add a remote) to enable branches, commits and PR review.";
|
|
@@ -20402,6 +20553,38 @@ async function postReleaseToX(version, cycleClosed) {
|
|
|
20402
20553
|
return false;
|
|
20403
20554
|
}
|
|
20404
20555
|
}
|
|
20556
|
+
function buildHostedReleaseOutput(params) {
|
|
20557
|
+
const { version, branch, cyclePart, warningsBlock, skipVersion } = params;
|
|
20558
|
+
const tagAnnotation = `Release ${version}`;
|
|
20559
|
+
const titleSuffix = skipVersion ? " (skip version)" : "";
|
|
20560
|
+
const gitHalf = skipVersion ? "the git half of the release (the CHANGELOG.md commit and the branch push) must run on your own machine" : "the git half of the release (tag, push, CHANGELOG.md) must run on your own machine";
|
|
20561
|
+
const commands = skipVersion ? `git checkout ${branch}
|
|
20562
|
+
git pull
|
|
20563
|
+
git push origin ${branch}
|
|
20564
|
+
` : `git checkout ${branch}
|
|
20565
|
+
git pull
|
|
20566
|
+
git tag -a ${version} -m "${tagAnnotation}"
|
|
20567
|
+
git push origin ${branch}
|
|
20568
|
+
git push origin ${version}
|
|
20569
|
+
`;
|
|
20570
|
+
const identityBlock = skipVersion ? "" : `If git reports "Author identity unknown" or "Committer identity unknown" (no global git identity configured), pass your identity inline instead of writing global config:
|
|
20571
|
+
\`\`\`
|
|
20572
|
+
git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -m "${tagAnnotation}"
|
|
20573
|
+
\`\`\`
|
|
20574
|
+
|
|
20575
|
+
`;
|
|
20576
|
+
return `## Release ${version}${titleSuffix} \u2014 cycle closed in the DB
|
|
20577
|
+
|
|
20578
|
+
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20579
|
+
` + warningsBlock + `
|
|
20580
|
+
The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so ${gitHalf}.
|
|
20581
|
+
|
|
20582
|
+
Finish the release locally:
|
|
20583
|
+
\`\`\`
|
|
20584
|
+
` + commands + `\`\`\`
|
|
20585
|
+
|
|
20586
|
+
` + identityBlock + `Next: cycle closed! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`;
|
|
20587
|
+
}
|
|
20405
20588
|
var releaseTool = {
|
|
20406
20589
|
name: "release",
|
|
20407
20590
|
description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote. Pass skipVersion=true to update CHANGELOG and close the cycle without creating a tag or bumping version numbers.",
|
|
@@ -20579,33 +20762,12 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
20579
20762
|
branchMerges: [],
|
|
20580
20763
|
changelogEmitted: false
|
|
20581
20764
|
});
|
|
20582
|
-
const tagAnnotation = `Release ${version}`;
|
|
20583
20765
|
const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
|
|
20584
20766
|
const warningsBlock = closed.warnings.length > 0 ? `
|
|
20585
20767
|
\u26A0\uFE0F Warnings: ${closed.warnings.join("; ")}
|
|
20586
20768
|
` : "";
|
|
20587
20769
|
return textResponse(
|
|
20588
|
-
|
|
20589
|
-
|
|
20590
|
-
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20591
|
-
` + warningsBlock + `
|
|
20592
|
-
The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so the git half of the release (tag, push, CHANGELOG.md) must run on your own machine.
|
|
20593
|
-
|
|
20594
|
-
Finish the release locally:
|
|
20595
|
-
\`\`\`
|
|
20596
|
-
git checkout ${branch}
|
|
20597
|
-
git pull
|
|
20598
|
-
git tag -a ${version} -m "${tagAnnotation}"
|
|
20599
|
-
git push origin ${branch}
|
|
20600
|
-
git push origin ${version}
|
|
20601
|
-
\`\`\`
|
|
20602
|
-
|
|
20603
|
-
If git reports "Author identity unknown" or "Committer identity unknown" (no global git identity configured), pass your identity inline instead of writing global config:
|
|
20604
|
-
\`\`\`
|
|
20605
|
-
git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -m "${tagAnnotation}"
|
|
20606
|
-
\`\`\`
|
|
20607
|
-
|
|
20608
|
-
Next: cycle closed! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`
|
|
20770
|
+
buildHostedReleaseOutput({ version, branch, cyclePart, warningsBlock, skipVersion: skipVersion ?? false })
|
|
20609
20771
|
);
|
|
20610
20772
|
}
|
|
20611
20773
|
tracker.mark("remote-project-guard");
|
|
@@ -20862,6 +21024,107 @@ function detectWorktreeCollision(input) {
|
|
|
20862
21024
|
// src/services/build.ts
|
|
20863
21025
|
init_git();
|
|
20864
21026
|
|
|
21027
|
+
// src/lib/build-checkpoint.ts
|
|
21028
|
+
import { createHash as createHash4 } from "crypto";
|
|
21029
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
21030
|
+
import { join as join11 } from "path";
|
|
21031
|
+
var BUILD_CHECKPOINT_VERSION = 1;
|
|
21032
|
+
function cwdHash(cwd) {
|
|
21033
|
+
return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
21034
|
+
}
|
|
21035
|
+
function safeTaskId(taskId) {
|
|
21036
|
+
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
21037
|
+
}
|
|
21038
|
+
function checkpointDir(cwd) {
|
|
21039
|
+
return join11(cwd, ".papi", "state");
|
|
21040
|
+
}
|
|
21041
|
+
function checkpointPath(cwd, taskId) {
|
|
21042
|
+
return join11(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
21043
|
+
}
|
|
21044
|
+
function writeBuildCheckpoint(input) {
|
|
21045
|
+
try {
|
|
21046
|
+
const dir = checkpointDir(input.cwd);
|
|
21047
|
+
if (!existsSync7(dir)) {
|
|
21048
|
+
mkdirSync2(dir, { recursive: true });
|
|
21049
|
+
}
|
|
21050
|
+
const checkpoint = {
|
|
21051
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
21052
|
+
taskId: input.taskId,
|
|
21053
|
+
branch: input.branch,
|
|
21054
|
+
step: input.step,
|
|
21055
|
+
lastCommitSha: input.lastCommitSha,
|
|
21056
|
+
modifiedFiles: input.modifiedFiles,
|
|
21057
|
+
cwd: realpathOrSelf(input.cwd),
|
|
21058
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21059
|
+
};
|
|
21060
|
+
writeFileSync3(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
21061
|
+
} catch {
|
|
21062
|
+
}
|
|
21063
|
+
}
|
|
21064
|
+
function readBuildCheckpoint(key) {
|
|
21065
|
+
try {
|
|
21066
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
21067
|
+
if (!existsSync7(path7)) return null;
|
|
21068
|
+
const parsed = JSON.parse(readFileSync6(path7, "utf-8"));
|
|
21069
|
+
if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
|
|
21070
|
+
return null;
|
|
21071
|
+
}
|
|
21072
|
+
return {
|
|
21073
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
21074
|
+
taskId: parsed.taskId,
|
|
21075
|
+
branch: parsed.branch ?? null,
|
|
21076
|
+
step: "branch_ready",
|
|
21077
|
+
lastCommitSha: parsed.lastCommitSha ?? null,
|
|
21078
|
+
modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
|
|
21079
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
|
|
21080
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
|
|
21081
|
+
};
|
|
21082
|
+
} catch {
|
|
21083
|
+
return null;
|
|
21084
|
+
}
|
|
21085
|
+
}
|
|
21086
|
+
function clearBuildCheckpoint(key) {
|
|
21087
|
+
try {
|
|
21088
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
21089
|
+
if (existsSync7(path7)) {
|
|
21090
|
+
unlinkSync2(path7);
|
|
21091
|
+
}
|
|
21092
|
+
} catch {
|
|
21093
|
+
}
|
|
21094
|
+
}
|
|
21095
|
+
function writeBuildCheckpointIfLocal(input) {
|
|
21096
|
+
if (!hasLocalWorkspace()) return;
|
|
21097
|
+
writeBuildCheckpoint(input);
|
|
21098
|
+
}
|
|
21099
|
+
function readBuildCheckpointIfLocal(key) {
|
|
21100
|
+
if (!hasLocalWorkspace()) return null;
|
|
21101
|
+
return readBuildCheckpoint(key);
|
|
21102
|
+
}
|
|
21103
|
+
function clearBuildCheckpointIfLocal(key) {
|
|
21104
|
+
if (!hasLocalWorkspace()) return;
|
|
21105
|
+
clearBuildCheckpoint(key);
|
|
21106
|
+
}
|
|
21107
|
+
function formatResumeNote(cp) {
|
|
21108
|
+
const files = cp.modifiedFiles.filter((f) => f && f.trim());
|
|
21109
|
+
const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
|
|
21110
|
+
- \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
|
|
21111
|
+
const lines = [
|
|
21112
|
+
"> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
|
|
21113
|
+
`> - Branch: \`${cp.branch ?? "unknown"}\``,
|
|
21114
|
+
"> - Last step: branch ready",
|
|
21115
|
+
`> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
|
|
21116
|
+
`> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
|
|
21117
|
+
">",
|
|
21118
|
+
"> Modified files at last checkpoint:",
|
|
21119
|
+
...fileList.split("\n").map((l) => `> ${l}`),
|
|
21120
|
+
">",
|
|
21121
|
+
"> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
|
|
21122
|
+
"",
|
|
21123
|
+
""
|
|
21124
|
+
];
|
|
21125
|
+
return lines.join("\n");
|
|
21126
|
+
}
|
|
21127
|
+
|
|
20865
21128
|
// src/lib/diff-inspector.ts
|
|
20866
21129
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
20867
21130
|
var TRIGGER_SURFACE_GLOBS = [
|
|
@@ -21097,51 +21360,22 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21097
21360
|
if (staged.length > 0) {
|
|
21098
21361
|
return safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
21099
21362
|
}
|
|
21363
|
+
const modified = getModifiedFiles(cwd);
|
|
21364
|
+
if (modified.length === 0) {
|
|
21365
|
+
return "Auto-commit: skipped (no working-tree changes).";
|
|
21366
|
+
}
|
|
21367
|
+
const commitResult = safeRun(() => stageAllAndCommit(cwd, message));
|
|
21100
21368
|
if (predictedFiles && predictedFiles.length > 0) {
|
|
21101
|
-
const modified = getModifiedFiles(cwd);
|
|
21102
|
-
if (modified.length === 0) {
|
|
21103
|
-
return "Auto-commit: skipped (no working-tree changes).";
|
|
21104
|
-
}
|
|
21105
|
-
const dirname7 = (p) => {
|
|
21106
|
-
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
21107
|
-
return idx > 0 ? p.slice(0, idx) : "";
|
|
21108
|
-
};
|
|
21109
21369
|
const cleanedPredicted = sanitisePredictedFiles(predictedFiles);
|
|
21110
|
-
const
|
|
21111
|
-
|
|
21112
|
-
|
|
21113
|
-
|
|
21114
|
-
(
|
|
21115
|
-
);
|
|
21116
|
-
if (scoped.length === 0 && untrackedInScope.length === 0) {
|
|
21117
|
-
const modSample = modified.slice(0, 5).join(", ");
|
|
21118
|
-
const predSample = cleanedPredicted.slice(0, 5).join(", ");
|
|
21119
|
-
return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
21120
|
-
}
|
|
21121
|
-
const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
|
|
21122
|
-
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
21123
|
-
const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
|
|
21124
|
-
const adjacentUntracked = untracked.filter(
|
|
21125
|
-
(p) => !inScopeSet.has(p) && isUnderScopedDir(p)
|
|
21126
|
-
);
|
|
21127
|
-
const toStage = [...scoped, ...untrackedInScope, ...adjacentUntracked];
|
|
21128
|
-
const toStageSet = new Set(toStage);
|
|
21129
|
-
const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
|
|
21130
|
-
const droppedModified = modified.filter((p) => !scopedSet.has(p));
|
|
21131
|
-
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (untrackedInScope.length > 0 ? ` + ${untrackedInScope.length} new file(s) named in the handoff` : "") + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
21132
|
-
if (droppedModified.length > 0) {
|
|
21133
|
-
const sample = droppedModified.slice(0, 10).join(", ");
|
|
21134
|
-
const more = droppedModified.length > 10 ? ` (+${droppedModified.length - 10} more)` : "";
|
|
21135
|
-
line += ` \u26A0\uFE0F ${droppedModified.length} modified file(s) outside FILES LIKELY TOUCHED were NOT staged: ${sample}${more}. If they belong to this task, stage them manually (\`git add <paths>\`) and re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
21136
|
-
}
|
|
21137
|
-
if (droppedUntracked.length > 0) {
|
|
21138
|
-
const sample = droppedUntracked.slice(0, 10).join(", ");
|
|
21139
|
-
const more = droppedUntracked.length > 10 ? ` (+${droppedUntracked.length - 10} more)` : "";
|
|
21140
|
-
line += ` \u26A0\uFE0F ${droppedUntracked.length} untracked file(s) were NOT committed: ${sample}${more}. If they belong to this task, run \`git add <paths> && git commit --amend --no-edit\` before pushing \u2014 otherwise the committed tree may not build on checkout/CI.`;
|
|
21370
|
+
const outOfScope = modified.filter((p) => !isPathInPredictedScope(p, cleanedPredicted));
|
|
21371
|
+
if (outOfScope.length > 0) {
|
|
21372
|
+
const sample = outOfScope.slice(0, 10).join(", ");
|
|
21373
|
+
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
21374
|
+
return `${commitResult} (staged all ${modified.length} changed file(s)). \u2139\uFE0F Scope drift: ${outOfScope.length} committed file(s) were outside the handoff's FILES LIKELY TOUCHED \u2014 handoff under-predicted: ${sample}${more}.`;
|
|
21141
21375
|
}
|
|
21142
|
-
return
|
|
21376
|
+
return `${commitResult} (staged all ${modified.length} changed file(s), all within FILES LIKELY TOUCHED).`;
|
|
21143
21377
|
}
|
|
21144
|
-
return
|
|
21378
|
+
return `${commitResult} (staged all ${modified.length} changed file(s)).`;
|
|
21145
21379
|
}
|
|
21146
21380
|
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
21147
21381
|
const lines = [];
|
|
@@ -21618,8 +21852,9 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21618
21852
|
await persistBranchName(adapter2, taskId, startedBranch);
|
|
21619
21853
|
}
|
|
21620
21854
|
buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
|
|
21855
|
+
let startSha = null;
|
|
21621
21856
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
21622
|
-
|
|
21857
|
+
startSha = getHeadCommitSha(config2.projectRoot);
|
|
21623
21858
|
if (startSha) taskStartShaMap.set(taskId, startSha);
|
|
21624
21859
|
}
|
|
21625
21860
|
let phaseChanges = [];
|
|
@@ -21638,6 +21873,14 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21638
21873
|
);
|
|
21639
21874
|
} catch {
|
|
21640
21875
|
}
|
|
21876
|
+
writeBuildCheckpointIfLocal({
|
|
21877
|
+
cwd: config2.projectRoot,
|
|
21878
|
+
taskId,
|
|
21879
|
+
branch: getCurrentBranch(config2.projectRoot),
|
|
21880
|
+
step: "branch_ready",
|
|
21881
|
+
lastCommitSha: startSha,
|
|
21882
|
+
modifiedFiles: getModifiedFiles(config2.projectRoot)
|
|
21883
|
+
});
|
|
21641
21884
|
return {
|
|
21642
21885
|
task,
|
|
21643
21886
|
branchLines,
|
|
@@ -21652,17 +21895,17 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
21652
21895
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
21653
21896
|
return;
|
|
21654
21897
|
}
|
|
21655
|
-
const papiDir =
|
|
21656
|
-
if (!
|
|
21657
|
-
|
|
21898
|
+
const papiDir = join12(projectRoot, ".papi");
|
|
21899
|
+
if (!existsSync8(papiDir)) {
|
|
21900
|
+
mkdirSync3(papiDir, { recursive: true });
|
|
21658
21901
|
}
|
|
21659
|
-
const scopePath =
|
|
21660
|
-
|
|
21902
|
+
const scopePath = join12(papiDir, "active-task-scope.txt");
|
|
21903
|
+
writeFileSync4(scopePath, content, "utf-8");
|
|
21661
21904
|
}
|
|
21662
21905
|
function clearActiveTaskScope(projectRoot) {
|
|
21663
|
-
const scopePath =
|
|
21664
|
-
if (
|
|
21665
|
-
|
|
21906
|
+
const scopePath = join12(projectRoot, ".papi", "active-task-scope.txt");
|
|
21907
|
+
if (existsSync8(scopePath)) {
|
|
21908
|
+
unlinkSync3(scopePath);
|
|
21666
21909
|
}
|
|
21667
21910
|
}
|
|
21668
21911
|
function sanitiseResponseExcerpt(raw) {
|
|
@@ -21681,7 +21924,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
21681
21924
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
21682
21925
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
21683
21926
|
try {
|
|
21684
|
-
const content =
|
|
21927
|
+
const content = readFileSync7(absolutePath, "utf-8").slice(0, 2e3);
|
|
21685
21928
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21686
21929
|
if (fmMatch) {
|
|
21687
21930
|
const fm = fmMatch[1];
|
|
@@ -22078,14 +22321,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22078
22321
|
let docWarning;
|
|
22079
22322
|
try {
|
|
22080
22323
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
22081
|
-
const docsDir =
|
|
22082
|
-
if (
|
|
22324
|
+
const docsDir = join12(config2.projectRoot, "docs");
|
|
22325
|
+
if (existsSync8(docsDir)) {
|
|
22083
22326
|
const scanDir = (dir, depth = 0) => {
|
|
22084
22327
|
if (depth > 8) return [];
|
|
22085
22328
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
22086
22329
|
const files = [];
|
|
22087
22330
|
for (const e of entries) {
|
|
22088
|
-
const full =
|
|
22331
|
+
const full = join12(dir, e.name);
|
|
22089
22332
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
22090
22333
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
22091
22334
|
}
|
|
@@ -22100,7 +22343,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22100
22343
|
const failed = [];
|
|
22101
22344
|
for (const docPath of unregistered) {
|
|
22102
22345
|
try {
|
|
22103
|
-
const meta = extractDocMeta(
|
|
22346
|
+
const meta = extractDocMeta(join12(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
22104
22347
|
await adapter2.registerDoc({
|
|
22105
22348
|
title: meta.title,
|
|
22106
22349
|
type: meta.type,
|
|
@@ -22134,8 +22377,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22134
22377
|
}
|
|
22135
22378
|
} catch {
|
|
22136
22379
|
}
|
|
22380
|
+
if (hasLocalWorkspace()) {
|
|
22381
|
+
try {
|
|
22382
|
+
clearActiveTaskScope(config2.projectRoot);
|
|
22383
|
+
} catch {
|
|
22384
|
+
}
|
|
22385
|
+
}
|
|
22137
22386
|
try {
|
|
22138
|
-
|
|
22387
|
+
clearBuildCheckpointIfLocal({ cwd: config2.projectRoot, taskId });
|
|
22139
22388
|
} catch {
|
|
22140
22389
|
}
|
|
22141
22390
|
return {
|
|
@@ -22161,7 +22410,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22161
22410
|
localPreview
|
|
22162
22411
|
};
|
|
22163
22412
|
}
|
|
22164
|
-
async function cancelBuild(adapter2, taskId, reason) {
|
|
22413
|
+
async function cancelBuild(adapter2, taskId, reason, projectRoot) {
|
|
22165
22414
|
const task = await adapter2.getTask(taskId);
|
|
22166
22415
|
if (!task) {
|
|
22167
22416
|
throw new Error(`Task "${taskId}" not found.`);
|
|
@@ -22170,6 +22419,18 @@ async function cancelBuild(adapter2, taskId, reason) {
|
|
|
22170
22419
|
status: "Cancelled",
|
|
22171
22420
|
closureReason: reason
|
|
22172
22421
|
});
|
|
22422
|
+
if (projectRoot) {
|
|
22423
|
+
if (hasLocalWorkspace()) {
|
|
22424
|
+
try {
|
|
22425
|
+
clearActiveTaskScope(projectRoot);
|
|
22426
|
+
} catch {
|
|
22427
|
+
}
|
|
22428
|
+
}
|
|
22429
|
+
try {
|
|
22430
|
+
clearBuildCheckpointIfLocal({ cwd: projectRoot, taskId });
|
|
22431
|
+
} catch {
|
|
22432
|
+
}
|
|
22433
|
+
}
|
|
22173
22434
|
return { task, reason };
|
|
22174
22435
|
}
|
|
22175
22436
|
|
|
@@ -22252,8 +22513,8 @@ ${instructions}`;
|
|
|
22252
22513
|
}
|
|
22253
22514
|
|
|
22254
22515
|
// src/tools/doc-registry.ts
|
|
22255
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
22256
|
-
import { join as
|
|
22516
|
+
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
22517
|
+
import { join as join13, relative } from "path";
|
|
22257
22518
|
import { homedir as homedir3 } from "os";
|
|
22258
22519
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
22259
22520
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
@@ -22451,7 +22712,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
22451
22712
|
const lines = docs.map((d) => {
|
|
22452
22713
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
22453
22714
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
22454
|
-
const missingNote = root && d.path && !
|
|
22715
|
+
const missingNote = root && d.path && !existsSync9(join13(root, d.path)) ? `
|
|
22455
22716
|
> \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there. Check \`git stash list\` for a papi-autostash entry, or re-create/deregister the doc.` : "";
|
|
22456
22717
|
return `### ${d.title}
|
|
22457
22718
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -22465,12 +22726,12 @@ ${d.summary}
|
|
|
22465
22726
|
${lines.join("\n---\n\n")}`);
|
|
22466
22727
|
}
|
|
22467
22728
|
function scanMdFiles(dir, rootDir) {
|
|
22468
|
-
if (!
|
|
22729
|
+
if (!existsSync9(dir)) return [];
|
|
22469
22730
|
const files = [];
|
|
22470
22731
|
try {
|
|
22471
22732
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
22472
22733
|
for (const entry of entries) {
|
|
22473
|
-
const full =
|
|
22734
|
+
const full = join13(dir, entry.name);
|
|
22474
22735
|
if (entry.isDirectory()) {
|
|
22475
22736
|
files.push(...scanMdFiles(full, rootDir));
|
|
22476
22737
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -22483,7 +22744,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
22483
22744
|
}
|
|
22484
22745
|
function extractTitle(filePath) {
|
|
22485
22746
|
try {
|
|
22486
|
-
const content =
|
|
22747
|
+
const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
|
|
22487
22748
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
22488
22749
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
22489
22750
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -22495,7 +22756,7 @@ function extractTitle(filePath) {
|
|
|
22495
22756
|
async function detectUnregisteredDocsNote(adapter2, config2) {
|
|
22496
22757
|
try {
|
|
22497
22758
|
if (!adapter2.searchDocs || !hasLocalWorkspace()) return "";
|
|
22498
|
-
const docsDir =
|
|
22759
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22499
22760
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22500
22761
|
if (docsFiles.length === 0) return "";
|
|
22501
22762
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -22521,17 +22782,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
22521
22782
|
const includePlans = args.include_plans ?? false;
|
|
22522
22783
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
22523
22784
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
22524
|
-
const docsDir =
|
|
22785
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22525
22786
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22526
22787
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
22527
22788
|
let unregisteredPlans = [];
|
|
22528
22789
|
if (includePlans) {
|
|
22529
|
-
const plansDir =
|
|
22530
|
-
if (
|
|
22790
|
+
const plansDir = join13(homedir3(), ".claude", "plans");
|
|
22791
|
+
if (existsSync9(plansDir)) {
|
|
22531
22792
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
22532
22793
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
22533
22794
|
path: f,
|
|
22534
|
-
title: extractTitle(
|
|
22795
|
+
title: extractTitle(join13(plansDir, f.replace("plans/", "")))
|
|
22535
22796
|
}));
|
|
22536
22797
|
}
|
|
22537
22798
|
}
|
|
@@ -22542,7 +22803,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
22542
22803
|
if (unregisteredDocs.length > 0) {
|
|
22543
22804
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
22544
22805
|
for (const f of unregisteredDocs) {
|
|
22545
|
-
const title = extractTitle(
|
|
22806
|
+
const title = extractTitle(join13(config2.projectRoot, f));
|
|
22546
22807
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
22547
22808
|
}
|
|
22548
22809
|
}
|
|
@@ -22715,97 +22976,6 @@ async function handleDocReorder(adapter2, args) {
|
|
|
22715
22976
|
|
|
22716
22977
|
// src/tools/build.ts
|
|
22717
22978
|
init_git();
|
|
22718
|
-
|
|
22719
|
-
// src/lib/build-checkpoint.ts
|
|
22720
|
-
import { createHash as createHash4 } from "crypto";
|
|
22721
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
22722
|
-
import { join as join13 } from "path";
|
|
22723
|
-
var BUILD_CHECKPOINT_VERSION = 1;
|
|
22724
|
-
function cwdHash(cwd) {
|
|
22725
|
-
return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
22726
|
-
}
|
|
22727
|
-
function safeTaskId(taskId) {
|
|
22728
|
-
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
22729
|
-
}
|
|
22730
|
-
function checkpointDir(cwd) {
|
|
22731
|
-
return join13(cwd, ".papi", "state");
|
|
22732
|
-
}
|
|
22733
|
-
function checkpointPath(cwd, taskId) {
|
|
22734
|
-
return join13(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
22735
|
-
}
|
|
22736
|
-
function writeBuildCheckpoint(input) {
|
|
22737
|
-
try {
|
|
22738
|
-
const dir = checkpointDir(input.cwd);
|
|
22739
|
-
if (!existsSync9(dir)) {
|
|
22740
|
-
mkdirSync3(dir, { recursive: true });
|
|
22741
|
-
}
|
|
22742
|
-
const checkpoint = {
|
|
22743
|
-
version: BUILD_CHECKPOINT_VERSION,
|
|
22744
|
-
taskId: input.taskId,
|
|
22745
|
-
branch: input.branch,
|
|
22746
|
-
step: input.step,
|
|
22747
|
-
lastCommitSha: input.lastCommitSha,
|
|
22748
|
-
modifiedFiles: input.modifiedFiles,
|
|
22749
|
-
cwd: realpathOrSelf(input.cwd),
|
|
22750
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22751
|
-
};
|
|
22752
|
-
writeFileSync4(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
22753
|
-
} catch {
|
|
22754
|
-
}
|
|
22755
|
-
}
|
|
22756
|
-
function readBuildCheckpoint(key) {
|
|
22757
|
-
try {
|
|
22758
|
-
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
22759
|
-
if (!existsSync9(path7)) return null;
|
|
22760
|
-
const parsed = JSON.parse(readFileSync8(path7, "utf-8"));
|
|
22761
|
-
if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
|
|
22762
|
-
return null;
|
|
22763
|
-
}
|
|
22764
|
-
return {
|
|
22765
|
-
version: BUILD_CHECKPOINT_VERSION,
|
|
22766
|
-
taskId: parsed.taskId,
|
|
22767
|
-
branch: parsed.branch ?? null,
|
|
22768
|
-
step: "branch_ready",
|
|
22769
|
-
lastCommitSha: parsed.lastCommitSha ?? null,
|
|
22770
|
-
modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
|
|
22771
|
-
cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
|
|
22772
|
-
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
|
|
22773
|
-
};
|
|
22774
|
-
} catch {
|
|
22775
|
-
return null;
|
|
22776
|
-
}
|
|
22777
|
-
}
|
|
22778
|
-
function clearBuildCheckpoint(key) {
|
|
22779
|
-
try {
|
|
22780
|
-
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
22781
|
-
if (existsSync9(path7)) {
|
|
22782
|
-
unlinkSync3(path7);
|
|
22783
|
-
}
|
|
22784
|
-
} catch {
|
|
22785
|
-
}
|
|
22786
|
-
}
|
|
22787
|
-
function formatResumeNote(cp) {
|
|
22788
|
-
const files = cp.modifiedFiles.filter((f) => f && f.trim());
|
|
22789
|
-
const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
|
|
22790
|
-
- \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
|
|
22791
|
-
const lines = [
|
|
22792
|
-
"> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
|
|
22793
|
-
`> - Branch: \`${cp.branch ?? "unknown"}\``,
|
|
22794
|
-
"> - Last step: branch ready",
|
|
22795
|
-
`> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
|
|
22796
|
-
`> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
|
|
22797
|
-
">",
|
|
22798
|
-
"> Modified files at last checkpoint:",
|
|
22799
|
-
...fileList.split("\n").map((l) => `> ${l}`),
|
|
22800
|
-
">",
|
|
22801
|
-
"> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
|
|
22802
|
-
"",
|
|
22803
|
-
""
|
|
22804
|
-
];
|
|
22805
|
-
return lines.join("\n");
|
|
22806
|
-
}
|
|
22807
|
-
|
|
22808
|
-
// src/tools/build.ts
|
|
22809
22979
|
var buildListTool = {
|
|
22810
22980
|
name: "build_list",
|
|
22811
22981
|
description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
|
|
@@ -23177,21 +23347,13 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
23177
23347
|
}
|
|
23178
23348
|
let resumeNote = "";
|
|
23179
23349
|
if (scopeTask?.status === "In Progress") {
|
|
23180
|
-
const existing =
|
|
23350
|
+
const existing = readBuildCheckpointIfLocal({ cwd: config2.projectRoot, taskId });
|
|
23181
23351
|
if (existing) resumeNote = formatResumeNote(existing);
|
|
23182
23352
|
}
|
|
23183
23353
|
await tracker.recordStep("started");
|
|
23184
23354
|
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
23185
23355
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
23186
23356
|
await tracker.recordStep("branch_ready");
|
|
23187
|
-
writeBuildCheckpoint({
|
|
23188
|
-
cwd: config2.projectRoot,
|
|
23189
|
-
taskId,
|
|
23190
|
-
branch: getCurrentBranch(config2.projectRoot),
|
|
23191
|
-
step: "branch_ready",
|
|
23192
|
-
lastCommitSha: getHeadCommitSha(config2.projectRoot),
|
|
23193
|
-
modifiedFiles: getModifiedFiles(config2.projectRoot)
|
|
23194
|
-
});
|
|
23195
23357
|
tracker.mark("start_decorate_handoff");
|
|
23196
23358
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
23197
23359
|
const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
|
|
@@ -23410,7 +23572,6 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
23410
23572
|
preview
|
|
23411
23573
|
}, { light }, clientName);
|
|
23412
23574
|
tracker.mark("complete_format");
|
|
23413
|
-
clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
23414
23575
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
23415
23576
|
await tracker.recordStep("report_written");
|
|
23416
23577
|
if ((result.autoTriagedCount ?? 0) > 0) {
|
|
@@ -23607,7 +23768,7 @@ function formatCompleteResult(result) {
|
|
|
23607
23768
|
}
|
|
23608
23769
|
return lines.join("\n");
|
|
23609
23770
|
}
|
|
23610
|
-
async function handleBuildCancel(adapter2, args) {
|
|
23771
|
+
async function handleBuildCancel(adapter2, config2, args) {
|
|
23611
23772
|
const taskId = args.task_id;
|
|
23612
23773
|
if (!taskId) {
|
|
23613
23774
|
return errorResponse("task_id is required.");
|
|
@@ -23617,7 +23778,7 @@ async function handleBuildCancel(adapter2, args) {
|
|
|
23617
23778
|
return errorResponse("reason is required.");
|
|
23618
23779
|
}
|
|
23619
23780
|
try {
|
|
23620
|
-
const result = await cancelBuild(adapter2, taskId, reason);
|
|
23781
|
+
const result = await cancelBuild(adapter2, taskId, reason, config2.projectRoot);
|
|
23621
23782
|
return textResponse(`Cancelled **${result.task.id}** (${result.task.title}).
|
|
23622
23783
|
|
|
23623
23784
|
Reason: ${result.reason}`);
|
|
@@ -27133,9 +27294,15 @@ var TASK_REF = /\btask-\d+\b/gi;
|
|
|
27133
27294
|
var RECENT_CYCLE_WINDOW = 5;
|
|
27134
27295
|
var MAX_CANDIDATES = 5;
|
|
27135
27296
|
async function findUnblockCandidates(adapter2, currentCycle) {
|
|
27297
|
+
try {
|
|
27298
|
+
const blockedProbe = await adapter2.queryBoard({ status: ["Blocked"], compact: true });
|
|
27299
|
+
if (blockedProbe.length === 0) return [];
|
|
27300
|
+
} catch {
|
|
27301
|
+
return [];
|
|
27302
|
+
}
|
|
27136
27303
|
let allTasks = [];
|
|
27137
27304
|
try {
|
|
27138
|
-
allTasks = await adapter2.queryBoard();
|
|
27305
|
+
allTasks = await adapter2.queryBoard({ compact: true });
|
|
27139
27306
|
} catch {
|
|
27140
27307
|
return [];
|
|
27141
27308
|
}
|
|
@@ -27721,7 +27888,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
27721
27888
|
lines.push(`**Nearing Closure:** ${hierarchy.phasesNearingClosure.join(", ")}`);
|
|
27722
27889
|
}
|
|
27723
27890
|
if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
|
|
27724
|
-
|
|
27891
|
+
const crit = hierarchy.stageExitCriteria;
|
|
27892
|
+
const met = crit.filter((c) => c.met).length;
|
|
27893
|
+
const total = crit.length;
|
|
27894
|
+
lines.push(`**Stage Exit Criteria [${met}/${total} met]:** ${crit.map((c) => `${c.met ? "[x]" : "[ ]"} ${c.text}`).join(" | ")}`);
|
|
27895
|
+
if (met === total) {
|
|
27896
|
+
lines.push(" \u21B3 All exit criteria met \u2014 run `strategy_review` to propose advancing the stage.");
|
|
27897
|
+
}
|
|
27725
27898
|
}
|
|
27726
27899
|
lines.push("");
|
|
27727
27900
|
}
|
|
@@ -27926,9 +28099,9 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
27926
28099
|
for (const issue of alerts) {
|
|
27927
28100
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
27928
28101
|
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
27929
|
-
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
28102
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})${issue.id ? ` \xB7 id \`${issue.id}\`` : ""}`);
|
|
27930
28103
|
}
|
|
27931
|
-
lines.push("
|
|
28104
|
+
lines.push("_Already fixed? `discovered_issue_resolve <id>` clears it for good (won't reappear). Not yet? `idea` with P1 priority to log it as a backlog task._");
|
|
27932
28105
|
alertsNote = lines.join("\n");
|
|
27933
28106
|
}
|
|
27934
28107
|
if (allLowSev.length > 0) {
|
|
@@ -27938,18 +28111,18 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
27938
28111
|
for (const issue of unactioned) {
|
|
27939
28112
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
27940
28113
|
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
27941
|
-
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
28114
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})${issue.id ? ` \xB7 id \`${issue.id}\`` : ""}`);
|
|
27942
28115
|
}
|
|
27943
|
-
lines.push("
|
|
28116
|
+
lines.push("_Already fixed? `discovered_issue_resolve <id>` clears it. Otherwise `idea` to log as a backlog task._");
|
|
27944
28117
|
unactionedIssuesNote = lines.join("\n");
|
|
27945
28118
|
}
|
|
27946
28119
|
return { alertsNote, unactionedIssuesNote };
|
|
27947
28120
|
}
|
|
27948
|
-
async function computeTeamSummary(adapter2) {
|
|
28121
|
+
async function computeTeamSummary(adapter2, contributorsInput) {
|
|
27949
28122
|
if (typeof adapter2.listContributors !== "function") return void 0;
|
|
27950
28123
|
let members;
|
|
27951
28124
|
try {
|
|
27952
|
-
members = (await adapter2.listContributors()).length;
|
|
28125
|
+
members = (await (contributorsInput ?? adapter2.listContributors())).length;
|
|
27953
28126
|
} catch {
|
|
27954
28127
|
return void 0;
|
|
27955
28128
|
}
|
|
@@ -27966,11 +28139,11 @@ async function computeTeamSummary(adapter2) {
|
|
|
27966
28139
|
const reviewQueue = tasks.filter((t) => t.status === "In Review").length;
|
|
27967
28140
|
return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
|
|
27968
28141
|
}
|
|
27969
|
-
async function computeReleaseHistory(adapter2) {
|
|
28142
|
+
async function computeReleaseHistory(adapter2, contributorsInput) {
|
|
27970
28143
|
if (typeof adapter2.listContributors !== "function") return void 0;
|
|
27971
28144
|
let contributors;
|
|
27972
28145
|
try {
|
|
27973
|
-
contributors = await adapter2.listContributors();
|
|
28146
|
+
contributors = await (contributorsInput ?? adapter2.listContributors());
|
|
27974
28147
|
} catch {
|
|
27975
28148
|
return void 0;
|
|
27976
28149
|
}
|
|
@@ -28301,13 +28474,13 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28301
28474
|
// "writing to the wrong project" for multi-project users on a shared key / stateless
|
|
28302
28475
|
// HTTP transport where a once-per-session gate can't work.
|
|
28303
28476
|
tracked("project-banner", async () => {
|
|
28304
|
-
if (!adapter2.getProjectInfo) return { banner: "", name: void 0 };
|
|
28477
|
+
if (!adapter2.getProjectInfo) return { banner: "", name: void 0, repoConnected: void 0 };
|
|
28305
28478
|
const projectInfo = await adapter2.getProjectInfo();
|
|
28306
|
-
if (!projectInfo) return { banner: "", name: void 0 };
|
|
28479
|
+
if (!projectInfo) return { banner: "", name: void 0, repoConnected: void 0 };
|
|
28307
28480
|
const banner = getProjectConnectionBanner(projectInfo.name, projectInfo.slug);
|
|
28308
28481
|
return { banner: banner ? `
|
|
28309
28482
|
> ${banner}
|
|
28310
|
-
` : "", name: projectInfo.name };
|
|
28483
|
+
` : "", name: projectInfo.name, repoConnected: !!projectInfo.repo_url };
|
|
28311
28484
|
}),
|
|
28312
28485
|
// Session guidance — proactive nudges (doc_register, context bloat, mode switch)
|
|
28313
28486
|
tracked("session-guidance", async () => {
|
|
@@ -28445,7 +28618,7 @@ ${versionDrift}` : "";
|
|
|
28445
28618
|
const patternsNote = patternsOutcome.status === "fulfilled" ? patternsOutcome.value : "";
|
|
28446
28619
|
const { alertsNote, unactionedIssuesNote } = discoveredIssuesOutcome.status === "fulfilled" ? discoveredIssuesOutcome.value : { alertsNote: "", unactionedIssuesNote: "" };
|
|
28447
28620
|
const skillProposalsNote = skillScanOutcome.status === "fulfilled" ? skillScanOutcome.value : "";
|
|
28448
|
-
const projectBannerResult = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : { banner: "", name: void 0 };
|
|
28621
|
+
const projectBannerResult = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : { banner: "", name: void 0, repoConnected: void 0 };
|
|
28449
28622
|
const projectBannerNote = projectBannerResult.banner;
|
|
28450
28623
|
const projectName = projectBannerResult.name;
|
|
28451
28624
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
@@ -28486,16 +28659,27 @@ ${versionDrift}` : "";
|
|
|
28486
28659
|
preBuildCheckNote = lines.join("\n");
|
|
28487
28660
|
}
|
|
28488
28661
|
}
|
|
28489
|
-
tracker.mark("
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
|
|
28493
|
-
|
|
28494
|
-
|
|
28662
|
+
tracker.mark("parallel-tail");
|
|
28663
|
+
const sharedContributorsPromise = typeof adapter2.listContributors === "function" ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
28664
|
+
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
|
|
28665
|
+
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
|
|
28666
|
+
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
28667
|
+
listAgents(config2.projectRoot),
|
|
28668
|
+
// task-2071 (MU-3) + task-2072 (MU-5): team summary + release-history — both
|
|
28669
|
+
// multi-member only; solo projects get undefined from both, so orient stays
|
|
28670
|
+
// byte-identical there.
|
|
28671
|
+
tracked("team-summary", () => computeTeamSummary(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
28672
|
+
tracked("release-history", () => computeReleaseHistory(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
28673
|
+
// task-2751 (C332): resolve every task-NNNN mentioned in the Carry-Forward
|
|
28674
|
+
// prose to its title so orient can name it inline. Built from the board
|
|
28675
|
+
// already in hand — no extra query.
|
|
28676
|
+
resolveCarryForwardRefs(healthResult.carryForward, allTasks, adapter2)
|
|
28677
|
+
]);
|
|
28678
|
+
const unblockSection = formatUnblockSection(unblockCandidates);
|
|
28679
|
+
const unblockNote = unblockSection ? `
|
|
28495
28680
|
|
|
28496
|
-
${
|
|
28497
|
-
|
|
28498
|
-
}
|
|
28681
|
+
${unblockSection}` : "";
|
|
28682
|
+
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
28499
28683
|
let deferredGateNote = "";
|
|
28500
28684
|
if (deepHousekeeping) {
|
|
28501
28685
|
try {
|
|
@@ -28507,16 +28691,22 @@ ${section}`;
|
|
|
28507
28691
|
} catch {
|
|
28508
28692
|
}
|
|
28509
28693
|
}
|
|
28694
|
+
let onboardingCoachingNote = "";
|
|
28695
|
+
try {
|
|
28696
|
+
const adsForCoaching = await sharedActiveDecisionsPromise;
|
|
28697
|
+
onboardingCoachingNote = formatOnboardingCoachingBlock({
|
|
28698
|
+
surface: "orient",
|
|
28699
|
+
repoConnected: projectBannerResult.repoConnected,
|
|
28700
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
28701
|
+
hasActiveDecisions: adsForCoaching.length > 0,
|
|
28702
|
+
cycleNumber: currentCycle,
|
|
28703
|
+
hasActiveCycle: currentCycle > 0 && !cycleIsComplete
|
|
28704
|
+
});
|
|
28705
|
+
} catch {
|
|
28706
|
+
}
|
|
28510
28707
|
tracker.mark("format-summary");
|
|
28511
|
-
const subAgents = await listAgents(config2.projectRoot);
|
|
28512
|
-
const [teamSummaryLine, releaseHistoryLine] = await Promise.all([
|
|
28513
|
-
tracked("team-summary", () => computeTeamSummary(adapter2))().catch(() => void 0),
|
|
28514
|
-
tracked("release-history", () => computeReleaseHistory(adapter2))().catch(() => void 0)
|
|
28515
|
-
]);
|
|
28516
|
-
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
28517
28708
|
const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
|
|
28518
|
-
|
|
28519
|
-
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
28709
|
+
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
28520
28710
|
} catch (err) {
|
|
28521
28711
|
const message = err instanceof Error ? err.message : String(err);
|
|
28522
28712
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -28571,7 +28761,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
28571
28761
|
// src/tools/hierarchy.ts
|
|
28572
28762
|
var hierarchyUpdateTool = {
|
|
28573
28763
|
name: "hierarchy_update",
|
|
28574
|
-
description: "
|
|
28764
|
+
description: "Create or update a horizon, stage, or phase in the project hierarchy (AD-14). For stages and horizons this UPSERTS: if the named entity exists it is updated, otherwise it is CREATED (pass a label). Phases are update-only (they evolve via plan/strategy). For stages you can set exit_criteria (a checklist), or flip a single criterion with set_criterion_met. NEVER auto-advances \u2014 progression is human-in-loop via strategy_review. Does not call the Anthropic API.",
|
|
28575
28765
|
annotations: { title: "Update Hierarchy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
28576
28766
|
inputSchema: {
|
|
28577
28767
|
type: "object",
|
|
@@ -28579,129 +28769,218 @@ var hierarchyUpdateTool = {
|
|
|
28579
28769
|
level: {
|
|
28580
28770
|
type: "string",
|
|
28581
28771
|
enum: ["phase", "stage", "horizon"],
|
|
28582
|
-
description: "Which hierarchy level to update."
|
|
28772
|
+
description: "Which hierarchy level to create/update."
|
|
28583
28773
|
},
|
|
28584
28774
|
name: {
|
|
28585
28775
|
type: "string",
|
|
28586
|
-
description: "The label or ID of the
|
|
28776
|
+
description: "The label, slug, or ID of the entity. On create, becomes the label if `label` is omitted."
|
|
28777
|
+
},
|
|
28778
|
+
label: {
|
|
28779
|
+
type: "string",
|
|
28780
|
+
description: 'Display label. Required to CREATE a new stage/horizon when `name` does not match an existing one (e.g. "S2: Alpha Cohort").'
|
|
28781
|
+
},
|
|
28782
|
+
slug: {
|
|
28783
|
+
type: "string",
|
|
28784
|
+
description: "Explicit slug for a new entity (stage/horizon). Auto-derived from the label when omitted."
|
|
28785
|
+
},
|
|
28786
|
+
description: {
|
|
28787
|
+
type: "string",
|
|
28788
|
+
description: "Optional longer description (stage/horizon)."
|
|
28587
28789
|
},
|
|
28588
28790
|
status: {
|
|
28589
28791
|
type: "string",
|
|
28590
28792
|
enum: ["Not Started", "In Progress", "Done", "Deferred"],
|
|
28591
|
-
description:
|
|
28793
|
+
description: 'The status to set. On create, defaults to "Not Started".'
|
|
28794
|
+
},
|
|
28795
|
+
sort_order: {
|
|
28796
|
+
type: "number",
|
|
28797
|
+
description: "Display order for a new entity. Auto-computed (max existing + 10) when omitted."
|
|
28798
|
+
},
|
|
28799
|
+
horizon: {
|
|
28800
|
+
type: "string",
|
|
28801
|
+
description: "Parent horizon (name/slug/id) when CREATING a stage. Defaults to the sole horizon if only one exists."
|
|
28592
28802
|
},
|
|
28593
28803
|
exit_criteria: {
|
|
28594
28804
|
type: "array",
|
|
28595
28805
|
items: { type: "string" },
|
|
28596
|
-
description:
|
|
28806
|
+
description: "Checklist defining when a STAGE is done. Each item is a completion condition. REPLACES existing criteria (resets met state \u2014 use set_criterion_met to flip one)."
|
|
28807
|
+
},
|
|
28808
|
+
set_criterion_met: {
|
|
28809
|
+
type: "object",
|
|
28810
|
+
description: "Flip a single stage exit criterion (task-1625). { criterion_id, met, evidence? }.",
|
|
28811
|
+
properties: {
|
|
28812
|
+
criterion_id: { type: "string", description: "The ExitCriterion id to flip." },
|
|
28813
|
+
met: { type: "boolean", description: "true = met, false = unmet." },
|
|
28814
|
+
evidence: { type: "string", description: 'Optional evidence for a met criterion (e.g. "task-1234 shipped").' }
|
|
28815
|
+
},
|
|
28816
|
+
required: ["criterion_id", "met"]
|
|
28597
28817
|
}
|
|
28598
28818
|
},
|
|
28599
28819
|
required: ["level", "name"]
|
|
28600
28820
|
}
|
|
28601
28821
|
};
|
|
28602
28822
|
var VALID_STATUSES3 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
|
|
28823
|
+
function slugify(input) {
|
|
28824
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
|
|
28825
|
+
}
|
|
28826
|
+
function nextSortOrder(existing) {
|
|
28827
|
+
if (existing.length === 0) return 10;
|
|
28828
|
+
return Math.max(...existing.map((e) => e.sortOrder)) + 10;
|
|
28829
|
+
}
|
|
28603
28830
|
async function handleHierarchyUpdate(adapter2, args) {
|
|
28604
28831
|
const level = args.level;
|
|
28605
28832
|
const name = args.name;
|
|
28833
|
+
const label = args.label;
|
|
28834
|
+
const slug = args.slug;
|
|
28835
|
+
const description = args.description;
|
|
28606
28836
|
const status = args.status;
|
|
28837
|
+
const sortOrder = typeof args.sort_order === "number" ? args.sort_order : void 0;
|
|
28838
|
+
const horizonRef = args.horizon;
|
|
28607
28839
|
const exitCriteria = args.exit_criteria;
|
|
28840
|
+
const setCriterion = args.set_criterion_met;
|
|
28608
28841
|
if (!level || !name) {
|
|
28609
28842
|
return errorResponse("Missing required parameters: level, name.");
|
|
28610
28843
|
}
|
|
28611
|
-
if (!status && !exitCriteria) {
|
|
28612
|
-
return errorResponse("Nothing to update. Provide at least one of: status, exit_criteria.");
|
|
28613
|
-
}
|
|
28614
28844
|
if (level !== "phase" && level !== "stage" && level !== "horizon") {
|
|
28615
28845
|
return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
|
|
28616
28846
|
}
|
|
28617
28847
|
if (status && !VALID_STATUSES3.has(status)) {
|
|
28618
28848
|
return errorResponse(`Invalid status "${status}". Must be one of: Not Started, In Progress, Done, Deferred.`);
|
|
28619
28849
|
}
|
|
28620
|
-
if (exitCriteria !== void 0 && level !== "stage") {
|
|
28621
|
-
return errorResponse("exit_criteria can only be
|
|
28850
|
+
if ((exitCriteria !== void 0 || setCriterion !== void 0) && level !== "stage") {
|
|
28851
|
+
return errorResponse("exit_criteria and set_criterion_met can only be used on stages.");
|
|
28852
|
+
}
|
|
28853
|
+
if (!status && exitCriteria === void 0 && setCriterion === void 0 && !label) {
|
|
28854
|
+
return errorResponse("Nothing to do. Provide at least one of: status, exit_criteria, set_criterion_met, or label (to create).");
|
|
28622
28855
|
}
|
|
28623
28856
|
try {
|
|
28624
28857
|
if (level === "phase") {
|
|
28625
|
-
|
|
28626
|
-
return errorResponse("Phase management is not supported by the current adapter.");
|
|
28627
|
-
}
|
|
28628
|
-
const phases = await adapter2.readPhases();
|
|
28629
|
-
const phase = phases.find(
|
|
28630
|
-
(p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name
|
|
28631
|
-
);
|
|
28632
|
-
if (!phase) {
|
|
28633
|
-
const available = phases.map((p) => p.label).join(", ");
|
|
28634
|
-
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"}`);
|
|
28635
|
-
}
|
|
28636
|
-
if (!status) {
|
|
28637
|
-
return errorResponse("status is required for phase updates.");
|
|
28638
|
-
}
|
|
28639
|
-
if (phase.status === status) {
|
|
28640
|
-
return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
28641
|
-
}
|
|
28642
|
-
const oldStatus2 = phase.status;
|
|
28643
|
-
await adapter2.updatePhaseStatus(phase.id, status);
|
|
28644
|
-
return textResponse(`Phase updated: **${phase.label}** ${oldStatus2} \u2192 ${status}`);
|
|
28858
|
+
return await handlePhase(adapter2, name, status);
|
|
28645
28859
|
}
|
|
28646
28860
|
if (level === "stage") {
|
|
28647
|
-
|
|
28648
|
-
return errorResponse("Stage management is not supported by the current adapter.");
|
|
28649
|
-
}
|
|
28650
|
-
const stages = await adapter2.readStages();
|
|
28651
|
-
const stage = stages.find(
|
|
28652
|
-
(s) => s.label.toLowerCase() === name.toLowerCase() || s.id === name || s.slug === name
|
|
28653
|
-
);
|
|
28654
|
-
if (!stage) {
|
|
28655
|
-
const available = stages.map((s) => s.label).join(", ");
|
|
28656
|
-
return errorResponse(`Stage "${name}" not found. Available stages: ${available || "none"}`);
|
|
28657
|
-
}
|
|
28658
|
-
const resultLines = [];
|
|
28659
|
-
if (status) {
|
|
28660
|
-
if (!adapter2.updateStageStatus) {
|
|
28661
|
-
return errorResponse("Stage status updates are not supported by the current adapter.");
|
|
28662
|
-
}
|
|
28663
|
-
if (stage.status === status) {
|
|
28664
|
-
resultLines.push(`Stage "${stage.label}" is already "${status}".`);
|
|
28665
|
-
} else {
|
|
28666
|
-
const oldStatus2 = stage.status;
|
|
28667
|
-
await adapter2.updateStageStatus(stage.id, status);
|
|
28668
|
-
resultLines.push(`Stage updated: **${stage.label}** ${oldStatus2} \u2192 ${status}`);
|
|
28669
|
-
}
|
|
28670
|
-
}
|
|
28671
|
-
if (exitCriteria !== void 0) {
|
|
28672
|
-
if (!adapter2.updateStageExitCriteria) {
|
|
28673
|
-
return errorResponse("Exit criteria updates are not supported by the current adapter.");
|
|
28674
|
-
}
|
|
28675
|
-
await adapter2.updateStageExitCriteria(stage.id, exitCriteria);
|
|
28676
|
-
resultLines.push(`Exit criteria set (${exitCriteria.length} item${exitCriteria.length !== 1 ? "s" : ""}):`);
|
|
28677
|
-
exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
|
|
28678
|
-
}
|
|
28679
|
-
return textResponse(resultLines.join("\n"));
|
|
28680
|
-
}
|
|
28681
|
-
if (!adapter2.readHorizons || !adapter2.updateHorizonStatus) {
|
|
28682
|
-
return errorResponse("Horizon management is not supported by the current adapter.");
|
|
28683
|
-
}
|
|
28684
|
-
const horizons = await adapter2.readHorizons();
|
|
28685
|
-
const horizon = horizons.find(
|
|
28686
|
-
(h) => h.label.toLowerCase() === name.toLowerCase() || h.id === name || h.slug === name
|
|
28687
|
-
);
|
|
28688
|
-
if (!horizon) {
|
|
28689
|
-
const available = horizons.map((h) => h.label).join(", ");
|
|
28690
|
-
return errorResponse(`Horizon "${name}" not found. Available horizons: ${available || "none"}`);
|
|
28861
|
+
return await handleStage(adapter2, { name, label, slug, description, status, sortOrder, horizonRef, exitCriteria, setCriterion });
|
|
28691
28862
|
}
|
|
28692
|
-
|
|
28693
|
-
return errorResponse("status is required for horizon updates.");
|
|
28694
|
-
}
|
|
28695
|
-
if (horizon.status === status) {
|
|
28696
|
-
return textResponse(`Horizon "${horizon.label}" is already "${status}". No change made.`);
|
|
28697
|
-
}
|
|
28698
|
-
const oldStatus = horizon.status;
|
|
28699
|
-
await adapter2.updateHorizonStatus(horizon.id, status);
|
|
28700
|
-
return textResponse(`Horizon updated: **${horizon.label}** ${oldStatus} \u2192 ${status}`);
|
|
28863
|
+
return await handleHorizon(adapter2, { name, label, slug, description, status, sortOrder });
|
|
28701
28864
|
} catch (err) {
|
|
28702
28865
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
28703
28866
|
}
|
|
28704
28867
|
}
|
|
28868
|
+
async function handlePhase(adapter2, name, status) {
|
|
28869
|
+
if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
|
|
28870
|
+
return errorResponse("Phase management is not supported by the current adapter.");
|
|
28871
|
+
}
|
|
28872
|
+
if (!status) return errorResponse("status is required for phase updates.");
|
|
28873
|
+
const phases = await adapter2.readPhases();
|
|
28874
|
+
const phase = phases.find((p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name);
|
|
28875
|
+
if (!phase) {
|
|
28876
|
+
const available = phases.map((p) => p.label).join(", ");
|
|
28877
|
+
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"} (phases are update-only \u2014 create them via plan/setup).`);
|
|
28878
|
+
}
|
|
28879
|
+
if (phase.status === status) return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
28880
|
+
const oldStatus = phase.status;
|
|
28881
|
+
await adapter2.updatePhaseStatus(phase.id, status);
|
|
28882
|
+
return textResponse(`Phase updated: **${phase.label}** ${oldStatus} \u2192 ${status}`);
|
|
28883
|
+
}
|
|
28884
|
+
async function handleStage(adapter2, a) {
|
|
28885
|
+
if (!adapter2.readStages) {
|
|
28886
|
+
return errorResponse("Stage management is not supported by the current adapter.");
|
|
28887
|
+
}
|
|
28888
|
+
const stages = await adapter2.readStages();
|
|
28889
|
+
let stage = stages.find(
|
|
28890
|
+
(s) => s.label.toLowerCase() === a.name.toLowerCase() || s.id === a.name || s.slug === a.name
|
|
28891
|
+
);
|
|
28892
|
+
const resultLines = [];
|
|
28893
|
+
let created = false;
|
|
28894
|
+
if (!stage) {
|
|
28895
|
+
if (!adapter2.createHorizon || !adapter2.createStage || !adapter2.readHorizons) {
|
|
28896
|
+
return errorResponse("Stage creation is not supported by the current adapter.");
|
|
28897
|
+
}
|
|
28898
|
+
const createLabel = a.label ?? a.name;
|
|
28899
|
+
const horizons = await adapter2.readHorizons();
|
|
28900
|
+
let parent;
|
|
28901
|
+
if (a.horizonRef) {
|
|
28902
|
+
parent = horizons.find((h) => h.label.toLowerCase() === a.horizonRef.toLowerCase() || h.id === a.horizonRef || h.slug === a.horizonRef);
|
|
28903
|
+
if (!parent) {
|
|
28904
|
+
const available = horizons.map((h) => h.label).join(", ");
|
|
28905
|
+
return errorResponse(`Parent horizon "${a.horizonRef}" not found. Available horizons: ${available || "none"}.`);
|
|
28906
|
+
}
|
|
28907
|
+
} else if (horizons.length === 1) {
|
|
28908
|
+
parent = horizons[0];
|
|
28909
|
+
} else if (horizons.length === 0) {
|
|
28910
|
+
return errorResponse('No horizon exists to attach the stage to. Create a horizon first (level:"horizon", label:"H1: ...").');
|
|
28911
|
+
} else {
|
|
28912
|
+
return errorResponse(`Multiple horizons exist \u2014 pass \`horizon\` to say which one the stage belongs to: ${horizons.map((h) => h.label).join(", ")}.`);
|
|
28913
|
+
}
|
|
28914
|
+
const newId = await adapter2.createStage({
|
|
28915
|
+
slug: a.slug ?? slugify(createLabel),
|
|
28916
|
+
label: createLabel,
|
|
28917
|
+
description: a.description,
|
|
28918
|
+
status: a.status ?? "Not Started",
|
|
28919
|
+
sortOrder: a.sortOrder ?? nextSortOrder(stages.filter((s) => s.horizonId === parent.id)),
|
|
28920
|
+
horizonId: parent.id
|
|
28921
|
+
});
|
|
28922
|
+
const refreshed = await adapter2.readStages();
|
|
28923
|
+
stage = refreshed.find((s) => s.id === newId);
|
|
28924
|
+
if (!stage) return errorResponse("Stage was created but could not be re-read.");
|
|
28925
|
+
created = true;
|
|
28926
|
+
resultLines.push(`Stage created: **${stage.label}** (under ${parent.label}, status ${stage.status})`);
|
|
28927
|
+
}
|
|
28928
|
+
if (a.status && !created) {
|
|
28929
|
+
if (!adapter2.updateStageStatus) return errorResponse("Stage status updates are not supported by the current adapter.");
|
|
28930
|
+
if (stage.status === a.status) {
|
|
28931
|
+
resultLines.push(`Stage "${stage.label}" is already "${a.status}".`);
|
|
28932
|
+
} else {
|
|
28933
|
+
const oldStatus = stage.status;
|
|
28934
|
+
await adapter2.updateStageStatus(stage.id, a.status);
|
|
28935
|
+
resultLines.push(`Stage updated: **${stage.label}** ${oldStatus} \u2192 ${a.status}`);
|
|
28936
|
+
}
|
|
28937
|
+
}
|
|
28938
|
+
if (a.exitCriteria !== void 0) {
|
|
28939
|
+
if (!adapter2.updateStageExitCriteria) return errorResponse("Exit criteria updates are not supported by the current adapter.");
|
|
28940
|
+
await adapter2.updateStageExitCriteria(stage.id, a.exitCriteria);
|
|
28941
|
+
resultLines.push(`Exit criteria set (${a.exitCriteria.length} item${a.exitCriteria.length !== 1 ? "s" : ""}, all unmet):`);
|
|
28942
|
+
a.exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
|
|
28943
|
+
}
|
|
28944
|
+
if (a.setCriterion !== void 0) {
|
|
28945
|
+
if (!adapter2.setCriterionMet) return errorResponse("set_criterion_met is not supported by the current adapter.");
|
|
28946
|
+
await adapter2.setCriterionMet(stage.id, a.setCriterion.criterion_id, a.setCriterion.met, a.setCriterion.evidence ?? null);
|
|
28947
|
+
const after = (await adapter2.readStages()).find((s) => s.id === stage.id);
|
|
28948
|
+
const crit = after?.exitCriteria?.find((c) => c.id === a.setCriterion.criterion_id);
|
|
28949
|
+
if (!crit) {
|
|
28950
|
+
resultLines.push(`\u26A0\uFE0F Criterion id "${a.setCriterion.criterion_id}" not found on this stage \u2014 no change.`);
|
|
28951
|
+
} else {
|
|
28952
|
+
const met = after?.exitCriteria?.filter((c) => c.met).length ?? 0;
|
|
28953
|
+
const total = after?.exitCriteria?.length ?? 0;
|
|
28954
|
+
resultLines.push(`Criterion "${crit.text}" \u2192 ${crit.met ? "met" : "unmet"}. [${met}/${total} criteria met]`);
|
|
28955
|
+
}
|
|
28956
|
+
}
|
|
28957
|
+
return textResponse(resultLines.join("\n"));
|
|
28958
|
+
}
|
|
28959
|
+
async function handleHorizon(adapter2, a) {
|
|
28960
|
+
if (!adapter2.readHorizons) {
|
|
28961
|
+
return errorResponse("Horizon management is not supported by the current adapter.");
|
|
28962
|
+
}
|
|
28963
|
+
const horizons = await adapter2.readHorizons();
|
|
28964
|
+
const horizon = horizons.find((h) => h.label.toLowerCase() === a.name.toLowerCase() || h.id === a.name || h.slug === a.name);
|
|
28965
|
+
if (!horizon) {
|
|
28966
|
+
if (!adapter2.createHorizon) return errorResponse("Horizon creation is not supported by the current adapter.");
|
|
28967
|
+
const createLabel = a.label ?? a.name;
|
|
28968
|
+
const newId = await adapter2.createHorizon({
|
|
28969
|
+
slug: a.slug ?? slugify(createLabel),
|
|
28970
|
+
label: createLabel,
|
|
28971
|
+
description: a.description,
|
|
28972
|
+
status: a.status ?? "Not Started",
|
|
28973
|
+
sortOrder: a.sortOrder ?? nextSortOrder(horizons)
|
|
28974
|
+
});
|
|
28975
|
+
return textResponse(`Horizon created: **${createLabel}** (id ${newId}, status ${a.status ?? "Not Started"})`);
|
|
28976
|
+
}
|
|
28977
|
+
if (!a.status) return errorResponse("status is required to update an existing horizon (or pass a new name to create one).");
|
|
28978
|
+
if (horizon.status === a.status) return textResponse(`Horizon "${horizon.label}" is already "${a.status}". No change made.`);
|
|
28979
|
+
if (!adapter2.updateHorizonStatus) return errorResponse("Horizon status updates are not supported by the current adapter.");
|
|
28980
|
+
const oldStatus = horizon.status;
|
|
28981
|
+
await adapter2.updateHorizonStatus(horizon.id, a.status);
|
|
28982
|
+
return textResponse(`Horizon updated: **${horizon.label}** ${oldStatus} \u2192 ${a.status}`);
|
|
28983
|
+
}
|
|
28705
28984
|
|
|
28706
28985
|
// src/services/zoom-out.ts
|
|
28707
28986
|
var BUDGET_SOFT = 12e4;
|
|
@@ -30887,7 +31166,7 @@ function createServer(adapter2, config2) {
|
|
|
30887
31166
|
case "build_execute":
|
|
30888
31167
|
return handleBuildExecute(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
30889
31168
|
case "build_cancel":
|
|
30890
|
-
return handleBuildCancel(adapter2, safeArgs);
|
|
31169
|
+
return handleBuildCancel(adapter2, config2, safeArgs);
|
|
30891
31170
|
case "idea":
|
|
30892
31171
|
return handleIdea(adapter2, config2, safeArgs);
|
|
30893
31172
|
case "backlog_import":
|