fix(notices+db): complete NOTICE_KIND tagging, fix slice-dep query, cap error storage

NOTICE_KIND tagging:
- auto.js: ctrl-c-pause (USER_VISIBLE), auto-start-failed/session-lock-lost/
  stopAuto/debug-summary-written (SYSTEM_NOTICE), auto-no-command-ctx (USER_VISIBLE)
- loop.js: model-policy-blocked SYSTEM_NOTICE→BLOCKING_NOTICE (user must act),
  solver-eval results/infra-stop/consecutive-cooldowns (SYSTEM_NOTICE),
  phase-timeout/credential-cooldown-wait/iteration-error (TOOL_NOTICE); fix import order
- register-hooks.js: destructive-command (TOOL_NOTICE), gemini-preflight (SYSTEM_NOTICE)
- provider-error-pause.js: auto-resume (TOOL_NOTICE), scheduled-resume (SYSTEM_NOTICE),
  permanent-pause (BLOCKING_NOTICE)
- uok-parity-summary.js: parity warning (SYSTEM_NOTICE)

sf-db fixes:
- getActiveSliceFromDb: use slice_dependencies junction table instead of
  json_each(s.depends) — junction table is kept in sync by syncSliceDependencies
- capErrorForStorage: cap UOK run error blobs at 4 KB; excess spills to
  .sf/runtime/errors/<runId>.txt to prevent DB bloat from large stack traces

ARCHITECTURE.md:
- Document DB-first invariant; remove .sf/DECISIONS.md/.REQUIREMENTS.md/.KNOWLEDGE.md
  from tracked-file list (they are rendered projections, not authoritative sources)
- Add .sf/traces/ and .sf/metrics.db to gitignored list
- Update system-context assembly order to show DB-sourced decisions/requirements
- Correct system-context.ts → system-context.js

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-10 20:26:18 +02:00
parent ad380d5602
commit 61b4fecdaf
7 changed files with 146 additions and 43 deletions

View file

@ -28,46 +28,43 @@ Singularity Forge (SF) is the product. It runs long-horizon coding work through
**Tracked in git** (travel with the branch, per ADR-001):
```
.sf/milestones/ — roadmaps, plans, summaries, task plans
.sf/milestones/ — roadmaps, plans, summaries, task plans (rendered projections from DB)
.sf/PROJECT.md — project overview
.sf/DECISIONS.md — architectural decisions register
.sf/REQUIREMENTS.md — requirements register
.sf/QUEUE.md — work queue / backlog
.sf/KNOWLEDGE.md — project-specific rules for agents
```
**Gitignored** (runtime/ephemeral — managed by `ensureGitInfoExclude()` in `.git/info/exclude`):
```
.sf/activity/ — JSONL session dumps
.sf/audit/ — audit trail entries
.sf/audit/ — audit trail entries (primary: events.jsonl)
.sf/exec/ — in-flight execution state
.sf/forensics/ — crash forensics
.sf/journal/ — SF journal entries
.sf/model-benchmarks/ — model benchmark results
.sf/parallel/ — parallel dispatch coordination
.sf/reports/ — generated reports
.sf/runtime/ — dispatch records, timeout tracking
.sf/runtime/ — dispatch records, timeout tracking, error spill files
.sf/traces/ — per-session trace JSONL (gate runs, git ops); latest symlink
.sf/worktrees/ — git worktree working directories
.sf/auto.lock — crash detection sentinel
.sf/metrics.json — token/cost accumulator
.sf/metrics.db — token/cost metrics (dedicated DB, separate from sf.db)
.sf/sf.db* — SQLite canonical structured state, priority order, validation/gate state, and UOK ledgers
.sf/STATE.md — derived state cache
.sf/notifications.jsonl, .sf/routing-history.json, .sf/self-feedback.jsonl, .sf/repo-meta.json
```
The symlink case uses a blanket `.sf` gitignore pattern (git cannot traverse symlinks). The directory case uses granular patterns so planning artifacts remain trackable.
**DB-first invariant:** `sf.db` is the single source of truth for all structured state (milestones, slices, tasks, decisions, requirements, memories, self-feedback). Markdown files under `.sf/` are rendered projections or human-editable inputs — they are never the authoritative source when the DB is open. Agents write to DB via tool calls (`save_decision`, `save_knowledge`, `save_requirement`, `update_requirement`), not by appending to `.md` files.
## Key flows
**Autonomous dispatch loop** (`src/resources/extensions/sf/auto/`):
1. UOK reconciles the DB-backed ledger, projections, and runtime diagnostics into a typed state snapshot
2. Controller selects the next dispatch unit (research, plan, implement, verify, etc.) from canonical state
3. A fresh agent context is started with the task plan injected via `system-context.ts`
1. UOK reconciles the DB-backed ledger and runtime diagnostics into a typed state snapshot
2. Controller selects the next dispatch unit (research, plan, implement, verify, etc.) from canonical DB state
3. A fresh agent context is started with the task plan injected via `system-context.js`
4. Agent writes artifacts to disk, commits, exits
5. UOK records completion/recovery, updates projections, and repeats until milestone completes or a gate fails
**System context assembly** (`bootstrap/system-context.ts`):
`PREFERENCES.md``KNOWLEDGE.md``ARCHITECTURE.md``CODEBASE.md` → code intelligence → memories → worktree/VCS blocks
**System context assembly** (`bootstrap/system-context.js`):
`PREFERENCES.md`project knowledge (DB memories table) → `ARCHITECTURE.md``CODEBASE.md` → code intelligence → active decisions (DB) → active requirements (DB) → self-feedback (DB) → worktree/VCS blocks
**Write gate** (`bootstrap/write-gate.ts`):
All file writes in autonomous mode pass through a gate. Protected files (CLAUDE.md, CODEBASE.md, certain spec files) require explicit override.
@ -102,6 +99,7 @@ PhaseDiscuss → PhasePlan → PhaseExecute → PhaseMerge → PhaseComplete
- Failed gates can trigger automatic remediation slices (new plan → execute loop)
- Stuck-loop detection: if the same unit repeats without progress after N attempts, invoke recovery protocol (timeout, manual review, or skip)
- Crash recovery: `.sf/auto.lock` sentinel + `sf.db` WAL enables recovery from agent crash mid-phase
- Run errors are capped at 4 KB in `uok_runs.error`; payloads exceeding that spill to `.sf/runtime/errors/<runId>.txt`
## Gate Verdict Semantics
@ -115,6 +113,8 @@ Every gate runs in parallel and returns one of three verdicts:
**Critical rule:** `omitted` must have a one-line reason (e.g., "no auth surface"). Unexplained omitted verdicts are treated as failures and re-dispatched with explicit instruction to pick `passed` or `failed`.
Gate run history is written to `.sf/traces/<traceId>.jsonl` (append-only JSONL, not DB). Gate circuit-breaker state lives in the `gate_circuit_breakers` table in `sf.db`.
## Outcome Learning for Model Selection
UOK tracks model success/failure per task-type using Bayesian updating:
@ -130,21 +130,28 @@ P(model_i succeeds | task_type) = (successes + prior) / (total_trials + prior_we
- Prior weights prevent early abandonment (new models get benefit of the doubt)
- Used by `benchmark-selector.ts` to route future similar tasks to higher-scoring models
**Current limitation:** Learning updates episodically (per-task), not continuously; recovery paths don't feed learning back.
## Self-Evolution Mechanisms
### Self-Report Collection
Agents and gates file `sf_self_report` with anomalies during dispatch:
- Example: "validation-reviewer prompt lacks explicit rubric for criterion vs. implementation gap"
- Reports captured in `upstream-feedback.jsonl` and `.sf/SELF-FEEDBACK.md` (when dogfooding)
- **Status:** Collection works; triage pipeline incomplete (reports not automatically processed into fixes)
Agents and gates file issues via the `report_issue` tool during dispatch:
- Reports stored in `self_feedback` table in `sf.db`
- Triage pipeline (`triage-self-feedback.js`) runs at session start to cluster and prioritize entries
- High/critical entries surfaced in system context for the next planning round
- **Status:** Collection and triage injection are active
### Knowledge Compounding
`KNOWLEDGE.md` stores judgment-log entries from completed slices:
- Format: `[when] [what] [why] [confidence]` (e.g., "Python 3.12 incompatible with X library — avoid for now")
- Persists across milestones; can be injected into future dispatch prompts
- **Status:** Storage works; injection not automatic (requires manual configuration)
Knowledge entries are stored in the `memories` table in `sf.db` (category: `knowledge`):
- Agents write via `save_knowledge` tool (not by appending to files)
- Injected into agent prompts via `system-context.js` (DB query, keyword-scoped, budget-capped)
- `knowledge-compounding.js` distills high-confidence judgment-log entries after each milestone close
- **Status:** Storage, injection, and compounding are all active
### Requirement Promotion
`requirement-promoter.js` sweeps `self_feedback` entries at session start:
- Clusters recurring feedback by kind (count ≥ 5 or spanning ≥ 3 milestones)
- Promotes clusters to the `requirements` table via `upsertRequirement`
- Promoted entries are marked resolved in `self_feedback`
- **Status:** Active
### Gate-Based Pattern Detection
Gates can detect and report repeated failure patterns (e.g., "same requirement-validation failure in S01 and S03")
@ -155,6 +162,6 @@ Gates can detect and report repeated failure patterns (e.g., "same requirement-v
- UOK and the dispatch controller are pure TypeScript — no LLM decisions in the dispatch loop itself.
- Each dispatch unit runs in a fresh context — no cross-turn state accumulation.
- Planning artifacts are tracked in git; runtime artifacts are never committed.
- DB-backed state is the only executable truth for migrated milestones: planning hierarchy, `sequence` priority/order, validation assessments, gate runs, quality gates, UOK runtime policy, and outcome ledgers all come from SQLite. Markdown/JSON projections are human views, exports, diagnostics, or explicit recovery/import inputs; normal runtime does not fall back to them when `.sf/sf.db` exists and opens.
- **DB-first:** `sf.db` is the only executable truth. Agents read decisions, requirements, and knowledge from DB-injected context; they write back via tool calls. `.md` projection files are rendered outputs, not inputs.
- `SF_RUNTIME_PATTERNS` in `gitignore.ts` is the canonical source of truth for runtime paths. `git-service.ts` (`RUNTIME_EXCLUSION_PATHS`) and `worktree-manager.ts` (`SKIP_*` arrays) must stay synchronized with it.
- The user is the end-gate. SF delivers for review, not to production.

View file

@ -228,7 +228,10 @@ function registerCtrlCInterceptor(ctx) {
_ctrlCUnsubscribe = ctx.ui.onTerminalInput((data) => {
if (data !== "\x03") return undefined;
if (!s.active) return undefined;
ctx.ui.notify("Ctrl+C received — pausing autonomous mode.", "info");
ctx.ui.notify("Ctrl+C received — pausing autonomous mode.", "info", {
noticeKind: NOTICE_KIND.USER_VISIBLE,
dedupe_key: "auto-ctrl-c-pause",
});
void pauseAuto(ctx, null, "ctrl-c-interrupt");
return { consume: true };
});
@ -319,7 +322,9 @@ function normalizeSessionFilePath(raw) {
export function startAutoDetached(ctx, pi, base, verboseMode, options) {
void startAuto(ctx, pi, base, verboseMode, options).catch((err) => {
const message = getErrorMessage(err);
ctx.ui.notify(`Auto-start failed: ${message}`, "error");
ctx.ui.notify(`Auto-start failed: ${message}`, "error", {
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
});
logWarning("engine", `auto start error: ${message}`, { file: "auto.ts" });
debugLog("auto-start-failed", { error: message });
});
@ -715,7 +720,10 @@ function handleLostSessionLock(ctx, lockStatus) {
: lockStatus?.failureReason === "compromised"
? `Session lock (${lockFilePath}) was compromised during heartbeat checks (PID ${process.pid}). This can happen after long event loop stalls during subagent execution.${recoverySuggestion}`
: `Session lock lost (${lockFilePath}). Stopping gracefully.${recoverySuggestion}`;
ctx?.ui.notify(message, "error");
ctx?.ui.notify(message, "error", {
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: `session-lock-lost:${lockStatus?.failureReason ?? "unknown"}`,
});
ctx?.ui.setStatus("sf-auto", undefined);
safeSetWidget(ctx, "sf-progress", undefined);
ctx?.ui.setFooter(undefined);
@ -973,8 +981,10 @@ export async function stopAuto(ctx, pi, reason) {
reason !== undefined && reason.toLowerCase().includes("block");
const stopMeta = {
kind: "terminal",
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
...(isBlocked ? { blocking: true } : {}),
source: "workflow",
dedupe_key: "autonomous-stopped-ledger-summary",
};
const ledger = getLedger();
if (ledger && ledger.units.length > 0) {
@ -1014,7 +1024,10 @@ export async function stopAuto(ctx, pi, reason) {
if (isDebugEnabled()) {
const logPath = writeDebugSummary();
if (logPath) {
ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
ctx?.ui.notify(`Debug log written → ${logPath}`, "info", {
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: "debug-summary-written",
});
}
}
} catch (e) {
@ -1447,6 +1460,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
ctx.ui.notify(
"Autonomous mode requires a command context with newSession. Run /autonomous once first, then use the keyboard shortcut.",
"warning",
{
noticeKind: NOTICE_KIND.USER_VISIBLE,
dedupe_key: "auto-no-command-ctx",
},
);
debugLog("startAuto", { phase: "no-command-ctx", skipping: true });
return;

View file

@ -14,6 +14,7 @@ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js";
import { runAutomaticAutonomousSolverEval } from "../autonomous-solver-eval.js";
import { debugLog } from "../debug-logger.js";
import { resolveEngine } from "../engine-resolver.js";
import { NOTICE_KIND } from "../notification-store.js";
import { sfRoot } from "../paths.js";
import { getDatabase } from "../sf-db.js";
import {
@ -22,7 +23,6 @@ import {
} from "../uok/execution-graph.js";
import { resolveUokFlags } from "../uok/flags.js";
import { clearRunawayRecoveredRuntimeRecords } from "../uok/unit-runtime.js";
import { NOTICE_KIND } from "../notification-store.js";
import { logWarning } from "../workflow-logger.js";
import {
COOLDOWN_FALLBACK_WAIT_MS,
@ -384,16 +384,28 @@ async function runExitSolverEval(ctx, s, deps, iteration) {
ctx.ui.notify(
`Autonomous solver eval recorded: ${result.report.reportPath}`,
"info",
{
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: "autonomous-solver-eval-recorded",
},
);
} else if (result.ok && result.report) {
ctx.ui.notify(
`Autonomous solver eval wrote ${result.report.reportPath}, but DB evidence was not recorded.`,
"warning",
{
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: "autonomous-solver-eval-db-miss",
},
);
} else if (!result.ok) {
ctx.ui.notify(
`Autonomous solver eval did not record: ${result.error}`,
"warning",
{
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: `autonomous-solver-eval-fail:${String(result.error ?? "").slice(0, 120)}`,
},
);
}
debugLog("autoLoop", {
@ -406,7 +418,10 @@ async function runExitSolverEval(ctx, s, deps, iteration) {
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
ctx.ui.notify(`Autonomous solver eval hook failed: ${message}`, "warning");
ctx.ui.notify(`Autonomous solver eval hook failed: ${message}`, "warning", {
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: `solver-eval-hook-fail:${message.slice(0, 120)}`,
});
debugLog("autoLoop", {
phase: "solver-eval-auto-failed",
iteration,
@ -929,6 +944,10 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Health issues detected with slice references — queuing reassess-roadmap instead of pausing.`,
"warning",
{
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: "doctor-health-reassess-roadmap",
},
);
const { buildReassessRoadmapPrompt } = await import(
"../auto-prompts.js"
@ -1128,7 +1147,7 @@ export async function autoLoop(ctx, pi, s, deps) {
`Autonomous mode paused: model-policy denied dispatch for ${loopErr.unitType}/${loopErr.unitId}. ${msg}`,
"error",
{
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
noticeKind: NOTICE_KIND.BLOCKING_NOTICE,
dedupe_key: `model-policy-blocked:${loopErr.unitType}:${loopErr.unitId}`,
},
);
@ -1171,6 +1190,7 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Autonomous mode stopped: infrastructure error ${infraCode}${msg}`,
"error",
{ noticeKind: NOTICE_KIND.SYSTEM_NOTICE },
);
await deps.stopAuto(
ctx,
@ -1187,6 +1207,10 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Phase "${phaseName}" timed out (${loopState.consecutiveFinalizeTimeouts} consecutive) — skipping iteration and continuing.`,
"warning",
{
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: `phase-timeout:${phaseName}`,
},
);
debugLog("autoLoop", {
phase: "phase-timeout",
@ -1217,6 +1241,7 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Autonomous mode stopped: ${consecutiveCooldowns} consecutive credential cooldowns — rate limit or quota may be persistently exhausted.`,
"error",
{ noticeKind: NOTICE_KIND.SYSTEM_NOTICE },
);
await deps.stopAuto(
ctx,
@ -1234,6 +1259,10 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Credentials in cooldown (${consecutiveCooldowns}/${MAX_COOLDOWN_RETRIES}) — waiting ${Math.round(waitMs / 1000)}s before retrying.`,
"warning",
{
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: "autonomous-credential-cooldown-wait",
},
);
await new Promise((resolve) => setTimeout(resolve, waitMs));
finishTurn("retry", "timeout", msg);
@ -1257,6 +1286,7 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Autonomous mode stopped: ${consecutiveErrors} consecutive iteration failures:\n${errorHistory}`,
"error",
{ noticeKind: NOTICE_KIND.SYSTEM_NOTICE, merge: false },
);
await deps.stopAuto(
ctx,
@ -1270,11 +1300,18 @@ export async function autoLoop(ctx, pi, s, deps) {
ctx.ui.notify(
`Iteration error (attempt ${consecutiveErrors}): ${msg}. Invalidating caches and retrying.`,
"warning",
{
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: `iteration-error-retry:${msg.slice(0, 240)}`,
},
);
deps.invalidateAllCaches();
} else {
// 1st error: log and retry — transient failures happen
ctx.ui.notify(`Iteration error: ${msg}. Retrying.`, "warning");
ctx.ui.notify(`Iteration error: ${msg}. Retrying.`, "warning", {
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: `iteration-error-retry:${msg.slice(0, 240)}`,
});
}
finishTurn("retry", "execution", msg);
}

View file

@ -1137,6 +1137,10 @@ export function registerHooks(pi, ecosystemHandlers = []) {
ctx.ui.notify(
`Destructive command detected: ${classification.labels.join(", ")}`,
"warning",
{
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: `destructive-command:${classification.labels.join(",")}`,
},
);
}
}
@ -1504,6 +1508,10 @@ export function registerHooks(pi, ecosystemHandlers = []) {
ctx.ui.notify(
`Gemini preflight: ${formatTokenCount(totalTokens)} tokens (${pct}% of ${formatTokenCount(contextWindow)} context).`,
"warning",
{
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: `gemini-preflight:${resolvedModel.id}`,
},
);
}
}

View file

@ -5,6 +5,8 @@
* an automatic resume after a delay. For permanent errors (auth, billing),
* pauses indefinitely user must manually resume.
*/
import { NOTICE_KIND } from "./notification-store.js";
export async function pauseAutoForProviderError(
ui,
errorDetail,
@ -24,6 +26,10 @@ export async function pauseAutoForProviderError(
ui.notify(
`${reason}${errorDetail}. Resuming automatically in ${delaySec}s...`,
"warning",
{
noticeKind: NOTICE_KIND.TOOL_NOTICE,
dedupe_key: `provider-error-auto-resume:${options.isRateLimit ? "rate" : "transient"}`,
},
);
await pause();
// Schedule resume after the delay.
@ -31,13 +37,20 @@ export async function pauseAutoForProviderError(
const resumeMsg = options.isRateLimit
? "Rate limit window elapsed. Resuming autonomous mode."
: "Server error recovery delay elapsed. Resuming autonomous mode.";
ui.notify(resumeMsg, "info");
ui.notify(resumeMsg, "info", {
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: "provider-error-scheduled-resume",
});
options.resume();
}, options.retryAfterMs);
} else {
ui.notify(
`Autonomous mode paused due to provider error${errorDetail}`,
"warning",
{
noticeKind: NOTICE_KIND.BLOCKING_NOTICE,
dedupe_key: `provider-pause:${String(errorDetail).slice(0, 120)}`,
},
);
await pause();
}

View file

@ -5135,17 +5135,19 @@ export function getActiveMilestoneFromDb() {
}
export function getActiveSliceFromDb(milestoneId) {
if (!currentDb) return null;
// Single query: find the first non-complete slice whose dependencies are all satisfied.
// Uses json_each() to expand the JSON depends array and checks each dep is complete.
// Find the first non-complete slice whose dependencies are all satisfied.
// Uses the slice_dependencies junction table (kept in sync by syncSliceDependencies).
const row = currentDb
.prepare(`SELECT s.* FROM slices s
WHERE s.milestone_id = :mid
AND s.status NOT IN ('complete', 'done', 'skipped')
AND NOT EXISTS (
SELECT 1 FROM json_each(s.depends) AS dep
WHERE dep.value NOT IN (
SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped')
)
SELECT 1 FROM slice_dependencies d
WHERE d.milestone_id = :mid
AND d.slice_id = s.id
AND d.depends_on_slice_id NOT IN (
SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped')
)
)
ORDER BY s.sequence, s.id
LIMIT 1`)
@ -5894,6 +5896,21 @@ export function recordUokRunStart(entry) {
":updated_at": now,
});
}
const MAX_ERROR_STORED_BYTES = 4096;
function capErrorForStorage(error, runId) {
if (!error || error.length <= MAX_ERROR_STORED_BYTES) return error;
try {
const errDir = join(dirname(currentPath), "runtime", "errors");
mkdirSync(errDir, { recursive: true });
writeFileSync(join(errDir, `${runId}.txt`), error, "utf-8");
} catch {
// non-fatal — best-effort spill
}
const head = error.slice(0, 2048);
const tail = error.slice(-2048);
const dropped = error.length - MAX_ERROR_STORED_BYTES;
return `${head}\n\n[...${dropped} chars truncated — full error in .sf/runtime/errors/${runId}.txt]\n\n${tail}`;
}
export function recordUokRunExit(entry) {
if (!currentDb) return;
const now = entry.endedAt ?? new Date().toISOString();
@ -5918,7 +5935,7 @@ export function recordUokRunExit(entry) {
":status": entry.status ?? "ok",
":started_at": entry.startedAt ?? now,
":ended_at": now,
":error": entry.error ?? null,
":error": entry.error ? capErrorForStorage(entry.error, entry.runId) : null,
":flags_json": JSON.stringify(entry.flags ?? {}),
":updated_at": now,
});

View file

@ -4,6 +4,7 @@ import {
hasCurrentParityWarning,
writeParityReport,
} from "./uok/parity-report.js";
import { NOTICE_KIND } from "./notification-store.js";
/**
* Read the last UOK parity report from <basePath>/.sf/runtime/uok-parity-report.json
* and surface any divergences or errors via ctx.ui?.notify?.().
@ -31,7 +32,10 @@ export async function summarizeParityReport(basePath, ctx, pi) {
`UOK parity report shows ${mismatches} critical mismatch${mismatches === 1 ? "" : "es"}, ` +
`${errors} error${errors === 1 ? "" : "s"} since ${report.generatedAt}. ` +
`Inspect .sf/runtime/uok-parity-report.json.`;
ctx.ui?.notify?.(msg, "warning");
ctx.ui?.notify?.(msg, "warning", {
noticeKind: NOTICE_KIND.SYSTEM_NOTICE,
dedupe_key: "uok-parity-report-current-warning",
});
} else {
const pathCount = Object.keys(report.paths ?? {}).length;
pi?.logInfo?.(