fix: stabilize headless bootstrap and prompt history
This commit is contained in:
parent
deeb4dbd4e
commit
3c84bd2fed
6 changed files with 113 additions and 41 deletions
19
TODO.md
19
TODO.md
|
|
@ -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`.
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue