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 {
|
||||
getActiveMilestoneFromDb,
|
||||
getMilestoneSlices,
|
||||
getSliceTasks,
|
||||
isDbAvailable,
|
||||
listEscalationArtifacts,
|
||||
} from "./sf-db.js";
|
||||
|
||||
function usage(): string {
|
||||
|
|
@ -60,22 +60,39 @@ export async function handleEscalate(
|
|||
ctx.ui.notify("No active milestone — nothing to list.", "info");
|
||||
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;
|
||||
for (const slice of getMilestoneSlices(ms.id)) {
|
||||
for (const task of getSliceTasks(ms.id, slice.id)) {
|
||||
if (task.escalation_pending !== 1) continue;
|
||||
if (!task.escalation_artifact_path) continue;
|
||||
const art = readEscalationArtifact(task.escalation_artifact_path);
|
||||
if (!art || art.respondedAt) continue;
|
||||
count++;
|
||||
lines.push(` ${slice.id}/${task.id}: ${art.question}`);
|
||||
for (const task of tasks) {
|
||||
if (!task.escalation_artifact_path) continue;
|
||||
const art = readEscalationArtifact(task.escalation_artifact_path);
|
||||
if (!art) continue;
|
||||
count++;
|
||||
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(` recommend: ${art.recommendation}`);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
ctx.ui.notify(lines.join("\n"), "info");
|
||||
|
|
|
|||
|
|
@ -2216,12 +2216,77 @@ export function insertSlice(s: {
|
|||
* Idempotent — safe to call on already-refined slices.
|
||||
*/
|
||||
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");
|
||||
currentDb
|
||||
.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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue