feat(S05/T01): Schema v10 adds replan_triggered_at column; deriveStateF…

- src/resources/extensions/gsd/gsd-db.ts
- src/resources/extensions/gsd/state.ts
- src/resources/extensions/gsd/triage-resolution.ts
- src/resources/extensions/gsd/tests/flag-file-db.test.ts
- src/resources/extensions/gsd/tests/derive-state-db.test.ts
This commit is contained in:
TÂCHES 2026-03-23 11:46:28 -06:00
parent b8aaded95e
commit 64908fc822
7 changed files with 434 additions and 9 deletions

View file

@ -42,7 +42,7 @@
## Tasks
- [ ] **T01: Schema v10 + flag-file DB migration in deriveStateFromDb** `est:45m`
- [x] **T01: Schema v10 + flag-file DB migration in deriveStateFromDb** `est:45m`
- Why: The architecturally novel piece — REPLAN.md and REPLAN-TRIGGER.md detection in `deriveStateFromDb()` must use DB queries instead of disk-file checks. Schema v10 adds the `replan_triggered_at` column. Triage-resolution must also write the column.
- Files: `src/resources/extensions/gsd/gsd-db.ts`, `src/resources/extensions/gsd/state.ts`, `src/resources/extensions/gsd/triage-resolution.ts`, `src/resources/extensions/gsd/tests/flag-file-db.test.ts`
- Do: (1) Bump SCHEMA_VERSION to 10, add `replan_triggered_at TEXT DEFAULT NULL` to slices CREATE TABLE DDL and v10 migration block. (2) Update `SliceRow` interface and `rowToSlice()`. (3) In `deriveStateFromDb()`, replace `resolveSliceFile(... "REPLAN")` with `getReplanHistory(mid, sid).length > 0` check, replace `resolveSliceFile(... "REPLAN-TRIGGER")` with checking `getSlice(mid, sid)?.replan_triggered_at`. (4) In `triage-resolution.ts` `executeReplan()`, after writing the disk file, also write the `replan_triggered_at` column via `UPDATE slices SET replan_triggered_at = :ts`. (5) Write `flag-file-db.test.ts` testing: blocker→replan detection via DB (no disk file), REPLAN-TRIGGER via DB column (no disk file), loop protection (replan_history exists = no replanning phase).

View file

