sf snapshot: uncommitted changes after 258m inactivity
This commit is contained in:
parent
e154dad930
commit
8088489e38
13 changed files with 383 additions and 42 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Singularity Forge (SF) is an autonomous agent orchestration system. It runs long-horizon coding work through the Unified Operation Kernel (UOK): milestones → slices → tasks. Each dispatch unit runs a fresh AI context, writes its output to disk, then terminates. UOK owns lifecycle, recovery, and the DB-backed run ledger; runtime files under `.sf/runtime/` are projections for query, UI, and compatibility. A deterministic controller (not an LLM) reads canonical state and decides what to dispatch next. The user is the end-gate — autonomous mode delivers work to human review, it does not merge to production unattended.
|
Singularity Forge (SF) is the product. It runs long-horizon coding work through the Unified Operation Kernel (UOK): milestones → slices → tasks. Each dispatch unit runs a fresh AI context, writes its output to disk, then terminates. UOK owns lifecycle, recovery, and the DB-backed run ledger; runtime files under `.sf/runtime/` are projections for query, UI, and compatibility. A deterministic controller (not an LLM) reads canonical state and decides what to dispatch next. Core changes follow purpose-driven TDD: purpose and consumer first, then failing tests, then implementation. The user is the end-gate — autonomous mode delivers work to human review, it does not merge to production unattended.
|
||||||
|
|
||||||
## Codemap
|
## Codemap
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,29 @@ A practical cut of the 56 NEW items in `SPEC.md` into tiers. Not every spec item
|
||||||
|
|
||||||
This document is the answer to: **what should we actually ship for v3?**
|
This document is the answer to: **what should we actually ship for v3?**
|
||||||
|
|
||||||
|
## Strategic frame — 2026-05
|
||||||
|
|
||||||
|
We are already on a strong base: Forge is the product, UOK is the kernel, and core work is gated by purpose-driven TDD plus the eight PDD fields. The goal of this build plan is not to turn SF into a generic CLI coder. The goal is to sharpen Forge's autonomous single-repo execution while borrowing the best ideas from adjacent systems.
|
||||||
|
|
||||||
|
This file is a **planning document**, not a verified implementation ledger. An item can be mapped here and still be open, partial, or only folded into milestone planning. Close-out still requires code evidence, tests, and milestone artifacts that prove the behavior exists in the repo.
|
||||||
|
|
||||||
|
Use external comparisons to sharpen, not to steer identity:
|
||||||
|
|
||||||
|
- **Claude Code / Codex** — interaction and execution ergonomics
|
||||||
|
- **Aider / gsd-2** — direct execution and repo work loop
|
||||||
|
- **Plandex** — workflow decomposition and staged progress
|
||||||
|
- **ACE Coder** — future multi-repo and large-scale convergence patterns, not the near-term product path for Forge
|
||||||
|
|
||||||
|
The end state is not "SF plus a pile of borrowed references." The end state is that proven workflow, execution, and reliability patterns are absorbed into Forge and UOK as first-party behavior.
|
||||||
|
|
||||||
|
## High-level milestone sequence
|
||||||
|
|
||||||
|
1. **Stabilize the core.** Keep UOK, purpose-driven TDD, the eight PDD fields, and repo-local state/evidence as the non-negotiable base.
|
||||||
|
2. **Sharpen single-repo execution.** Port the highest-value correctness and workflow ideas from pi-mono, gsd-2, and adjacent CLI systems where they improve Forge without changing its product identity.
|
||||||
|
3. **Deepen autonomous reliability.** Improve evidence capture, recovery, verification, and self-improvement loops inside the single-repo boundary.
|
||||||
|
4. **Polish product surfaces.** Make the autonomous workflow legible in TUI, CLI, and docs without introducing separate planning semantics.
|
||||||
|
5. **Absorb and converge deliberately.** Fold proven external patterns into Forge/UOK as native behavior, and keep interfaces/concepts compatible with ACE Coder where useful, while letting Forge and ACE grow from their different starting points.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tier 0 — Pi-mono ports (sf: do these FIRST)
|
## Tier 0 — Pi-mono ports (sf: do these FIRST)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,27 @@
|
||||||
|
|
||||||
Every BUILD_PLAN.md tier item mapped to a milestone. **Rule D015**: every new milestone must cite which BUILD_PLAN tier/item it implements.
|
Every BUILD_PLAN.md tier item mapped to a milestone. **Rule D015**: every new milestone must cite which BUILD_PLAN tier/item it implements.
|
||||||
|
|
||||||
|
This file answers **where work belongs**, not **whether code is done**. "Mapped" means a BUILD_PLAN item has a milestone/slice home. It does **not** mean the implementation is verified in the current repo.
|
||||||
|
|
||||||
|
## Mapping vs. code truth
|
||||||
|
|
||||||
|
- **Mapped** — the item has a milestone/slice destination.
|
||||||
|
- **Verified in code** — the behavior exists in the repo and has evidence/tests/artifacts.
|
||||||
|
- **Open** — still planned or partially folded in, but not yet verified as complete.
|
||||||
|
- **Deferred** — intentionally out of the active plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-level milestone direction
|
||||||
|
|
||||||
|
These are the strategy bands above the itemized mapping:
|
||||||
|
|
||||||
|
1. **Core foundation** — UOK, purpose-driven TDD, eight-field PDD gate, repo-local state
|
||||||
|
2. **Single-repo sharpening** — adopt the best execution/workflow ideas from pi-mono, gsd-2, Claude Code, Codex, Aider, and Plandex where they strengthen Forge
|
||||||
|
3. **Autonomous reliability** — evidence, recovery, verification, and self-improvement loops
|
||||||
|
4. **Surface coherence** — CLI, TUI, docs, and workflow language all reflect the same UOK-driven model
|
||||||
|
5. **ACE convergence prep** — keep concepts compatible with ACE Coder without turning Forge into the multi-repo system
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tier 0 — Pi-mono ports → **M006**
|
## Tier 0 — Pi-mono ports → **M006**
|
||||||
|
|
@ -44,4 +65,6 @@ All mapped. See BUILD_PLAN.md for item-level status.
|
||||||
| Tier 2 | 7 (M012, M009, M013, M016) | 0 |
|
| Tier 2 | 7 (M012, M009, M013, M016) | 0 |
|
||||||
| Tier 3+ | 0 | deferred |
|
| Tier 3+ | 0 | deferred |
|
||||||
|
|
||||||
**Zero gaps.** Every BUILD_PLAN tier item is either mapped to a milestone or explicitly deferred.
|
**Zero mapping gaps.** Every BUILD_PLAN tier item is either mapped to a milestone or explicitly deferred.
|
||||||
|
|
||||||
|
That does **not** mean zero implementation gaps. Open `TODO`, `NEW`, and `⬜` markers in `BUILD_PLAN.md`, this map, and milestone artifacts still represent real work until they are reconciled against code evidence.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# SF
|
# SF
|
||||||
|
|
||||||
**The evolution of [Singularity Forge](https://github.com/sf-build/get-shit-done) — now a real coding agent.**
|
**The evolution of [Singularity Forge](https://github.com/sf-build/get-shit-done) — now a standalone autonomous repo operator.**
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/singularity-forge)
|
[](https://www.npmjs.com/package/singularity-forge)
|
||||||
[](https://www.npmjs.com/package/singularity-forge)
|
[](https://www.npmjs.com/package/singularity-forge)
|
||||||
|
|
@ -15,6 +15,10 @@ The original SF went viral as a prompt framework for Claude Code. It worked, but
|
||||||
|
|
||||||
This version is different. SF is now a standalone CLI built on the [Pi SDK](https://github.com/badlogic/pi-mono), which gives it direct TypeScript access to the agent harness itself. That means SF can actually _do_ what v1 could only _ask_ the LLM to do: clear context between tasks, inject exactly the right files at dispatch time, manage git branches, track cost and tokens, detect stuck loops, recover from crashes, and auto-advance through an entire milestone without human intervention.
|
This version is different. SF is now a standalone CLI built on the [Pi SDK](https://github.com/badlogic/pi-mono), which gives it direct TypeScript access to the agent harness itself. That means SF can actually _do_ what v1 could only _ask_ the LLM to do: clear context between tasks, inject exactly the right files at dispatch time, manage git branches, track cost and tokens, detect stuck loops, recover from crashes, and auto-advance through an entire milestone without human intervention.
|
||||||
|
|
||||||
|
Forge is the product. The Unified Operation Kernel (UOK) is the internal runtime kernel. Core behavior is governed by purpose-driven TDD and the eight PDD fields: purpose, consumer, contract, failure boundary, evidence, non-goals, invariants, and assumptions.
|
||||||
|
|
||||||
|
We sharpen Forge against the best external ideas we can find — Claude Code and Codex for ergonomics, Aider and gsd-2 for execution, Plandex for workflow structure — but those are reference inputs, not the destination. Forge stays focused on autonomous single-repo execution. ACE Coder is the separate multi-repo and large-scale path.
|
||||||
|
|
||||||
One command. Walk away. Come back to a built project with clean git history.
|
One command. Walk away. Come back to a built project with clean git history.
|
||||||
|
|
||||||
<pre><code>npm install -g singularity-forge@latest</code></pre>
|
<pre><code>npm install -g singularity-forge@latest</code></pre>
|
||||||
|
|
@ -153,7 +157,7 @@ The original SF was a collection of markdown prompts installed into `~/.claude/c
|
||||||
- **No crash recovery.** If the session died mid-task, you started over.
|
- **No crash recovery.** If the session died mid-task, you started over.
|
||||||
- **No observability.** No cost tracking, no progress dashboard, no stuck detection.
|
- **No observability.** No cost tracking, no progress dashboard, no stuck detection.
|
||||||
|
|
||||||
SF v2 solves all of these because it's not a prompt framework anymore — it's a TypeScript application that _controls_ the agent session.
|
SF v2 solves all of these because it's not a prompt framework anymore — it's a TypeScript application that _controls_ the agent session. Forge is the product; UOK is the internal kernel that drives the run loop.
|
||||||
|
|
||||||
| | v1 (Prompt Framework) | v2 (Agent Application) |
|
| | v1 (Prompt Framework) | v2 (Agent Application) |
|
||||||
| -------------------- | ---------------------------- | ------------------------------------------------------- |
|
| -------------------- | ---------------------------- | ------------------------------------------------------- |
|
||||||
|
|
|
||||||
13
VISION.md
13
VISION.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Vision
|
# Vision
|
||||||
|
|
||||||
SF is the orchestration layer between you and AI coding agents. It handles planning, execution, verification, and shipping so you can focus on what to build, not how to wrangle the tools.
|
SF is an autonomous single-repo software operator. Forge is the product; UOK is the internal execution kernel. It handles planning, execution, verification, and shipping so you can focus on what to build, not how to wrangle the tools.
|
||||||
|
|
||||||
## Who it's for
|
## Who it's for
|
||||||
|
|
||||||
|
|
@ -14,10 +14,21 @@ Anyone who codes with AI agents — solo developers shipping faster, open-source
|
||||||
|
|
||||||
**Tests are the contract.** If you change behavior, the tests tell you what you broke. Write tests for new behavior. Trust the test suite.
|
**Tests are the contract.** If you change behavior, the tests tell you what you broke. Write tests for new behavior. Trust the test suite.
|
||||||
|
|
||||||
|
**Purpose-driven TDD.** The eight PDD fields — purpose, consumer, contract, failure boundary, evidence, non-goals, invariants, and assumptions — are the core gate. Non-trivial work should not move to implementation before purpose is explicit and a falsifier exists.
|
||||||
|
|
||||||
**Ship fast, fix fast.** Get it out, iterate quickly, don't let perfect be the enemy of good. Every release should work, but we'd rather ship and patch than delay and accumulate.
|
**Ship fast, fix fast.** Get it out, iterate quickly, don't let perfect be the enemy of good. Every release should work, but we'd rather ship and patch than delay and accumulate.
|
||||||
|
|
||||||
**Provider-agnostic.** SF works with any LLM provider. No architectural decisions should privilege one provider over another.
|
**Provider-agnostic.** SF works with any LLM provider. No architectural decisions should privilege one provider over another.
|
||||||
|
|
||||||
|
**Sharpen by comparison, not imitation.** Learn from Claude Code, Codex, Aider, gsd-2, and Plandex where they are strong, but do not collapse Forge into a generic coder CLI. Forge's differentiator is autonomous single-repo execution on top of UOK. When an external pattern proves itself, absorb it into SF/UOK as first-party behavior instead of leaving it as a permanent comparison layer.
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
- **Forge** grows as the single-repo product.
|
||||||
|
- **UOK** leads the runtime model and execution semantics.
|
||||||
|
- **ACE Coder** grows the multi-repo and large-scale orchestration path.
|
||||||
|
- External CLIs are comparison inputs used to sharpen workflow and execution choices.
|
||||||
|
|
||||||
## What we won't accept
|
## What we won't accept
|
||||||
|
|
||||||
These save everyone time. Don't open PRs for:
|
These save everyone time. Don't open PRs for:
|
||||||
|
|
|
||||||
29
docs/records/2026-05-07-cli-agent-code-survey.md
Normal file
29
docs/records/2026-05-07-cli-agent-code-survey.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# CLI Agent Code Survey — 2026-05-07
|
||||||
|
|
||||||
|
We compared Forge-relevant CLI agent implementations to pull workflow and autonomy patterns into SF planning.
|
||||||
|
|
||||||
|
## What was checked
|
||||||
|
|
||||||
|
- `claude-code`
|
||||||
|
- `codex`
|
||||||
|
- `gemini-cli`
|
||||||
|
- `opencode`
|
||||||
|
- `aider`
|
||||||
|
- `goose`
|
||||||
|
- `qwen-code`
|
||||||
|
- `crush`
|
||||||
|
- `plandex`
|
||||||
|
- agentless-style repos: `Agentless`, `open-codex`, `RA.Aid`, `letta-code`, `neovate-code`, `amazon-q-developer-cli`
|
||||||
|
- `ace-coder` for curator, memory, and autonomy patterns
|
||||||
|
|
||||||
|
## Where the code lives
|
||||||
|
|
||||||
|
All reference checkouts are local under `/home/mhugo/code/`.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- `plandex` is the closest workflow match for SF planning.
|
||||||
|
- `claude-code`, `aider`, `codex`, and `gemini-cli` are the best ergonomics references.
|
||||||
|
- `ace-coder` is our own codebase and the long-term direction is convergence between Forge and ACE.
|
||||||
|
- The code survey is done; future planning can rely on the local checkouts instead of rescanning remote repos.
|
||||||
|
|
||||||
28
docs/records/2026-05-07-strategy-alignment.md
Normal file
28
docs/records/2026-05-07-strategy-alignment.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Strategy Alignment — 2026-05-07
|
||||||
|
|
||||||
|
Aligned the top-level SF docs and roadmap framing around the current architecture and end goal.
|
||||||
|
|
||||||
|
## Canonical direction
|
||||||
|
|
||||||
|
- **Forge** is the product.
|
||||||
|
- **UOK** is the internal execution kernel and leads runtime semantics.
|
||||||
|
- **Purpose-driven TDD** and the **eight PDD fields** are the core gate.
|
||||||
|
- **ACE Coder** is the multi-repo and large-scale path.
|
||||||
|
|
||||||
|
## How external systems are used
|
||||||
|
|
||||||
|
External CLIs are comparison inputs used to sharpen SF, not the destination:
|
||||||
|
|
||||||
|
- **Claude Code / Codex** for interaction and execution ergonomics
|
||||||
|
- **Aider / gsd-2** for direct execution patterns
|
||||||
|
- **Plandex** for workflow structure
|
||||||
|
|
||||||
|
When a pattern proves itself in practice, it should be absorbed into Forge/UOK as first-party behavior rather than preserved as a permanent external dependency in the product story.
|
||||||
|
|
||||||
|
## High-level milestone framing
|
||||||
|
|
||||||
|
1. Stabilize the UOK + PDD/TDD core.
|
||||||
|
2. Sharpen single-repo execution from external references where the fit is real.
|
||||||
|
3. Deepen autonomous reliability, evidence, recovery, and self-improvement.
|
||||||
|
4. Keep product surfaces coherent across CLI, TUI, and docs.
|
||||||
|
5. Absorb proven patterns into Forge/UOK and prepare concept-level convergence with ACE without collapsing the Forge/ACE split.
|
||||||
|
|
@ -7,3 +7,5 @@ Repo-memory audits, decision ledgers, context-gardening notes, and records-keepe
|
||||||
| Date | Note | Summary |
|
| Date | Note | Summary |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| 2026-05-01 | [repo-vcs and notifications](./2026-05-01-repo-vcs-and-notifications.md) | repo-vcs skill landed; notification specs drafted; JSDoc annotations added; placeholder docs filled |
|
| 2026-05-01 | [repo-vcs and notifications](./2026-05-01-repo-vcs-and-notifications.md) | repo-vcs skill landed; notification specs drafted; JSDoc annotations added; placeholder docs filled |
|
||||||
|
| 2026-05-07 | [cli agent code survey](./2026-05-07-cli-agent-code-survey.md) | compared local CLI agent checkouts; Plandex is the workflow analog; ACE is owned code and future convergence target |
|
||||||
|
| 2026-05-07 | [strategy alignment](./2026-05-07-strategy-alignment.md) | aligned top-level docs and roadmap framing around Forge as product, UOK as kernel, and external CLIs as sharpening inputs |
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "sf-tui",
|
"id": "sf-tui",
|
||||||
"name": "SF TUI",
|
"name": "SF TUI",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Adds SF-specific header, footer, prompt stash, color, emoji, and marketplace UI controls",
|
"description": "Adds SF-specific header, footer, prompt history, color, emoji, and marketplace UI controls",
|
||||||
"tier": "bundled",
|
"tier": "bundled",
|
||||||
"requires": { "platform": ">=2.29.0" },
|
"requires": { "platform": ">=2.29.0" },
|
||||||
"provides": {
|
"provides": {
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,24 @@
|
||||||
* Features:
|
* Features:
|
||||||
* - Powerline footer: git branch, diff stats, last commit, model, cost, context
|
* - Powerline footer: git branch, diff stats, last commit, model, cost, context
|
||||||
* - Header: project name + branch + model
|
* - Header: project name + branch + model
|
||||||
* - Prompt history stash: Ctrl+Alt+H overlay
|
* - Prompt history: Ctrl+Alt+H overlay
|
||||||
*/
|
*/
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import { Key } from "@singularity-forge/pi-tui";
|
import { Key } from "@singularity-forge/pi-tui";
|
||||||
import { isAutoActive } from "../sf/auto.js";
|
import { isAutoActive } from "../sf/auto.js";
|
||||||
|
import { projectRoot } from "../sf/commands/context.js";
|
||||||
import { registerSessionColor } from "./color-band.js";
|
import { registerSessionColor } from "./color-band.js";
|
||||||
import { registerSessionEmoji } from "./emoji.js";
|
import { registerSessionEmoji } from "./emoji.js";
|
||||||
import { renderFooter } from "./footer.js";
|
import { renderFooter } from "./footer.js";
|
||||||
import { invalidateGitStatus } from "./git.js";
|
import { invalidateGitStatus } from "./git.js";
|
||||||
import { renderHeader } from "./header.js";
|
import { renderHeader } from "./header.js";
|
||||||
import { openMarketplaceOverlay } from "./marketplace.js";
|
import { openMarketplaceOverlay } from "./marketplace.js";
|
||||||
import { openStashOverlay, pushStash, readStash, writeStash } from "./stash.js";
|
import {
|
||||||
|
appendPromptHistory,
|
||||||
|
openPromptHistoryOverlay,
|
||||||
|
pushPromptHistory,
|
||||||
|
readPromptHistory,
|
||||||
|
} from "./prompt-history.js";
|
||||||
|
|
||||||
function installHeader(ctx) {
|
function installHeader(ctx) {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
|
|
@ -45,19 +52,30 @@ function installFooter(ctx) {
|
||||||
export default function sfTui(pi) {
|
export default function sfTui(pi) {
|
||||||
registerSessionEmoji(pi);
|
registerSessionEmoji(pi);
|
||||||
registerSessionColor(pi);
|
registerSessionColor(pi);
|
||||||
const stash = readStash();
|
const promptHistory = readPromptHistory();
|
||||||
|
const promptHistorySessionId = randomUUID();
|
||||||
|
let projectBasePath = null;
|
||||||
let wasAutoActive = false;
|
let wasAutoActive = false;
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
|
try {
|
||||||
|
projectBasePath = projectRoot();
|
||||||
|
const projectPromptHistory = readPromptHistory(projectBasePath);
|
||||||
|
promptHistory.splice(0, promptHistory.length, ...projectPromptHistory);
|
||||||
|
} catch {
|
||||||
|
projectBasePath = null;
|
||||||
|
}
|
||||||
installHeader(ctx);
|
installHeader(ctx);
|
||||||
installFooter(ctx);
|
installFooter(ctx);
|
||||||
|
const openProjectPromptHistory = (overlayCtx) =>
|
||||||
|
openPromptHistoryOverlay(overlayCtx, projectBasePath ?? undefined);
|
||||||
pi.registerShortcut(Key.ctrlAlt("h"), {
|
pi.registerShortcut(Key.ctrlAlt("h"), {
|
||||||
description: "Open prompt history stash",
|
description: "Open prompt history",
|
||||||
handler: openStashOverlay,
|
handler: openProjectPromptHistory,
|
||||||
});
|
});
|
||||||
pi.registerShortcut(Key.ctrlShift("h"), {
|
pi.registerShortcut(Key.ctrlShift("h"), {
|
||||||
description: "Open prompt history stash (fallback)",
|
description: "Open prompt history (fallback)",
|
||||||
handler: openStashOverlay,
|
handler: openProjectPromptHistory,
|
||||||
});
|
});
|
||||||
pi.registerShortcut(Key.ctrlAlt("m"), {
|
pi.registerShortcut(Key.ctrlAlt("m"), {
|
||||||
description: "Open marketplace browser",
|
description: "Open marketplace browser",
|
||||||
|
|
@ -68,8 +86,18 @@ export default function sfTui(pi) {
|
||||||
pi.on("before_agent_start", async (event) => {
|
pi.on("before_agent_start", async (event) => {
|
||||||
const prompt = event.prompt?.trim();
|
const prompt = event.prompt?.trim();
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
pushStash(stash, prompt);
|
pushPromptHistory(promptHistory, prompt);
|
||||||
writeStash(stash);
|
appendPromptHistory(
|
||||||
|
prompt,
|
||||||
|
projectBasePath ?? undefined,
|
||||||
|
promptHistorySessionId,
|
||||||
|
);
|
||||||
|
pi.appendEntry("sf-prompt-history", {
|
||||||
|
prompt,
|
||||||
|
projectRoot: projectBasePath,
|
||||||
|
sessionId: promptHistorySessionId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
pi.on("tool_result", async (_event, ctx) => {
|
pi.on("tool_result", async (_event, ctx) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import {
|
import {
|
||||||
|
|
@ -9,39 +9,87 @@ import {
|
||||||
} from "@singularity-forge/pi-tui";
|
} from "@singularity-forge/pi-tui";
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
function stashPath() {
|
const SCAN_LINE_LIMIT = 2000;
|
||||||
return join(homedir(), ".sf", "agent", "prompt-history.json");
|
function promptHistoryPath() {
|
||||||
|
return join(homedir(), ".sf", "agent", "prompt-history.jsonl");
|
||||||
}
|
}
|
||||||
export function readStash() {
|
function readEntries() {
|
||||||
try {
|
try {
|
||||||
const path = stashPath();
|
const path = promptHistoryPath();
|
||||||
if (!existsSync(path)) return [];
|
if (!existsSync(path)) return [];
|
||||||
const d = JSON.parse(readFileSync(path, "utf-8"));
|
return readFileSync(path, "utf-8")
|
||||||
return d.history.filter(
|
.split(/\r?\n/)
|
||||||
(h) => typeof h === "string" && h.trim().length > 0,
|
.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];
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function writeStash(history) {
|
function normalizeHistory(history) {
|
||||||
|
const seen = new Set();
|
||||||
|
const merged = [];
|
||||||
|
for (const item of history) {
|
||||||
|
const text = String(item ?? "").trim();
|
||||||
|
if (!text || seen.has(text)) continue;
|
||||||
|
seen.add(text);
|
||||||
|
merged.push(text);
|
||||||
|
if (merged.length >= LIMIT) break;
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
function appendEntries(entries) {
|
||||||
try {
|
try {
|
||||||
const path = stashPath();
|
const path = promptHistoryPath();
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
writeFileSync(
|
appendFileSync(
|
||||||
path,
|
path,
|
||||||
JSON.stringify(
|
entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
||||||
{ version: 1, history: history.slice(0, LIMIT) },
|
{ encoding: "utf-8", mode: 0o600 },
|
||||||
null,
|
|
||||||
2,
|
|
||||||
) + "\n",
|
|
||||||
"utf-8",
|
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
/* non-fatal */
|
/* non-fatal */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function pushStash(history, text) {
|
export function readPromptHistory(basePath) {
|
||||||
|
if (!basePath) return [];
|
||||||
|
return normalizeHistory(
|
||||||
|
readEntries()
|
||||||
|
.filter((entry) => entry.projectRoot === basePath)
|
||||||
|
.map((entry) => entry.prompt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function appendPromptHistory(prompt, basePath, sessionId) {
|
||||||
|
if (!basePath) return;
|
||||||
|
const normalized = normalizeHistory([prompt]);
|
||||||
|
if (!normalized.length) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const entries = normalized.toReversed().map((prompt, index) => ({
|
||||||
|
version: 1,
|
||||||
|
prompt,
|
||||||
|
projectRoot: basePath,
|
||||||
|
sessionId: sessionId ?? null,
|
||||||
|
timestamp: now - (normalized.length - index - 1),
|
||||||
|
}));
|
||||||
|
appendEntries(entries);
|
||||||
|
}
|
||||||
|
export function pushPromptHistory(history, text) {
|
||||||
const t = text.trim();
|
const t = text.trim();
|
||||||
if (!t || history[0] === t) return;
|
if (!t || history[0] === t) return;
|
||||||
history.unshift(t);
|
history.unshift(t);
|
||||||
|
|
@ -53,7 +101,7 @@ function preview(text, maxWidth) {
|
||||||
const c = text.replace(/\s+/g, " ").trim();
|
const c = text.replace(/\s+/g, " ").trim();
|
||||||
return c ? truncateToWidth(c, maxWidth, "…") : "(empty)";
|
return c ? truncateToWidth(c, maxWidth, "…") : "(empty)";
|
||||||
}
|
}
|
||||||
class StashOverlay {
|
class PromptHistoryOverlay {
|
||||||
tui;
|
tui;
|
||||||
theme;
|
theme;
|
||||||
done;
|
done;
|
||||||
|
|
@ -138,7 +186,7 @@ class StashOverlay {
|
||||||
}
|
}
|
||||||
lines.push(pad(box("")));
|
lines.push(pad(box("")));
|
||||||
lines.push(pad(th.fg("dim", "├" + "─".repeat(bw) + "┤")));
|
lines.push(pad(th.fg("dim", "├" + "─".repeat(bw) + "┤")));
|
||||||
lines.push(pad(box(th.fg("dim", `${this.items.length} stashed prompts`))));
|
lines.push(pad(box(th.fg("dim", `${this.items.length} prompts`))));
|
||||||
lines.push(pad(th.fg("dim", "╰" + "─".repeat(bw) + "╯")));
|
lines.push(pad(th.fg("dim", "╰" + "─".repeat(bw) + "╯")));
|
||||||
lines.push("");
|
lines.push("");
|
||||||
this.cacheL = lines;
|
this.cacheL = lines;
|
||||||
|
|
@ -146,22 +194,22 @@ class StashOverlay {
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function openStashOverlay(ctx) {
|
export async function openPromptHistoryOverlay(ctx, basePath) {
|
||||||
if (!ctx.hasUI) {
|
if (!ctx.hasUI) {
|
||||||
ctx.ui.notify("Prompt history requires interactive mode", "error");
|
ctx.ui.notify("Prompt history requires interactive mode", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const items = readStash();
|
const items = readPromptHistory(basePath ?? undefined);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
"No stashed prompts yet. Send a message to build history.",
|
"No prompt history yet. Send a message to build history.",
|
||||||
"info",
|
"info",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selected = await ctx.ui.custom(
|
const selected = await ctx.ui.custom(
|
||||||
(tui, theme, _kb, done) => {
|
(tui, theme, _kb, done) => {
|
||||||
const o = new StashOverlay(tui, theme, items, done);
|
const o = new PromptHistoryOverlay(tui, theme, items, done);
|
||||||
return {
|
return {
|
||||||
render: (w) => o.render(w),
|
render: (w) => o.render(w),
|
||||||
invalidate: () => o.invalidate(),
|
invalidate: () => o.invalidate(),
|
||||||
|
|
@ -33,8 +33,6 @@ import {
|
||||||
buildRunUatPrompt,
|
buildRunUatPrompt,
|
||||||
buildValidateMilestonePrompt,
|
buildValidateMilestonePrompt,
|
||||||
buildWorkflowPreferencesPrompt,
|
buildWorkflowPreferencesPrompt,
|
||||||
checkNeedsReassessment,
|
|
||||||
checkNeedsRunUat,
|
|
||||||
} from "./auto-prompts.js";
|
} from "./auto-prompts.js";
|
||||||
import { hasImplementationArtifacts } from "./auto-recovery.js";
|
import { hasImplementationArtifacts } from "./auto-recovery.js";
|
||||||
import { getCanonicalMilestonePlan } from "./canonical-milestone-plan.js";
|
import { getCanonicalMilestonePlan } from "./canonical-milestone-plan.js";
|
||||||
|
|
@ -51,6 +49,10 @@ import {
|
||||||
parseDeferredRequirements,
|
parseDeferredRequirements,
|
||||||
resolveAllOverrides,
|
resolveAllOverrides,
|
||||||
} from "./files.js";
|
} from "./files.js";
|
||||||
|
import {
|
||||||
|
checkNeedsReassessment,
|
||||||
|
checkNeedsRunUat,
|
||||||
|
} from "./workflow-helpers.js";
|
||||||
import {
|
import {
|
||||||
getRelevantMemoriesRanked,
|
getRelevantMemoriesRanked,
|
||||||
isDbAvailable as isMemoryDbAvailable,
|
isDbAvailable as isMemoryDbAvailable,
|
||||||
|
|
|
||||||
143
src/resources/extensions/sf/tests/prompt-history.test.mjs
Normal file
143
src/resources/extensions/sf/tests/prompt-history.test.mjs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
mkdtempSync,
|
||||||
|
readFileSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
appendPromptHistory,
|
||||||
|
readPromptHistory,
|
||||||
|
} from "../../sf-tui/prompt-history.js";
|
||||||
|
|
||||||
|
describe("prompt history", () => {
|
||||||
|
let oldHome;
|
||||||
|
let homeDir;
|
||||||
|
let projectDir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
oldHome = process.env.HOME;
|
||||||
|
homeDir = mkdtempSync(join(tmpdir(), "sf-home-"));
|
||||||
|
projectDir = mkdtempSync(join(tmpdir(), "sf-project-"));
|
||||||
|
process.env.HOME = homeDir;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.HOME = oldHome;
|
||||||
|
rmSync(homeDir, { recursive: true, force: true });
|
||||||
|
rmSync(projectDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appendPromptHistory_when_projectPathProvided_persists_tagged_global_entry", () => {
|
||||||
|
appendPromptHistory("first prompt", projectDir, "session-1");
|
||||||
|
|
||||||
|
const globalPath = join(homeDir, ".sf", "agent", "prompt-history.jsonl");
|
||||||
|
|
||||||
|
expect(existsSync(globalPath)).toBe(true);
|
||||||
|
expect(
|
||||||
|
readFileSync(globalPath, "utf-8")
|
||||||
|
.trim()
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => JSON.parse(line)),
|
||||||
|
).toMatchObject([
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
prompt: "first prompt",
|
||||||
|
projectRoot: projectDir,
|
||||||
|
sessionId: "session-1",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("readPromptHistory_when_history_contains_multiple_projects_returns_current_project_only", () => {
|
||||||
|
const globalPath = join(homeDir, ".sf", "agent", "prompt-history.jsonl");
|
||||||
|
mkdirSync(join(homeDir, ".sf", "agent"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
globalPath,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
prompt: "shared",
|
||||||
|
projectRoot: projectDir,
|
||||||
|
sessionId: "session-1",
|
||||||
|
timestamp: 1,
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
prompt: "project",
|
||||||
|
projectRoot: projectDir,
|
||||||
|
sessionId: "session-1",
|
||||||
|
timestamp: 2,
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
prompt: "other project",
|
||||||
|
projectRoot: join(projectDir, "other"),
|
||||||
|
sessionId: "session-2",
|
||||||
|
timestamp: 3,
|
||||||
|
}),
|
||||||
|
].join("\n") + "\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readPromptHistory(projectDir)).toEqual(["project", "shared"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("readPromptHistory_when_project_history_has_duplicates_returns_newest_unique_prompts", () => {
|
||||||
|
const globalPath = join(homeDir, ".sf", "agent", "prompt-history.jsonl");
|
||||||
|
mkdirSync(join(homeDir, ".sf", "agent"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
globalPath,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
prompt: "repeat",
|
||||||
|
projectRoot: projectDir,
|
||||||
|
sessionId: "session-1",
|
||||||
|
timestamp: 1,
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
prompt: "newest",
|
||||||
|
projectRoot: projectDir,
|
||||||
|
sessionId: "session-1",
|
||||||
|
timestamp: 2,
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
prompt: "repeat",
|
||||||
|
projectRoot: projectDir,
|
||||||
|
sessionId: "session-2",
|
||||||
|
timestamp: 3,
|
||||||
|
}),
|
||||||
|
].join("\n") + "\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readPromptHistory(projectDir)).toEqual(["repeat", "newest"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 });
|
||||||
|
writeFileSync(
|
||||||
|
globalPath,
|
||||||
|
JSON.stringify({ version: 1, history: ["global leak"] }, null, 2) + "\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readPromptHistory(projectDir)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appendPromptHistory_when_projectPathMissing_does_not_persist_history", () => {
|
||||||
|
appendPromptHistory("global leak");
|
||||||
|
const globalPath = join(homeDir, ".sf", "agent", "prompt-history.jsonl");
|
||||||
|
|
||||||
|
expect(existsSync(globalPath)).toBe(false);
|
||||||
|
expect(readPromptHistory()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue