diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 645f33d0e..000000000 --- a/TODO.md +++ /dev/null @@ -1,19 +0,0 @@ -# Raw Dump Inbox - -This file is only a temporary human dump inbox. Do not treat it as durable -runtime memory or an approved backlog. - -## Untriaged Notes - -No untriaged notes. Add raw dumps here temporarily, then promote them to -`docs/plans/`, `docs/adr/`, `docs/specs/`, or another durable project artifact -before starting implementation. - -## Processed Notes - -- SF auto-loop hardening was converted into SF milestone state on 2026-05-02. - Continue from `.sf/STATE.md`, `.sf/milestones/M013/`, and - `.sf/milestones/M014/` instead of reusing the old raw dump. -- Feature gaps and UOK self-evolution research from 2026-05-06 were triaged on - 2026-05-06 into `docs/plans/todo-triage-2026-05-06-plan.md` and existing - durable docs such as `docs/dev/UOK-SELF-EVOLUTION.md`. diff --git a/src/resources/extensions/sf-tui/index.js b/src/resources/extensions/sf-tui/index.js index b1d88b96c..64594e923 100644 --- a/src/resources/extensions/sf-tui/index.js +++ b/src/resources/extensions/sf-tui/index.js @@ -53,11 +53,13 @@ export default function sfTui(pi) { registerSessionEmoji(pi); registerSessionColor(pi); const promptHistory = readPromptHistory(); - const promptHistorySessionId = randomUUID(); + let promptHistorySessionId = randomUUID(); let projectBasePath = null; let wasAutoActive = false; pi.on("session_start", async (_event, ctx) => { if (!ctx.hasUI) return; + promptHistorySessionId = + ctx.sessionManager?.getSessionId?.() ?? promptHistorySessionId; try { projectBasePath = projectRoot(); const projectPromptHistory = readPromptHistory(projectBasePath); diff --git a/src/resources/extensions/sf-tui/prompt-history.js b/src/resources/extensions/sf-tui/prompt-history.js index 03a5c83b9..8d4ef37ec 100644 --- a/src/resources/extensions/sf-tui/prompt-history.js +++ b/src/resources/extensions/sf-tui/prompt-history.js @@ -13,6 +13,30 @@ const SCAN_LINE_LIMIT = 2000; function promptHistoryPath() { return join(homedir(), ".sf", "agent", "prompt-history.jsonl"); } +function isEnvTruthy(value) { + return ["1", "true", "TRUE", "yes", "YES"].includes(String(value ?? "")); +} +function parseEntryLine(line) { + try { + const text = line.trim(); + if (!text) return []; + const entry = JSON.parse(text); + if ( + !entry || + typeof entry !== "object" || + entry.version !== 1 || + typeof entry.prompt !== "string" || + entry.prompt.trim().length === 0 || + typeof entry.projectRoot !== "string" || + entry.projectRoot.trim().length === 0 + ) { + return []; + } + return [entry]; + } catch { + return []; + } +} function readEntries() { try { const path = promptHistoryPath(); @@ -21,23 +45,7 @@ function readEntries() { .split(/\r?\n/) .reverse() .slice(0, SCAN_LINE_LIMIT) - .flatMap((line) => { - const text = line.trim(); - if (!text) return []; - const entry = JSON.parse(text); - if ( - !entry || - typeof entry !== "object" || - entry.version !== 1 || - typeof entry.prompt !== "string" || - entry.prompt.trim().length === 0 || - typeof entry.projectRoot !== "string" || - entry.projectRoot.trim().length === 0 - ) { - return []; - } - return [entry]; - }); + .flatMap(parseEntryLine); } catch { return []; } @@ -76,6 +84,7 @@ export function readPromptHistory(basePath) { ); } export function appendPromptHistory(prompt, basePath, sessionId) { + if (isEnvTruthy(process.env.SF_SKIP_PROMPT_HISTORY)) return; if (!basePath) return; const normalized = normalizeHistory([prompt]); if (!normalized.length) return; diff --git a/src/resources/extensions/sf/auto-dispatch.js b/src/resources/extensions/sf/auto-dispatch.js index 4cb05b253..c8d45dadc 100644 --- a/src/resources/extensions/sf/auto-dispatch.js +++ b/src/resources/extensions/sf/auto-dispatch.js @@ -49,10 +49,6 @@ import { parseDeferredRequirements, resolveAllOverrides, } from "./files.js"; -import { - checkNeedsReassessment, - checkNeedsRunUat, -} from "./workflow-helpers.js"; import { getRelevantMemoriesRanked, isDbAvailable as isMemoryDbAvailable, @@ -100,6 +96,10 @@ import { readUnitRuntimeRecord, } from "./uok/unit-runtime.js"; import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js"; +import { + checkNeedsReassessment, + checkNeedsRunUat, +} from "./workflow-helpers.js"; import { logError, logWarning } from "./workflow-logger.js"; const MAX_PARALLEL_RESEARCH_SLICES = 8; diff --git a/src/resources/extensions/sf/tests/paths-unique-milestone.test.mjs b/src/resources/extensions/sf/tests/paths-unique-milestone.test.mjs new file mode 100644 index 000000000..347386978 --- /dev/null +++ b/src/resources/extensions/sf/tests/paths-unique-milestone.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "vitest"; + +import { + clearPathCache, + relMilestoneFile, + resolveMilestoneFile, + resolveMilestonePath, +} from "../paths.js"; + +test("resolveMilestonePath_when_unique_id_has_bare_disk_dir_uses_canonical_dir", () => { + const root = mkdtempSync(join(tmpdir(), "sf-paths-unique-")); + mkdirSync(join(root, ".sf", "milestones", "M001"), { recursive: true }); + writeFileSync( + join(root, ".sf", "milestones", "M001", "M001-CONTEXT.md"), + "# Context\n", + ); + clearPathCache(); + + assert.equal( + resolveMilestonePath(root, "M001-6377a4"), + join(root, ".sf", "milestones", "M001"), + ); + assert.equal( + resolveMilestoneFile(root, "M001-6377a4", "CONTEXT"), + join(root, ".sf", "milestones", "M001", "M001-CONTEXT.md"), + ); +}); + +test("relMilestoneFile_when_unique_id_resolves_bare_dir_uses_bare_file_id", () => { + const root = mkdtempSync(join(tmpdir(), "sf-paths-unique-rel-")); + mkdirSync(join(root, ".sf", "milestones", "M001"), { recursive: true }); + clearPathCache(); + + assert.equal( + relMilestoneFile(root, "M001-6377a4", "ROADMAP"), + ".sf/milestones/M001/M001-ROADMAP.md", + ); +}); diff --git a/src/resources/extensions/sf/tests/prompt-history.test.mjs b/src/resources/extensions/sf/tests/prompt-history.test.mjs index 5f2f6b15c..c1545087f 100644 --- a/src/resources/extensions/sf/tests/prompt-history.test.mjs +++ b/src/resources/extensions/sf/tests/prompt-history.test.mjs @@ -16,18 +16,26 @@ import { describe("prompt history", () => { let oldHome; + let oldSkipPromptHistory; let homeDir; let projectDir; beforeEach(() => { oldHome = process.env.HOME; + oldSkipPromptHistory = process.env.SF_SKIP_PROMPT_HISTORY; homeDir = mkdtempSync(join(tmpdir(), "sf-home-")); projectDir = mkdtempSync(join(tmpdir(), "sf-project-")); process.env.HOME = homeDir; + delete process.env.SF_SKIP_PROMPT_HISTORY; }); afterEach(() => { process.env.HOME = oldHome; + if (oldSkipPromptHistory === undefined) { + delete process.env.SF_SKIP_PROMPT_HISTORY; + } else { + process.env.SF_SKIP_PROMPT_HISTORY = oldSkipPromptHistory; + } rmSync(homeDir, { recursive: true, force: true }); rmSync(projectDir, { recursive: true, force: true }); }); @@ -121,6 +129,27 @@ describe("prompt history", () => { expect(readPromptHistory(projectDir)).toEqual(["repeat", "newest"]); }); + it("readPromptHistory_when_jsonl_contains_malformed_line_skips_that_line", () => { + const globalPath = join(homeDir, ".sf", "agent", "prompt-history.jsonl"); + mkdirSync(join(homeDir, ".sf", "agent"), { recursive: true }); + writeFileSync( + globalPath, + [ + "{not json", + JSON.stringify({ + version: 1, + prompt: "valid", + projectRoot: projectDir, + sessionId: "session-1", + timestamp: 1, + }), + ].join("\n") + "\n", + "utf-8", + ); + + expect(readPromptHistory(projectDir)).toEqual(["valid"]); + }); + it("readPromptHistory_when_legacy_untagged_history_exists_ignores_it", () => { const globalPath = join(homeDir, ".sf", "agent", "prompt-history.json"); mkdirSync(join(homeDir, ".sf", "agent"), { recursive: true }); @@ -140,4 +169,13 @@ describe("prompt history", () => { expect(existsSync(globalPath)).toBe(false); expect(readPromptHistory()).toEqual([]); }); + + it("appendPromptHistory_when_skip_env_enabled_does_not_persist_history", () => { + process.env.SF_SKIP_PROMPT_HISTORY = "1"; + + appendPromptHistory("secret prompt", projectDir); + const globalPath = join(homeDir, ".sf", "agent", "prompt-history.jsonl"); + + expect(existsSync(globalPath)).toBe(false); + }); });