feat(sf): port 3 gsd-2 DB helpers + improve /sf escalate list
Three small DB helpers from gsd-2 that SF was missing, plus a UX
improvement to /sf escalate list that uses one of them.
PDD spec:
setSliceSketchFlag(milestoneId, sliceId, isSketch) — generalized
sketch-flag setter. Replaces my narrower clearSliceSketch (which
remains as a thin wrapper for callers that only zero). Use this
when a re-plan flow wants to revert a slice back to sketch state.
autoHealSketchFlags(milestoneId, hasPlanFile) — safety net for
progressive planning. Predicate-based: caller passes a function
that resolves whether a PLAN file exists for a slice, function
flips is_sketch=0 for any slice that has both is_sketch=1 AND a
plan file. Catches DB-FS drift after crashes/manual edits.
listEscalationArtifacts(milestoneId, includeResolved=false) —
cross-slice DB-side filter for /sf escalate list. Replaces my
hand-rolled inner-loop over getMilestoneSlices() + getSliceTasks()
+ filter — single SQL query, sorted by sequence, faster.
UX improvement to commands-escalate.ts:
- /sf escalate list: now uses listEscalationArtifacts; shows
PENDING / awaiting-review / resolved status badges per entry.
- /sf escalate list --all: includes resolved entries (audit trail).
- Better hint message when none active: 'Use --all to include
resolved'.
Verified:
- typecheck clean (one parallel-session-introduced error in
self-feedback-drain.ts is unrelated — they import a missing
utils/error.ts; will land when their commit does).
- escalation-feature.test.ts (21 tests) + sf-db.test.ts (16
tests) still pass — no regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
82633b6f5e
commit
0f0aee5bf0
2 changed files with 95 additions and 13 deletions
|
|
@ -13,9 +13,9 @@ import type { ExtensionCommandContext } from "@singularity-forge/pi-coding-agent
|
||||||
import { readEscalationArtifact, resolveEscalation } from "./escalation.js";
|
import { readEscalationArtifact, resolveEscalation } from "./escalation.js";
|
||||||
import {
|
import {
|
||||||
getActiveMilestoneFromDb,
|
getActiveMilestoneFromDb,
|
||||||
getMilestoneSlices,
|
|
||||||
getSliceTasks,
|
getSliceTasks,
|
||||||
isDbAvailable,
|
isDbAvailable,
|
||||||
|
listEscalationArtifacts,
|
||||||
} from "./sf-db.js";
|
} from "./sf-db.js";
|
||||||
|
|
||||||
function usage(): string {
|
function usage(): string {
|
||||||
|
|
@ -60,22 +60,39 @@ export async function handleEscalate(
|
||||||
ctx.ui.notify("No active milestone — nothing to list.", "info");
|
ctx.ui.notify("No active milestone — nothing to list.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lines: string[] = [`Pending escalations for milestone ${ms.id}:`];
|
// Pass --all to also list resolved escalations (audit trail).
|
||||||
|
const includeResolved = rest.includes("--all");
|
||||||
|
const tasks = listEscalationArtifacts(ms.id, includeResolved);
|
||||||
|
const header = includeResolved
|
||||||
|
? `Escalations for milestone ${ms.id} (active + resolved):`
|
||||||
|
: `Active escalations for milestone ${ms.id}:`;
|
||||||
|
const lines: string[] = [header];
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const slice of getMilestoneSlices(ms.id)) {
|
for (const task of tasks) {
|
||||||
for (const task of getSliceTasks(ms.id, slice.id)) {
|
|
||||||
if (task.escalation_pending !== 1) continue;
|
|
||||||
if (!task.escalation_artifact_path) continue;
|
if (!task.escalation_artifact_path) continue;
|
||||||
const art = readEscalationArtifact(task.escalation_artifact_path);
|
const art = readEscalationArtifact(task.escalation_artifact_path);
|
||||||
if (!art || art.respondedAt) continue;
|
if (!art) continue;
|
||||||
count++;
|
count++;
|
||||||
lines.push(` ${slice.id}/${task.id}: ${art.question}`);
|
const status = task.escalation_pending === 1
|
||||||
|
? "PENDING"
|
||||||
|
: task.escalation_awaiting_review === 1
|
||||||
|
? "awaiting-review"
|
||||||
|
: art.respondedAt
|
||||||
|
? `resolved (${art.userChoice})`
|
||||||
|
: "(unknown)";
|
||||||
|
lines.push(` ${task.slice_id}/${task.id} [${status}]: ${art.question}`);
|
||||||
|
if (status === "PENDING") {
|
||||||
lines.push(` options: ${art.options.map((o) => o.id).join(", ")}`);
|
lines.push(` options: ${art.options.map((o) => o.id).join(", ")}`);
|
||||||
lines.push(` recommend: ${art.recommendation}`);
|
lines.push(` recommend: ${art.recommendation}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
ctx.ui.notify("No pending escalations.", "info");
|
ctx.ui.notify(
|
||||||
|
includeResolved
|
||||||
|
? "No escalations recorded."
|
||||||
|
: "No active escalations. Use /sf escalate list --all to include resolved.",
|
||||||
|
"info",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.ui.notify(lines.join("\n"), "info");
|
ctx.ui.notify(lines.join("\n"), "info");
|
||||||
|
|
|
||||||
|
|
@ -2216,12 +2216,77 @@ export function insertSlice(s: {
|
||||||
* Idempotent — safe to call on already-refined slices.
|
* Idempotent — safe to call on already-refined slices.
|
||||||
*/
|
*/
|
||||||
export function clearSliceSketch(milestoneId: string, sliceId: string): void {
|
export function clearSliceSketch(milestoneId: string, sliceId: string): void {
|
||||||
|
setSliceSketchFlag(milestoneId, sliceId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADR-011 (gsd-2 parity): generalized sketch-flag setter — flip true or false.
|
||||||
|
* Idempotent. Use this instead of clearSliceSketch when you also need to
|
||||||
|
* mark a slice as a sketch (e.g., a re-plan flow that wants to revert to
|
||||||
|
* sketch-then-refine).
|
||||||
|
*/
|
||||||
|
export function setSliceSketchFlag(
|
||||||
|
milestoneId: string,
|
||||||
|
sliceId: string,
|
||||||
|
isSketch: boolean,
|
||||||
|
): void {
|
||||||
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
|
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
|
||||||
currentDb
|
currentDb
|
||||||
.prepare(
|
.prepare(
|
||||||
`UPDATE slices SET is_sketch = 0 WHERE milestone_id = :mid AND id = :sid`,
|
`UPDATE slices SET is_sketch = :is_sketch WHERE milestone_id = :mid AND id = :sid`,
|
||||||
)
|
)
|
||||||
.run({ ":mid": milestoneId, ":sid": sliceId });
|
.run({
|
||||||
|
":is_sketch": isSketch ? 1 : 0,
|
||||||
|
":mid": milestoneId,
|
||||||
|
":sid": sliceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADR-011 auto-heal: reconcile stale is_sketch=1 rows whose PLAN file already
|
||||||
|
* exists on disk. The caller passes a predicate that uses the canonical path
|
||||||
|
* resolver so path logic stays in one place. Safe to call repeatedly — only
|
||||||
|
* flips rows that meet the predicate.
|
||||||
|
*/
|
||||||
|
export function autoHealSketchFlags(
|
||||||
|
milestoneId: string,
|
||||||
|
hasPlanFile: (sliceId: string) => boolean,
|
||||||
|
): void {
|
||||||
|
if (!currentDb) return;
|
||||||
|
const rows = currentDb
|
||||||
|
.prepare(
|
||||||
|
`SELECT id FROM slices WHERE milestone_id = :mid AND is_sketch = 1`,
|
||||||
|
)
|
||||||
|
.all({ ":mid": milestoneId }) as Array<{ id: string }>;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (hasPlanFile(row.id)) {
|
||||||
|
setSliceSketchFlag(milestoneId, row.id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ADR-011 P2 (gsd-2 parity): list tasks across a milestone that have an
|
||||||
|
* escalation artifact path. By default returns only ACTIVE escalations
|
||||||
|
* (pending OR awaiting_review); pass includeResolved=true to also return
|
||||||
|
* resolved-but-still-recorded entries (audit trail).
|
||||||
|
*
|
||||||
|
* Used by `/sf escalate list` to enumerate cross-slice escalations.
|
||||||
|
*/
|
||||||
|
export function listEscalationArtifacts(
|
||||||
|
milestoneId: string,
|
||||||
|
includeResolved = false,
|
||||||
|
): TaskRow[] {
|
||||||
|
if (!currentDb) return [];
|
||||||
|
const filter = includeResolved
|
||||||
|
? "escalation_artifact_path IS NOT NULL"
|
||||||
|
: "(escalation_pending = 1 OR escalation_awaiting_review = 1) AND escalation_artifact_path IS NOT NULL";
|
||||||
|
const rows = currentDb
|
||||||
|
.prepare(
|
||||||
|
`SELECT * FROM tasks WHERE milestone_id = :mid AND ${filter} ORDER BY slice_id, sequence, id`,
|
||||||
|
)
|
||||||
|
.all({ ":mid": milestoneId }) as Array<Record<string, unknown>>;
|
||||||
|
return rows.map(rowToTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function upsertSlicePlanning(
|
export function upsertSlicePlanning(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue