diff --git a/src/resources/extensions/sf/bootstrap/db-tools.js b/src/resources/extensions/sf/bootstrap/db-tools.js index d38eae872..1b6cab3d8 100644 --- a/src/resources/extensions/sf/bootstrap/db-tools.js +++ b/src/resources/extensions/sf/bootstrap/db-tools.js @@ -86,7 +86,7 @@ export function registerDbTools(pi) { "Record an architectural or technical decision and return its auto-assigned ID (e.g. D001). " + "Call this whenever a non-trivial choice is made about architecture, libraries, patterns, or observability so the rationale is durable and reviewable.", promptSnippet: - "Record a project decision (auto-assigns ID, regenerates DECISIONS.md)", + "Record a project decision (auto-assigns ID, persists to DB)", promptGuidelines: [ "Call save_decision for architectural, library, pattern, or observability choices — not for task-level implementation details.", "Decision IDs are auto-assigned — never guess or provide one.", @@ -143,7 +143,6 @@ export function registerDbTools(pi) { return new Text(theme.fg("error", `Error: ${message}`), 0, 0); } let text = theme.fg("success", `Decision ${d?.id ?? ""} saved`); - if (d?.id) text += theme.fg("dim", ` → DECISIONS.md`); return new Text(text, 0, 0); }, }; @@ -212,8 +211,7 @@ export function registerDbTools(pi) { description: "Update an existing requirement by ID and return confirmation — only fields you provide are changed. " + "Call this when a requirement's status, validation evidence, description, or owning slice changes after it was first recorded.", - promptSnippet: - "Update an existing requirement by ID (only provided fields are changed)", + promptSnippet: "Update an existing requirement by ID (only provided fields are changed)", promptGuidelines: [ "id is required and must be an existing requirement identifier (e.g. R001).", "All other fields are optional — only the fields you provide are updated.", @@ -260,7 +258,6 @@ export function registerDbTools(pi) { ); } let text = theme.fg("success", `Requirement ${d?.id ?? ""} updated`); - text += theme.fg("dim", ` → REQUIREMENTS.md`); return new Text(text, 0, 0); }, }; @@ -327,7 +324,7 @@ export function registerDbTools(pi) { "Record a new requirement and return its auto-assigned ID (e.g. R001). " + "Call this when a functional, non-functional, or operational requirement is identified that the project must satisfy.", promptSnippet: - "Record a new requirement (auto-assigns ID, regenerates REQUIREMENTS.md)", + "Record a new requirement (auto-assigns ID, persists to DB)", promptGuidelines: [ "Requirement IDs are auto-assigned — never guess or provide one.", "class, description, why, and source are required; all other fields are optional.", @@ -377,7 +374,6 @@ export function registerDbTools(pi) { ); } let text = theme.fg("success", `Requirement ${d?.id ?? ""} saved`); - text += theme.fg("dim", ` → REQUIREMENTS.md`); return new Text(text, 0, 0); }, }; @@ -726,6 +722,116 @@ export function registerDbTools(pi) { }, }; pi.registerTool(selfReportTool); + // ─── save_knowledge ─────────────────────────────────────────────── + const knowledgeSaveExecute = async ( + _toolCallId, + params, + _signal, + _onUpdate, + _ctx, + ) => { + const dbAvailable = await ensureDbOpen(); + if (!dbAvailable) { + return { + content: [ + { + type: "text", + text: "Error: SF database is not available. Cannot save knowledge entry.", + }, + ], + details: { operation: "save_knowledge", error: "db_unavailable" }, + }; + } + try { + const { randomUUID } = await import("node:crypto"); + const { insertMemoryRow } = await import("../sf-db.js"); + const id = `K-${randomUUID()}`; + const now = new Date().toISOString(); + const confidenceScore = params.confidence === "medium" ? 0.6 : params.confidence === "low" ? 0.3 : 0.9; + insertMemoryRow({ + id, + category: params.category ?? "knowledge", + content: params.content, + confidence: confidenceScore, + sourceUnitType: "agent", + sourceUnitId: null, + createdAt: now, + updatedAt: now, + tags: params.tags ?? [], + }); + return { + content: [{ type: "text", text: `Saved knowledge entry ${id}` }], + details: { operation: "save_knowledge", id }, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + logError("tool", `save_knowledge tool failed: ${msg}`, { + tool: "save_knowledge", + error: String(err), + }); + return { + content: [{ type: "text", text: `Error saving knowledge: ${msg}` }], + details: { operation: "save_knowledge", error: msg }, + }; + } + }; + const knowledgeSaveTool = { + name: "save_knowledge", + label: "Save Knowledge", + description: + "Persist a non-obvious rule, recurring gotcha, or useful pattern discovered during execution to the project knowledge base. " + + "Only call this when the insight would save future agents from repeating your investigation — not for obvious observations.", + promptSnippet: "Save a knowledge entry to the project knowledge base (DB)", + promptGuidelines: [ + "content is required. Write a complete, self-contained insight — future agents won't have your context.", + "category defaults to 'knowledge'. Use 'architecture', 'tooling', or 'process' for other categories.", + "confidence defaults to 'high'. Use 'medium' for tentative observations.", + "tags is an optional array of strings for filtering (e.g. ['mcp', 'transport']).", + "Do NOT call this for task-level implementation details or obvious observations.", + ], + parameters: Type.Object({ + content: Type.String({ + description: "The knowledge insight to persist — complete and self-contained", + }), + category: Type.Optional( + Type.String({ + description: "Category (default: 'knowledge'). E.g. 'architecture', 'tooling', 'process'", + }), + ), + confidence: Type.Optional( + Type.String({ + description: "Confidence level: 'high' (default) or 'medium'", + }), + ), + tags: Type.Optional( + Type.Array(Type.String(), { + description: "Optional string tags for filtering (e.g. ['mcp', 'transport'])", + }), + ), + }), + execute: knowledgeSaveExecute, + renderCall(args, theme) { + let text = theme.fg("toolTitle", theme.bold("save_knowledge ")); + if (args.category) text += theme.fg("accent", `[${args.category}] `); + if (args.content) + text += theme.fg("muted", args.content.slice(0, 60) + (args.content.length > 60 ? "…" : "")); + return new Text(text, 0, 0); + }, + renderResult(result, _options, theme) { + const d = result.details; + if (result.isError || d?.error) { + const textContent = result.content?.find?.((item) => item?.type === "text")?.text; + const message = d?.reason ?? textContent ?? d?.error ?? "unknown"; + return new Text(theme.fg("error", `Error: ${message}`), 0, 0); + } + return new Text( + theme.fg("success", `Knowledge ${d?.id ?? ""} saved`), + 0, + 0, + ); + }, + }; + pi.registerTool(knowledgeSaveTool); // ─── resolve_issue ──────────────────────────────────────── // Agent-callable resolver for inline self-feedback repair turns. The // inline-fix prompt must not rely on hand-editing JSONL: the tool updates diff --git a/src/resources/extensions/sf/knowledge-compounding.js b/src/resources/extensions/sf/knowledge-compounding.js index b7a8a4f96..fade4a555 100644 --- a/src/resources/extensions/sf/knowledge-compounding.js +++ b/src/resources/extensions/sf/knowledge-compounding.js @@ -1,89 +1,78 @@ /** * Knowledge compounding — distills high-confidence judgment-log entries from a - * milestone window into .sf/KNOWLEDGE.md after milestone close. + * milestone window into the DB memories table after milestone close. * * Called by postUnitPostVerification after complete-milestone, alongside * scaffold-keeper and record-promoter. Failure is always non-fatal. * - * Strategy (stub implementation): + * Strategy: * - Read judgment-log.jsonl entries with confidence=high for the given milestone - * - For each, generate: `- [M00X] {decision}: {reasoning}` - * - Append under `## Learned during M00X` section in .sf/KNOWLEDGE.md - * - Deduplicate against existing entries (exact decision+reasoning match) + * - Deduplicate against existing memories rows (content match, DB-first) + * - Insert new entries into the memories table with a numeric confidence score */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { randomUUID } from "node:crypto"; import { readJudgmentLog } from "./judgment-log.js"; -import { sfRoot } from "./paths.js"; +import { insertMemoryRow, isDbAvailable, getActiveMemories } from "./sf-db.js"; + +/** Map judgment-log string confidence to a numeric score for the REAL column. */ +function confidenceScore(s) { + if (s === "high") return 0.9; + if (s === "medium") return 0.6; + return 0.3; +} + /** - * Compound high-confidence judgment-log entries into KNOWLEDGE.md. + * Compound high-confidence judgment-log entries into the DB memories table. + * + * Purpose: persist reusable lessons from a milestone into the knowledge base so + * future agents benefit from them via system-context injection. * * @param basePath - project root (cwd) * @param milestoneId - milestone just completed (e.g. "M001") */ export function compoundLearningsIntoKnowledge(basePath, milestoneId) { - const sfDir = sfRoot(basePath); - const knowledgePath = join(sfDir, "KNOWLEDGE.md"); - const sectionHeading = `## Learned during ${milestoneId}`; // Read high-confidence entries for this milestone const entries = readJudgmentLog(basePath, milestoneId).filter( (e) => e.confidence === "high", ); if (entries.length === 0) return { added: 0, skipped: 0 }; - // Load or initialise KNOWLEDGE.md - let existing = ""; - if (existsSync(knowledgePath)) { - try { - existing = readFileSync(knowledgePath, "utf-8"); - } catch { - return { added: 0, skipped: 0 }; + + // Dedup against existing DB memories (content-exact match) + const existingContent = new Set(); + if (isDbAvailable()) { + for (const m of getActiveMemories({ category: "knowledge" })) { + existingContent.add(m.content); } } - // Deduplicate: collect existing learned lines under our section (or globally) - const existingLines = new Set( - existing - .split("\n") - .map((l) => l.trim()) - .filter((l) => l.startsWith("- [")), - ); + let added = 0; let skipped = 0; - const newLines = []; + + if (!isDbAvailable()) return { added: 0, skipped: entries.length }; + + const now = new Date().toISOString(); for (const entry of entries) { - const line = `- [${milestoneId}] ${entry.decision}: ${entry.reasoning}`; - if (existingLines.has(line)) { + const content = `[${milestoneId}] ${entry.decision}: ${entry.reasoning}`; + if (existingContent.has(content)) { skipped++; - } else { - newLines.push(line); - added++; + continue; + } + try { + insertMemoryRow({ + id: `K-${randomUUID()}`, + category: "knowledge", + content, + confidence: confidenceScore(entry.confidence), + sourceUnitType: "complete-milestone", + sourceUnitId: milestoneId, + createdAt: now, + updatedAt: now, + tags: ["judgment-log", milestoneId], + }); + added++; + } catch { + // non-fatal } - } - if (added === 0) return { added: 0, skipped }; - // Append or create section - let updated; - if (existing.includes(sectionHeading)) { - // Section already exists — append after the heading line - const idx = existing.indexOf(sectionHeading); - const afterHeading = existing.indexOf("\n", idx + sectionHeading.length); - const insertPos = afterHeading !== -1 ? afterHeading + 1 : existing.length; - updated = - existing.slice(0, insertPos) + - newLines.join("\n") + - "\n" + - existing.slice(insertPos); - } else if (!existing.trim()) { - // New file - updated = `# Project Knowledge\n\n${sectionHeading}\n\n${newLines.join("\n")}\n`; - } else { - // Append new section at end - updated = - existing.trimEnd() + `\n\n${sectionHeading}\n\n${newLines.join("\n")}\n`; - } - try { - mkdirSync(dirname(knowledgePath), { recursive: true }); - writeFileSync(knowledgePath, updated, "utf-8"); - } catch { - return { added: 0, skipped }; } return { added, skipped }; } diff --git a/src/resources/extensions/sf/prompts/complete-milestone.md b/src/resources/extensions/sf/prompts/complete-milestone.md index 16d87dcb9..df6bed82a 100644 --- a/src/resources/extensions/sf/prompts/complete-milestone.md +++ b/src/resources/extensions/sf/prompts/complete-milestone.md @@ -22,7 +22,7 @@ Then: 5. Verify each **success criterion** from the milestone definition in `{{roadmapPath}}`. For each criterion, confirm it was met with specific evidence from slice summaries, test results, or observable behavior. Record any criterion that was NOT met as a **verification failure**. 6. Verify the milestone's **definition of done** — all slices are `[x]`, all slice summaries exist, and any cross-slice integration points work correctly. Record any unmet items as a **verification failure**. 7. If the roadmap includes a **Horizontal Checklist**, verify each item was addressed during the milestone. Note unchecked items in the milestone summary. -8. Fill the **Decision Re-evaluation** table in the milestone summary. For each key decision from `.sf/DECISIONS.md` made during this milestone, evaluate whether it is still valid given what was actually built. Flag decisions that should be revisited next milestone. +8. Fill the **Decision Re-evaluation** table in the milestone summary. For each key decision already inlined in the context above (from the DB decisions table) that was made during this milestone, evaluate whether it is still valid given what was actually built. Flag decisions that should be revisited next milestone. 9. Validate **requirement status transitions**. For each requirement that changed status during this milestone, confirm the transition is supported by evidence. Requirements can move between Active, Validated, Deferred, Blocked, or Out of Scope — but only with proof. ### Parallel Follow-up Classification @@ -42,14 +42,14 @@ If work falls into the second bucket, do not fail the milestone just because it **Failure path** (verification failed): - Do NOT call `complete_milestone` — the milestone must not be marked as complete. - Do NOT update `.sf/PROJECT.md` to reflect completion. -- Do NOT update `.sf/REQUIREMENTS.md` to mark requirements as validated. +- Do NOT call `update_requirement` to mark requirements as validated. - A non-pass validation verdict is a verification failure, even if it is terminal for validation-loop purposes. - Write a clear summary of what failed and why to help the next attempt. - Say: "Milestone {{milestoneId}} verification FAILED — not complete." and stop. **Success path** (all verifications passed — continue with steps 10–14): -10. For each requirement whose status changed in step 9, call `update_requirement` with the requirement ID and updated `status` and `validation` fields — the tool regenerates `.sf/REQUIREMENTS.md` automatically. Do this BEFORE completing the milestone so requirement updates are persisted. +10. For each requirement whose status changed in step 9, call `update_requirement` with the requirement ID and updated `status` and `validation` fields. Do this BEFORE completing the milestone so requirement updates are persisted. 11. **Persist completion through `complete_milestone`.** Call it with the parameters below as soon as steps 2, 4, 5, 6, and 10 are satisfied. Do not keep reading historical commits or re-running broad test suites after the requirements are updated. The tool updates the milestone status in the DB, renders `{{milestoneSummaryPath}}`, and validates all slices are complete before proceeding. **Required parameters:** @@ -69,8 +69,8 @@ If work falls into the second bucket, do not fail the milestone just because it - `followUps` (string) — Follow-up items for future milestones - `deviations` (string) — Deviations from the original plan 12. Update `.sf/PROJECT.md`: use the `write` tool with `path: ".sf/PROJECT.md"` and `content` containing the full updated document reflecting milestone completion and current project state. Do NOT use the `edit` tool for this — PROJECT.md is a full-document refresh. -13. Review all slice summaries for cross-cutting lessons, patterns, or gotchas that emerged during this milestone. Append any non-obvious, reusable insights to `.sf/KNOWLEDGE.md`. -13b. Review `.sf/SELF-FEEDBACK.md` (if present — it lives only when sf is dogfooded on forge) and the global `~/.sf/agent/upstream-feedback.jsonl`. For any sf-internal anomaly that recurred across multiple slices in this milestone but is not yet captured in either log, file it now via `report_issue`. The milestone-close agent is the last line of defense for systemic sf bugs that single-task agents missed. +13. Review all slice summaries for cross-cutting lessons, patterns, or gotchas that emerged during this milestone. Call `save_knowledge` for any non-obvious, reusable insights. +13b. Review the global `~/.sf/agent/upstream-feedback.jsonl`. For any sf-internal anomaly that recurred across multiple slices in this milestone but is not yet captured, file it now via `report_issue`. The milestone-close agent is the last line of defense for systemic sf bugs that single-task agents missed. 14. Do not commit manually — the system auto-commits your changes after this unit completes. - Say: "Milestone {{milestoneId}} complete."