gsd-pi 2.59.0-dev.3de3832 → 2.59.0-dev.d77b3dd
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/resources/extensions/gsd/auto/phases.js +54 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +8 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +40 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +70 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +51 -5
- package/dist/resources/extensions/gsd/captures.js +54 -1
- package/dist/resources/extensions/gsd/complexity-classifier.js +1 -1
- package/dist/resources/extensions/gsd/context-masker.js +68 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +7 -0
- package/dist/resources/extensions/gsd/gsd-db.js +2 -2
- package/dist/resources/extensions/gsd/model-router.js +123 -4
- package/dist/resources/extensions/gsd/phase-anchor.js +56 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +46 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +6 -1
- package/dist/resources/extensions/gsd/rethink.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/status-guards.js +4 -3
- package/dist/resources/extensions/gsd/triage-resolution.js +128 -1
- package/dist/resources/extensions/gsd/triage-ui.js +12 -3
- package/dist/resources/skills/btw/SKILL.md +42 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +60 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +12 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +48 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +78 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +53 -4
- package/src/resources/extensions/gsd/captures.ts +71 -2
- package/src/resources/extensions/gsd/complexity-classifier.ts +1 -1
- package/src/resources/extensions/gsd/context-masker.ts +74 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +7 -0
- package/src/resources/extensions/gsd/gsd-db.ts +2 -2
- package/src/resources/extensions/gsd/model-router.ts +171 -8
- package/src/resources/extensions/gsd/phase-anchor.ts +71 -0
- package/src/resources/extensions/gsd/preferences-types.ts +9 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +38 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +6 -1
- package/src/resources/extensions/gsd/rethink.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/status-guards.ts +4 -3
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +87 -1
- package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/stop-backtrack.test.ts +216 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +1 -1
- package/src/resources/extensions/gsd/triage-resolution.ts +144 -1
- package/src/resources/extensions/gsd/triage-ui.ts +12 -3
- package/src/resources/skills/btw/SKILL.md +42 -0
- /package/dist/web/standalone/.next/static/{Y_HG7cJVptjBpkVSQQiFi → t_cBZAENjaOJIRST3dw08}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Y_HG7cJVptjBpkVSQQiFi → t_cBZAENjaOJIRST3dw08}/_ssgManifest.js +0 -0
|
@@ -487,11 +487,47 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
487
487
|
}
|
|
488
488
|
// ─── runGuards ────────────────────────────────────────────────────────────────
|
|
489
489
|
/**
|
|
490
|
-
* Phase 2: Guards — budget ceiling, context window, secrets re-check.
|
|
490
|
+
* Phase 2: Guards — stop directives, budget ceiling, context window, secrets re-check.
|
|
491
491
|
* Returns break to exit the loop, or next to proceed to dispatch.
|
|
492
492
|
*/
|
|
493
493
|
export async function runGuards(ic, mid) {
|
|
494
494
|
const { ctx, pi, s, deps, prefs } = ic;
|
|
495
|
+
// ── Stop/Backtrack directive guard (#3487) ──
|
|
496
|
+
// Check for unexecuted stop or backtrack captures BEFORE dispatching any unit.
|
|
497
|
+
// This ensures user "halt" directives are honored immediately.
|
|
498
|
+
try {
|
|
499
|
+
const { loadStopCaptures, markCaptureExecuted } = await import("../captures.js");
|
|
500
|
+
const stopCaptures = loadStopCaptures(s.basePath);
|
|
501
|
+
if (stopCaptures.length > 0) {
|
|
502
|
+
const first = stopCaptures[0];
|
|
503
|
+
const isBacktrack = first.classification === "backtrack";
|
|
504
|
+
const label = isBacktrack
|
|
505
|
+
? `Backtrack directive: ${first.text}`
|
|
506
|
+
: `Stop directive: ${first.text}`;
|
|
507
|
+
ctx.ui.notify(label, "warning");
|
|
508
|
+
deps.sendDesktopNotification("GSD", label, "warning", "stop-directive", basename(s.originalBasePath || s.basePath));
|
|
509
|
+
// Mark all stop/backtrack captures as executed so they don't re-fire
|
|
510
|
+
for (const cap of stopCaptures) {
|
|
511
|
+
markCaptureExecuted(s.basePath, cap.id);
|
|
512
|
+
}
|
|
513
|
+
// For backtrack captures, write the backtrack trigger before pausing
|
|
514
|
+
if (isBacktrack) {
|
|
515
|
+
try {
|
|
516
|
+
const { executeBacktrack } = await import("../triage-resolution.js");
|
|
517
|
+
executeBacktrack(s.basePath, mid, first);
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
debugLog("guards", { phase: "backtrack-execution-error", error: String(e) });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
await deps.pauseAuto(ctx, pi);
|
|
524
|
+
debugLog("autoLoop", { phase: "exit", reason: isBacktrack ? "user-backtrack" : "user-stop" });
|
|
525
|
+
return { action: "break", reason: isBacktrack ? "user-backtrack" : "user-stop" };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
debugLog("guards", { phase: "stop-guard-error", error: String(e) });
|
|
530
|
+
}
|
|
495
531
|
// Budget ceiling guard
|
|
496
532
|
const budgetCeiling = prefs?.budget_ceiling;
|
|
497
533
|
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
@@ -843,6 +879,23 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
843
879
|
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
844
880
|
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
845
881
|
}
|
|
882
|
+
// Write phase handoff anchor after successful research/planning completion
|
|
883
|
+
const anchorPhases = new Set(["research-milestone", "research-slice", "plan-milestone", "plan-slice"]);
|
|
884
|
+
if (artifactVerified && mid && anchorPhases.has(unitType)) {
|
|
885
|
+
try {
|
|
886
|
+
const { writePhaseAnchor } = await import("../phase-anchor.js");
|
|
887
|
+
writePhaseAnchor(s.basePath, mid, {
|
|
888
|
+
phase: unitType,
|
|
889
|
+
milestoneId: mid,
|
|
890
|
+
generatedAt: new Date().toISOString(),
|
|
891
|
+
intent: `Completed ${unitType} for ${unitId}`,
|
|
892
|
+
decisions: [],
|
|
893
|
+
blockers: [],
|
|
894
|
+
nextSteps: [],
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
catch { /* non-fatal — anchor is advisory */ }
|
|
898
|
+
}
|
|
846
899
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified, ...(unitResult.errorContext ? { errorContext: unitResult.errorContext } : {}) }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
|
|
847
900
|
return { action: "next", data: { unitStartedAt: s.currentUnit?.startedAt } };
|
|
848
901
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and fallback chains.
|
|
5
5
|
*/
|
|
6
6
|
import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js";
|
|
7
|
-
import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
|
|
7
|
+
import { classifyUnitComplexity, tierLabel, extractTaskMetadata } from "./complexity-classifier.js";
|
|
8
8
|
import { resolveModelForComplexity, escalateTier } from "./model-router.js";
|
|
9
9
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
10
10
|
import { unitPhaseLabel } from "./auto-dashboard.js";
|
|
@@ -68,14 +68,19 @@ export async function selectAndApplyModel(ctx, pi, unitType, unitId, basePath, p
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
// Extract task metadata for capability scoring
|
|
72
|
+
const taskMeta = unitType === "execute-task"
|
|
73
|
+
? extractTaskMetadata(unitId, basePath)
|
|
74
|
+
: undefined;
|
|
75
|
+
const routingResult = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds, unitType, taskMeta);
|
|
72
76
|
if (routingResult.wasDowngraded) {
|
|
73
77
|
effectiveModelConfig = {
|
|
74
78
|
primary: routingResult.modelId,
|
|
75
79
|
fallbacks: routingResult.fallbacks,
|
|
76
80
|
};
|
|
77
81
|
if (verbose) {
|
|
78
|
-
|
|
82
|
+
const method = routingResult.selectionMethod === "capability-scored" ? "capability-scored" : "tier-only";
|
|
83
|
+
ctx.ui.notify(`Dynamic routing [${tierLabel(classification.tier)}]: ${routingResult.modelId} (${method} — ${classification.reason})`, "info");
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
routingTierLabel = ` [${tierLabel(classification.tier)}]`;
|
|
@@ -26,7 +26,7 @@ import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getA
|
|
|
26
26
|
import { renderPlanCheckboxes } from "./markdown-renderer.js";
|
|
27
27
|
import { consumeSignal } from "./session-status-io.js";
|
|
28
28
|
import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, resolveHookArtifactPath, } from "./post-unit-hooks.js";
|
|
29
|
-
import { hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
29
|
+
import { hasPendingCaptures, loadPendingCaptures, revertExecutorResolvedCaptures } from "./captures.js";
|
|
30
30
|
import { debugLog } from "./debug-logger.js";
|
|
31
31
|
import { runSafely } from "./auto-utils.js";
|
|
32
32
|
/** Enqueue a sidecar item (hook, triage, or quick-task) for the main loop to
|
|
@@ -478,6 +478,45 @@ export async function postUnitPostVerification(pctx) {
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
|
+
// ── Fast-path stop detection (#3487) ──
|
|
482
|
+
// Before waiting for triage, check if any PENDING captures contain explicit
|
|
483
|
+
// stop/halt language. If so, pause immediately — don't wait for triage.
|
|
484
|
+
if (s.currentUnit && s.currentUnit.type !== "triage-captures") {
|
|
485
|
+
try {
|
|
486
|
+
const pending = loadPendingCaptures(s.basePath);
|
|
487
|
+
// Match only when the capture text starts with a stop/halt directive word,
|
|
488
|
+
// or the entire text is short and dominated by such a word. This avoids
|
|
489
|
+
// false positives on captures like "add a pause button" or "stop the timer
|
|
490
|
+
// from re-rendering" — those are feature descriptions, not halt directives.
|
|
491
|
+
const STOP_PATTERN = /^(stop|halt|abort|don'?t continue|pause|cease)\b/i;
|
|
492
|
+
const stopCapture = pending.find(c => STOP_PATTERN.test(c.text.trim()));
|
|
493
|
+
if (stopCapture) {
|
|
494
|
+
ctx.ui.notify(`Stop directive detected in pending capture ${stopCapture.id}: "${stopCapture.text}" — pausing auto-mode.`, "warning");
|
|
495
|
+
debugLog("postUnit", { phase: "fast-stop", captureId: stopCapture.id });
|
|
496
|
+
await pauseAuto(ctx, pi);
|
|
497
|
+
return "stopped";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
debugLog("postUnit", { phase: "fast-stop-error", error: String(e) });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// ── Capture protection: revert executor-silenced captures (#3487) ──
|
|
505
|
+
// Non-triage agents can write **Status:** resolved to CAPTURES.md, bypassing
|
|
506
|
+
// the triage pipeline. Revert those to pending before the triage check.
|
|
507
|
+
if (s.currentUnit &&
|
|
508
|
+
s.currentUnit.type !== "triage-captures") {
|
|
509
|
+
try {
|
|
510
|
+
const reverted = revertExecutorResolvedCaptures(s.basePath);
|
|
511
|
+
if (reverted > 0) {
|
|
512
|
+
debugLog("postUnit", { phase: "capture-protection", reverted });
|
|
513
|
+
ctx.ui.notify(`Reverted ${reverted} capture${reverted === 1 ? "" : "s"} silenced by executor — re-queuing for triage.`, "warning");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
debugLog("postUnit", { phase: "capture-protection-error", error: String(e) });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
481
520
|
// ── Triage check ──
|
|
482
521
|
if (!s.stepMode &&
|
|
483
522
|
s.currentUnit &&
|
|
@@ -17,6 +17,7 @@ import { existsSync } from "node:fs";
|
|
|
17
17
|
import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
|
|
18
18
|
import { getPendingGates } from "./gsd-db.js";
|
|
19
19
|
import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
|
|
20
|
+
import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
|
|
20
21
|
// ─── Preamble Cap ─────────────────────────────────────────────────────────────
|
|
21
22
|
const MAX_PREAMBLE_CHARS = 30_000;
|
|
22
23
|
function capPreamble(preamble) {
|
|
@@ -797,6 +798,10 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
|
|
|
797
798
|
const researchPath = resolveMilestoneFile(base, mid, "RESEARCH");
|
|
798
799
|
const researchRel = relMilestoneFile(base, mid, "RESEARCH");
|
|
799
800
|
const inlined = [];
|
|
801
|
+
// Inject phase handoff anchor from research phase (if available)
|
|
802
|
+
const researchAnchor = readPhaseAnchor(base, mid, "research-milestone");
|
|
803
|
+
if (researchAnchor)
|
|
804
|
+
inlined.push(formatAnchorForPrompt(researchAnchor));
|
|
800
805
|
inlined.push(await inlineFile(contextPath, contextRel, "Milestone Context"));
|
|
801
806
|
const researchInline = await inlineFileOptional(researchPath, researchRel, "Milestone Research");
|
|
802
807
|
if (researchInline)
|
|
@@ -919,6 +924,10 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
|
|
|
919
924
|
const researchPath = resolveSliceFile(base, mid, sid, "RESEARCH");
|
|
920
925
|
const researchRel = relSliceFile(base, mid, sid, "RESEARCH");
|
|
921
926
|
const inlined = [];
|
|
927
|
+
// Inject phase handoff anchor from research phase (if available)
|
|
928
|
+
const researchSliceAnchor = readPhaseAnchor(base, mid, "research-slice");
|
|
929
|
+
if (researchSliceAnchor)
|
|
930
|
+
inlined.push(formatAnchorForPrompt(researchSliceAnchor));
|
|
922
931
|
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
923
932
|
const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
|
|
924
933
|
if (researchInline)
|
|
@@ -974,6 +983,8 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
|
|
|
974
983
|
? level
|
|
975
984
|
: { level: level };
|
|
976
985
|
const inlineLevel = opts.level ?? resolveInlineLevel();
|
|
986
|
+
// Inject phase handoff anchor from planning phase (if available)
|
|
987
|
+
const planAnchor = readPhaseAnchor(base, mid, "plan-slice");
|
|
977
988
|
const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
|
|
978
989
|
const priorLines = priorSummaries.length > 0
|
|
979
990
|
? priorSummaries.map(p => `- \`${p}\``).join("\n")
|
|
@@ -1042,9 +1053,11 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
|
|
|
1042
1053
|
const runtimeContext = runtimeContent
|
|
1043
1054
|
? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
|
|
1044
1055
|
: "";
|
|
1056
|
+
const phaseAnchorSection = planAnchor ? formatAnchorForPrompt(planAnchor) : "";
|
|
1045
1057
|
return loadPrompt("execute-task", {
|
|
1046
1058
|
overridesSection,
|
|
1047
1059
|
runtimeContext,
|
|
1060
|
+
phaseAnchorSection,
|
|
1048
1061
|
workingDirectory: base,
|
|
1049
1062
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
|
|
1050
1063
|
planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
|
|
@@ -832,6 +832,76 @@ export function registerDbTools(pi) {
|
|
|
832
832
|
};
|
|
833
833
|
pi.registerTool(sliceCompleteTool);
|
|
834
834
|
registerAlias(pi, sliceCompleteTool, "gsd_complete_slice", "gsd_slice_complete");
|
|
835
|
+
// ─── gsd_skip_slice (#3477 / #3487) ───────────────────────────────────
|
|
836
|
+
const skipSliceExecute = async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
837
|
+
const dbAvailable = await ensureDbOpen();
|
|
838
|
+
if (!dbAvailable) {
|
|
839
|
+
return {
|
|
840
|
+
content: [{ type: "text", text: "Error: GSD database is not available. Cannot skip slice." }],
|
|
841
|
+
details: { operation: "skip_slice", error: "db_unavailable" },
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const { getSlice, updateSliceStatus } = await import("../gsd-db.js");
|
|
846
|
+
const { invalidateStateCache } = await import("../state.js");
|
|
847
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
848
|
+
if (!slice) {
|
|
849
|
+
return {
|
|
850
|
+
content: [{ type: "text", text: `Error: Slice ${params.sliceId} not found in milestone ${params.milestoneId}` }],
|
|
851
|
+
details: { operation: "skip_slice", error: "slice_not_found" },
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (slice.status === "complete" || slice.status === "done") {
|
|
855
|
+
return {
|
|
856
|
+
content: [{ type: "text", text: `Error: Slice ${params.sliceId} is already complete — cannot skip.` }],
|
|
857
|
+
details: { operation: "skip_slice", error: "already_complete" },
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (slice.status === "skipped") {
|
|
861
|
+
return {
|
|
862
|
+
content: [{ type: "text", text: `Slice ${params.sliceId} is already skipped.` }],
|
|
863
|
+
details: { operation: "skip_slice", sliceId: params.sliceId, milestoneId: params.milestoneId },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
updateSliceStatus(params.milestoneId, params.sliceId, "skipped");
|
|
867
|
+
invalidateStateCache();
|
|
868
|
+
return {
|
|
869
|
+
content: [{ type: "text", text: `Skipped slice ${params.sliceId} (${params.milestoneId}). Reason: ${params.reason ?? "User-directed skip"}. Auto-mode will advance past this slice.` }],
|
|
870
|
+
details: {
|
|
871
|
+
operation: "skip_slice",
|
|
872
|
+
sliceId: params.sliceId,
|
|
873
|
+
milestoneId: params.milestoneId,
|
|
874
|
+
reason: params.reason,
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
880
|
+
logError("tool", `skip_slice tool failed: ${msg}`, { tool: "gsd_skip_slice", error: String(err) });
|
|
881
|
+
return {
|
|
882
|
+
content: [{ type: "text", text: `Error skipping slice: ${msg}` }],
|
|
883
|
+
details: { operation: "skip_slice", error: msg },
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
pi.registerTool({
|
|
888
|
+
name: "gsd_skip_slice",
|
|
889
|
+
label: "Skip Slice",
|
|
890
|
+
description: "Mark a slice as skipped so auto-mode advances past it without executing. " +
|
|
891
|
+
"The slice data is preserved for reference. The state machine treats skipped slices like completed ones for dependency satisfaction.",
|
|
892
|
+
promptSnippet: "Skip a GSD slice (mark as skipped, auto-mode will advance past it)",
|
|
893
|
+
promptGuidelines: [
|
|
894
|
+
"Use gsd_skip_slice when a slice should be bypassed — descoped, superseded, or no longer relevant.",
|
|
895
|
+
"Cannot skip a slice that is already complete.",
|
|
896
|
+
"Skipped slices satisfy downstream dependencies just like completed slices.",
|
|
897
|
+
],
|
|
898
|
+
parameters: Type.Object({
|
|
899
|
+
sliceId: Type.String({ description: "Slice ID (e.g. S02)" }),
|
|
900
|
+
milestoneId: Type.String({ description: "Milestone ID (e.g. M003)" }),
|
|
901
|
+
reason: Type.Optional(Type.String({ description: "Reason for skipping this slice" })),
|
|
902
|
+
}),
|
|
903
|
+
execute: skipSliceExecute,
|
|
904
|
+
});
|
|
835
905
|
// ─── gsd_complete_milestone ────────────────────────────────────────────
|
|
836
906
|
const milestoneCompleteExecute = async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
837
907
|
const dbAvailable = await ensureDbOpen();
|
|
@@ -244,16 +244,62 @@ export function registerHooks(pi) {
|
|
|
244
244
|
await syncServiceTierStatus(ctx);
|
|
245
245
|
});
|
|
246
246
|
pi.on("before_provider_request", async (event) => {
|
|
247
|
+
const payload = event.payload;
|
|
248
|
+
if (!payload || typeof payload !== "object")
|
|
249
|
+
return;
|
|
250
|
+
// ── Observation Masking ─────────────────────────────────────────────
|
|
251
|
+
// Replace old tool results with placeholders to reduce context bloat.
|
|
252
|
+
// Only active during auto-mode when context_management.observation_masking is enabled.
|
|
253
|
+
if (isAutoActive()) {
|
|
254
|
+
try {
|
|
255
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
256
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
257
|
+
const cmConfig = prefs?.preferences.context_management;
|
|
258
|
+
// Observation masking: replace old tool results with placeholders
|
|
259
|
+
if (cmConfig?.observation_masking !== false) {
|
|
260
|
+
const keepTurns = cmConfig?.observation_mask_turns ?? 8;
|
|
261
|
+
const { createObservationMask } = await import("../context-masker.js");
|
|
262
|
+
const mask = createObservationMask(keepTurns);
|
|
263
|
+
const messages = payload.messages;
|
|
264
|
+
if (Array.isArray(messages)) {
|
|
265
|
+
payload.messages = mask(messages);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Tool result truncation: cap individual tool result content length.
|
|
269
|
+
// In pi-ai format, toolResult messages have role: "toolResult" and content: TextContent[].
|
|
270
|
+
// Creates new objects to avoid mutating shared conversation state.
|
|
271
|
+
const maxChars = cmConfig?.tool_result_max_chars ?? 800;
|
|
272
|
+
const msgs = payload.messages;
|
|
273
|
+
if (Array.isArray(msgs)) {
|
|
274
|
+
payload.messages = msgs.map((msg) => {
|
|
275
|
+
// Match toolResult messages (role: "toolResult", content is array of content blocks)
|
|
276
|
+
if (msg?.role === "toolResult" && Array.isArray(msg.content)) {
|
|
277
|
+
const blocks = msg.content;
|
|
278
|
+
const totalLen = blocks.reduce((sum, b) => sum + (typeof b.text === "string" ? b.text.length : 0), 0);
|
|
279
|
+
if (totalLen > maxChars) {
|
|
280
|
+
const truncated = blocks.map(b => {
|
|
281
|
+
if (typeof b.text === "string" && b.text.length > maxChars) {
|
|
282
|
+
return { ...b, text: b.text.slice(0, maxChars) + "\n…[truncated]" };
|
|
283
|
+
}
|
|
284
|
+
return b;
|
|
285
|
+
});
|
|
286
|
+
return { ...msg, content: truncated };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return msg;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch { /* non-fatal */ }
|
|
294
|
+
}
|
|
295
|
+
// ── Service Tier ────────────────────────────────────────────────────
|
|
247
296
|
const modelId = event.model?.id;
|
|
248
297
|
if (!modelId)
|
|
249
|
-
return;
|
|
298
|
+
return payload;
|
|
250
299
|
const { getEffectiveServiceTier, supportsServiceTier } = await import("../service-tier.js");
|
|
251
300
|
const tier = getEffectiveServiceTier();
|
|
252
301
|
if (!tier || !supportsServiceTier(modelId))
|
|
253
|
-
return;
|
|
254
|
-
const payload = event.payload;
|
|
255
|
-
if (!payload || typeof payload !== "object")
|
|
256
|
-
return;
|
|
302
|
+
return payload;
|
|
257
303
|
payload.service_tier = tier;
|
|
258
304
|
return payload;
|
|
259
305
|
});
|
|
@@ -14,7 +14,7 @@ import { gsdRoot } from "./paths.js";
|
|
|
14
14
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
15
|
const CAPTURES_FILENAME = "CAPTURES.md";
|
|
16
16
|
const VALID_CLASSIFICATIONS = [
|
|
17
|
-
"quick-task", "inject", "defer", "replan", "note",
|
|
17
|
+
"quick-task", "inject", "defer", "replan", "note", "stop", "backtrack",
|
|
18
18
|
];
|
|
19
19
|
// ─── Path Resolution ──────────────────────────────────────────────────────────
|
|
20
20
|
/**
|
|
@@ -216,6 +216,59 @@ export function loadActionableCaptures(basePath, currentMilestoneId) {
|
|
|
216
216
|
!c.resolvedInMilestone ||
|
|
217
217
|
c.resolvedInMilestone === currentMilestoneId));
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Load unexecuted stop captures — user directives to halt auto-mode.
|
|
221
|
+
* These are checked in the pre-dispatch guard pipeline (runGuards) to
|
|
222
|
+
* pause auto-mode before the next unit is dispatched.
|
|
223
|
+
*/
|
|
224
|
+
export function loadStopCaptures(basePath) {
|
|
225
|
+
return loadAllCaptures(basePath).filter(c => c.status === "resolved" && !c.executed &&
|
|
226
|
+
(c.classification === "stop" || c.classification === "backtrack"));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Load unexecuted backtrack captures specifically — captures directing
|
|
230
|
+
* auto-mode to abandon current milestone and return to a previous one.
|
|
231
|
+
*/
|
|
232
|
+
export function loadBacktrackCaptures(basePath) {
|
|
233
|
+
return loadAllCaptures(basePath).filter(c => c.status === "resolved" && !c.executed && c.classification === "backtrack");
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Revert captures that were silenced by non-triage agents.
|
|
237
|
+
*
|
|
238
|
+
* When an execute-task or other non-triage agent writes `**Status:** resolved`
|
|
239
|
+
* to CAPTURES.md, it bypasses the triage pipeline entirely. This function
|
|
240
|
+
* detects such captures (resolved but missing the Classification field that
|
|
241
|
+
* triage always writes) and reverts them to pending so the triage sidecar
|
|
242
|
+
* picks them up properly.
|
|
243
|
+
*
|
|
244
|
+
* Returns the number of captures reverted.
|
|
245
|
+
*/
|
|
246
|
+
export function revertExecutorResolvedCaptures(basePath) {
|
|
247
|
+
const filePath = resolveCapturesPath(basePath);
|
|
248
|
+
if (!existsSync(filePath))
|
|
249
|
+
return 0;
|
|
250
|
+
let content = readFileSync(filePath, "utf-8");
|
|
251
|
+
let reverted = 0;
|
|
252
|
+
const all = loadAllCaptures(basePath);
|
|
253
|
+
for (const capture of all) {
|
|
254
|
+
// A properly triaged capture has both resolved status AND a classification.
|
|
255
|
+
// An executor-silenced capture has resolved status but NO classification.
|
|
256
|
+
if (capture.status === "resolved" && !capture.classification) {
|
|
257
|
+
const sectionRegex = new RegExp(`(### ${escapeRegex(capture.id)}\\n(?:(?!### ).)*?)(?=### |$)`, "s");
|
|
258
|
+
const match = sectionRegex.exec(content);
|
|
259
|
+
if (match) {
|
|
260
|
+
let section = match[1];
|
|
261
|
+
section = section.replace(/\*\*Status:\*\*\s*resolved/i, "**Status:** pending");
|
|
262
|
+
content = content.replace(sectionRegex, section);
|
|
263
|
+
reverted++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (reverted > 0) {
|
|
268
|
+
writeFileSync(filePath, content, "utf-8");
|
|
269
|
+
}
|
|
270
|
+
return reverted;
|
|
271
|
+
}
|
|
219
272
|
/**
|
|
220
273
|
* Retroactively stamp a capture with a milestone ID.
|
|
221
274
|
*
|
|
@@ -149,7 +149,7 @@ function analyzePlanComplexity(unitId, basePath) {
|
|
|
149
149
|
/**
|
|
150
150
|
* Extract task metadata from the task plan file on disk.
|
|
151
151
|
*/
|
|
152
|
-
function extractTaskMetadata(unitId, basePath) {
|
|
152
|
+
export function extractTaskMetadata(unitId, basePath) {
|
|
153
153
|
const meta = {};
|
|
154
154
|
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
155
155
|
if (!mid || !sid || !tid)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation masking for GSD auto-mode sessions.
|
|
3
|
+
*
|
|
4
|
+
* Replaces tool result content older than N turns with a placeholder.
|
|
5
|
+
* Reduces context bloat between compactions with zero LLM overhead.
|
|
6
|
+
* Preserves message ordering, roles, and all assistant/user messages.
|
|
7
|
+
*
|
|
8
|
+
* Operates on the pi-ai Message[] format (post-convertToLlm, pre-provider):
|
|
9
|
+
* - toolResult messages: { role: "toolResult", content: TextContent[] }
|
|
10
|
+
* - bash results are already converted to: { role: "user", content: [{type:"text",text:"..."}] }
|
|
11
|
+
* and start with "Ran `" from bashExecutionToText.
|
|
12
|
+
*/
|
|
13
|
+
const MASK_PLACEHOLDER = "[result masked — within summarized history]";
|
|
14
|
+
const MASK_CONTENT_BLOCK = [{ type: "text", text: MASK_PLACEHOLDER }];
|
|
15
|
+
function findTurnBoundary(messages, keepRecentTurns) {
|
|
16
|
+
let turnsSeen = 0;
|
|
17
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
18
|
+
const m = messages[i];
|
|
19
|
+
// In the LLM payload, genuine user turns have role "user".
|
|
20
|
+
// Tool results have role "toolResult" and are excluded by this check.
|
|
21
|
+
if (m.role === "user") {
|
|
22
|
+
// Skip bash-result user messages (converted from bashExecution) — these aren't real user turns
|
|
23
|
+
if (isBashResultUserMessage(m))
|
|
24
|
+
continue;
|
|
25
|
+
turnsSeen++;
|
|
26
|
+
if (turnsSeen >= keepRecentTurns)
|
|
27
|
+
return i;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detect user messages that originated from bashExecution.
|
|
34
|
+
* After convertToLlm, these are {role: "user", content: [{type:"text", text:"Ran `cmd`\n..."}]}.
|
|
35
|
+
* The bashExecutionToText format always starts with "Ran `".
|
|
36
|
+
*/
|
|
37
|
+
function isBashResultUserMessage(m) {
|
|
38
|
+
if (m.role !== "user" || !Array.isArray(m.content))
|
|
39
|
+
return false;
|
|
40
|
+
const first = m.content[0];
|
|
41
|
+
return first && typeof first === "object" && "text" in first &&
|
|
42
|
+
typeof first.text === "string" && first.text.startsWith("Ran `");
|
|
43
|
+
}
|
|
44
|
+
function isMaskableMessage(m) {
|
|
45
|
+
// Tool result messages (role: "toolResult" in pi-ai format)
|
|
46
|
+
if (m.role === "toolResult")
|
|
47
|
+
return true;
|
|
48
|
+
// Bash-result user messages (converted from bashExecution by convertToLlm)
|
|
49
|
+
if (isBashResultUserMessage(m))
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
export function createObservationMask(keepRecentTurns = 8) {
|
|
54
|
+
return (messages) => {
|
|
55
|
+
const boundary = findTurnBoundary(messages, keepRecentTurns);
|
|
56
|
+
if (boundary === 0)
|
|
57
|
+
return messages;
|
|
58
|
+
return messages.map((m, i) => {
|
|
59
|
+
if (i >= boundary)
|
|
60
|
+
return m;
|
|
61
|
+
if (isMaskableMessage(m)) {
|
|
62
|
+
// Content may be string or array of content blocks — always replace with array
|
|
63
|
+
return { ...m, content: MASK_CONTENT_BLOCK };
|
|
64
|
+
}
|
|
65
|
+
return m;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -189,6 +189,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
189
189
|
- `budget_pressure`: boolean — downgrade model tier when budget is under pressure. Default: `true`.
|
|
190
190
|
- `cross_provider`: boolean — allow routing across different providers. Default: `true`.
|
|
191
191
|
- `hooks`: boolean — enable routing hooks. Default: `true`.
|
|
192
|
+
- `capability_routing`: boolean — enable capability-profile scoring for model selection within a tier. Requires `enabled: true`. Default: `false`.
|
|
193
|
+
|
|
194
|
+
- `context_management`: configures context hygiene for auto-mode sessions. Keys:
|
|
195
|
+
- `observation_masking`: boolean — mask old tool results to reduce context bloat. Default: `true`.
|
|
196
|
+
- `observation_mask_turns`: number — keep this many recent turns verbatim (1-50). Default: `8`.
|
|
197
|
+
- `compaction_threshold_percent`: number — trigger compaction at this % of context window (0.5-0.95). Lower values fire compaction earlier, reducing drift. Default: `0.70`.
|
|
198
|
+
- `tool_result_max_chars`: number — max chars per tool result in GSD sessions (200-10000). Default: `800`.
|
|
192
199
|
|
|
193
200
|
- `auto_visualize`: boolean — show a visualizer hint after each milestone completion in auto-mode. Default: `false`.
|
|
194
201
|
|
|
@@ -1374,11 +1374,11 @@ export function getActiveSliceFromDb(milestoneId) {
|
|
|
1374
1374
|
// Uses json_each() to expand the JSON depends array and checks each dep is complete.
|
|
1375
1375
|
const row = currentDb.prepare(`SELECT s.* FROM slices s
|
|
1376
1376
|
WHERE s.milestone_id = :mid
|
|
1377
|
-
AND s.status NOT IN ('complete', 'done')
|
|
1377
|
+
AND s.status NOT IN ('complete', 'done', 'skipped')
|
|
1378
1378
|
AND NOT EXISTS (
|
|
1379
1379
|
SELECT 1 FROM json_each(s.depends) AS dep
|
|
1380
1380
|
WHERE dep.value NOT IN (
|
|
1381
|
-
SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done')
|
|
1381
|
+
SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped')
|
|
1382
1382
|
)
|
|
1383
1383
|
)
|
|
1384
1384
|
ORDER BY s.sequence, s.id
|