diff --git a/.sf/backups/db/maintenance.json b/.sf/backups/db/maintenance.json index e4dbc63e0..65d977734 100644 --- a/.sf/backups/db/maintenance.json +++ b/.sf/backups/db/maintenance.json @@ -1,3 +1,3 @@ { - "lastFullVacuumAt": "2026-05-09T17:40:16.686Z" + "lastFullVacuumAt": "2026-05-09T23:40:22.903Z" } diff --git a/.sf/backups/db/sf.db.2026-05-09T23-29-17-802Z b/.sf/backups/db/sf.db.2026-05-09T23-29-17-802Z new file mode 100644 index 000000000..805da686d Binary files /dev/null and b/.sf/backups/db/sf.db.2026-05-09T23-29-17-802Z differ diff --git a/.sf/metrics.db b/.sf/metrics.db index e75c08b0b..760695760 100644 Binary files a/.sf/metrics.db and b/.sf/metrics.db differ diff --git a/.sf/metrics.db-shm b/.sf/metrics.db-shm index 73090e026..03a865adc 100644 Binary files a/.sf/metrics.db-shm and b/.sf/metrics.db-shm differ diff --git a/.sf/metrics.db-wal b/.sf/metrics.db-wal index e064a1bdd..c86e3cdc2 100644 Binary files a/.sf/metrics.db-wal and b/.sf/metrics.db-wal differ diff --git a/scaffold_files_content.txt b/scaffold_files_content.txt new file mode 100644 index 000000000..9a5918aa5 --- /dev/null +++ b/scaffold_files_content.txt @@ -0,0 +1,452 @@ +export const SCAFFOLD_FILES = [ + { + path: ".siftignore", + content: `.git/** +.sf/** +.bg-shell/** +.pytest_cache/** +.venv/** +venv/** +node_modules/** +**/node_modules/** +**/__pycache__/** +*.pyc +*.egg-info/** +build/** +dist/** +target/** +vendor/** +coverage/** +.cache/** +tmp/** +*.log +`, + }, + { + path: "AGENTS.md", + content: `# Agent Map + +Keep this file short. Use it as a table of contents for agents and humans. + +- Treat the repo as a purpose-to-software pipeline: intent -> purpose/consumer/contract/evidence -> tests -> implementation -> verification. +- Read \`ARCHITECTURE.md\` first for the system map and invariants. +- Read \`docs/PLANS.md\` and \`docs/exec-plans/active/\` for current work. +- Read \`docs/QUALITY_SCORE.md\`, \`docs/RELIABILITY.md\`, and \`docs/SECURITY.md\` before changing production behavior. +- Put durable product decisions in \`docs/product-specs/\`. +- Put durable design and architecture decisions in \`docs/design-docs/\`. +- Put generated reference material in \`docs/generated/\`. +- Use \`docs/RECORDS_KEEPER.md\` as the repo-order checklist after meaningful changes. +- Use the \`records-keeper\` skill when repo docs, plans, or architecture records need triage. +- Follow deeper \`AGENTS.md\` files when present. The closest one to the changed file wins. + +Before implementation, inspect the relevant docs and source files, state observed facts before inferred facts, name the real consumer, and define the command or eval that proves the change. +`, + }, + { + path: "src/AGENTS.md", + content: `# Source Agent Notes + +- Start by mapping the owning module and its tests. +- Preserve existing public contracts unless the active plan explicitly changes them. +- Prefer typed/domain helpers over ad hoc parsing or duplicated logic. +- Keep edits scoped to the smallest module boundary that satisfies the plan. +- Update \`ARCHITECTURE.md\` when a source change creates a new subsystem or invariant. +`, + }, + { + path: "tests/AGENTS.md", + content: `# Test Agent Notes + +- Treat tests as executable specs, not coverage decoration. +- Add regression tests for changed behavior and failure modes. +- Prefer focused tests that name the behavior under test. +- Include the exact verification command in the plan or completion summary. +`, + }, + { + path: "ARCHITECTURE.md", + content: `# Architecture + +This file is the short map of the codebase. Keep it current and compact. + +## Purpose + +Describe the product, its users, and the job this repository exists to do. + +## Codemap + +- \`src/\`: primary implementation. +- \`tests/\`: behavior and regression coverage. +- \`docs/\`: durable product, design, plan, reliability, and security context. + +## Invariants + +- Prefer small, named modules with clear ownership. +- Behavior changes need tests or an explicit eval. +- Keep generated artifacts out of hand-written design docs. +- Update this map when new top-level concepts or directories become important. +`, + }, + { + path: "docs/design-docs/index.md", + content: `# Design Docs + +Durable design decisions live here. Link active proposals, completed decisions, and rejected alternatives. +`, + }, + { + path: "docs/AGENTS.md", + content: `# Docs Agent Notes + +- Docs are the durable project memory. Keep them concise, navigable, and current. +- Treat \`docs/adr/0000-purpose-to-software-compiler.md\` as the root SF product contract. +- Put stable decisions here; keep transient execution state in active plans. +- Prefer links to source paths, commands, and eval artifacts over broad prose. +- When docs and code disagree, inspect the code and update the stale document. +- Run the records keeper checklist in \`RECORDS_KEEPER.md\` after meaningful code, product, or architecture changes. +`, + }, + { + path: "docs/records/AGENTS.md", + content: `# Records Agent Notes + +- Keep repository memory ordered, current, and easy to inspect. +- Prefer moving durable facts to the narrowest canonical document over duplicating them. +- Preserve historical decisions; mark superseded records instead of deleting useful context. +- Escalate conflicts between docs and source by citing the exact files that disagree. +`, + }, + { + path: "docs/records/index.md", + content: `# Records + +This folder holds repo-memory audits, decision ledgers, context-gardening notes, and records-keeper outputs. +`, + }, + { + path: "docs/RECORDS_KEEPER.md", + content: `# Records Keeper + +The records keeper keeps repo memory ordered after meaningful changes. Run this checklist at milestone close, after architecture changes, after product behavior changes, and whenever docs/source disagree. + +Use the \`records-keeper\` skill for this workflow when SF skills are available. Use \`context-doctor\` instead when stale state lives under \`.sf/\` or the memory store. + +## Canonical Homes + +- Root \`AGENTS.md\`: short routing map for agents. +- \`ARCHITECTURE.md\`: short system map, boundaries, invariants, critical flows, and verification. +- \`docs/product-specs/\`: durable user-facing behavior and product decisions. +- \`docs/design-docs/\`: durable design and architecture decisions. +- \`docs/exec-plans/\`: active/completed work plans and technical debt. +- \`docs/generated/\`: generated references only. +- \`docs/records/\`: audits, ledgers, and context-gardening outputs. + +## Checklist + +- Root map is current: \`AGENTS.md\` points to the right canonical docs and local \`AGENTS.md\` files. +- Architecture is current: new subsystems, boundaries, invariants, data/state, or critical flows are reflected in \`ARCHITECTURE.md\`. +- Product specs are current: user-visible behavior changes are reflected in \`docs/product-specs/\`. +- Execution plans are filed: active work is in \`docs/exec-plans/active/\`; completed summaries and evidence are in \`docs/exec-plans/completed/\`. +- Debt is visible: discovered cleanup is listed in \`docs/exec-plans/tech-debt-tracker.md\`. +- Generated docs are marked: generated material stays under \`docs/generated/\` or clearly says how to regenerate it. +- Contradictions are resolved: stale docs are updated or marked superseded with links to the source of truth. +- Verification is recorded: changed checks, evals, and commands are listed in the relevant plan or quality document. + +## Output + +When records work is non-trivial, write a dated note under \`docs/records/\` with: + +- What changed. +- What canonical docs were updated. +- What contradictions were found. +- What remains unresolved. +`, + }, + { + path: "docs/design-docs/AGENTS.md", + content: `# Design Doc Agent Notes + +- Capture problem, context, options, decision, consequences, and validation. +- Separate observed facts from inferred product or architecture intent. +- Record rejected alternatives when they would prevent repeated debate. +`, + }, + { + path: "docs/design-docs/core-beliefs.md", + content: `# Core Beliefs + +- The repo should explain itself to humans and agents. +- Plans should carry acceptance criteria, falsifiers, and verification commands. +- Architecture should be mechanically checkable where possible. +`, + }, + { + path: "docs/exec-plans/active/index.md", + content: `# Active Execution Plans + +Link active plans here. Each plan should state purpose, scope, tasks, acceptance criteria, and verification. +`, + }, + { + path: "docs/exec-plans/AGENTS.md", + content: `# Execution Plan Agent Notes + +- Every plan needs purpose, scope, tasks, acceptance criteria, falsifier, and verification. +- Active plans live in \`active/\`; completed evidence summaries live in \`completed/\`. +- Add discovered cleanup to \`tech-debt-tracker.md\` instead of hiding it in chat. +`, + }, + { + path: "docs/exec-plans/completed/index.md", + content: `# Completed Execution Plans + +Move finished plan summaries here with evidence links and follow-up debt. +`, + }, + { + path: "docs/exec-plans/tech-debt-tracker.md", + content: `# Tech Debt Tracker + +Track cleanup discovered during implementation. Include owner, impact, proposed fix, and verification. +`, + }, + { + path: "docs/generated/db-schema.md", + content: `# Database Schema + +Generated or refreshed schema notes belong here. Do not hand-maintain stale schema copies. +`, + }, + { + path: "docs/product-specs/index.md", + content: `# Product Specs + +Durable user-facing behavior, workflows, and product decisions live here. +`, + }, + { + path: "docs/product-specs/AGENTS.md", + content: `# Product Spec Agent Notes + +- Describe the user, job-to-be-done, workflow, edge cases, and non-goals. +- Keep implementation details out unless they are product-visible constraints. +- Update specs when behavior changes, especially onboarding, permissions, billing, or destructive actions. +`, + }, + { + path: "docs/product-specs/new-user-onboarding.md", + content: `# New User Onboarding + +Describe the first-run experience, success criteria, and failure states when this product has an onboarding flow. +`, + }, + { + path: "docs/references/design-system-reference-llms.txt", + content: `Reference slot for design-system guidance intended for LLM consumption. +`, + }, + { + path: "docs/references/nixpacks-llms.txt", + content: `Reference slot for Nixpacks deployment/build guidance intended for LLM consumption. +`, + }, + { + path: "docs/references/uv-llms.txt", + content: `Reference slot for uv/Python tooling guidance intended for LLM consumption. +`, + }, + { + path: "docs/DESIGN.md", + content: `# Design + +Record interaction patterns, visual constraints, and design-system usage here. +`, + }, + { + path: "docs/FRONTEND.md", + content: `# Frontend + +Record frontend architecture, component ownership, accessibility constraints, and browser support here. +`, + }, + { + path: "docs/PLANS.md", + content: `# Plans + +Use this as the index for current and upcoming work. Link detailed plans in \`docs/exec-plans/\`. +`, + }, + { + path: "docs/PRODUCT_SENSE.md", + content: `# Product Sense + +Capture user goals, non-goals, tradeoffs, and examples of good product judgment for this repo. +`, + }, + { + path: "docs/QUALITY_SCORE.md", + content: `# Quality Score + +Define what good looks like for this repo. Include fast checks, slow checks, evals, and known blind spots. + +Use these principles: + +- Make code legible to agents with semantic names and explicit boundaries. +- Prefer small, testable modules over files that require broad context to edit. +- Enforce style, architecture, and reliability rules mechanically where possible. +- Keep a cleanup loop for stale docs, generated artifacts, and accumulated implementation debt. +`, + }, + { + path: "docs/RELIABILITY.md", + content: `# Reliability + +Document expected failure modes, recovery paths, observability, and release checks here. +`, + }, + { + path: "docs/SECURITY.md", + content: `# Security + +Document trust boundaries, secrets handling, dependency risk, and security review requirements here. +`, + }, + { + path: "docs/design-docs/ADR-TEMPLATE.md", + content: `# ADR-NNN: Title + +**Status:** Proposed | Accepted | Rejected | Superseded by ADR-NNN +**Date:** YYYY-MM-DD + +## Context + +What is the problem or situation that requires a decision? Include constraints and the forces at play. + +## Decision + +What is the change being made or the approach being adopted? + +## Consequences + +What becomes easier or harder after this decision? Include positive and negative outcomes. + +## Alternatives Considered + +What other options were evaluated and why were they not chosen? +`, + }, + { + path: ".sf/harness/AGENTS.md", + content: `# Harness Agent Notes + +The harness is SF-local operational scaffolding the agent can read and verify against. + +- \`specs/\`: behavior contracts. Each spec states what "done" looks like and the command that proves it. +- \`evals/\`: task definitions for behaviors tests cannot cover — model output quality, multi-turn flows, agent decisions. +- \`graders/\`: reusable grader scripts (code-based checks, LLM-judge prompts used by evals). + +**Rule:** Before marking a task done, run the relevant spec's verification command. Record the result in the completion summary or execution plan. +`, + }, + { + path: ".sf/harness/specs/AGENTS.md", + content: `# Harness Specs Agent Notes + +Each spec file in this directory: + +- States the behavior being specified (not the implementation). +- Includes the exact command that proves the spec passes. +- Is referenced by the relevant execution plan or ADR. + +Write the spec before implementation. Run it after. Record the result. +`, + }, + { + path: ".sf/harness/specs/bootstrap.md", + content: `# Bootstrap Spec: Agent Legibility + +Verifies that this repo is minimally agent-legible. + +## Criteria + +- [ ] \`AGENTS.md\` exists at repo root and is non-empty. +- [ ] \`ARCHITECTURE.md\` exists at repo root and is non-empty. +- [ ] \`docs/exec-plans/active/\` exists. +- [ ] \`docs/exec-plans/tech-debt-tracker.md\` exists. +- [ ] \`docs/design-docs/ADR-TEMPLATE.md\` exists. + +## Verification command + +\`\`\`bash +for f in AGENTS.md ARCHITECTURE.md docs/exec-plans/active/index.md docs/exec-plans/tech-debt-tracker.md docs/design-docs/ADR-TEMPLATE.md .sf/harness/specs/bootstrap.md; do [ -s "$f" ] && echo "OK: $f" || echo "MISSING: $f"; done +\`\`\` + +All lines should start with \`OK:\` for the bootstrap spec to pass. +`, + }, + { + path: ".sf/harness/evals/AGENTS.md", + content: `# Harness Evals Agent Notes + +Evals verify behavior that unit tests cannot cover — model output quality, agent decisions, multi-turn flows. + +Each eval should include: +- The input fixture or prompt +- The expected output or scoring rubric +- The command to run it (\`promptfoo eval\`, custom script, etc.) + +Keep evals deterministic where possible. Log results to \`docs/records/\` at milestone close. +`, + }, + { + path: ".sf/harness/graders/AGENTS.md", + content: `# Harness Graders Agent Notes + +Graders are reusable scripts or prompts that score eval outputs. + +- Code-based graders: shell scripts or test files that check structured outputs deterministically. +- LLM-judge graders: prompt templates that ask a model to score free-text output against a rubric. + +Prefer code-based graders. Add LLM-judge graders only when deterministic checking is impossible. +`, + }, + { + path: ".sf/PRINCIPLES.md", + content: `# Principles + +Durable design philosophy. Things this codebase believes are true. + +Add entries as you make decisions. Each entry: 1-2 sentences. Cite the rationale (the why, not just the what). + +## Examples + +- (replace with your own) +`, + }, + { + path: ".sf/TASTE.md", + content: `# Taste + +What good code looks like here. Idioms, conventions, "we prefer X over Y" calls. + +Add entries as you notice patterns worth preserving. Each entry: 1-2 sentences with a concrete example. + +## Examples + +- (replace with your own) +`, + }, + { + path: ".sf/ANTI-GOALS.md", + content: `# Anti-goals + +What we explicitly DON'T want. Things that look attractive but we've decided against. + +This is gold — most wrong agent calls come from not knowing what to avoid. Each entry: 1-2 sentences with the rationale. + +## Examples + +- (replace with your own) +`, + }, +]; diff --git a/scripts/check-circular-deps.mjs b/scripts/check-circular-deps.mjs index 4a1b8aba9..59f036d06 100644 --- a/scripts/check-circular-deps.mjs +++ b/scripts/check-circular-deps.mjs @@ -38,7 +38,7 @@ try { /\/tests?\//, ], detectiveOptions: { - es6: { mixedImports: true }, + es6: { mixedImports: true, skipAsyncImports: true }, }, }); } catch (err) { diff --git a/src/resources/extensions/bg-shell/bg-events.js b/src/resources/extensions/bg-shell/bg-events.js new file mode 100644 index 000000000..97c271d44 --- /dev/null +++ b/src/resources/extensions/bg-shell/bg-events.js @@ -0,0 +1,42 @@ +/** + * Shared bg-shell event and alert primitives. + * + * Extracted from process-manager.js to break the circular dependencies: + * output-formatter.js ↔ process-manager.js (cycle 8) + * readiness-detector.js ↔ process-manager.js (cycle 9) + * + * Both output-formatter.js and readiness-detector.js import addEvent/pushAlert + * from here instead of from process-manager.js, removing the back-edges. + * + * Purpose: append timestamped events to a bg process record and buffer alerts + * for injection into the next agent context turn. + * + * Consumer: output-formatter.js, process-manager.js, readiness-detector.js. + */ + +import { MAX_EVENTS } from "./types.js"; + +/** Pending alerts to inject into the next agent context */ +export let pendingAlerts = []; +const MAX_PENDING_ALERTS = 50; + +/** Replace the pendingAlerts array (used by the extension entry point) */ +export function setPendingAlerts(alerts) { + pendingAlerts = alerts; +} + +export function addEvent(bg, event) { + const ev = { ...event, timestamp: Date.now() }; + bg.events.push(ev); + if (bg.events.length > MAX_EVENTS) { + bg.events.splice(0, bg.events.length - MAX_EVENTS); + } +} + +export function pushAlert(bg, message) { + const prefix = bg ? `[bg:${bg.id} ${bg.label}] ` : ""; + pendingAlerts.push(`${prefix}${message}`); + if (pendingAlerts.length > MAX_PENDING_ALERTS) { + pendingAlerts.splice(0, pendingAlerts.length - MAX_PENDING_ALERTS); + } +} diff --git a/src/resources/extensions/bg-shell/output-formatter.js b/src/resources/extensions/bg-shell/output-formatter.js index 0fff96636..56efb8fa3 100644 --- a/src/resources/extensions/bg-shell/output-formatter.js +++ b/src/resources/extensions/bg-shell/output-formatter.js @@ -6,7 +6,7 @@ import { DEFAULT_MAX_LINES, truncateHead, } from "@singularity-forge/pi-coding-agent"; -import { addEvent, pushAlert } from "./process-manager.js"; +import { addEvent, pushAlert } from "./bg-events.js"; import { transitionToReady } from "./readiness-detector.js"; import { BUILD_COMPLETE_PATTERN_UNION, diff --git a/src/resources/extensions/bg-shell/process-manager.js b/src/resources/extensions/bg-shell/process-manager.js index 1b9c909ee..d1837ea0a 100644 --- a/src/resources/extensions/bg-shell/process-manager.js +++ b/src/resources/extensions/bg-shell/process-manager.js @@ -11,19 +11,21 @@ import { sanitizeCommand, } from "@singularity-forge/pi-coding-agent"; import { rewriteCommandWithRtk } from "../shared/rtk.js"; +import { + addEvent, + pendingAlerts, + pushAlert, + setPendingAlerts, +} from "./bg-events.js"; import { analyzeLine } from "./output-formatter.js"; import { startPortProbing, transitionToReady } from "./readiness-detector.js"; -import { DEAD_PROCESS_TTL, MAX_BUFFER_LINES, MAX_EVENTS } from "./types.js"; +import { DEAD_PROCESS_TTL, MAX_BUFFER_LINES } from "./types.js"; import { formatUptime, restoreWindowsVTInput } from "./utilities.js"; +// Re-export event/alert helpers so existing consumers (bg-shell-lifecycle.js) +// continue to work without changing their import paths. +export { addEvent, pendingAlerts, pushAlert, setPendingAlerts }; // ── Process Registry ─────────────────────────────────────────────────────── export const processes = new Map(); -/** Pending alerts to inject into the next agent context */ -export let pendingAlerts = []; -const MAX_PENDING_ALERTS = 50; -/** Replace the pendingAlerts array (used by the extension entry point) */ -export function setPendingAlerts(alerts) { - pendingAlerts = alerts; -} export function addOutputLine(bg, stream, line) { bg.output.push({ stream, line, ts: Date.now() }); if (stream === "stdout") bg.stdoutLineCount++; @@ -35,20 +37,6 @@ export function addOutputLine(bg, stream, line) { bg.lastReadIndex = Math.max(0, bg.lastReadIndex - excess); } } -export function addEvent(bg, event) { - const ev = { ...event, timestamp: Date.now() }; - bg.events.push(ev); - if (bg.events.length > MAX_EVENTS) { - bg.events.splice(0, bg.events.length - MAX_EVENTS); - } -} -export function pushAlert(bg, message) { - const prefix = bg ? `[bg:${bg.id} ${bg.label}] ` : ""; - pendingAlerts.push(`${prefix}${message}`); - if (pendingAlerts.length > MAX_PENDING_ALERTS) { - pendingAlerts.splice(0, pendingAlerts.length - MAX_PENDING_ALERTS); - } -} export function getInfo(p) { return { id: p.id, diff --git a/src/resources/extensions/bg-shell/readiness-detector.js b/src/resources/extensions/bg-shell/readiness-detector.js index 29c0eea5f..163af027e 100644 --- a/src/resources/extensions/bg-shell/readiness-detector.js +++ b/src/resources/extensions/bg-shell/readiness-detector.js @@ -2,7 +2,7 @@ * Readiness detection: port probing, pattern matching, wait-for-ready. */ import { createConnection } from "node:net"; -import { addEvent, pushAlert } from "./process-manager.js"; +import { addEvent, pushAlert } from "./bg-events.js"; import { DEFAULT_READY_TIMEOUT, PORT_PROBE_TIMEOUT, diff --git a/src/resources/extensions/sf/agentic-docs-scaffold.js b/src/resources/extensions/sf/agentic-docs-scaffold.js index 4fe4ca222..521ef33a4 100644 --- a/src/resources/extensions/sf/agentic-docs-scaffold.js +++ b/src/resources/extensions/sf/agentic-docs-scaffold.js @@ -16,6 +16,8 @@ import { stampScaffoldFile, } from "./scaffold-versioning.js"; import { logWarning } from "./workflow-logger.js"; +import { SCAFFOLD_FILES } from "./scaffold-constants.js"; +export { SCAFFOLD_FILES }; /** * Files in SCAFFOLD_FILES that intentionally do not carry an inline @@ -37,466 +39,6 @@ const LEGACY_ROOT_HARNESS_TARGETS = { "harness/evals/AGENTS.md": ".sf/harness/evals/AGENTS.md", "harness/graders/AGENTS.md": ".sf/harness/graders/AGENTS.md", }; -/** - * Canonical scaffold file templates SF manages for agent legibility. - * - * Includes AGENTS.md (routing map), ARCHITECTURE.md (system overview), and docs - * tree structure (product specs, design docs, execution plans, records, generated). - * Phase C syncs these to disk, stamps them with version markers, and records manifest - * entries (ADR-021). - */ -export const SCAFFOLD_FILES = [ - { - path: ".siftignore", - content: `.git/** -.sf/** -.bg-shell/** -.pytest_cache/** -.venv/** -venv/** -node_modules/** -**/node_modules/** -**/__pycache__/** -*.pyc -*.egg-info/** -build/** -dist/** -target/** -vendor/** -coverage/** -.cache/** -tmp/** -*.log -`, - }, - { - path: "AGENTS.md", - content: `# Agent Map - -Keep this file short. Use it as a table of contents for agents and humans. - -- Treat the repo as a purpose-to-software pipeline: intent -> purpose/consumer/contract/evidence -> tests -> implementation -> verification. -- Read \`ARCHITECTURE.md\` first for the system map and invariants. -- Read \`docs/PLANS.md\` and \`docs/exec-plans/active/\` for current work. -- Read \`docs/QUALITY_SCORE.md\`, \`docs/RELIABILITY.md\`, and \`docs/SECURITY.md\` before changing production behavior. -- Put durable product decisions in \`docs/product-specs/\`. -- Put durable design and architecture decisions in \`docs/design-docs/\`. -- Put generated reference material in \`docs/generated/\`. -- Use \`docs/RECORDS_KEEPER.md\` as the repo-order checklist after meaningful changes. -- Use the \`records-keeper\` skill when repo docs, plans, or architecture records need triage. -- Follow deeper \`AGENTS.md\` files when present. The closest one to the changed file wins. - -Before implementation, inspect the relevant docs and source files, state observed facts before inferred facts, name the real consumer, and define the command or eval that proves the change. -`, - }, - { - path: "src/AGENTS.md", - content: `# Source Agent Notes - -- Start by mapping the owning module and its tests. -- Preserve existing public contracts unless the active plan explicitly changes them. -- Prefer typed/domain helpers over ad hoc parsing or duplicated logic. -- Keep edits scoped to the smallest module boundary that satisfies the plan. -- Update \`ARCHITECTURE.md\` when a source change creates a new subsystem or invariant. -`, - }, - { - path: "tests/AGENTS.md", - content: `# Test Agent Notes - -- Treat tests as executable specs, not coverage decoration. -- Add regression tests for changed behavior and failure modes. -- Prefer focused tests that name the behavior under test. -- Include the exact verification command in the plan or completion summary. -`, - }, - { - path: "ARCHITECTURE.md", - content: `# Architecture - -This file is the short map of the codebase. Keep it current and compact. - -## Purpose - -Describe the product, its users, and the job this repository exists to do. - -## Codemap - -- \`src/\`: primary implementation. -- \`tests/\`: behavior and regression coverage. -- \`docs/\`: durable product, design, plan, reliability, and security context. - -## Invariants - -- Prefer small, named modules with clear ownership. -- Behavior changes need tests or an explicit eval. -- Keep generated artifacts out of hand-written design docs. -- Update this map when new top-level concepts or directories become important. -`, - }, - { - path: "docs/design-docs/index.md", - content: `# Design Docs - -Durable design decisions live here. Link active proposals, completed decisions, and rejected alternatives. -`, - }, - { - path: "docs/AGENTS.md", - content: `# Docs Agent Notes - -- Docs are the durable project memory. Keep them concise, navigable, and current. -- Treat \`docs/adr/0000-purpose-to-software-compiler.md\` as the root SF product contract. -- Put stable decisions here; keep transient execution state in active plans. -- Prefer links to source paths, commands, and eval artifacts over broad prose. -- When docs and code disagree, inspect the code and update the stale document. -- Run the records keeper checklist in \`RECORDS_KEEPER.md\` after meaningful code, product, or architecture changes. -`, - }, - { - path: "docs/records/AGENTS.md", - content: `# Records Agent Notes - -- Keep repository memory ordered, current, and easy to inspect. -- Prefer moving durable facts to the narrowest canonical document over duplicating them. -- Preserve historical decisions; mark superseded records instead of deleting useful context. -- Escalate conflicts between docs and source by citing the exact files that disagree. -`, - }, - { - path: "docs/records/index.md", - content: `# Records - -This folder holds repo-memory audits, decision ledgers, context-gardening notes, and records-keeper outputs. -`, - }, - { - path: "docs/RECORDS_KEEPER.md", - content: `# Records Keeper - -The records keeper keeps repo memory ordered after meaningful changes. Run this checklist at milestone close, after architecture changes, after product behavior changes, and whenever docs/source disagree. - -Use the \`records-keeper\` skill for this workflow when SF skills are available. Use \`context-doctor\` instead when stale state lives under \`.sf/\` or the memory store. - -## Canonical Homes - -- Root \`AGENTS.md\`: short routing map for agents. -- \`ARCHITECTURE.md\`: short system map, boundaries, invariants, critical flows, and verification. -- \`docs/product-specs/\`: durable user-facing behavior and product decisions. -- \`docs/design-docs/\`: durable design and architecture decisions. -- \`docs/exec-plans/\`: active/completed work plans and technical debt. -- \`docs/generated/\`: generated references only. -- \`docs/records/\`: audits, ledgers, and context-gardening outputs. - -## Checklist - -- Root map is current: \`AGENTS.md\` points to the right canonical docs and local \`AGENTS.md\` files. -- Architecture is current: new subsystems, boundaries, invariants, data/state, or critical flows are reflected in \`ARCHITECTURE.md\`. -- Product specs are current: user-visible behavior changes are reflected in \`docs/product-specs/\`. -- Execution plans are filed: active work is in \`docs/exec-plans/active/\`; completed summaries and evidence are in \`docs/exec-plans/completed/\`. -- Debt is visible: discovered cleanup is listed in \`docs/exec-plans/tech-debt-tracker.md\`. -- Generated docs are marked: generated material stays under \`docs/generated/\` or clearly says how to regenerate it. -- Contradictions are resolved: stale docs are updated or marked superseded with links to the source of truth. -- Verification is recorded: changed checks, evals, and commands are listed in the relevant plan or quality document. - -## Output - -When records work is non-trivial, write a dated note under \`docs/records/\` with: - -- What changed. -- What canonical docs were updated. -- What contradictions were found. -- What remains unresolved. -`, - }, - { - path: "docs/design-docs/AGENTS.md", - content: `# Design Doc Agent Notes - -- Capture problem, context, options, decision, consequences, and validation. -- Separate observed facts from inferred product or architecture intent. -- Record rejected alternatives when they would prevent repeated debate. -`, - }, - { - path: "docs/design-docs/core-beliefs.md", - content: `# Core Beliefs - -- The repo should explain itself to humans and agents. -- Plans should carry acceptance criteria, falsifiers, and verification commands. -- Architecture should be mechanically checkable where possible. -`, - }, - { - path: "docs/exec-plans/active/index.md", - content: `# Active Execution Plans - -Link active plans here. Each plan should state purpose, scope, tasks, acceptance criteria, and verification. -`, - }, - { - path: "docs/exec-plans/AGENTS.md", - content: `# Execution Plan Agent Notes - -- Every plan needs purpose, scope, tasks, acceptance criteria, falsifier, and verification. -- Active plans live in \`active/\`; completed evidence summaries live in \`completed/\`. -- Add discovered cleanup to \`tech-debt-tracker.md\` instead of hiding it in chat. -`, - }, - { - path: "docs/exec-plans/completed/index.md", - content: `# Completed Execution Plans - -Move finished plan summaries here with evidence links and follow-up debt. -`, - }, - { - path: "docs/exec-plans/tech-debt-tracker.md", - content: `# Tech Debt Tracker - -Track cleanup discovered during implementation. Include owner, impact, proposed fix, and verification. -`, - }, - { - path: "docs/generated/db-schema.md", - content: `# Database Schema - -Generated or refreshed schema notes belong here. Do not hand-maintain stale schema copies. -`, - }, - { - path: "docs/product-specs/index.md", - content: `# Product Specs - -Durable user-facing behavior, workflows, and product decisions live here. -`, - }, - { - path: "docs/product-specs/AGENTS.md", - content: `# Product Spec Agent Notes - -- Describe the user, job-to-be-done, workflow, edge cases, and non-goals. -- Keep implementation details out unless they are product-visible constraints. -- Update specs when behavior changes, especially onboarding, permissions, billing, or destructive actions. -`, - }, - { - path: "docs/product-specs/new-user-onboarding.md", - content: `# New User Onboarding - -Describe the first-run experience, success criteria, and failure states when this product has an onboarding flow. -`, - }, - { - path: "docs/references/design-system-reference-llms.txt", - content: `Reference slot for design-system guidance intended for LLM consumption. -`, - }, - { - path: "docs/references/nixpacks-llms.txt", - content: `Reference slot for Nixpacks deployment/build guidance intended for LLM consumption. -`, - }, - { - path: "docs/references/uv-llms.txt", - content: `Reference slot for uv/Python tooling guidance intended for LLM consumption. -`, - }, - { - path: "docs/DESIGN.md", - content: `# Design - -Record interaction patterns, visual constraints, and design-system usage here. -`, - }, - { - path: "docs/FRONTEND.md", - content: `# Frontend - -Record frontend architecture, component ownership, accessibility constraints, and browser support here. -`, - }, - { - path: "docs/PLANS.md", - content: `# Plans - -Use this as the index for current and upcoming work. Link detailed plans in \`docs/exec-plans/\`. -`, - }, - { - path: "docs/PRODUCT_SENSE.md", - content: `# Product Sense - -Capture user goals, non-goals, tradeoffs, and examples of good product judgment for this repo. -`, - }, - { - path: "docs/QUALITY_SCORE.md", - content: `# Quality Score - -Define what good looks like for this repo. Include fast checks, slow checks, evals, and known blind spots. - -Use these principles: - -- Make code legible to agents with semantic names and explicit boundaries. -- Prefer small, testable modules over files that require broad context to edit. -- Enforce style, architecture, and reliability rules mechanically where possible. -- Keep a cleanup loop for stale docs, generated artifacts, and accumulated implementation debt. -`, - }, - { - path: "docs/RELIABILITY.md", - content: `# Reliability - -Document expected failure modes, recovery paths, observability, and release checks here. -`, - }, - { - path: "docs/SECURITY.md", - content: `# Security - -Document trust boundaries, secrets handling, dependency risk, and security review requirements here. -`, - }, - { - path: "docs/design-docs/ADR-TEMPLATE.md", - content: `# ADR-NNN: Title - -**Status:** Proposed | Accepted | Rejected | Superseded by ADR-NNN -**Date:** YYYY-MM-DD - -## Context - -What is the problem or situation that requires a decision? Include constraints and the forces at play. - -## Decision - -What is the change being made or the approach being adopted? - -## Consequences - -What becomes easier or harder after this decision? Include positive and negative outcomes. - -## Alternatives Considered - -What other options were evaluated and why were they not chosen? -`, - }, - { - path: ".sf/harness/AGENTS.md", - content: `# Harness Agent Notes - -The harness is SF-local operational scaffolding the agent can read and verify against. - -- \`specs/\`: behavior contracts. Each spec states what "done" looks like and the command that proves it. -- \`evals/\`: task definitions for behaviors tests cannot cover — model output quality, multi-turn flows, agent decisions. -- \`graders/\`: reusable grader scripts (code-based checks, LLM-judge prompts used by evals). - -**Rule:** Before marking a task done, run the relevant spec's verification command. Record the result in the completion summary or execution plan. -`, - }, - { - path: ".sf/harness/specs/AGENTS.md", - content: `# Harness Specs Agent Notes - -Each spec file in this directory: - -- States the behavior being specified (not the implementation). -- Includes the exact command that proves the spec passes. -- Is referenced by the relevant execution plan or ADR. - -Write the spec before implementation. Run it after. Record the result. -`, - }, - { - path: ".sf/harness/specs/bootstrap.md", - content: `# Bootstrap Spec: Agent Legibility - -Verifies that this repo is minimally agent-legible. - -## Criteria - -- [ ] \`AGENTS.md\` exists at repo root and is non-empty. -- [ ] \`ARCHITECTURE.md\` exists at repo root and is non-empty. -- [ ] \`docs/exec-plans/active/\` exists. -- [ ] \`docs/exec-plans/tech-debt-tracker.md\` exists. -- [ ] \`docs/design-docs/ADR-TEMPLATE.md\` exists. - -## Verification command - -\`\`\`bash -for f in AGENTS.md ARCHITECTURE.md docs/exec-plans/active/index.md docs/exec-plans/tech-debt-tracker.md docs/design-docs/ADR-TEMPLATE.md .sf/harness/specs/bootstrap.md; do [ -s "$f" ] && echo "OK: $f" || echo "MISSING: $f"; done -\`\`\` - -All lines should start with \`OK:\` for the bootstrap spec to pass. -`, - }, - { - path: ".sf/harness/evals/AGENTS.md", - content: `# Harness Evals Agent Notes - -Evals verify behavior that unit tests cannot cover — model output quality, agent decisions, multi-turn flows. - -Each eval should include: -- The input fixture or prompt -- The expected output or scoring rubric -- The command to run it (\`promptfoo eval\`, custom script, etc.) - -Keep evals deterministic where possible. Log results to \`docs/records/\` at milestone close. -`, - }, - { - path: ".sf/harness/graders/AGENTS.md", - content: `# Harness Graders Agent Notes - -Graders are reusable scripts or prompts that score eval outputs. - -- Code-based graders: shell scripts or test files that check structured outputs deterministically. -- LLM-judge graders: prompt templates that ask a model to score free-text output against a rubric. - -Prefer code-based graders. Add LLM-judge graders only when deterministic checking is impossible. -`, - }, - { - path: ".sf/PRINCIPLES.md", - content: `# Principles - -Durable design philosophy. Things this codebase believes are true. - -Add entries as you make decisions. Each entry: 1-2 sentences. Cite the rationale (the why, not just the what). - -## Examples - -- (replace with your own) -`, - }, - { - path: ".sf/TASTE.md", - content: `# Taste - -What good code looks like here. Idioms, conventions, "we prefer X over Y" calls. - -Add entries as you notice patterns worth preserving. Each entry: 1-2 sentences with a concrete example. - -## Examples - -- (replace with your own) -`, - }, - { - path: ".sf/ANTI-GOALS.md", - content: `# Anti-goals - -What we explicitly DON'T want. Things that look attractive but we've decided against. - -This is gold — most wrong agent calls come from not knowing what to avoid. Each entry: 1-2 sentences with the rationale. - -## Examples - -- (replace with your own) -`, - }, -]; function pruneEmptyDir(path) { try { diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index c23d1a521..37e14e555 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -1088,20 +1088,26 @@ export async function stopAuto(ctx, pi, reason) { const rawReason = reason ?? "stop"; const normalizedReason = rawReason.startsWith("Blocked:") ? "blocked" - : rawReason.startsWith("Merge conflict") - ? "merge-conflict" - : rawReason.startsWith("Merge error") || - rawReason.startsWith("Merge failed") - ? "merge-failed" - : rawReason.startsWith("slice-merge-conflict") - ? "slice-merge-conflict" - : rawReason === "All milestones complete" - ? "all-complete" - : rawReason === "No active milestone" - ? "no-active-milestone" - : rawReason === "stop" || rawReason === "pause" - ? rawReason - : "other"; + : rawReason.startsWith("Cannot dispatch") + ? "prior-slice-blocker" + : rawReason.startsWith("SF resources were updated") + ? "resources-stale" + : rawReason.startsWith("Stuck:") + ? "stuck" + : rawReason.startsWith("Merge conflict") + ? "merge-conflict" + : rawReason.startsWith("Merge error") || + rawReason.startsWith("Merge failed") + ? "merge-failed" + : rawReason.startsWith("slice-merge-conflict") + ? "slice-merge-conflict" + : rawReason === "All milestones complete" + ? "all-complete" + : rawReason === "No active milestone" + ? "no-active-milestone" + : rawReason === "stop" || rawReason === "pause" + ? rawReason + : "other"; emitAutoExit(s.originalBasePath || s.basePath, { reason: normalizedReason, milestoneId: s.currentMilestoneId ?? undefined, diff --git a/src/resources/extensions/sf/commands-prefs-wizard.js b/src/resources/extensions/sf/commands-prefs-wizard.js index 73ac1764a..051f7ad3b 100644 --- a/src/resources/extensions/sf/commands-prefs-wizard.js +++ b/src/resources/extensions/sf/commands-prefs-wizard.js @@ -23,6 +23,15 @@ import { loadProjectSFPreferences, resolveAllSkillReferences, } from "./preferences.js"; +// Re-export from thin shared module so external importers keep working. +export { + serializePreferencesToFrontmatter, + yamlSafeString, +} from "./preferences-serializer.js"; +import { + serializePreferencesToFrontmatter, + yamlSafeString, +} from "./preferences-serializer.js"; /** Extract body content after frontmatter closing delimiter, or null if none. */ function extractBodyAfterFrontmatter(content) { @@ -840,113 +849,6 @@ export async function handlePrefsWizard(ctx, scope) { await ctx.reload(); ctx.ui.notify(`Saved ${scope} preferences to ${path}`, "info"); } -/** Wrap a YAML value in double quotes if it contains special characters. */ -export function yamlSafeString(val) { - if (typeof val !== "string") return String(val); - if (/[:#{[\]'"`,|>&*!?@%]/.test(val) || val.trim() !== val || val === "") { - return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; - } - return val; -} -export function serializePreferencesToFrontmatter(prefs) { - const lines = []; - function serializeValue(key, value, indent) { - const prefix = " ".repeat(indent); - if (value === null || value === undefined) return; - if (Array.isArray(value)) { - if (value.length === 0) { - return; // Omit empty arrays — avoids parse/serialize cycle bug with "[]" strings - } - lines.push(`${prefix}${key}:`); - for (const item of value) { - if (typeof item === "object" && item !== null) { - const entries = Object.entries(item); - if (entries.length > 0) { - const [firstKey, firstVal] = entries[0]; - lines.push(`${prefix} - ${firstKey}: ${yamlSafeString(firstVal)}`); - for (let i = 1; i < entries.length; i++) { - const [k, v] = entries[i]; - if (Array.isArray(v)) { - lines.push(`${prefix} ${k}:`); - for (const arrItem of v) { - lines.push(`${prefix} - ${yamlSafeString(arrItem)}`); - } - } else { - lines.push(`${prefix} ${k}: ${yamlSafeString(v)}`); - } - } - } - } else { - lines.push(`${prefix} - ${yamlSafeString(item)}`); - } - } - return; - } - if (typeof value === "object") { - const entries = Object.entries(value); - if (entries.length === 0) { - return; // Omit empty objects — avoids parse/serialize cycle bug with "{}" strings - } - lines.push(`${prefix}${key}:`); - for (const [k, v] of entries) { - serializeValue(k, v, indent + 1); - } - return; - } - lines.push(`${prefix}${key}: ${yamlSafeString(value)}`); - } - // Ordered keys for consistent output - const orderedKeys = [ - "version", - "mode", - "always_use_skills", - "prefer_skills", - "avoid_skills", - "skill_rules", - "custom_instructions", - "models", - "skill_discovery", - "skill_staleness_days", - "auto_supervisor", - "uat_dispatch", - "unique_milestone_ids", - "budget_ceiling", - "budget_enforcement", - "context_pause_threshold", - "notifications", - "cmux", - "remote_questions", - "git", - "post_unit_hooks", - "pre_dispatch_hooks", - "dynamic_routing", - "uok", - "token_profile", - "phases", - "parallel", - "auto_visualize", - "auto_report", - "verification_commands", - "verification_auto_fix", - "verification_max_retries", - "search_provider", - "context_selection", - ]; - const seen = new Set(); - for (const key of orderedKeys) { - if (key in prefs) { - serializeValue(key, prefs[key], 0); - seen.add(key); - } - } - // Any remaining keys not in the ordered list - for (const [key, value] of Object.entries(prefs)) { - if (!seen.has(key)) { - serializeValue(key, value, 0); - } - } - return lines.join("\n") + "\n"; -} export async function ensurePreferencesFile(path, ctx, scope) { if (!existsSync(path)) { const template = await loadFile( diff --git a/src/resources/extensions/sf/dispatch-guard.js b/src/resources/extensions/sf/dispatch-guard.js index 8906d94f3..47a86b3cb 100644 --- a/src/resources/extensions/sf/dispatch-guard.js +++ b/src/resources/extensions/sf/dispatch-guard.js @@ -1,6 +1,6 @@ // SF Dispatch Guard — prevents out-of-order slice dispatch import { readFileSync } from "node:fs"; -import { findMilestoneIds } from "./guided-flow.js"; +import { findMilestoneIds } from "./milestone-ids.js"; import { parseRoadmap } from "./parsers.js"; import { resolveMilestoneFile } from "./paths.js"; import { getMilestoneSlices, isDbAvailable } from "./sf-db.js"; diff --git a/src/resources/extensions/sf/doctor-runtime-checks.js b/src/resources/extensions/sf/doctor-runtime-checks.js index efcbd16b5..08e035137 100644 --- a/src/resources/extensions/sf/doctor-runtime-checks.js +++ b/src/resources/extensions/sf/doctor-runtime-checks.js @@ -339,6 +339,11 @@ export async function checkRuntimeHealth( // Non-fatal — activity log check failed } // ── STATE.md health ─────────────────────────────────────────────────── + // STATE.md is a display-only projection rendered from the DB. No program + // logic reads it — staleness does not affect correctness, only human + // readability. The engine rewrites it after every DB-mutating operation. + // We only fix the missing-file case so the /sf status panel has something + // to show on first init. try { const stateFilePath = resolveSfRootFile(basePath, "STATE"); const milestonesPath = milestonesDir(basePath); @@ -358,44 +363,6 @@ export async function checkRuntimeHealth( await saveFile(stateFilePath, buildStateMarkdownForCheck(state)); fixesApplied.push("created STATE.md from derived state"); } - } else { - // Check if STATE.md is stale by comparing active milestone/slice/phase - const currentContent = readFileSync(stateFilePath, "utf-8"); - const state = await deriveState(basePath); - const freshContent = buildStateMarkdownForCheck(state); - // Extract key fields for comparison — don't compare full content - // since timestamp/formatting differences are normal - const extractFields = (content) => { - const milestone = - content.match(/\*\*Active Milestone:\*\*\s*(.+)/)?.[1]?.trim() ?? - ""; - const slice = - content.match(/\*\*Active Slice:\*\*\s*(.+)/)?.[1]?.trim() ?? ""; - const phase = - content.match(/\*\*Phase:\*\*\s*(.+)/)?.[1]?.trim() ?? ""; - return { milestone, slice, phase }; - }; - const current = extractFields(currentContent); - const fresh = extractFields(freshContent); - if ( - current.milestone !== fresh.milestone || - current.slice !== fresh.slice || - current.phase !== fresh.phase - ) { - issues.push({ - severity: "warning", - code: "state_file_stale", - scope: "project", - unitId: "project", - message: `STATE.md is stale — shows "${current.phase}" but derived state is "${fresh.phase}"`, - file: ".sf/STATE.md", - fixable: true, - }); - if (shouldFix("state_file_stale")) { - await saveFile(stateFilePath, freshContent); - fixesApplied.push("rebuilt STATE.md from derived state"); - } - } } } } catch { diff --git a/src/resources/extensions/sf/doctor.js b/src/resources/extensions/sf/doctor.js index 47c383d24..b64504204 100644 --- a/src/resources/extensions/sf/doctor.js +++ b/src/resources/extensions/sf/doctor.js @@ -926,13 +926,6 @@ export { formatDoctorReportJson, summarizeDoctorIssues, } from "./doctor-format.js"; -export { - computeProgressScore, - computeProgressScoreWithContext, - formatProgressLine, - formatProgressReport, -} from "./progress-score.js"; - /** * Characters that are used as delimiters in SF state management documents * and should not appear in milestone or slice titles. diff --git a/src/resources/extensions/sf/git-constants.js b/src/resources/extensions/sf/git-constants.js index 70dbd0863..e8a2fc73f 100644 --- a/src/resources/extensions/sf/git-constants.js +++ b/src/resources/extensions/sf/git-constants.js @@ -1,6 +1,12 @@ /** * Shared git constants used across git-service and native-git-bridge. */ +/** + * Regex that matches valid git branch/ref name characters. + * Extracted here to allow preferences-validation.js to validate branch names + * without importing the full git-service module (which would create a cycle). + */ +export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-/.]+$/; /** * Environment overlay suppressing interactive git prompts, editors, and git-svn noise. * Set LC_ALL=C for English output so stderr string checks work across locales. diff --git a/src/resources/extensions/sf/git-service.js b/src/resources/extensions/sf/git-service.js index ace1db004..68a469bce 100644 --- a/src/resources/extensions/sf/git-service.js +++ b/src/resources/extensions/sf/git-service.js @@ -24,7 +24,7 @@ import { import { getErrorMessage } from "./error-utils.js"; import { SF_GIT_ERROR, SF_MERGE_CONFLICT, SFError } from "./errors.js"; import { normalizePlannedFileReference } from "./files.js"; -import { GIT_NO_PROMPT_ENV } from "./git-constants.js"; +import { GIT_NO_PROMPT_ENV, VALID_BRANCH_NAME } from "./git-constants.js"; import { SF_RUNTIME_PATTERNS } from "./git-runtime-patterns.js"; import { _resetHasChangesCache, @@ -43,9 +43,7 @@ import { } from "./native-git-bridge.js"; import { sfRoot } from "./paths.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; -import { detectWorktreeName } from "./worktree.js"; -/** Regex for valid git branch names (alphanumeric, hyphens, underscores, slashes). */ -export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-/.]+$/; +import { detectWorktreeName } from "./worktree-detect.js"; /** * Build a meaningful conventional commit message from task execution context. * Format: `{type}: {description}` (clean conventional commit — no SF IDs in subject). diff --git a/src/resources/extensions/sf/migrate/command.js b/src/resources/extensions/sf/migrate/command.js index eb3610922..0165671a7 100644 --- a/src/resources/extensions/sf/migrate/command.js +++ b/src/resources/extensions/sf/migrate/command.js @@ -12,13 +12,11 @@ import { existsSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { showNextAction } from "../../shared/tui.js"; import { sfRoot } from "../paths.js"; -import { - generatePreview, - parsePlanningDirectory, - transformToSF, - validatePlanningDirectory, - writeSFDirectory, -} from "./index.js"; +import { parsePlanningDirectory } from "./parser.js"; +import { generatePreview } from "./preview.js"; +import { transformToSF } from "./transformer.js"; +import { validatePlanningDirectory } from "./validator.js"; +import { writeSFDirectory } from "./writer.js"; /** Format preview stats for embedding in the review prompt. */ function formatPreviewStats(preview) { diff --git a/src/resources/extensions/sf/milestone-id-sort.js b/src/resources/extensions/sf/milestone-id-sort.js new file mode 100644 index 000000000..2c107c20f --- /dev/null +++ b/src/resources/extensions/sf/milestone-id-sort.js @@ -0,0 +1,22 @@ +/** + * Pure milestone ID sort helpers — no external SF dependencies. + * + * Extracted from milestone-ids.js to break the circular dependency between + * milestone-ids.js and queue-order.js. Both files import from here. + * + * Purpose: provide extractMilestoneSeq and milestoneIdSort without importing + * from either queue-order.js or milestone-ids.js. + * + * Consumer: milestone-ids.js, queue-order.js. + */ + +/** Extract the trailing sequential number from a milestone ID. Returns 0 for non-matches. */ +export function extractMilestoneSeq(id) { + const m = id.match(/^M(\d{3})(?:-[a-z0-9]{6})?$/); + return m ? parseInt(m[1], 10) : 0; +} + +/** Comparator for sorting milestone IDs by sequential number. */ +export function milestoneIdSort(a, b) { + return extractMilestoneSeq(a) - extractMilestoneSeq(b); +} diff --git a/src/resources/extensions/sf/milestone-ids.js b/src/resources/extensions/sf/milestone-ids.js index 933fe11b6..8171676cb 100644 --- a/src/resources/extensions/sf/milestone-ids.js +++ b/src/resources/extensions/sf/milestone-ids.js @@ -7,6 +7,7 @@ import { randomInt } from "node:crypto"; import { existsSync, readdirSync } from "node:fs"; import { getErrorMessage } from "./error-utils.js"; +import { extractMilestoneSeq, milestoneIdSort } from "./milestone-id-sort.js"; import { milestonesDir } from "./paths.js"; import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js"; import { logWarning } from "./workflow-logger.js"; @@ -14,12 +15,9 @@ import { getAllMilestones } from "./sf-db.js"; // ─── Regex ────────────────────────────────────────────────────────────────── /** Matches both classic `M001` and unique `M001-abc123` formats (anchored). */ export const MILESTONE_ID_RE = /^M\d{3}(?:-[a-z0-9]{6})?$/; +// Re-export sort helpers for consumers that import them from this module. +export { extractMilestoneSeq, milestoneIdSort }; // ─── Parsing & Extraction ─────────────────────────────────────────────────── -/** Extract the trailing sequential number from a milestone ID. Returns 0 for non-matches. */ -export function extractMilestoneSeq(id) { - const m = id.match(/^M(\d{3})(?:-[a-z0-9]{6})?$/); - return m ? parseInt(m[1], 10) : 0; -} /** Structured parse of a milestone ID into optional suffix and sequence number. */ export function parseMilestoneId(id) { const m = id.match(/^M(\d{3})(?:-([a-z0-9]{6}))?$/); @@ -29,11 +27,6 @@ export function parseMilestoneId(id) { num: parseInt(m[1], 10), }; } -// ─── Sorting ──────────────────────────────────────────────────────────────── -/** Comparator for sorting milestone IDs by sequential number. */ -export function milestoneIdSort(a, b) { - return extractMilestoneSeq(a) - extractMilestoneSeq(b); -} // ─── Generation ───────────────────────────────────────────────────────────── /** Generate a 6-char lowercase `[a-z0-9]` suffix using crypto.randomInt(). */ export function generateMilestoneSuffix() { diff --git a/src/resources/extensions/sf/parallel-eligibility.js b/src/resources/extensions/sf/parallel-eligibility.js index 294fb96db..636b963e6 100644 --- a/src/resources/extensions/sf/parallel-eligibility.js +++ b/src/resources/extensions/sf/parallel-eligibility.js @@ -5,10 +5,15 @@ * dependency satisfaction and file overlap across slice plans. */ import { findMilestoneIds } from "./guided-flow.js"; -import { getWorkerStatuses } from "./parallel-orchestrator.js"; import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js"; import { deriveState } from "./state.js"; +// Lazy import to break the cycle: parallel-eligibility ↔ parallel-orchestrator +let _parallelOrchestrator; +async function getParallelOrchestrator() { + return (_parallelOrchestrator ??= await import("./parallel-orchestrator.js")); +} + // ─── File Collection ───────────────────────────────────────────────────────── /** * Collect all `filesLikelyTouched` across every slice plan in a milestone. @@ -167,6 +172,7 @@ export async function analyzeParallelEligibility(basePath) { overlappingIds.add(overlap.mid1); overlappingIds.add(overlap.mid2); } + const { getWorkerStatuses } = await getParallelOrchestrator(); const runningWorkerIds = new Set( getWorkerStatuses(basePath) .filter((w) => w.state === "running") diff --git a/src/resources/extensions/sf/preferences-models.js b/src/resources/extensions/sf/preferences-models.js index 73f83bf3b..9d58ed116 100644 --- a/src/resources/extensions/sf/preferences-models.js +++ b/src/resources/extensions/sf/preferences-models.js @@ -18,10 +18,25 @@ import { } from "./auto-runaway-guard.js"; import { selectByBenchmarks } from "./benchmark-selector.js"; import { defaultRoutingConfig } from "./model-router.js"; -import { - getGlobalSFPreferencesPath, - loadEffectiveSFPreferences, -} from "./preferences.js"; + +// ─── Lazy loader — breaks the preferences.js ↔ preferences-models.js cycle ── +// preferences.js imports resolveProfileDefaults from here, and needs +// loadEffectiveSFPreferences / getGlobalSFPreferencesPath from preferences.js. +// Injecting them via _initPrefsLoader (called at the end of preferences.js +// module evaluation) avoids the static circular import. +let _loadEffectiveSFPreferences; +let _getGlobalSFPreferencesPath; +/** @internal — called once by preferences.js after its exports are defined. */ +export function _initPrefsLoader(loadFn, getPathFn) { + _loadEffectiveSFPreferences = loadFn; + _getGlobalSFPreferencesPath = getPathFn; +} +function loadEffectiveSFPreferences() { + return _loadEffectiveSFPreferences(); +} +function getGlobalSFPreferencesPath() { + return _getGlobalSFPreferencesPath(); +} import { getConfiguredEnvApiKey } from "./provider-env-auth.js"; const OPENCODE_FREE_MODEL_IDS = new Set([ diff --git a/src/resources/extensions/sf/preferences-serializer.js b/src/resources/extensions/sf/preferences-serializer.js new file mode 100644 index 000000000..3f2e28273 --- /dev/null +++ b/src/resources/extensions/sf/preferences-serializer.js @@ -0,0 +1,145 @@ +/** + * Thin preferences serializer — no intra-repo imports. + * + * Extracted from commands-prefs-wizard.js so that preferences-template-upgrade.js + * can serialize preferences without importing the full wizard module (which + * transitively imports preferences.js, creating a cycle through + * preferences.js → preferences-template-upgrade.js → commands-prefs-wizard.js → preferences.js). + * + * commands-prefs-wizard.js re-exports both symbols for back-compat. + * + * Purpose: convert a raw SFPreferences object to YAML frontmatter text + * in a deterministic key order, with YAML-safe quoting for string values. + * Consumer: preferences-template-upgrade.js (upgradePreferencesFileIfDrifted), + * commands-prefs-wizard.js (wizard configure* functions). + */ + +/** + * Quote a scalar value for safe inclusion in YAML frontmatter. + * Non-string scalars are coerced to string; strings that contain YAML + * metacharacters or leading/trailing whitespace are double-quoted. + * + * @param {unknown} val + * @returns {string} + */ +export function yamlSafeString(val) { + if (typeof val !== "string") return String(val); + if (/[:#{[\]'"`,|>&*!?@%]/.test(val) || val.trim() !== val || val === "") { + return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; + } + return val; +} + +/** + * Serialize an SFPreferences object to YAML frontmatter text (without the + * surrounding `---` delimiters). Keys are emitted in a canonical order for + * stable diffs; unknown keys are appended after the ordered set. + * + * Purpose: produce the YAML block written between `---` markers in + * PREFERENCES.md. Relies only on yamlSafeString — no filesystem or + * preferences-loading imports needed. + * + * Consumer: preferences-template-upgrade.js, commands-prefs-wizard.js. + * + * @param {Record} prefs + * @returns {string} + */ +export function serializePreferencesToFrontmatter(prefs) { + const lines = []; + function serializeValue(key, value, indent) { + const prefix = " ".repeat(indent); + if (value === null || value === undefined) return; + if (Array.isArray(value)) { + if (value.length === 0) { + return; // Omit empty arrays — avoids parse/serialize cycle bug with "[]" strings + } + lines.push(`${prefix}${key}:`); + for (const item of value) { + if (typeof item === "object" && item !== null) { + const entries = Object.entries(item); + if (entries.length > 0) { + const [firstKey, firstVal] = entries[0]; + lines.push(`${prefix} - ${firstKey}: ${yamlSafeString(firstVal)}`); + for (let i = 1; i < entries.length; i++) { + const [k, v] = entries[i]; + if (Array.isArray(v)) { + lines.push(`${prefix} ${k}:`); + for (const arrItem of v) { + lines.push(`${prefix} - ${yamlSafeString(arrItem)}`); + } + } else { + lines.push(`${prefix} ${k}: ${yamlSafeString(v)}`); + } + } + } + } else { + lines.push(`${prefix} - ${yamlSafeString(item)}`); + } + } + return; + } + if (typeof value === "object") { + const entries = Object.entries(value); + if (entries.length === 0) { + return; // Omit empty objects — avoids parse/serialize cycle bug with "{}" strings + } + lines.push(`${prefix}${key}:`); + for (const [k, v] of entries) { + serializeValue(k, v, indent + 1); + } + return; + } + lines.push(`${prefix}${key}: ${yamlSafeString(value)}`); + } + // Ordered keys for consistent output + const orderedKeys = [ + "version", + "mode", + "always_use_skills", + "prefer_skills", + "avoid_skills", + "skill_rules", + "custom_instructions", + "models", + "skill_discovery", + "skill_staleness_days", + "auto_supervisor", + "uat_dispatch", + "unique_milestone_ids", + "budget_ceiling", + "budget_enforcement", + "context_pause_threshold", + "notifications", + "cmux", + "remote_questions", + "git", + "post_unit_hooks", + "pre_dispatch_hooks", + "dynamic_routing", + "uok", + "token_profile", + "phases", + "parallel", + "auto_visualize", + "auto_report", + "verification_commands", + "verification_auto_fix", + "verification_max_retries", + "search_provider", + "context_selection", + ]; + const seen = new Set(); + for (const key of orderedKeys) { + if (key in prefs) { + serializeValue(key, prefs[key], 0); + seen.add(key); + } + } + // Any remaining keys not in the ordered list + for (const [key, value] of Object.entries(prefs)) { + if (!seen.has(key)) { + serializeValue(key, value, 0); + } + } + return lines.join("\n") + "\n"; +} diff --git a/src/resources/extensions/sf/preferences-template-upgrade.js b/src/resources/extensions/sf/preferences-template-upgrade.js index 14b4cc41b..05e3551b6 100644 --- a/src/resources/extensions/sf/preferences-template-upgrade.js +++ b/src/resources/extensions/sf/preferences-template-upgrade.js @@ -12,7 +12,7 @@ * have to read drift advisories; sf keeps its own house in order. */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js"; +import { serializePreferencesToFrontmatter } from "./preferences-serializer.js"; export function detectTemplateDrift(prefs) { const toVersion = process.env.SF_VERSION || "0.0.0"; const fromVersion = prefs.last_synced_with_sf || "0.0.0"; diff --git a/src/resources/extensions/sf/preferences-validation.js b/src/resources/extensions/sf/preferences-validation.js index 5141ed349..9082f571d 100644 --- a/src/resources/extensions/sf/preferences-validation.js +++ b/src/resources/extensions/sf/preferences-validation.js @@ -6,7 +6,7 @@ * together with any errors and warnings. */ import { normalizeStringArray } from "../shared/format-utils.js"; -import { VALID_BRANCH_NAME } from "./git-service.js"; +import { VALID_BRANCH_NAME } from "./git-constants.js"; import { CURRENT_PREFERENCES_SCHEMA_VERSION, checkPreferencesDrift, diff --git a/src/resources/extensions/sf/preferences.js b/src/resources/extensions/sf/preferences.js index 27ae7b5dd..1128987ae 100644 --- a/src/resources/extensions/sf/preferences.js +++ b/src/resources/extensions/sf/preferences.js @@ -15,7 +15,7 @@ import { dirname, join, resolve } from "node:path"; import { parse as parseYaml } from "yaml"; import { normalizeStringArray } from "../shared/format-utils.js"; import { sfRoot } from "./paths.js"; -import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js"; +import { resolveProfileDefaults as _resolveProfileDefaults, _initPrefsLoader } from "./preferences-models.js"; import { upgradePreferencesFileIfDrifted } from "./preferences-template-upgrade.js"; import { formatSkillRef, @@ -923,3 +923,8 @@ export function getHotCacheTurns() { const mode = prefs?.mode ?? "solo"; return mode === "team" ? 10 : 5; } + +// Inject the loader functions into preferences-models.js to break the +// preferences.js ↔ preferences-models.js circular import. This runs after +// all exports in this module are defined, so the injected references are live. +_initPrefsLoader(loadEffectiveSFPreferences, getGlobalSFPreferencesPath); diff --git a/src/resources/extensions/sf/queue-order.js b/src/resources/extensions/sf/queue-order.js index 6b7f75335..d126e1c9e 100644 --- a/src/resources/extensions/sf/queue-order.js +++ b/src/resources/extensions/sf/queue-order.js @@ -7,7 +7,7 @@ */ import { join } from "node:path"; import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js"; -import { milestoneIdSort } from "./milestone-ids.js"; +import { milestoneIdSort } from "./milestone-id-sort.js"; import { sfRoot } from "./paths.js"; import { getAllMilestones, diff --git a/src/resources/extensions/sf/scaffold-constants.js b/src/resources/extensions/sf/scaffold-constants.js new file mode 100644 index 000000000..a959beaae --- /dev/null +++ b/src/resources/extensions/sf/scaffold-constants.js @@ -0,0 +1,462 @@ +/** + * Canonical scaffold file list — shared between agentic-docs-scaffold.js and scaffold-drift.js. + * + * Extracted to break the circular dependency between those two modules. + * Neither agentic-docs-scaffold.js nor scaffold-drift.js import from each other; + * both import SCAFFOLD_FILES from here. + * + * Purpose: single source of truth for the files SF manages in every project. + * Consumer: agentic-docs-scaffold.js, scaffold-drift.js. + */ +export const SCAFFOLD_FILES = [ + { + path: ".siftignore", + content: `.git/** +.sf/** +.bg-shell/** +.pytest_cache/** +.venv/** +venv/** +node_modules/** +**/node_modules/** +**/__pycache__/** +*.pyc +*.egg-info/** +build/** +dist/** +target/** +vendor/** +coverage/** +.cache/** +tmp/** +*.log +`, + }, + { + path: "AGENTS.md", + content: `# Agent Map + +Keep this file short. Use it as a table of contents for agents and humans. + +- Treat the repo as a purpose-to-software pipeline: intent -> purpose/consumer/contract/evidence -> tests -> implementation -> verification. +- Read \`ARCHITECTURE.md\` first for the system map and invariants. +- Read \`docs/PLANS.md\` and \`docs/exec-plans/active/\` for current work. +- Read \`docs/QUALITY_SCORE.md\`, \`docs/RELIABILITY.md\`, and \`docs/SECURITY.md\` before changing production behavior. +- Put durable product decisions in \`docs/product-specs/\`. +- Put durable design and architecture decisions in \`docs/design-docs/\`. +- Put generated reference material in \`docs/generated/\`. +- Use \`docs/RECORDS_KEEPER.md\` as the repo-order checklist after meaningful changes. +- Use the \`records-keeper\` skill when repo docs, plans, or architecture records need triage. +- Follow deeper \`AGENTS.md\` files when present. The closest one to the changed file wins. + +Before implementation, inspect the relevant docs and source files, state observed facts before inferred facts, name the real consumer, and define the command or eval that proves the change. +`, + }, + { + path: "src/AGENTS.md", + content: `# Source Agent Notes + +- Start by mapping the owning module and its tests. +- Preserve existing public contracts unless the active plan explicitly changes them. +- Prefer typed/domain helpers over ad hoc parsing or duplicated logic. +- Keep edits scoped to the smallest module boundary that satisfies the plan. +- Update \`ARCHITECTURE.md\` when a source change creates a new subsystem or invariant. +`, + }, + { + path: "tests/AGENTS.md", + content: `# Test Agent Notes + +- Treat tests as executable specs, not coverage decoration. +- Add regression tests for changed behavior and failure modes. +- Prefer focused tests that name the behavior under test. +- Include the exact verification command in the plan or completion summary. +`, + }, + { + path: "ARCHITECTURE.md", + content: `# Architecture + +This file is the short map of the codebase. Keep it current and compact. + +## Purpose + +Describe the product, its users, and the job this repository exists to do. + +## Codemap + +- \`src/\`: primary implementation. +- \`tests/\`: behavior and regression coverage. +- \`docs/\`: durable product, design, plan, reliability, and security context. + +## Invariants + +- Prefer small, named modules with clear ownership. +- Behavior changes need tests or an explicit eval. +- Keep generated artifacts out of hand-written design docs. +- Update this map when new top-level concepts or directories become important. +`, + }, + { + path: "docs/design-docs/index.md", + content: `# Design Docs + +Durable design decisions live here. Link active proposals, completed decisions, and rejected alternatives. +`, + }, + { + path: "docs/AGENTS.md", + content: `# Docs Agent Notes + +- Docs are the durable project memory. Keep them concise, navigable, and current. +- Treat \`docs/adr/0000-purpose-to-software-compiler.md\` as the root SF product contract. +- Put stable decisions here; keep transient execution state in active plans. +- Prefer links to source paths, commands, and eval artifacts over broad prose. +- When docs and code disagree, inspect the code and update the stale document. +- Run the records keeper checklist in \`RECORDS_KEEPER.md\` after meaningful code, product, or architecture changes. +`, + }, + { + path: "docs/records/AGENTS.md", + content: `# Records Agent Notes + +- Keep repository memory ordered, current, and easy to inspect. +- Prefer moving durable facts to the narrowest canonical document over duplicating them. +- Preserve historical decisions; mark superseded records instead of deleting useful context. +- Escalate conflicts between docs and source by citing the exact files that disagree. +`, + }, + { + path: "docs/records/index.md", + content: `# Records + +This folder holds repo-memory audits, decision ledgers, context-gardening notes, and records-keeper outputs. +`, + }, + { + path: "docs/RECORDS_KEEPER.md", + content: `# Records Keeper + +The records keeper keeps repo memory ordered after meaningful changes. Run this checklist at milestone close, after architecture changes, after product behavior changes, and whenever docs/source disagree. + +Use the \`records-keeper\` skill for this workflow when SF skills are available. Use \`context-doctor\` instead when stale state lives under \`.sf/\` or the memory store. + +## Canonical Homes + +- Root \`AGENTS.md\`: short routing map for agents. +- \`ARCHITECTURE.md\`: short system map, boundaries, invariants, critical flows, and verification. +- \`docs/product-specs/\`: durable user-facing behavior and product decisions. +- \`docs/design-docs/\`: durable design and architecture decisions. +- \`docs/exec-plans/\`: active/completed work plans and technical debt. +- \`docs/generated/\`: generated references only. +- \`docs/records/\`: audits, ledgers, and context-gardening outputs. + +## Checklist + +- Root map is current: \`AGENTS.md\` points to the right canonical docs and local \`AGENTS.md\` files. +- Architecture is current: new subsystems, boundaries, invariants, data/state, or critical flows are reflected in \`ARCHITECTURE.md\`. +- Product specs are current: user-visible behavior changes are reflected in \`docs/product-specs/\`. +- Execution plans are filed: active work is in \`docs/exec-plans/active/\`; completed summaries and evidence are in \`docs/exec-plans/completed/\`. +- Debt is visible: discovered cleanup is listed in \`docs/exec-plans/tech-debt-tracker.md\`. +- Generated docs are marked: generated material stays under \`docs/generated/\` or clearly says how to regenerate it. +- Contradictions are resolved: stale docs are updated or marked superseded with links to the source of truth. +- Verification is recorded: changed checks, evals, and commands are listed in the relevant plan or quality document. + +## Output + +When records work is non-trivial, write a dated note under \`docs/records/\` with: + +- What changed. +- What canonical docs were updated. +- What contradictions were found. +- What remains unresolved. +`, + }, + { + path: "docs/design-docs/AGENTS.md", + content: `# Design Doc Agent Notes + +- Capture problem, context, options, decision, consequences, and validation. +- Separate observed facts from inferred product or architecture intent. +- Record rejected alternatives when they would prevent repeated debate. +`, + }, + { + path: "docs/design-docs/core-beliefs.md", + content: `# Core Beliefs + +- The repo should explain itself to humans and agents. +- Plans should carry acceptance criteria, falsifiers, and verification commands. +- Architecture should be mechanically checkable where possible. +`, + }, + { + path: "docs/exec-plans/active/index.md", + content: `# Active Execution Plans + +Link active plans here. Each plan should state purpose, scope, tasks, acceptance criteria, and verification. +`, + }, + { + path: "docs/exec-plans/AGENTS.md", + content: `# Execution Plan Agent Notes + +- Every plan needs purpose, scope, tasks, acceptance criteria, falsifier, and verification. +- Active plans live in \`active/\`; completed evidence summaries live in \`completed/\`. +- Add discovered cleanup to \`tech-debt-tracker.md\` instead of hiding it in chat. +`, + }, + { + path: "docs/exec-plans/completed/index.md", + content: `# Completed Execution Plans + +Move finished plan summaries here with evidence links and follow-up debt. +`, + }, + { + path: "docs/exec-plans/tech-debt-tracker.md", + content: `# Tech Debt Tracker + +Track cleanup discovered during implementation. Include owner, impact, proposed fix, and verification. +`, + }, + { + path: "docs/generated/db-schema.md", + content: `# Database Schema + +Generated or refreshed schema notes belong here. Do not hand-maintain stale schema copies. +`, + }, + { + path: "docs/product-specs/index.md", + content: `# Product Specs + +Durable user-facing behavior, workflows, and product decisions live here. +`, + }, + { + path: "docs/product-specs/AGENTS.md", + content: `# Product Spec Agent Notes + +- Describe the user, job-to-be-done, workflow, edge cases, and non-goals. +- Keep implementation details out unless they are product-visible constraints. +- Update specs when behavior changes, especially onboarding, permissions, billing, or destructive actions. +`, + }, + { + path: "docs/product-specs/new-user-onboarding.md", + content: `# New User Onboarding + +Describe the first-run experience, success criteria, and failure states when this product has an onboarding flow. +`, + }, + { + path: "docs/references/design-system-reference-llms.txt", + content: `Reference slot for design-system guidance intended for LLM consumption. +`, + }, + { + path: "docs/references/nixpacks-llms.txt", + content: `Reference slot for Nixpacks deployment/build guidance intended for LLM consumption. +`, + }, + { + path: "docs/references/uv-llms.txt", + content: `Reference slot for uv/Python tooling guidance intended for LLM consumption. +`, + }, + { + path: "docs/DESIGN.md", + content: `# Design + +Record interaction patterns, visual constraints, and design-system usage here. +`, + }, + { + path: "docs/FRONTEND.md", + content: `# Frontend + +Record frontend architecture, component ownership, accessibility constraints, and browser support here. +`, + }, + { + path: "docs/PLANS.md", + content: `# Plans + +Use this as the index for current and upcoming work. Link detailed plans in \`docs/exec-plans/\`. +`, + }, + { + path: "docs/PRODUCT_SENSE.md", + content: `# Product Sense + +Capture user goals, non-goals, tradeoffs, and examples of good product judgment for this repo. +`, + }, + { + path: "docs/QUALITY_SCORE.md", + content: `# Quality Score + +Define what good looks like for this repo. Include fast checks, slow checks, evals, and known blind spots. + +Use these principles: + +- Make code legible to agents with semantic names and explicit boundaries. +- Prefer small, testable modules over files that require broad context to edit. +- Enforce style, architecture, and reliability rules mechanically where possible. +- Keep a cleanup loop for stale docs, generated artifacts, and accumulated implementation debt. +`, + }, + { + path: "docs/RELIABILITY.md", + content: `# Reliability + +Document expected failure modes, recovery paths, observability, and release checks here. +`, + }, + { + path: "docs/SECURITY.md", + content: `# Security + +Document trust boundaries, secrets handling, dependency risk, and security review requirements here. +`, + }, + { + path: "docs/design-docs/ADR-TEMPLATE.md", + content: `# ADR-NNN: Title + +**Status:** Proposed | Accepted | Rejected | Superseded by ADR-NNN +**Date:** YYYY-MM-DD + +## Context + +What is the problem or situation that requires a decision? Include constraints and the forces at play. + +## Decision + +What is the change being made or the approach being adopted? + +## Consequences + +What becomes easier or harder after this decision? Include positive and negative outcomes. + +## Alternatives Considered + +What other options were evaluated and why were they not chosen? +`, + }, + { + path: ".sf/harness/AGENTS.md", + content: `# Harness Agent Notes + +The harness is SF-local operational scaffolding the agent can read and verify against. + +- \`specs/\`: behavior contracts. Each spec states what "done" looks like and the command that proves it. +- \`evals/\`: task definitions for behaviors tests cannot cover — model output quality, multi-turn flows, agent decisions. +- \`graders/\`: reusable grader scripts (code-based checks, LLM-judge prompts used by evals). + +**Rule:** Before marking a task done, run the relevant spec's verification command. Record the result in the completion summary or execution plan. +`, + }, + { + path: ".sf/harness/specs/AGENTS.md", + content: `# Harness Specs Agent Notes + +Each spec file in this directory: + +- States the behavior being specified (not the implementation). +- Includes the exact command that proves the spec passes. +- Is referenced by the relevant execution plan or ADR. + +Write the spec before implementation. Run it after. Record the result. +`, + }, + { + path: ".sf/harness/specs/bootstrap.md", + content: `# Bootstrap Spec: Agent Legibility + +Verifies that this repo is minimally agent-legible. + +## Criteria + +- [ ] \`AGENTS.md\` exists at repo root and is non-empty. +- [ ] \`ARCHITECTURE.md\` exists at repo root and is non-empty. +- [ ] \`docs/exec-plans/active/\` exists. +- [ ] \`docs/exec-plans/tech-debt-tracker.md\` exists. +- [ ] \`docs/design-docs/ADR-TEMPLATE.md\` exists. + +## Verification command + +\`\`\`bash +for f in AGENTS.md ARCHITECTURE.md docs/exec-plans/active/index.md docs/exec-plans/tech-debt-tracker.md docs/design-docs/ADR-TEMPLATE.md .sf/harness/specs/bootstrap.md; do [ -s "$f" ] && echo "OK: $f" || echo "MISSING: $f"; done +\`\`\` + +All lines should start with \`OK:\` for the bootstrap spec to pass. +`, + }, + { + path: ".sf/harness/evals/AGENTS.md", + content: `# Harness Evals Agent Notes + +Evals verify behavior that unit tests cannot cover — model output quality, agent decisions, multi-turn flows. + +Each eval should include: +- The input fixture or prompt +- The expected output or scoring rubric +- The command to run it (\`promptfoo eval\`, custom script, etc.) + +Keep evals deterministic where possible. Log results to \`docs/records/\` at milestone close. +`, + }, + { + path: ".sf/harness/graders/AGENTS.md", + content: `# Harness Graders Agent Notes + +Graders are reusable scripts or prompts that score eval outputs. + +- Code-based graders: shell scripts or test files that check structured outputs deterministically. +- LLM-judge graders: prompt templates that ask a model to score free-text output against a rubric. + +Prefer code-based graders. Add LLM-judge graders only when deterministic checking is impossible. +`, + }, + { + path: ".sf/PRINCIPLES.md", + content: `# Principles + +Durable design philosophy. Things this codebase believes are true. + +Add entries as you make decisions. Each entry: 1-2 sentences. Cite the rationale (the why, not just the what). + +## Examples + +- (replace with your own) +`, + }, + { + path: ".sf/TASTE.md", + content: `# Taste + +What good code looks like here. Idioms, conventions, "we prefer X over Y" calls. + +Add entries as you notice patterns worth preserving. Each entry: 1-2 sentences with a concrete example. + +## Examples + +- (replace with your own) +`, + }, + { + path: ".sf/ANTI-GOALS.md", + content: `# Anti-goals + +What we explicitly DON'T want. Things that look attractive but we've decided against. + +This is gold — most wrong agent calls come from not knowing what to avoid. Each entry: 1-2 sentences with the rationale. + +## Examples + +- (replace with your own) +`, + }, +]; diff --git a/src/resources/extensions/sf/scaffold-drift.js b/src/resources/extensions/sf/scaffold-drift.js index cd8000e9f..8b0b89613 100644 --- a/src/resources/extensions/sf/scaffold-drift.js +++ b/src/resources/extensions/sf/scaffold-drift.js @@ -8,7 +8,7 @@ */ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { SCAFFOLD_FILES } from "./agentic-docs-scaffold.js"; +import { SCAFFOLD_FILES } from "./scaffold-constants.js"; import { bodyHash, extractMarker, diff --git a/src/resources/extensions/sf/session-forensics.js b/src/resources/extensions/sf/session-forensics.js index 3a4701f0b..1e380d40e 100644 --- a/src/resources/extensions/sf/session-forensics.js +++ b/src/resources/extensions/sf/session-forensics.js @@ -27,6 +27,7 @@ import { } from "./native-git-bridge.js"; import { nativeParseJsonlTail } from "./native-parser-bridge.js"; import { sfRuntimeRoot } from "./paths.js"; +import { getActiveMilestoneIdFromDb } from "./sf-db.js"; // ─── JSONL Parsing ──────────────────────────────────────────────────────────── // MAX_JSONL_BYTES and parseJSONL are imported from ./jsonl-utils.js @@ -265,16 +266,16 @@ export function getDeepDiagnostic(basePath, worktreePath) { return formatTraceSummary(trace); } /** - * Read the active milestone ID directly from STATE.md without async deriveState(). - * Looks for `**Active Milestone:** M001` pattern. + * Read the active milestone ID from the DB. + * + * Purpose: provide a sync-compatible active milestone lookup without reading + * the STATE.md cache, which can be stale between DB writes and file flushes. + * Consumer: auto.js getDeepDiagnostic, crash recovery briefing. */ -export function readActiveMilestoneId(basePath) { +export function readActiveMilestoneId(_basePath) { try { - const statePath = join(sfRuntimeRoot(basePath), "STATE.md"); - if (!existsSync(statePath)) return null; - const content = readFileSync(statePath, "utf-8"); - const match = /\*\*Active Milestone:\*\*\s*(\S+)/i.exec(content); - return match?.[1] ?? null; + const result = getActiveMilestoneIdFromDb(); + return result?.id ?? null; } catch { return null; } diff --git a/src/resources/extensions/sf/uok/unit-runtime.js b/src/resources/extensions/sf/uok/unit-runtime.js index 3a4ef41c3..81491f72c 100644 --- a/src/resources/extensions/sf/uok/unit-runtime.js +++ b/src/resources/extensions/sf/uok/unit-runtime.js @@ -21,7 +21,7 @@ import { resolveTaskFile, sfRoot, } from "../paths.js"; -import { getSlice, isDbAvailable } from "../sf-db.js"; +import { getSlice, getTask, isDbAvailable } from "../sf-db.js"; import { parseUnitId } from "../unit-id.js"; import { normalizeUnitLineage, @@ -474,21 +474,19 @@ export async function inspectExecuteTaskDurability(basePath, unitId) { if (!mid || !sid || !tid) return null; const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN"); const summaryAbs = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY"); - const stateAbs = join(sfRoot(basePath), "STATE.md"); const planPath = relSliceFile(basePath, mid, sid, "PLAN"); const summaryPath = relTaskFile(basePath, mid, sid, tid, "SUMMARY"); const planContent = planAbs ? await loadFile(planAbs) : null; - const stateContent = existsSync(stateAbs) - ? readFileSync(stateAbs, "utf-8") - : ""; const summaryExists = !!(summaryAbs && existsSync(summaryAbs)); const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const taskChecked = !!planContent && new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m").test(planContent); - const nextActionAdvanced = !new RegExp(`Execute ${tid}\\b`).test( - stateContent, - ); + // Use DB task status instead of reading STATE.md — STATE.md is a display + // projection and may lag behind DB writes by a loop iteration. + const taskRow = isDbAvailable() ? getTask(mid, sid, tid) : null; + const taskStatus = taskRow?.status ?? "pending"; + const nextActionAdvanced = taskStatus !== "pending" && taskStatus !== "in_progress"; // Must-have coverage: load task plan and count mentions in summary let mustHaveCount = 0; let mustHavesMentionedInSummary = 0; diff --git a/src/resources/extensions/sf/visualizer-data.js b/src/resources/extensions/sf/visualizer-data.js index c516ccfe5..5cdf00b22 100644 --- a/src/resources/extensions/sf/visualizer-data.js +++ b/src/resources/extensions/sf/visualizer-data.js @@ -3,7 +3,6 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { countPendingCaptures, loadAllCaptures } from "./captures.js"; import { runEnvironmentChecks } from "./doctor-environment.js"; -import { getHealthHistory } from "./doctor-proactive.js"; import { runProviderChecks } from "./doctor-providers.js"; import { loadFile, parseSummary } from "./files.js"; import { @@ -25,7 +24,6 @@ import { sfRoot, } from "./paths.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; -import { computeProgressScore } from "./progress-score.js"; import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js"; import { generateSkillHealthReport } from "./skill-health.js"; import { deriveState } from "./state.js"; @@ -355,7 +353,7 @@ function loadKnowledge(basePath) { return { rules, patterns, lessons, exists: true }; } // ─── Health Loader ──────────────────────────────────────────────────────────── -function loadHealth(units, totals, basePath) { +async function loadHealth(units, totals, basePath) { const prefs = loadEffectiveSFPreferences(); const budgetCeiling = prefs?.preferences?.budget_ceiling; const tokenProfile = prefs?.preferences?.token_profile ?? "standard"; @@ -440,8 +438,10 @@ function loadHealth(units, totals, basePath) { // Current progress score — only meaningful when autonomous mode has health data let progressScore = null; try { + const { getHealthHistory } = await import("./doctor-proactive.js"); const history = getHealthHistory(); if (history.length > 0) { + const { computeProgressScore } = await import("./progress-score.js"); const score = computeProgressScore(); progressScore = { level: score.level, @@ -704,7 +704,7 @@ export async function loadVisualizerData(basePath) { pendingCount, totalCount: allCaptures.length, }; - const health = loadHealth(units, totals, basePath); + const health = await loadHealth(units, totals, basePath); const stats = buildVisualizerStats(milestones, changelog.entries); const discussion = loadDiscussionState(basePath, milestones); return { diff --git a/src/resources/extensions/sf/workflow-logger.js b/src/resources/extensions/sf/workflow-logger.js index 15899c34d..418bf14e0 100644 --- a/src/resources/extensions/sf/workflow-logger.js +++ b/src/resources/extensions/sf/workflow-logger.js @@ -26,8 +26,7 @@ import { import { join } from "node:path"; import { withFileLockSync } from "./file-lock.js"; import { appendNotification } from "./notification-store.js"; -import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js"; -import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js"; + // ─── Buffer & Persistent Audit ────────────────────────────────────────── const MAX_BUFFER = 100; @@ -230,32 +229,10 @@ function _push(severity, component, message, context) { if (_buffer.length > MAX_BUFFER) { _buffer.shift(); } - if (_auditBasePath && isAuditEnvelopeEnabled()) { - try { - emitUokAuditEvent( - _auditBasePath, - buildAuditEnvelope({ - traceId: `workflow-log:${component}`, - turnId: context?.id, - causedBy: context?.fn ?? context?.tool, - category: "orchestration", - type: - severity === "error" ? "workflow-log-error" : "workflow-log-warn", - payload: { - component, - message, - context: context ?? {}, - }, - }), - ); - } catch (auditEmitErr) { - // Best-effort: unified audit projection must never block workflow logger. - // Increment failure counter so doctor/status can surface persistent divergence. - _auditEmitFailureCount++; - _writeStderr( - `[sf:workflow-logger] unified-audit emit failed: ${auditEmitErr.message}\n`, - ); - } + if (_auditBasePath) { + // Fire-and-forget: audit emission is best-effort and must not block the sync logger. + // Dynamic imports break the sf-db → workflow-logger → uok/audit → sf-db cycle. + _tryEmitAuditEvent(_auditBasePath, severity, component, message, context); } // Persist errors to .sf/audit-log.jsonl so they survive context resets. // Only error-severity entries are persisted — warnings are ephemeral (stderr + buffer) @@ -284,6 +261,33 @@ function _push(severity, component, message, context) { } } } +async function _tryEmitAuditEvent(basePath, severity, component, message, context) { + try { + const { isAuditEnvelopeEnabled } = await import("./uok/audit-toggle.js"); + if (!isAuditEnvelopeEnabled()) return; + const { buildAuditEnvelope, emitUokAuditEvent } = await import("./uok/audit.js"); + emitUokAuditEvent( + basePath, + buildAuditEnvelope({ + traceId: `workflow-log:${component}`, + turnId: context?.id, + causedBy: context?.fn ?? context?.tool, + category: "orchestration", + type: severity === "error" ? "workflow-log-error" : "workflow-log-warn", + payload: { + component, + message, + context: context ?? {}, + }, + }), + ); + } catch (auditEmitErr) { + _auditEmitFailureCount++; + _writeStderr( + `[sf:workflow-logger] unified-audit emit failed: ${auditEmitErr.message}\n`, + ); + } +} function _writeStderr(message) { if (!_stderrEnabled) return; process.stderr.write(message); diff --git a/src/resources/extensions/sf/worktree-detect.js b/src/resources/extensions/sf/worktree-detect.js new file mode 100644 index 000000000..bb43a5dbe --- /dev/null +++ b/src/resources/extensions/sf/worktree-detect.js @@ -0,0 +1,57 @@ +/** + * Thin worktree-detection helpers with no intra-repo imports. + * + * Extracted from worktree.js so that git-service.js can import + * detectWorktreeName without pulling in the full worktree module + * (which in turn imports GitServiceImpl from git-service.js, creating + * a cycle). worktree.js re-exports both symbols for back-compat. + * + * Purpose: provide a zero-dependency path for detecting whether a + * basePath lives inside a SF worktree (.sf/worktrees/). + */ + +/** + * Locate the worktree segment within a normalized path string. + * Returns the string indices needed by detectWorktreeName and + * resolveProjectRoot; returns null if not inside a SF worktree. + * + * @param {string} normalizedPath — forward-slash-normalised absolute path. + */ +function findWorktreeSegment(normalizedPath) { + // Direct layout: /.sf/worktrees/ + const directMarker = "/.sf/worktrees/"; + const idx = normalizedPath.indexOf(directMarker); + if (idx !== -1) { + return { sfIdx: idx, afterWorktrees: idx + directMarker.length }; + } + // Symlink-resolved layout: /.sf/projects//worktrees/ + const symlinkRe = /\/\.sf\/projects\/[a-f0-9]+\/worktrees\//; + const match = normalizedPath.match(symlinkRe); + if (match && match.index !== undefined) { + return { + sfIdx: match.index, + afterWorktrees: match.index + match[0].length, + }; + } + return null; +} + +/** + * Detect the active worktree name from the current working directory. + * Returns null if not inside a SF worktree (.sf/worktrees//). + * + * Consumer: git-service.js (GitServiceImpl.getTargetBranch) and worktree.js. + * + * @param {string} basePath + * @returns {string | null} + */ +export function detectWorktreeName(basePath) { + const normalizedPath = basePath.replaceAll("\\", "/"); + const seg = findWorktreeSegment(normalizedPath); + if (!seg) return null; + const afterMarker = normalizedPath.slice(seg.afterWorktrees); + const name = afterMarker.split("/")[0]; + return name || null; +} + +export { findWorktreeSegment }; diff --git a/src/resources/extensions/sf/worktree.js b/src/resources/extensions/sf/worktree.js index c782e5bab..11a354c04 100644 --- a/src/resources/extensions/sf/worktree.js +++ b/src/resources/extensions/sf/worktree.js @@ -16,8 +16,14 @@ import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { GitServiceImpl, writeIntegrationBranch } from "./git-service.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; +import { + detectWorktreeName, + findWorktreeSegment, +} from "./worktree-detect.js"; export { MergeConflictError } from "./git-service.js"; +// Re-export for consumers that import detectWorktreeName from ./worktree.js +export { detectWorktreeName } from "./worktree-detect.js"; // ─── Lazy GitServiceImpl Cache ───────────────────────────────────────────── let cachedService = null; @@ -68,43 +74,6 @@ export function captureIntegrationBranch(basePath, milestoneId) { writeIntegrationBranch(basePath, milestoneId, current); } // ─── Pure Utility Functions (unchanged) ──────────────────────────────────── -/** - * Find the worktrees segment in a path, supporting both direct - * (`/.sf/worktrees/`) and symlink-resolved (`/.sf/projects//worktrees/`) - * layouts. When `.sf` is a symlink to `~/.sf/projects/`, resolved - * paths contain the intermediate `projects//` segment that the old - * single-marker check missed. - */ -function findWorktreeSegment(normalizedPath) { - // Direct layout: /.sf/worktrees/ - const directMarker = "/.sf/worktrees/"; - const idx = normalizedPath.indexOf(directMarker); - if (idx !== -1) { - return { sfIdx: idx, afterWorktrees: idx + directMarker.length }; - } - // Symlink-resolved layout: /.sf/projects//worktrees/ - const symlinkRe = /\/\.sf\/projects\/[a-f0-9]+\/worktrees\//; - const match = normalizedPath.match(symlinkRe); - if (match && match.index !== undefined) { - return { - sfIdx: match.index, - afterWorktrees: match.index + match[0].length, - }; - } - return null; -} -/** - * Detect the active worktree name from the current working directory. - * Returns null if not inside a SF worktree (.sf/worktrees//). - */ -export function detectWorktreeName(basePath) { - const normalizedPath = basePath.replaceAll("\\", "/"); - const seg = findWorktreeSegment(normalizedPath); - if (!seg) return null; - const afterMarker = normalizedPath.slice(seg.afterWorktrees); - const name = afterMarker.split("/")[0]; - return name || null; -} /** * Resolve the project root from a path that may be inside a worktree. * If the path contains a worktrees segment, returns the portion before