@papi-ai/server 0.7.69 → 0.7.70
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 +568 -318
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10729,6 +10729,14 @@ function validateHandoffScope(handoff) {
|
|
|
10729
10729
|
if (!isMeaningful(handoff.scopeBoundary)) invalid.push("scopeBoundary");
|
|
10730
10730
|
return invalid;
|
|
10731
10731
|
}
|
|
10732
|
+
function assertHandoffApplyPayloadNonEmpty(data, cycleNumber) {
|
|
10733
|
+
const handoffCount = data.cycleHandoffs?.length ?? 0;
|
|
10734
|
+
if (handoffCount === 0) {
|
|
10735
|
+
throw new Error(
|
|
10736
|
+
`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.`
|
|
10737
|
+
);
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10732
10740
|
async function prepareHandoffs(adapter2, _config, taskIds, force = false) {
|
|
10733
10741
|
const timer2 = startTimer();
|
|
10734
10742
|
const cycles = await adapter2.readCycles();
|
|
@@ -10785,10 +10793,8 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10785
10793
|
if (!data) {
|
|
10786
10794
|
throw new Error("Could not parse structured output. Ensure your output includes <!-- PAPI_STRUCTURED_OUTPUT --> with valid JSON.");
|
|
10787
10795
|
}
|
|
10796
|
+
assertHandoffApplyPayloadNonEmpty(data, cycleNumber);
|
|
10788
10797
|
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
10798
|
const taskIdsToWrite = handoffs.map((h) => h.taskId);
|
|
10793
10799
|
const existingHandoffSet = /* @__PURE__ */ new Set();
|
|
10794
10800
|
try {
|
|
@@ -13662,6 +13668,58 @@ async function buildSessionGuidance(callerKey) {
|
|
|
13662
13668
|
return signals.slice(0, 3);
|
|
13663
13669
|
}
|
|
13664
13670
|
|
|
13671
|
+
// src/services/onboarding-coaching.ts
|
|
13672
|
+
var ONBOARDING_EARLY_CYCLE_MAX = 2;
|
|
13673
|
+
var MAX_COACHING_LINES = 4;
|
|
13674
|
+
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.";
|
|
13675
|
+
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.";
|
|
13676
|
+
var COACH_CLICKABLE_TASKS = "Task cards on your dashboard expand on click. Open one to read its full build handoff, comments, and history.";
|
|
13677
|
+
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.';
|
|
13678
|
+
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>"`.';
|
|
13679
|
+
var COACH_NO_DECISIONS = 'No Active Decisions recorded yet. Capture your first with `idea "<a direction or constraint>"` so your project starts steering itself.';
|
|
13680
|
+
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.";
|
|
13681
|
+
var ONBOARDING_COACHING_HEADING = "## Getting Started";
|
|
13682
|
+
function buildOnboardingCoaching(state) {
|
|
13683
|
+
const lines = [];
|
|
13684
|
+
const isEarly = state.cycleNumber <= ONBOARDING_EARLY_CYCLE_MAX;
|
|
13685
|
+
const { surface } = state;
|
|
13686
|
+
if (state.repoConnected === false && (surface === "orient" || surface === "plan")) {
|
|
13687
|
+
lines.push(COACH_CONNECT_REPO);
|
|
13688
|
+
}
|
|
13689
|
+
if (state.hasLocalWorkspace && (surface === "setup" || surface === "orient" && isEarly)) {
|
|
13690
|
+
lines.push(COACH_ROOT_DIR);
|
|
13691
|
+
}
|
|
13692
|
+
if ((surface === "orient" || surface === "setup") && isEarly) {
|
|
13693
|
+
lines.push(COACH_CLICKABLE_TASKS);
|
|
13694
|
+
}
|
|
13695
|
+
if ((surface === "orient" || surface === "plan") && state.hasActiveCycle && isEarly) {
|
|
13696
|
+
lines.push(COACH_OFF_CYCLE);
|
|
13697
|
+
}
|
|
13698
|
+
if (surface === "orient" && isEarly) {
|
|
13699
|
+
lines.push(state.hasActiveDecisions === false ? COACH_NO_DECISIONS : COACH_SESSION_START);
|
|
13700
|
+
}
|
|
13701
|
+
if (surface === "plan" && state.bootstrapPlan === true) {
|
|
13702
|
+
lines.push(COACH_BACKLOG_IMPORT);
|
|
13703
|
+
}
|
|
13704
|
+
return lines.slice(0, MAX_COACHING_LINES);
|
|
13705
|
+
}
|
|
13706
|
+
function formatOnboardingCoachingBlock(state) {
|
|
13707
|
+
const lines = buildOnboardingCoaching(state);
|
|
13708
|
+
if (lines.length === 0) return "";
|
|
13709
|
+
return `
|
|
13710
|
+
|
|
13711
|
+
${ONBOARDING_COACHING_HEADING}
|
|
13712
|
+
${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
13713
|
+
}
|
|
13714
|
+
|
|
13715
|
+
// src/lib/hosted-mode.ts
|
|
13716
|
+
function isHostedTransport() {
|
|
13717
|
+
return Boolean(process.env.PORT || process.env.PAPI_HTTP_PORT);
|
|
13718
|
+
}
|
|
13719
|
+
function hasLocalWorkspace() {
|
|
13720
|
+
return !isHostedTransport();
|
|
13721
|
+
}
|
|
13722
|
+
|
|
13665
13723
|
// src/lib/per-caller-cache.ts
|
|
13666
13724
|
var DEFAULT_CALLER_KEY2 = "__default__";
|
|
13667
13725
|
var MAX_PER_CALLER_ENTRIES = 1e3;
|
|
@@ -13905,6 +13963,10 @@ function formatPlanResult(result) {
|
|
|
13905
13963
|
} else {
|
|
13906
13964
|
lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
|
|
13907
13965
|
}
|
|
13966
|
+
if (result.onboardingCoaching && result.onboardingCoaching.length > 0) {
|
|
13967
|
+
lines.push("", ONBOARDING_COACHING_HEADING);
|
|
13968
|
+
for (const c of result.onboardingCoaching) lines.push(`- ${c}`);
|
|
13969
|
+
}
|
|
13908
13970
|
if (result.contextBytes !== void 0) {
|
|
13909
13971
|
const kb = (result.contextBytes / 1024).toFixed(1);
|
|
13910
13972
|
lines.push(`---`, `Context: ${kb}KB`);
|
|
@@ -13986,7 +14048,15 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13986
14048
|
}, tracker);
|
|
13987
14049
|
const planProjectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
13988
14050
|
const projectBanner = planProjectInfo ? getProjectConnectionBanner(planProjectInfo.name, planProjectInfo.slug) ?? void 0 : void 0;
|
|
13989
|
-
const
|
|
14051
|
+
const onboardingCoaching = buildOnboardingCoaching({
|
|
14052
|
+
surface: "plan",
|
|
14053
|
+
repoConnected: planProjectInfo ? !!planProjectInfo.repo_url : void 0,
|
|
14054
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
14055
|
+
cycleNumber: result.cycleNumber + 1,
|
|
14056
|
+
hasActiveCycle: true,
|
|
14057
|
+
bootstrapPlan: result.mode === "bootstrap"
|
|
14058
|
+
});
|
|
14059
|
+
const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs, projectBanner, onboardingCoaching });
|
|
13990
14060
|
return {
|
|
13991
14061
|
...response,
|
|
13992
14062
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -14138,14 +14208,6 @@ import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "f
|
|
|
14138
14208
|
import { join as join4 } from "path";
|
|
14139
14209
|
import { homedir as homedir2 } from "os";
|
|
14140
14210
|
|
|
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
14211
|
// src/services/idea.ts
|
|
14150
14212
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
14151
14213
|
var OWNER_ACTION_PATTERNS = [
|
|
@@ -15602,6 +15664,13 @@ ${cleanContent}`;
|
|
|
15602
15664
|
} catch {
|
|
15603
15665
|
}
|
|
15604
15666
|
}
|
|
15667
|
+
if (data.hierarchyUpdates && data.hierarchyUpdates.length > 0) {
|
|
15668
|
+
try {
|
|
15669
|
+
await applyHierarchyUpdates(adapter2, data.hierarchyUpdates);
|
|
15670
|
+
} catch (err) {
|
|
15671
|
+
console.error("[strategy] applyHierarchyUpdates failed:", err instanceof Error ? err.message : String(err));
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15605
15674
|
const compressionThreshold = cycleNumber - 5;
|
|
15606
15675
|
if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
|
|
15607
15676
|
await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
|
|
@@ -15879,6 +15948,79 @@ function buildPhaseLabel(phase) {
|
|
|
15879
15948
|
if (!numMatch) return phase.label;
|
|
15880
15949
|
return `Phase ${numMatch[1]}: ${phase.label}`;
|
|
15881
15950
|
}
|
|
15951
|
+
function nextHierarchySort(existing) {
|
|
15952
|
+
return existing.length === 0 ? 10 : Math.max(...existing.map((e) => e.sortOrder)) + 10;
|
|
15953
|
+
}
|
|
15954
|
+
async function applyHierarchyUpdates(adapter2, updates) {
|
|
15955
|
+
if (!adapter2.readStages || !adapter2.readHorizons) return;
|
|
15956
|
+
const findHorizon = async (ref) => (await adapter2.readHorizons()).find(
|
|
15957
|
+
(h) => h.slug === ref || h.id === ref || h.label.toLowerCase() === ref.toLowerCase()
|
|
15958
|
+
);
|
|
15959
|
+
const findStage = async (ref) => (await adapter2.readStages()).find(
|
|
15960
|
+
(s) => s.slug === ref || s.id === ref || s.label.toLowerCase() === ref.toLowerCase()
|
|
15961
|
+
);
|
|
15962
|
+
for (const u of updates) {
|
|
15963
|
+
if (u.level === "horizon") {
|
|
15964
|
+
if (u.action === "create") {
|
|
15965
|
+
if (!await findHorizon(u.slug) && adapter2.createHorizon) {
|
|
15966
|
+
const horizons = await adapter2.readHorizons();
|
|
15967
|
+
await adapter2.createHorizon({
|
|
15968
|
+
slug: u.slug,
|
|
15969
|
+
label: u.label ?? u.slug,
|
|
15970
|
+
status: u.status ?? "In Progress",
|
|
15971
|
+
sortOrder: u.sortOrder ?? nextHierarchySort(horizons)
|
|
15972
|
+
});
|
|
15973
|
+
}
|
|
15974
|
+
} else if (u.action === "update_status" && u.status && adapter2.updateHorizonStatus) {
|
|
15975
|
+
const h = await findHorizon(u.slug);
|
|
15976
|
+
if (h) await adapter2.updateHorizonStatus(h.id, u.status);
|
|
15977
|
+
}
|
|
15978
|
+
continue;
|
|
15979
|
+
}
|
|
15980
|
+
if (u.action === "update_criterion" && u.criterionId && typeof u.met === "boolean" && adapter2.setCriterionMet) {
|
|
15981
|
+
const s = await findStage(u.slug);
|
|
15982
|
+
if (s) await adapter2.setCriterionMet(s.id, u.criterionId, u.met, u.evidence ?? null);
|
|
15983
|
+
} else if (u.action === "update_status" && u.status && adapter2.updateStageStatus) {
|
|
15984
|
+
const s = await findStage(u.slug);
|
|
15985
|
+
if (s) await adapter2.updateStageStatus(s.id, u.status);
|
|
15986
|
+
} else if (u.action === "create") {
|
|
15987
|
+
await createStageFromUpdate(adapter2, u, findStage, findHorizon);
|
|
15988
|
+
} else if (u.action === "advance" && adapter2.updateStageStatus) {
|
|
15989
|
+
const current = await findStage(u.slug);
|
|
15990
|
+
if (current) await adapter2.updateStageStatus(current.id, "Done");
|
|
15991
|
+
const newRef = u.newStageSlug ?? u.slug;
|
|
15992
|
+
const existingNext = await findStage(newRef);
|
|
15993
|
+
if (existingNext) {
|
|
15994
|
+
await adapter2.updateStageStatus(existingNext.id, "In Progress");
|
|
15995
|
+
} else if (u.newStageLabel) {
|
|
15996
|
+
await createStageFromUpdate(
|
|
15997
|
+
adapter2,
|
|
15998
|
+
{ ...u, slug: newRef, label: u.newStageLabel, status: "In Progress", horizon: u.horizon ?? current?.horizonId },
|
|
15999
|
+
findStage,
|
|
16000
|
+
findHorizon
|
|
16001
|
+
);
|
|
16002
|
+
}
|
|
16003
|
+
}
|
|
16004
|
+
}
|
|
16005
|
+
}
|
|
16006
|
+
async function createStageFromUpdate(adapter2, u, findStage, findHorizon) {
|
|
16007
|
+
if (!adapter2.createStage || !adapter2.readStages || !adapter2.readHorizons) return;
|
|
16008
|
+
if (await findStage(u.slug)) return;
|
|
16009
|
+
const horizons = await adapter2.readHorizons();
|
|
16010
|
+
const parent = u.horizon ? await findHorizon(u.horizon) : horizons.length === 1 ? horizons[0] : horizons.find((h) => h.status === "In Progress");
|
|
16011
|
+
if (!parent) return;
|
|
16012
|
+
const stagesInHorizon = (await adapter2.readStages()).filter((s) => s.horizonId === parent.id);
|
|
16013
|
+
const id = await adapter2.createStage({
|
|
16014
|
+
slug: u.slug,
|
|
16015
|
+
label: u.label ?? u.slug,
|
|
16016
|
+
status: u.status ?? "Not Started",
|
|
16017
|
+
sortOrder: u.sortOrder ?? nextHierarchySort(stagesInHorizon),
|
|
16018
|
+
horizonId: parent.id
|
|
16019
|
+
});
|
|
16020
|
+
if (id && u.exitCriteria?.length && adapter2.updateStageExitCriteria) {
|
|
16021
|
+
await adapter2.updateStageExitCriteria(id, u.exitCriteria);
|
|
16022
|
+
}
|
|
16023
|
+
}
|
|
15882
16024
|
async function applyPhaseUpdates(adapter2, currentPhases, updates) {
|
|
15883
16025
|
const phasesById = new Map(currentPhases.map((p) => [p.id, p]));
|
|
15884
16026
|
const labelMigrations = [];
|
|
@@ -19111,8 +19253,14 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
|
19111
19253
|
**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
19254
|
|
|
19113
19255
|
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.`;
|
|
19256
|
+
const coachingNote = formatOnboardingCoachingBlock({
|
|
19257
|
+
surface: "setup",
|
|
19258
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
19259
|
+
cycleNumber: 0,
|
|
19260
|
+
hasActiveCycle: false
|
|
19261
|
+
});
|
|
19114
19262
|
return textResponse(
|
|
19115
|
-
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}
|
|
19263
|
+
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${northStarNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}${filesNote}${coachingNote}
|
|
19116
19264
|
|
|
19117
19265
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
19118
19266
|
|
|
@@ -19483,8 +19631,8 @@ function buildPapiMetaFramingDirective(caps, inner) {
|
|
|
19483
19631
|
|
|
19484
19632
|
// src/services/build.ts
|
|
19485
19633
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
19486
|
-
import { readdirSync as readdirSync5, existsSync as
|
|
19487
|
-
import { join as
|
|
19634
|
+
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
19635
|
+
import { join as join12 } from "path";
|
|
19488
19636
|
|
|
19489
19637
|
// src/lib/db-only-notices.ts
|
|
19490
19638
|
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 +20550,38 @@ async function postReleaseToX(version, cycleClosed) {
|
|
|
20402
20550
|
return false;
|
|
20403
20551
|
}
|
|
20404
20552
|
}
|
|
20553
|
+
function buildHostedReleaseOutput(params) {
|
|
20554
|
+
const { version, branch, cyclePart, warningsBlock, skipVersion } = params;
|
|
20555
|
+
const tagAnnotation = `Release ${version}`;
|
|
20556
|
+
const titleSuffix = skipVersion ? " (skip version)" : "";
|
|
20557
|
+
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";
|
|
20558
|
+
const commands = skipVersion ? `git checkout ${branch}
|
|
20559
|
+
git pull
|
|
20560
|
+
git push origin ${branch}
|
|
20561
|
+
` : `git checkout ${branch}
|
|
20562
|
+
git pull
|
|
20563
|
+
git tag -a ${version} -m "${tagAnnotation}"
|
|
20564
|
+
git push origin ${branch}
|
|
20565
|
+
git push origin ${version}
|
|
20566
|
+
`;
|
|
20567
|
+
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:
|
|
20568
|
+
\`\`\`
|
|
20569
|
+
git -c user.name="Your Name" -c user.email="you@example.com" tag -a ${version} -m "${tagAnnotation}"
|
|
20570
|
+
\`\`\`
|
|
20571
|
+
|
|
20572
|
+
`;
|
|
20573
|
+
return `## Release ${version}${titleSuffix} \u2014 cycle closed in the DB
|
|
20574
|
+
|
|
20575
|
+
${cyclePart} is now marked **complete** in PAPI's database, so \`orient\` will no longer flag "Release has not been run."
|
|
20576
|
+
` + warningsBlock + `
|
|
20577
|
+
The hosted remote MCP transport (mcp.getpapi.ai) has no checkout of your project and no git binary, so ${gitHalf}.
|
|
20578
|
+
|
|
20579
|
+
Finish the release locally:
|
|
20580
|
+
\`\`\`
|
|
20581
|
+
` + commands + `\`\`\`
|
|
20582
|
+
|
|
20583
|
+
` + identityBlock + `Next: cycle closed! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`;
|
|
20584
|
+
}
|
|
20405
20585
|
var releaseTool = {
|
|
20406
20586
|
name: "release",
|
|
20407
20587
|
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 +20759,12 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
20579
20759
|
branchMerges: [],
|
|
20580
20760
|
changelogEmitted: false
|
|
20581
20761
|
});
|
|
20582
|
-
const tagAnnotation = `Release ${version}`;
|
|
20583
20762
|
const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
|
|
20584
20763
|
const warningsBlock = closed.warnings.length > 0 ? `
|
|
20585
20764
|
\u26A0\uFE0F Warnings: ${closed.warnings.join("; ")}
|
|
20586
20765
|
` : "";
|
|
20587
20766
|
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.`
|
|
20767
|
+
buildHostedReleaseOutput({ version, branch, cyclePart, warningsBlock, skipVersion: skipVersion ?? false })
|
|
20609
20768
|
);
|
|
20610
20769
|
}
|
|
20611
20770
|
tracker.mark("remote-project-guard");
|
|
@@ -20862,6 +21021,95 @@ function detectWorktreeCollision(input) {
|
|
|
20862
21021
|
// src/services/build.ts
|
|
20863
21022
|
init_git();
|
|
20864
21023
|
|
|
21024
|
+
// src/lib/build-checkpoint.ts
|
|
21025
|
+
import { createHash as createHash4 } from "crypto";
|
|
21026
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
21027
|
+
import { join as join11 } from "path";
|
|
21028
|
+
var BUILD_CHECKPOINT_VERSION = 1;
|
|
21029
|
+
function cwdHash(cwd) {
|
|
21030
|
+
return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
21031
|
+
}
|
|
21032
|
+
function safeTaskId(taskId) {
|
|
21033
|
+
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
21034
|
+
}
|
|
21035
|
+
function checkpointDir(cwd) {
|
|
21036
|
+
return join11(cwd, ".papi", "state");
|
|
21037
|
+
}
|
|
21038
|
+
function checkpointPath(cwd, taskId) {
|
|
21039
|
+
return join11(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
21040
|
+
}
|
|
21041
|
+
function writeBuildCheckpoint(input) {
|
|
21042
|
+
try {
|
|
21043
|
+
const dir = checkpointDir(input.cwd);
|
|
21044
|
+
if (!existsSync7(dir)) {
|
|
21045
|
+
mkdirSync2(dir, { recursive: true });
|
|
21046
|
+
}
|
|
21047
|
+
const checkpoint = {
|
|
21048
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
21049
|
+
taskId: input.taskId,
|
|
21050
|
+
branch: input.branch,
|
|
21051
|
+
step: input.step,
|
|
21052
|
+
lastCommitSha: input.lastCommitSha,
|
|
21053
|
+
modifiedFiles: input.modifiedFiles,
|
|
21054
|
+
cwd: realpathOrSelf(input.cwd),
|
|
21055
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21056
|
+
};
|
|
21057
|
+
writeFileSync3(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
21058
|
+
} catch {
|
|
21059
|
+
}
|
|
21060
|
+
}
|
|
21061
|
+
function readBuildCheckpoint(key) {
|
|
21062
|
+
try {
|
|
21063
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
21064
|
+
if (!existsSync7(path7)) return null;
|
|
21065
|
+
const parsed = JSON.parse(readFileSync6(path7, "utf-8"));
|
|
21066
|
+
if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
|
|
21067
|
+
return null;
|
|
21068
|
+
}
|
|
21069
|
+
return {
|
|
21070
|
+
version: BUILD_CHECKPOINT_VERSION,
|
|
21071
|
+
taskId: parsed.taskId,
|
|
21072
|
+
branch: parsed.branch ?? null,
|
|
21073
|
+
step: "branch_ready",
|
|
21074
|
+
lastCommitSha: parsed.lastCommitSha ?? null,
|
|
21075
|
+
modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
|
|
21076
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
|
|
21077
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
|
|
21078
|
+
};
|
|
21079
|
+
} catch {
|
|
21080
|
+
return null;
|
|
21081
|
+
}
|
|
21082
|
+
}
|
|
21083
|
+
function clearBuildCheckpoint(key) {
|
|
21084
|
+
try {
|
|
21085
|
+
const path7 = checkpointPath(key.cwd, key.taskId);
|
|
21086
|
+
if (existsSync7(path7)) {
|
|
21087
|
+
unlinkSync2(path7);
|
|
21088
|
+
}
|
|
21089
|
+
} catch {
|
|
21090
|
+
}
|
|
21091
|
+
}
|
|
21092
|
+
function formatResumeNote(cp) {
|
|
21093
|
+
const files = cp.modifiedFiles.filter((f) => f && f.trim());
|
|
21094
|
+
const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
|
|
21095
|
+
- \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
|
|
21096
|
+
const lines = [
|
|
21097
|
+
"> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
|
|
21098
|
+
`> - Branch: \`${cp.branch ?? "unknown"}\``,
|
|
21099
|
+
"> - Last step: branch ready",
|
|
21100
|
+
`> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
|
|
21101
|
+
`> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
|
|
21102
|
+
">",
|
|
21103
|
+
"> Modified files at last checkpoint:",
|
|
21104
|
+
...fileList.split("\n").map((l) => `> ${l}`),
|
|
21105
|
+
">",
|
|
21106
|
+
"> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
|
|
21107
|
+
"",
|
|
21108
|
+
""
|
|
21109
|
+
];
|
|
21110
|
+
return lines.join("\n");
|
|
21111
|
+
}
|
|
21112
|
+
|
|
20865
21113
|
// src/lib/diff-inspector.ts
|
|
20866
21114
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
20867
21115
|
var TRIGGER_SURFACE_GLOBS = [
|
|
@@ -21097,51 +21345,22 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21097
21345
|
if (staged.length > 0) {
|
|
21098
21346
|
return safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
21099
21347
|
}
|
|
21348
|
+
const modified = getModifiedFiles(cwd);
|
|
21349
|
+
if (modified.length === 0) {
|
|
21350
|
+
return "Auto-commit: skipped (no working-tree changes).";
|
|
21351
|
+
}
|
|
21352
|
+
const commitResult = safeRun(() => stageAllAndCommit(cwd, message));
|
|
21100
21353
|
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
21354
|
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.`;
|
|
21355
|
+
const outOfScope = modified.filter((p) => !isPathInPredictedScope(p, cleanedPredicted));
|
|
21356
|
+
if (outOfScope.length > 0) {
|
|
21357
|
+
const sample = outOfScope.slice(0, 10).join(", ");
|
|
21358
|
+
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
21359
|
+
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
21360
|
}
|
|
21142
|
-
return
|
|
21361
|
+
return `${commitResult} (staged all ${modified.length} changed file(s), all within FILES LIKELY TOUCHED).`;
|
|
21143
21362
|
}
|
|
21144
|
-
return
|
|
21363
|
+
return `${commitResult} (staged all ${modified.length} changed file(s)).`;
|
|
21145
21364
|
}
|
|
21146
21365
|
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
21147
21366
|
const lines = [];
|
|
@@ -21618,8 +21837,9 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21618
21837
|
await persistBranchName(adapter2, taskId, startedBranch);
|
|
21619
21838
|
}
|
|
21620
21839
|
buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
|
|
21840
|
+
let startSha = null;
|
|
21621
21841
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
21622
|
-
|
|
21842
|
+
startSha = getHeadCommitSha(config2.projectRoot);
|
|
21623
21843
|
if (startSha) taskStartShaMap.set(taskId, startSha);
|
|
21624
21844
|
}
|
|
21625
21845
|
let phaseChanges = [];
|
|
@@ -21638,6 +21858,14 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21638
21858
|
);
|
|
21639
21859
|
} catch {
|
|
21640
21860
|
}
|
|
21861
|
+
writeBuildCheckpoint({
|
|
21862
|
+
cwd: config2.projectRoot,
|
|
21863
|
+
taskId,
|
|
21864
|
+
branch: getCurrentBranch(config2.projectRoot),
|
|
21865
|
+
step: "branch_ready",
|
|
21866
|
+
lastCommitSha: startSha,
|
|
21867
|
+
modifiedFiles: getModifiedFiles(config2.projectRoot)
|
|
21868
|
+
});
|
|
21641
21869
|
return {
|
|
21642
21870
|
task,
|
|
21643
21871
|
branchLines,
|
|
@@ -21652,17 +21880,17 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
21652
21880
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
21653
21881
|
return;
|
|
21654
21882
|
}
|
|
21655
|
-
const papiDir =
|
|
21656
|
-
if (!
|
|
21657
|
-
|
|
21883
|
+
const papiDir = join12(projectRoot, ".papi");
|
|
21884
|
+
if (!existsSync8(papiDir)) {
|
|
21885
|
+
mkdirSync3(papiDir, { recursive: true });
|
|
21658
21886
|
}
|
|
21659
|
-
const scopePath =
|
|
21660
|
-
|
|
21887
|
+
const scopePath = join12(papiDir, "active-task-scope.txt");
|
|
21888
|
+
writeFileSync4(scopePath, content, "utf-8");
|
|
21661
21889
|
}
|
|
21662
21890
|
function clearActiveTaskScope(projectRoot) {
|
|
21663
|
-
const scopePath =
|
|
21664
|
-
if (
|
|
21665
|
-
|
|
21891
|
+
const scopePath = join12(projectRoot, ".papi", "active-task-scope.txt");
|
|
21892
|
+
if (existsSync8(scopePath)) {
|
|
21893
|
+
unlinkSync3(scopePath);
|
|
21666
21894
|
}
|
|
21667
21895
|
}
|
|
21668
21896
|
function sanitiseResponseExcerpt(raw) {
|
|
@@ -21681,7 +21909,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
21681
21909
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
21682
21910
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
21683
21911
|
try {
|
|
21684
|
-
const content =
|
|
21912
|
+
const content = readFileSync7(absolutePath, "utf-8").slice(0, 2e3);
|
|
21685
21913
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21686
21914
|
if (fmMatch) {
|
|
21687
21915
|
const fm = fmMatch[1];
|
|
@@ -22078,14 +22306,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22078
22306
|
let docWarning;
|
|
22079
22307
|
try {
|
|
22080
22308
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
22081
|
-
const docsDir =
|
|
22082
|
-
if (
|
|
22309
|
+
const docsDir = join12(config2.projectRoot, "docs");
|
|
22310
|
+
if (existsSync8(docsDir)) {
|
|
22083
22311
|
const scanDir = (dir, depth = 0) => {
|
|
22084
22312
|
if (depth > 8) return [];
|
|
22085
22313
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
22086
22314
|
const files = [];
|
|
22087
22315
|
for (const e of entries) {
|
|
22088
|
-
const full =
|
|
22316
|
+
const full = join12(dir, e.name);
|
|
22089
22317
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
22090
22318
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
22091
22319
|
}
|
|
@@ -22100,7 +22328,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22100
22328
|
const failed = [];
|
|
22101
22329
|
for (const docPath of unregistered) {
|
|
22102
22330
|
try {
|
|
22103
|
-
const meta = extractDocMeta(
|
|
22331
|
+
const meta = extractDocMeta(join12(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
22104
22332
|
await adapter2.registerDoc({
|
|
22105
22333
|
title: meta.title,
|
|
22106
22334
|
type: meta.type,
|
|
@@ -22138,6 +22366,10 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22138
22366
|
clearActiveTaskScope(config2.projectRoot);
|
|
22139
22367
|
} catch {
|
|
22140
22368
|
}
|
|
22369
|
+
try {
|
|
22370
|
+
clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
22371
|
+
} catch {
|
|
22372
|
+
}
|
|
22141
22373
|
return {
|
|
22142
22374
|
task,
|
|
22143
22375
|
report,
|
|
@@ -22252,8 +22484,8 @@ ${instructions}`;
|
|
|
22252
22484
|
}
|
|
22253
22485
|
|
|
22254
22486
|
// src/tools/doc-registry.ts
|
|
22255
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
22256
|
-
import { join as
|
|
22487
|
+
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
22488
|
+
import { join as join13, relative } from "path";
|
|
22257
22489
|
import { homedir as homedir3 } from "os";
|
|
22258
22490
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
22259
22491
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
@@ -22451,7 +22683,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
22451
22683
|
const lines = docs.map((d) => {
|
|
22452
22684
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
22453
22685
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
22454
|
-
const missingNote = root && d.path && !
|
|
22686
|
+
const missingNote = root && d.path && !existsSync9(join13(root, d.path)) ? `
|
|
22455
22687
|
> \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
22688
|
return `### ${d.title}
|
|
22457
22689
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -22465,12 +22697,12 @@ ${d.summary}
|
|
|
22465
22697
|
${lines.join("\n---\n\n")}`);
|
|
22466
22698
|
}
|
|
22467
22699
|
function scanMdFiles(dir, rootDir) {
|
|
22468
|
-
if (!
|
|
22700
|
+
if (!existsSync9(dir)) return [];
|
|
22469
22701
|
const files = [];
|
|
22470
22702
|
try {
|
|
22471
22703
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
22472
22704
|
for (const entry of entries) {
|
|
22473
|
-
const full =
|
|
22705
|
+
const full = join13(dir, entry.name);
|
|
22474
22706
|
if (entry.isDirectory()) {
|
|
22475
22707
|
files.push(...scanMdFiles(full, rootDir));
|
|
22476
22708
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -22483,7 +22715,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
22483
22715
|
}
|
|
22484
22716
|
function extractTitle(filePath) {
|
|
22485
22717
|
try {
|
|
22486
|
-
const content =
|
|
22718
|
+
const content = readFileSync8(filePath, "utf-8").slice(0, 1e3);
|
|
22487
22719
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
22488
22720
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
22489
22721
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -22495,7 +22727,7 @@ function extractTitle(filePath) {
|
|
|
22495
22727
|
async function detectUnregisteredDocsNote(adapter2, config2) {
|
|
22496
22728
|
try {
|
|
22497
22729
|
if (!adapter2.searchDocs || !hasLocalWorkspace()) return "";
|
|
22498
|
-
const docsDir =
|
|
22730
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22499
22731
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22500
22732
|
if (docsFiles.length === 0) return "";
|
|
22501
22733
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -22521,17 +22753,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
22521
22753
|
const includePlans = args.include_plans ?? false;
|
|
22522
22754
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
22523
22755
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
22524
|
-
const docsDir =
|
|
22756
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22525
22757
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22526
22758
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
22527
22759
|
let unregisteredPlans = [];
|
|
22528
22760
|
if (includePlans) {
|
|
22529
|
-
const plansDir =
|
|
22530
|
-
if (
|
|
22761
|
+
const plansDir = join13(homedir3(), ".claude", "plans");
|
|
22762
|
+
if (existsSync9(plansDir)) {
|
|
22531
22763
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
22532
22764
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
22533
22765
|
path: f,
|
|
22534
|
-
title: extractTitle(
|
|
22766
|
+
title: extractTitle(join13(plansDir, f.replace("plans/", "")))
|
|
22535
22767
|
}));
|
|
22536
22768
|
}
|
|
22537
22769
|
}
|
|
@@ -22542,7 +22774,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
22542
22774
|
if (unregisteredDocs.length > 0) {
|
|
22543
22775
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
22544
22776
|
for (const f of unregisteredDocs) {
|
|
22545
|
-
const title = extractTitle(
|
|
22777
|
+
const title = extractTitle(join13(config2.projectRoot, f));
|
|
22546
22778
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
22547
22779
|
}
|
|
22548
22780
|
}
|
|
@@ -22715,97 +22947,6 @@ async function handleDocReorder(adapter2, args) {
|
|
|
22715
22947
|
|
|
22716
22948
|
// src/tools/build.ts
|
|
22717
22949
|
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
22950
|
var buildListTool = {
|
|
22810
22951
|
name: "build_list",
|
|
22811
22952
|
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.",
|
|
@@ -23184,14 +23325,6 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
|
|
|
23184
23325
|
const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
|
|
23185
23326
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
|
|
23186
23327
|
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
23328
|
tracker.mark("start_decorate_handoff");
|
|
23196
23329
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
23197
23330
|
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 +23543,6 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
23410
23543
|
preview
|
|
23411
23544
|
}, { light }, clientName);
|
|
23412
23545
|
tracker.mark("complete_format");
|
|
23413
|
-
clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
|
|
23414
23546
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
23415
23547
|
await tracker.recordStep("report_written");
|
|
23416
23548
|
if ((result.autoTriagedCount ?? 0) > 0) {
|
|
@@ -27133,9 +27265,15 @@ var TASK_REF = /\btask-\d+\b/gi;
|
|
|
27133
27265
|
var RECENT_CYCLE_WINDOW = 5;
|
|
27134
27266
|
var MAX_CANDIDATES = 5;
|
|
27135
27267
|
async function findUnblockCandidates(adapter2, currentCycle) {
|
|
27268
|
+
try {
|
|
27269
|
+
const blockedProbe = await adapter2.queryBoard({ status: ["Blocked"], compact: true });
|
|
27270
|
+
if (blockedProbe.length === 0) return [];
|
|
27271
|
+
} catch {
|
|
27272
|
+
return [];
|
|
27273
|
+
}
|
|
27136
27274
|
let allTasks = [];
|
|
27137
27275
|
try {
|
|
27138
|
-
allTasks = await adapter2.queryBoard();
|
|
27276
|
+
allTasks = await adapter2.queryBoard({ compact: true });
|
|
27139
27277
|
} catch {
|
|
27140
27278
|
return [];
|
|
27141
27279
|
}
|
|
@@ -27721,7 +27859,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
27721
27859
|
lines.push(`**Nearing Closure:** ${hierarchy.phasesNearingClosure.join(", ")}`);
|
|
27722
27860
|
}
|
|
27723
27861
|
if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
|
|
27724
|
-
|
|
27862
|
+
const crit = hierarchy.stageExitCriteria;
|
|
27863
|
+
const met = crit.filter((c) => c.met).length;
|
|
27864
|
+
const total = crit.length;
|
|
27865
|
+
lines.push(`**Stage Exit Criteria [${met}/${total} met]:** ${crit.map((c) => `${c.met ? "[x]" : "[ ]"} ${c.text}`).join(" | ")}`);
|
|
27866
|
+
if (met === total) {
|
|
27867
|
+
lines.push(" \u21B3 All exit criteria met \u2014 run `strategy_review` to propose advancing the stage.");
|
|
27868
|
+
}
|
|
27725
27869
|
}
|
|
27726
27870
|
lines.push("");
|
|
27727
27871
|
}
|
|
@@ -27926,9 +28070,9 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
27926
28070
|
for (const issue of alerts) {
|
|
27927
28071
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
27928
28072
|
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
27929
|
-
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
28073
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})${issue.id ? ` \xB7 id \`${issue.id}\`` : ""}`);
|
|
27930
28074
|
}
|
|
27931
|
-
lines.push("
|
|
28075
|
+
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
28076
|
alertsNote = lines.join("\n");
|
|
27933
28077
|
}
|
|
27934
28078
|
if (allLowSev.length > 0) {
|
|
@@ -27938,18 +28082,18 @@ function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
|
27938
28082
|
for (const issue of unactioned) {
|
|
27939
28083
|
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
27940
28084
|
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
27941
|
-
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
28085
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})${issue.id ? ` \xB7 id \`${issue.id}\`` : ""}`);
|
|
27942
28086
|
}
|
|
27943
|
-
lines.push("
|
|
28087
|
+
lines.push("_Already fixed? `discovered_issue_resolve <id>` clears it. Otherwise `idea` to log as a backlog task._");
|
|
27944
28088
|
unactionedIssuesNote = lines.join("\n");
|
|
27945
28089
|
}
|
|
27946
28090
|
return { alertsNote, unactionedIssuesNote };
|
|
27947
28091
|
}
|
|
27948
|
-
async function computeTeamSummary(adapter2) {
|
|
28092
|
+
async function computeTeamSummary(adapter2, contributorsInput) {
|
|
27949
28093
|
if (typeof adapter2.listContributors !== "function") return void 0;
|
|
27950
28094
|
let members;
|
|
27951
28095
|
try {
|
|
27952
|
-
members = (await adapter2.listContributors()).length;
|
|
28096
|
+
members = (await (contributorsInput ?? adapter2.listContributors())).length;
|
|
27953
28097
|
} catch {
|
|
27954
28098
|
return void 0;
|
|
27955
28099
|
}
|
|
@@ -27966,11 +28110,11 @@ async function computeTeamSummary(adapter2) {
|
|
|
27966
28110
|
const reviewQueue = tasks.filter((t) => t.status === "In Review").length;
|
|
27967
28111
|
return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
|
|
27968
28112
|
}
|
|
27969
|
-
async function computeReleaseHistory(adapter2) {
|
|
28113
|
+
async function computeReleaseHistory(adapter2, contributorsInput) {
|
|
27970
28114
|
if (typeof adapter2.listContributors !== "function") return void 0;
|
|
27971
28115
|
let contributors;
|
|
27972
28116
|
try {
|
|
27973
|
-
contributors = await adapter2.listContributors();
|
|
28117
|
+
contributors = await (contributorsInput ?? adapter2.listContributors());
|
|
27974
28118
|
} catch {
|
|
27975
28119
|
return void 0;
|
|
27976
28120
|
}
|
|
@@ -28301,13 +28445,13 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28301
28445
|
// "writing to the wrong project" for multi-project users on a shared key / stateless
|
|
28302
28446
|
// HTTP transport where a once-per-session gate can't work.
|
|
28303
28447
|
tracked("project-banner", async () => {
|
|
28304
|
-
if (!adapter2.getProjectInfo) return { banner: "", name: void 0 };
|
|
28448
|
+
if (!adapter2.getProjectInfo) return { banner: "", name: void 0, repoConnected: void 0 };
|
|
28305
28449
|
const projectInfo = await adapter2.getProjectInfo();
|
|
28306
|
-
if (!projectInfo) return { banner: "", name: void 0 };
|
|
28450
|
+
if (!projectInfo) return { banner: "", name: void 0, repoConnected: void 0 };
|
|
28307
28451
|
const banner = getProjectConnectionBanner(projectInfo.name, projectInfo.slug);
|
|
28308
28452
|
return { banner: banner ? `
|
|
28309
28453
|
> ${banner}
|
|
28310
|
-
` : "", name: projectInfo.name };
|
|
28454
|
+
` : "", name: projectInfo.name, repoConnected: !!projectInfo.repo_url };
|
|
28311
28455
|
}),
|
|
28312
28456
|
// Session guidance — proactive nudges (doc_register, context bloat, mode switch)
|
|
28313
28457
|
tracked("session-guidance", async () => {
|
|
@@ -28445,7 +28589,7 @@ ${versionDrift}` : "";
|
|
|
28445
28589
|
const patternsNote = patternsOutcome.status === "fulfilled" ? patternsOutcome.value : "";
|
|
28446
28590
|
const { alertsNote, unactionedIssuesNote } = discoveredIssuesOutcome.status === "fulfilled" ? discoveredIssuesOutcome.value : { alertsNote: "", unactionedIssuesNote: "" };
|
|
28447
28591
|
const skillProposalsNote = skillScanOutcome.status === "fulfilled" ? skillScanOutcome.value : "";
|
|
28448
|
-
const projectBannerResult = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : { banner: "", name: void 0 };
|
|
28592
|
+
const projectBannerResult = projectBannerOutcome.status === "fulfilled" ? projectBannerOutcome.value : { banner: "", name: void 0, repoConnected: void 0 };
|
|
28449
28593
|
const projectBannerNote = projectBannerResult.banner;
|
|
28450
28594
|
const projectName = projectBannerResult.name;
|
|
28451
28595
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
@@ -28486,16 +28630,27 @@ ${versionDrift}` : "";
|
|
|
28486
28630
|
preBuildCheckNote = lines.join("\n");
|
|
28487
28631
|
}
|
|
28488
28632
|
}
|
|
28489
|
-
tracker.mark("
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
|
|
28493
|
-
|
|
28494
|
-
|
|
28633
|
+
tracker.mark("parallel-tail");
|
|
28634
|
+
const sharedContributorsPromise = typeof adapter2.listContributors === "function" ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
28635
|
+
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
|
|
28636
|
+
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
|
|
28637
|
+
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
28638
|
+
listAgents(config2.projectRoot),
|
|
28639
|
+
// task-2071 (MU-3) + task-2072 (MU-5): team summary + release-history — both
|
|
28640
|
+
// multi-member only; solo projects get undefined from both, so orient stays
|
|
28641
|
+
// byte-identical there.
|
|
28642
|
+
tracked("team-summary", () => computeTeamSummary(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
28643
|
+
tracked("release-history", () => computeReleaseHistory(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
28644
|
+
// task-2751 (C332): resolve every task-NNNN mentioned in the Carry-Forward
|
|
28645
|
+
// prose to its title so orient can name it inline. Built from the board
|
|
28646
|
+
// already in hand — no extra query.
|
|
28647
|
+
resolveCarryForwardRefs(healthResult.carryForward, allTasks, adapter2)
|
|
28648
|
+
]);
|
|
28649
|
+
const unblockSection = formatUnblockSection(unblockCandidates);
|
|
28650
|
+
const unblockNote = unblockSection ? `
|
|
28495
28651
|
|
|
28496
|
-
${
|
|
28497
|
-
|
|
28498
|
-
}
|
|
28652
|
+
${unblockSection}` : "";
|
|
28653
|
+
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
28499
28654
|
let deferredGateNote = "";
|
|
28500
28655
|
if (deepHousekeeping) {
|
|
28501
28656
|
try {
|
|
@@ -28507,16 +28662,22 @@ ${section}`;
|
|
|
28507
28662
|
} catch {
|
|
28508
28663
|
}
|
|
28509
28664
|
}
|
|
28665
|
+
let onboardingCoachingNote = "";
|
|
28666
|
+
try {
|
|
28667
|
+
const adsForCoaching = await sharedActiveDecisionsPromise;
|
|
28668
|
+
onboardingCoachingNote = formatOnboardingCoachingBlock({
|
|
28669
|
+
surface: "orient",
|
|
28670
|
+
repoConnected: projectBannerResult.repoConnected,
|
|
28671
|
+
hasLocalWorkspace: hasLocalWorkspace(),
|
|
28672
|
+
hasActiveDecisions: adsForCoaching.length > 0,
|
|
28673
|
+
cycleNumber: currentCycle,
|
|
28674
|
+
hasActiveCycle: currentCycle > 0 && !cycleIsComplete
|
|
28675
|
+
});
|
|
28676
|
+
} catch {
|
|
28677
|
+
}
|
|
28510
28678
|
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
28679
|
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);
|
|
28680
|
+
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
28681
|
} catch (err) {
|
|
28521
28682
|
const message = err instanceof Error ? err.message : String(err);
|
|
28522
28683
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -28571,7 +28732,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
28571
28732
|
// src/tools/hierarchy.ts
|
|
28572
28733
|
var hierarchyUpdateTool = {
|
|
28573
28734
|
name: "hierarchy_update",
|
|
28574
|
-
description: "
|
|
28735
|
+
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
28736
|
annotations: { title: "Update Hierarchy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
28576
28737
|
inputSchema: {
|
|
28577
28738
|
type: "object",
|
|
@@ -28579,129 +28740,218 @@ var hierarchyUpdateTool = {
|
|
|
28579
28740
|
level: {
|
|
28580
28741
|
type: "string",
|
|
28581
28742
|
enum: ["phase", "stage", "horizon"],
|
|
28582
|
-
description: "Which hierarchy level to update."
|
|
28743
|
+
description: "Which hierarchy level to create/update."
|
|
28583
28744
|
},
|
|
28584
28745
|
name: {
|
|
28585
28746
|
type: "string",
|
|
28586
|
-
description: "The label or ID of the
|
|
28747
|
+
description: "The label, slug, or ID of the entity. On create, becomes the label if `label` is omitted."
|
|
28748
|
+
},
|
|
28749
|
+
label: {
|
|
28750
|
+
type: "string",
|
|
28751
|
+
description: 'Display label. Required to CREATE a new stage/horizon when `name` does not match an existing one (e.g. "S2: Alpha Cohort").'
|
|
28752
|
+
},
|
|
28753
|
+
slug: {
|
|
28754
|
+
type: "string",
|
|
28755
|
+
description: "Explicit slug for a new entity (stage/horizon). Auto-derived from the label when omitted."
|
|
28756
|
+
},
|
|
28757
|
+
description: {
|
|
28758
|
+
type: "string",
|
|
28759
|
+
description: "Optional longer description (stage/horizon)."
|
|
28587
28760
|
},
|
|
28588
28761
|
status: {
|
|
28589
28762
|
type: "string",
|
|
28590
28763
|
enum: ["Not Started", "In Progress", "Done", "Deferred"],
|
|
28591
|
-
description:
|
|
28764
|
+
description: 'The status to set. On create, defaults to "Not Started".'
|
|
28765
|
+
},
|
|
28766
|
+
sort_order: {
|
|
28767
|
+
type: "number",
|
|
28768
|
+
description: "Display order for a new entity. Auto-computed (max existing + 10) when omitted."
|
|
28769
|
+
},
|
|
28770
|
+
horizon: {
|
|
28771
|
+
type: "string",
|
|
28772
|
+
description: "Parent horizon (name/slug/id) when CREATING a stage. Defaults to the sole horizon if only one exists."
|
|
28592
28773
|
},
|
|
28593
28774
|
exit_criteria: {
|
|
28594
28775
|
type: "array",
|
|
28595
28776
|
items: { type: "string" },
|
|
28596
|
-
description:
|
|
28777
|
+
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)."
|
|
28778
|
+
},
|
|
28779
|
+
set_criterion_met: {
|
|
28780
|
+
type: "object",
|
|
28781
|
+
description: "Flip a single stage exit criterion (task-1625). { criterion_id, met, evidence? }.",
|
|
28782
|
+
properties: {
|
|
28783
|
+
criterion_id: { type: "string", description: "The ExitCriterion id to flip." },
|
|
28784
|
+
met: { type: "boolean", description: "true = met, false = unmet." },
|
|
28785
|
+
evidence: { type: "string", description: 'Optional evidence for a met criterion (e.g. "task-1234 shipped").' }
|
|
28786
|
+
},
|
|
28787
|
+
required: ["criterion_id", "met"]
|
|
28597
28788
|
}
|
|
28598
28789
|
},
|
|
28599
28790
|
required: ["level", "name"]
|
|
28600
28791
|
}
|
|
28601
28792
|
};
|
|
28602
28793
|
var VALID_STATUSES3 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
|
|
28794
|
+
function slugify(input) {
|
|
28795
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
|
|
28796
|
+
}
|
|
28797
|
+
function nextSortOrder(existing) {
|
|
28798
|
+
if (existing.length === 0) return 10;
|
|
28799
|
+
return Math.max(...existing.map((e) => e.sortOrder)) + 10;
|
|
28800
|
+
}
|
|
28603
28801
|
async function handleHierarchyUpdate(adapter2, args) {
|
|
28604
28802
|
const level = args.level;
|
|
28605
28803
|
const name = args.name;
|
|
28804
|
+
const label = args.label;
|
|
28805
|
+
const slug = args.slug;
|
|
28806
|
+
const description = args.description;
|
|
28606
28807
|
const status = args.status;
|
|
28808
|
+
const sortOrder = typeof args.sort_order === "number" ? args.sort_order : void 0;
|
|
28809
|
+
const horizonRef = args.horizon;
|
|
28607
28810
|
const exitCriteria = args.exit_criteria;
|
|
28811
|
+
const setCriterion = args.set_criterion_met;
|
|
28608
28812
|
if (!level || !name) {
|
|
28609
28813
|
return errorResponse("Missing required parameters: level, name.");
|
|
28610
28814
|
}
|
|
28611
|
-
if (!status && !exitCriteria) {
|
|
28612
|
-
return errorResponse("Nothing to update. Provide at least one of: status, exit_criteria.");
|
|
28613
|
-
}
|
|
28614
28815
|
if (level !== "phase" && level !== "stage" && level !== "horizon") {
|
|
28615
28816
|
return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
|
|
28616
28817
|
}
|
|
28617
28818
|
if (status && !VALID_STATUSES3.has(status)) {
|
|
28618
28819
|
return errorResponse(`Invalid status "${status}". Must be one of: Not Started, In Progress, Done, Deferred.`);
|
|
28619
28820
|
}
|
|
28620
|
-
if (exitCriteria !== void 0 && level !== "stage") {
|
|
28621
|
-
return errorResponse("exit_criteria can only be
|
|
28821
|
+
if ((exitCriteria !== void 0 || setCriterion !== void 0) && level !== "stage") {
|
|
28822
|
+
return errorResponse("exit_criteria and set_criterion_met can only be used on stages.");
|
|
28823
|
+
}
|
|
28824
|
+
if (!status && exitCriteria === void 0 && setCriterion === void 0 && !label) {
|
|
28825
|
+
return errorResponse("Nothing to do. Provide at least one of: status, exit_criteria, set_criterion_met, or label (to create).");
|
|
28622
28826
|
}
|
|
28623
28827
|
try {
|
|
28624
28828
|
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}`);
|
|
28829
|
+
return await handlePhase(adapter2, name, status);
|
|
28645
28830
|
}
|
|
28646
28831
|
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"));
|
|
28832
|
+
return await handleStage(adapter2, { name, label, slug, description, status, sortOrder, horizonRef, exitCriteria, setCriterion });
|
|
28680
28833
|
}
|
|
28681
|
-
|
|
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"}`);
|
|
28691
|
-
}
|
|
28692
|
-
if (!status) {
|
|
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}`);
|
|
28834
|
+
return await handleHorizon(adapter2, { name, label, slug, description, status, sortOrder });
|
|
28701
28835
|
} catch (err) {
|
|
28702
28836
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
28703
28837
|
}
|
|
28704
28838
|
}
|
|
28839
|
+
async function handlePhase(adapter2, name, status) {
|
|
28840
|
+
if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
|
|
28841
|
+
return errorResponse("Phase management is not supported by the current adapter.");
|
|
28842
|
+
}
|
|
28843
|
+
if (!status) return errorResponse("status is required for phase updates.");
|
|
28844
|
+
const phases = await adapter2.readPhases();
|
|
28845
|
+
const phase = phases.find((p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name);
|
|
28846
|
+
if (!phase) {
|
|
28847
|
+
const available = phases.map((p) => p.label).join(", ");
|
|
28848
|
+
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"} (phases are update-only \u2014 create them via plan/setup).`);
|
|
28849
|
+
}
|
|
28850
|
+
if (phase.status === status) return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
28851
|
+
const oldStatus = phase.status;
|
|
28852
|
+
await adapter2.updatePhaseStatus(phase.id, status);
|
|
28853
|
+
return textResponse(`Phase updated: **${phase.label}** ${oldStatus} \u2192 ${status}`);
|
|
28854
|
+
}
|
|
28855
|
+
async function handleStage(adapter2, a) {
|
|
28856
|
+
if (!adapter2.readStages) {
|
|
28857
|
+
return errorResponse("Stage management is not supported by the current adapter.");
|
|
28858
|
+
}
|
|
28859
|
+
const stages = await adapter2.readStages();
|
|
28860
|
+
let stage = stages.find(
|
|
28861
|
+
(s) => s.label.toLowerCase() === a.name.toLowerCase() || s.id === a.name || s.slug === a.name
|
|
28862
|
+
);
|
|
28863
|
+
const resultLines = [];
|
|
28864
|
+
let created = false;
|
|
28865
|
+
if (!stage) {
|
|
28866
|
+
if (!adapter2.createHorizon || !adapter2.createStage || !adapter2.readHorizons) {
|
|
28867
|
+
return errorResponse("Stage creation is not supported by the current adapter.");
|
|
28868
|
+
}
|
|
28869
|
+
const createLabel = a.label ?? a.name;
|
|
28870
|
+
const horizons = await adapter2.readHorizons();
|
|
28871
|
+
let parent;
|
|
28872
|
+
if (a.horizonRef) {
|
|
28873
|
+
parent = horizons.find((h) => h.label.toLowerCase() === a.horizonRef.toLowerCase() || h.id === a.horizonRef || h.slug === a.horizonRef);
|
|
28874
|
+
if (!parent) {
|
|
28875
|
+
const available = horizons.map((h) => h.label).join(", ");
|
|
28876
|
+
return errorResponse(`Parent horizon "${a.horizonRef}" not found. Available horizons: ${available || "none"}.`);
|
|
28877
|
+
}
|
|
28878
|
+
} else if (horizons.length === 1) {
|
|
28879
|
+
parent = horizons[0];
|
|
28880
|
+
} else if (horizons.length === 0) {
|
|
28881
|
+
return errorResponse('No horizon exists to attach the stage to. Create a horizon first (level:"horizon", label:"H1: ...").');
|
|
28882
|
+
} else {
|
|
28883
|
+
return errorResponse(`Multiple horizons exist \u2014 pass \`horizon\` to say which one the stage belongs to: ${horizons.map((h) => h.label).join(", ")}.`);
|
|
28884
|
+
}
|
|
28885
|
+
const newId = await adapter2.createStage({
|
|
28886
|
+
slug: a.slug ?? slugify(createLabel),
|
|
28887
|
+
label: createLabel,
|
|
28888
|
+
description: a.description,
|
|
28889
|
+
status: a.status ?? "Not Started",
|
|
28890
|
+
sortOrder: a.sortOrder ?? nextSortOrder(stages.filter((s) => s.horizonId === parent.id)),
|
|
28891
|
+
horizonId: parent.id
|
|
28892
|
+
});
|
|
28893
|
+
const refreshed = await adapter2.readStages();
|
|
28894
|
+
stage = refreshed.find((s) => s.id === newId);
|
|
28895
|
+
if (!stage) return errorResponse("Stage was created but could not be re-read.");
|
|
28896
|
+
created = true;
|
|
28897
|
+
resultLines.push(`Stage created: **${stage.label}** (under ${parent.label}, status ${stage.status})`);
|
|
28898
|
+
}
|
|
28899
|
+
if (a.status && !created) {
|
|
28900
|
+
if (!adapter2.updateStageStatus) return errorResponse("Stage status updates are not supported by the current adapter.");
|
|
28901
|
+
if (stage.status === a.status) {
|
|
28902
|
+
resultLines.push(`Stage "${stage.label}" is already "${a.status}".`);
|
|
28903
|
+
} else {
|
|
28904
|
+
const oldStatus = stage.status;
|
|
28905
|
+
await adapter2.updateStageStatus(stage.id, a.status);
|
|
28906
|
+
resultLines.push(`Stage updated: **${stage.label}** ${oldStatus} \u2192 ${a.status}`);
|
|
28907
|
+
}
|
|
28908
|
+
}
|
|
28909
|
+
if (a.exitCriteria !== void 0) {
|
|
28910
|
+
if (!adapter2.updateStageExitCriteria) return errorResponse("Exit criteria updates are not supported by the current adapter.");
|
|
28911
|
+
await adapter2.updateStageExitCriteria(stage.id, a.exitCriteria);
|
|
28912
|
+
resultLines.push(`Exit criteria set (${a.exitCriteria.length} item${a.exitCriteria.length !== 1 ? "s" : ""}, all unmet):`);
|
|
28913
|
+
a.exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
|
|
28914
|
+
}
|
|
28915
|
+
if (a.setCriterion !== void 0) {
|
|
28916
|
+
if (!adapter2.setCriterionMet) return errorResponse("set_criterion_met is not supported by the current adapter.");
|
|
28917
|
+
await adapter2.setCriterionMet(stage.id, a.setCriterion.criterion_id, a.setCriterion.met, a.setCriterion.evidence ?? null);
|
|
28918
|
+
const after = (await adapter2.readStages()).find((s) => s.id === stage.id);
|
|
28919
|
+
const crit = after?.exitCriteria?.find((c) => c.id === a.setCriterion.criterion_id);
|
|
28920
|
+
if (!crit) {
|
|
28921
|
+
resultLines.push(`\u26A0\uFE0F Criterion id "${a.setCriterion.criterion_id}" not found on this stage \u2014 no change.`);
|
|
28922
|
+
} else {
|
|
28923
|
+
const met = after?.exitCriteria?.filter((c) => c.met).length ?? 0;
|
|
28924
|
+
const total = after?.exitCriteria?.length ?? 0;
|
|
28925
|
+
resultLines.push(`Criterion "${crit.text}" \u2192 ${crit.met ? "met" : "unmet"}. [${met}/${total} criteria met]`);
|
|
28926
|
+
}
|
|
28927
|
+
}
|
|
28928
|
+
return textResponse(resultLines.join("\n"));
|
|
28929
|
+
}
|
|
28930
|
+
async function handleHorizon(adapter2, a) {
|
|
28931
|
+
if (!adapter2.readHorizons) {
|
|
28932
|
+
return errorResponse("Horizon management is not supported by the current adapter.");
|
|
28933
|
+
}
|
|
28934
|
+
const horizons = await adapter2.readHorizons();
|
|
28935
|
+
const horizon = horizons.find((h) => h.label.toLowerCase() === a.name.toLowerCase() || h.id === a.name || h.slug === a.name);
|
|
28936
|
+
if (!horizon) {
|
|
28937
|
+
if (!adapter2.createHorizon) return errorResponse("Horizon creation is not supported by the current adapter.");
|
|
28938
|
+
const createLabel = a.label ?? a.name;
|
|
28939
|
+
const newId = await adapter2.createHorizon({
|
|
28940
|
+
slug: a.slug ?? slugify(createLabel),
|
|
28941
|
+
label: createLabel,
|
|
28942
|
+
description: a.description,
|
|
28943
|
+
status: a.status ?? "Not Started",
|
|
28944
|
+
sortOrder: a.sortOrder ?? nextSortOrder(horizons)
|
|
28945
|
+
});
|
|
28946
|
+
return textResponse(`Horizon created: **${createLabel}** (id ${newId}, status ${a.status ?? "Not Started"})`);
|
|
28947
|
+
}
|
|
28948
|
+
if (!a.status) return errorResponse("status is required to update an existing horizon (or pass a new name to create one).");
|
|
28949
|
+
if (horizon.status === a.status) return textResponse(`Horizon "${horizon.label}" is already "${a.status}". No change made.`);
|
|
28950
|
+
if (!adapter2.updateHorizonStatus) return errorResponse("Horizon status updates are not supported by the current adapter.");
|
|
28951
|
+
const oldStatus = horizon.status;
|
|
28952
|
+
await adapter2.updateHorizonStatus(horizon.id, a.status);
|
|
28953
|
+
return textResponse(`Horizon updated: **${horizon.label}** ${oldStatus} \u2192 ${a.status}`);
|
|
28954
|
+
}
|
|
28705
28955
|
|
|
28706
28956
|
// src/services/zoom-out.ts
|
|
28707
28957
|
var BUDGET_SOFT = 12e4;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.70",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.getpapi/papi",
|