@ -0,0 +1,92 @@
---
id: T01
parent: S05
milestone: M001
key_files:
- src/resources/extensions/gsd/gsd-db.ts
- src/resources/extensions/gsd/state.ts
- src/resources/extensions/gsd/triage-resolution.ts
- src/resources/extensions/gsd/tests/flag-file-db.test.ts
- src/resources/extensions/gsd/tests/derive-state-db.test.ts
key_decisions:
- deriveStateFromDb uses getReplanHistory().length for loop protection instead of disk REPLAN.md check
- deriveStateFromDb uses getSlice().replan_triggered_at for trigger detection instead of disk REPLAN-TRIGGER.md check
- triage-resolution.ts DB write is best-effort with silent catch — disk file remains primary for _deriveStateImpl fallback
- Updated existing Test 16 in derive-state-db.test.ts to seed DB column since the DB path no longer reads disk flag files
duration: ""
verification_result: passed
completed_at: 2026-03-23T17:46:00.398Z
blocker_discovered: false
---
# T01: Schema v10 adds replan_triggered_at column; deriveStateFromDb uses DB queries for REPLAN/REPLAN-TRIGGER detection instead of disk files
**Schema v10 adds replan_triggered_at column; deriveStateFromDb uses DB queries for REPLAN/REPLAN-TRIGGER detection instead of disk files**
## What Happened
Implemented schema v10 and migrated flag-file detection from disk-based to DB-based in deriveStateFromDb().
**Schema v10 in gsd-db.ts:**
- Bumped SCHEMA_VERSION from 9 to 10
- Added `replan_triggered_at TEXT DEFAULT NULL` column to slices CREATE TABLE DDL (after `sequence`)
- Added `if (currentVersion < 10)` migration block using `ensureColumn()` for existing DBs
- Updated `SliceRow` interface with `replan_triggered_at: string | null`
- Updated `rowToSlice()` to read the column
**deriveStateFromDb() in state.ts:**
- Replaced `resolveSliceFile(... "REPLAN")` loop protection with `getReplanHistory(mid, sid).length > 0` — checks if replan was already completed via DB instead of checking for REPLAN.md on disk
- Replaced `resolveSliceFile(... "REPLAN-TRIGGER")` detection with `getSlice(mid, sid)?.replan_triggered_at` non-null check — detects triage-initiated replan trigger from DB column instead of REPLAN-TRIGGER.md on disk
- Added `getReplanHistory` and `getSlice` to the gsd-db.js import
- Left `_deriveStateImpl()` fallback path completely untouched — it still uses disk-based detection
- Left CONTINUE.md detection untouched per D003
**triage-resolution.ts executeReplan():**
- After writing the disk REPLAN-TRIGGER.md file (kept for fallback path), also writes `replan_triggered_at` column via `UPDATE slices SET replan_triggered_at = :ts`
- Uses lazy `createRequire(import.meta.url)` pattern (consistent with codebase convention) with `isDbAvailable()` gate
- DB write is best-effort — catches errors silently since disk file is primary for fallback path
**derive-state-db.test.ts fix:**
- Test 16 ("replanning-slice via DB") was seeding only a REPLAN-TRIGGER.md disk file without setting `replan_triggered_at` in DB. Updated to also seed the DB column so the DB-backed detection works correctly.
**flag-file-db.test.ts (new, 6 test cases):**
1. blocker_discovered + no replan_history → phase is replanning-slice
2. blocker_discovered + replan_history exists → loop protection, phase is executing
3. replan_triggered_at set + no replan_history → phase is replanning-slice
4. replan_triggered_at set + replan_history exists → loop protection, phase is executing
5. no blocker, no trigger → phase is executing (baseline)
6. Diagnostic: replan_triggered_at column is queryable (observability surface verification)
## Verification
All three verification suites pass with zero failures:
- flag-file-db.test.ts: 14 assertions passed across 6 test cases (including diagnostic)
- derive-state-db.test.ts: 105 assertions passed (0 regressions after Test 16 fix)
- derive-state-crossval.test.ts: 189 assertions passed (0 regressions)
- schema-v9-sequence.test.ts: 7 tests passed (v9 migration still works under v10)
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/flag-file-db.test.ts` | 0 | ✅ pass | 2400ms |
| 2 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/derive-state-db.test.ts` | 0 | ✅ pass | 2400ms |
| 3 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/derive-state-crossval.test.ts` | 0 | ✅ pass | 2400ms |
| 4 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts` | 0 | ✅ pass | 2800ms |
## Deviations
Updated derive-state-db.test.ts Test 16 to seed replan_triggered_at DB column — the test was relying on disk-based REPLAN-TRIGGER.md detection which is now replaced by DB queries in deriveStateFromDb(). Added a 6th diagnostic test case in flag-file-db.test.ts beyond the 5 specified in the plan to verify observability surface (column queryability).
## Known Issues
None.
## Files Created/Modified
- `src/resources/extensions/gsd/gsd-db.ts`
- `src/resources/extensions/gsd/state.ts`
- `src/resources/extensions/gsd/triage-resolution.ts`
- `src/resources/extensions/gsd/tests/flag-file-db.test.ts`
- `src/resources/extensions/gsd/tests/derive-state-db.test.ts`

View file

@ -145,7 +145,7 @@ function openRawDb(path: string): unknown {
return new Database(path);
}
const SCHEMA_VERSION = 9;
const SCHEMA_VERSION = 10;
function initSchema(db: DbAdapter, fileBacked: boolean): void {
if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
@ -268,6 +268,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
integration_closure TEXT NOT NULL DEFAULT '',
observability_impact TEXT NOT NULL DEFAULT '',
sequence INTEGER DEFAULT 0,
replan_triggered_at TEXT DEFAULT NULL,
PRIMARY KEY (milestone_id, id),
FOREIGN KEY (milestone_id) REFERENCES milestones(id)
)
@ -604,6 +605,15 @@ function migrateSchema(db: DbAdapter): void {
});
}
if (currentVersion < 10) {
ensureColumn(db, "slices", "replan_triggered_at", `ALTER TABLE slices ADD COLUMN replan_triggered_at TEXT DEFAULT NULL`);
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
":version": 10,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
@ -1150,6 +1160,7 @@ export interface SliceRow {
integration_closure: string;
observability_impact: string;
sequence: number;
replan_triggered_at: string | null;
}
function rowToSlice(row: Record<string, unknown>): SliceRow {
@ -1171,6 +1182,7 @@ function rowToSlice(row: Record<string, unknown>): SliceRow {
integration_closure: (row["integration_closure"] as string) ?? "",
observability_impact: (row["observability_impact"] as string) ?? "",
sequence: (row["sequence"] as number) ?? 0,
replan_triggered_at: (row["replan_triggered_at"] as string) ?? null,
};
}

View file

@ -43,6 +43,8 @@ import {
getAllMilestones,
getMilestoneSlices,
getSliceTasks,
getReplanHistory,
getSlice,
type MilestoneRow,
type SliceRow,
type TaskRow,
@ -639,8 +641,10 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
}
if (blockerTaskId) {
const replanFile = resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "REPLAN");
if (!replanFile) {
// Loop protection: if replan_history has entries for this slice, a replan
// was already performed — don't re-enter replanning phase.
const replanHistory = getReplanHistory(activeMilestone.id, activeSlice.id);
if (replanHistory.length === 0) {
return {
activeMilestone, activeSlice, activeTask,
phase: 'replanning-slice',
@ -656,10 +660,11 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
// ── REPLAN-TRIGGER detection ─────────────────────────────────────────
if (!blockerTaskId) {
const replanTriggerFile = resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "REPLAN-TRIGGER");
if (replanTriggerFile) {
const replanFile = resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "REPLAN");
if (!replanFile) {
const sliceRow = getSlice(activeMilestone.id, activeSlice.id);
if (sliceRow?.replan_triggered_at) {
// Loop protection: if replan_history has entries, replan was already done
const replanHistory = getReplanHistory(activeMilestone.id, activeSlice.id);
if (replanHistory.length === 0) {
return {
activeMilestone, activeSlice, activeTask,
phase: 'replanning-slice',

View file

@ -738,6 +738,13 @@ async function main(): Promise<void> {
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
// Seed the replan_triggered_at column — DB path uses column instead of disk file
const { _getAdapter } = await import('../gsd-db.ts');
const adapter = _getAdapter();
adapter!.prepare(
"UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
).run({ ":ts": new Date().toISOString(), ":mid": "M001", ":sid": "S01" });
invalidateStateCache();
const dbState = await deriveStateFromDb(base);

View file

@ -0,0 +1,290 @@
/**
* flag-file-db.test.ts Verify that REPLAN.md and REPLAN-TRIGGER.md
* flag-file detection in deriveStateFromDb() works from DB-only data
* (no disk flag files needed when DB is seeded).
*
* Semantics:
* - blocker_discovered on a completed task replanning-slice (unless loop-protected)
* - replan_triggered_at column on slice replanning-slice (unless loop-protected)
* - Loop protection: replan_history entries for the slice skip replanning
*/
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { deriveStateFromDb, invalidateStateCache } from '../state.ts';
import {
openDatabase,
closeDatabase,
isDbAvailable,
insertMilestone,
insertSlice,
insertTask,
insertReplanHistory,
_getAdapter,
} from '../gsd-db.ts';
import { createTestContext } from './test-helpers.ts';
const { assertEq, assertTrue, report } = createTestContext();
// ─── Fixture Helpers ───────────────────────────────────────────────────────
function createFixtureBase(): string {
const base = mkdtempSync(join(tmpdir(), 'gsd-flag-file-db-'));
mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
return base;
}
function writeFile(base: string, relativePath: string, content: string): void {
const full = join(base, '.gsd', relativePath);
mkdirSync(join(full, '..'), { recursive: true });
writeFileSync(full, content);
}
function cleanup(base: string): void {
rmSync(base, { recursive: true, force: true });
}
const ROADMAP_CONTENT = `# M001: Flag-File DB Test
**Vision:** Test flag-file detection via DB.
## Slices
- [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
> After this: done.
`;
const PLAN_CONTENT = `# S01: Test Slice
**Goal:** Test replanning detection.
**Demo:** Tests pass.
## Tasks
- [x] **T01: Done Task** \`est:10m\`
Already done.
- [ ] **T02: Active Task** \`est:10m\`
Current task.
`;
// Minimal task plan file content — deriveStateFromDb checks the tasks dir has .md files
const TASK_PLAN_STUB = `# T02: Active Task\n\nDo stuff.\n`;
const TASK_SUMMARY_STUB = `---\nblocker_discovered: false\n---\n# T01 Summary\nDone.\n`;
// ═══════════════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════════════
async function main(): Promise<void> {
// ─── Test 1: blocker_discovered + no replan_history → replanning-slice ──
console.log('\n=== flag-file-db: blocker + no history → replanning ===');
{
const base = createFixtureBase();
try {
// Write disk files needed by deriveStateFromDb (roadmap check, task dir check)
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/tasks/T02-PLAN.md', TASK_PLAN_STUB);
openDatabase(':memory:');
assertTrue(isDbAvailable(), 'test1: DB is available');
insertMilestone({ id: 'M001', title: 'Flag-File DB Test', status: 'active' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'active', risk: 'low', depends: [] });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete', blockerDiscovered: true });
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Active Task', status: 'pending' });
// No replan_history entries, no disk REPLAN.md — should trigger replanning
invalidateStateCache();
const state = await deriveStateFromDb(base);
assertEq(state.phase, 'replanning-slice', 'test1: phase is replanning-slice');
assertTrue(state.blockers.length > 0, 'test1: has blockers');
assertTrue(state.blockers[0]?.includes('blocker'), 'test1: blocker message mentions blocker');
closeDatabase();
} finally {
closeDatabase();
cleanup(base);
}
}
// ─── Test 2: blocker_discovered + replan_history exists → loop protection → executing ──
console.log('\n=== flag-file-db: blocker + history → loop protection ===');
{
const base = createFixtureBase();
try {
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/tasks/T02-PLAN.md', TASK_PLAN_STUB);
openDatabase(':memory:');
insertMilestone({ id: 'M001', title: 'Flag-File DB Test', status: 'active' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'active', risk: 'low', depends: [] });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete', blockerDiscovered: true });
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Active Task', status: 'pending' });
// Insert replan_history entry — loop protection should kick in
insertReplanHistory({
milestoneId: 'M001',
sliceId: 'S01',
summary: 'Replan already completed for this slice',
});
invalidateStateCache();
const state = await deriveStateFromDb(base);
assertEq(state.phase, 'executing', 'test2: phase is executing (loop protection)');
closeDatabase();
} finally {
closeDatabase();
cleanup(base);
}
}
// ─── Test 3: replan_triggered_at set + no replan_history → replanning-slice ──
console.log('\n=== flag-file-db: trigger column + no history → replanning ===');
{
const base = createFixtureBase();
try {
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/tasks/T02-PLAN.md', TASK_PLAN_STUB);
openDatabase(':memory:');
insertMilestone({ id: 'M001', title: 'Flag-File DB Test', status: 'active' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'active', risk: 'low', depends: [] });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Active Task', status: 'pending' });
// Set replan_triggered_at directly via SQL (simulating triage-resolution.ts writing it)
const adapter = _getAdapter();
adapter!.prepare(
"UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
).run({ ":ts": new Date().toISOString(), ":mid": "M001", ":sid": "S01" });
invalidateStateCache();
const state = await deriveStateFromDb(base);
assertEq(state.phase, 'replanning-slice', 'test3: phase is replanning-slice');
assertTrue(state.blockers.length > 0, 'test3: has blockers');
assertTrue(state.blockers[0]?.includes('Triage replan trigger'), 'test3: blocker message mentions triage trigger');
closeDatabase();
} finally {
closeDatabase();
cleanup(base);
}
}
// ─── Test 4: replan_triggered_at set + replan_history exists → loop protection ──
console.log('\n=== flag-file-db: trigger column + history → loop protection ===');
{
const base = createFixtureBase();
try {
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/tasks/T02-PLAN.md', TASK_PLAN_STUB);
openDatabase(':memory:');
insertMilestone({ id: 'M001', title: 'Flag-File DB Test', status: 'active' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'active', risk: 'low', depends: [] });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Active Task', status: 'pending' });
// Set trigger column
const adapter = _getAdapter();
adapter!.prepare(
"UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
).run({ ":ts": new Date().toISOString(), ":mid": "M001", ":sid": "S01" });
// Also add replan_history — loop protection should prevent replanning
insertReplanHistory({
milestoneId: 'M001',
sliceId: 'S01',
summary: 'Replan already done',
});
invalidateStateCache();
const state = await deriveStateFromDb(base);
assertEq(state.phase, 'executing', 'test4: phase is executing (loop protection)');
closeDatabase();
} finally {
closeDatabase();
cleanup(base);
}
}
// ─── Test 5: no blocker, no trigger → phase is executing ──────────────
console.log('\n=== flag-file-db: no blocker, no trigger → executing ===');
{
const base = createFixtureBase();
try {
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
writeFile(base, 'milestones/M001/slices/S01/tasks/T02-PLAN.md', TASK_PLAN_STUB);
openDatabase(':memory:');
insertMilestone({ id: 'M001', title: 'Flag-File DB Test', status: 'active' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'active', risk: 'low', depends: [] });
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Active Task', status: 'pending' });
// No blocker, no trigger, no replan_history — normal executing
invalidateStateCache();
const state = await deriveStateFromDb(base);
assertEq(state.phase, 'executing', 'test5: phase is executing');
assertEq(state.activeTask?.id, 'T02', 'test5: activeTask is T02');
assertEq(state.blockers.length, 0, 'test5: no blockers');
closeDatabase();
} finally {
closeDatabase();
cleanup(base);
}
}
// ─── Diagnostic test: DB column inspection ──────────────────────────
console.log('\n=== flag-file-db: replan_triggered_at column is queryable ===');
{
openDatabase(':memory:');
insertMilestone({ id: 'M001', title: 'Diagnostic', status: 'active' });
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test', status: 'active', risk: 'low', depends: [] });
// Initially null
const adapter = _getAdapter();
const before = adapter!.prepare(
"SELECT id, replan_triggered_at FROM slices WHERE milestone_id = :mid",
).get({ ":mid": "M001" }) as Record<string, unknown>;
assertEq(before["replan_triggered_at"], null, 'diagnostic: replan_triggered_at initially null');
// After setting
adapter!.prepare(
"UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
).run({ ":ts": "2025-01-01T00:00:00Z", ":mid": "M001", ":sid": "S01" });
const after = adapter!.prepare(
"SELECT id, replan_triggered_at FROM slices WHERE milestone_id = :mid",
).get({ ":mid": "M001" }) as Record<string, unknown>;
assertEq(after["replan_triggered_at"], "2025-01-01T00:00:00Z", 'diagnostic: replan_triggered_at is set');
closeDatabase();
}
report();
}
main();

View file

@ -12,6 +12,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createRequire } from "node:module";
import { gsdRoot, milestonesDir } from "./paths.js";
import { MILESTONE_ID_RE } from "./milestone-ids.js";
import type { Classification, CaptureEntry } from "./captures.js";
@ -90,19 +91,37 @@ export function executeReplan(
const triggerPath = join(
basePath, ".gsd", "milestones", mid, "slices", sid, `${sid}-REPLAN-TRIGGER.md`,
);
const ts = new Date().toISOString();
const content = [
`# Replan Trigger`,
``,
`**Source:** Capture ${capture.id}`,
`**Capture:** ${capture.text}`,
`**Rationale:** ${capture.rationale ?? "User-initiated replan via capture triage"}`,
`**Triggered:** ${new Date().toISOString()}`,
`**Triggered:** ${ts}`,
``,
`This file was created by the triage pipeline. The next dispatch cycle`,
`will detect it and enter the replanning-slice phase.`,
].join("\n");
writeFileSync(triggerPath, content, "utf-8");
// Also write replan_triggered_at column for DB-backed detection
try {
const req = createRequire(import.meta.url);
const { isDbAvailable, _getAdapter } = req("./gsd-db.js");
if (isDbAvailable()) {
const adapter = _getAdapter();
if (adapter) {
adapter.prepare(
"UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
).run({ ":ts": ts, ":mid": mid, ":sid": sid });
}
}
} catch {
// DB write is best-effort — disk file is the primary trigger for fallback path
}
return true;
} catch {
return false;