fix: remove stale M001/M002 milestone dirs; fix dispatch-guard circular dep; fix telemetry normalization

- Remove stale .sf/milestones/M001/ and M002/ (not in DB, were blocking dispatch)
- dispatch-guard.js: import findMilestoneIds from milestone-ids.js directly (not
  via guided-flow.js, which is in the circular-dep cluster)
- auto.js: normalize 'Cannot dispatch' → prior-slice-blocker, 'SF resources updated'
  → resources-stale, 'Stuck:' → stuck in telemetry (was silently bucketing as 'other')

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-10 02:12:13 +02:00
parent ea360f6ad2
commit a3f2479a4c
37 changed files with 1340 additions and 769 deletions

View file

@ -1,3 +1,3 @@
{
"lastFullVacuumAt": "2026-05-09T17:40:16.686Z"
"lastFullVacuumAt": "2026-05-09T23:40:22.903Z"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

452
scaffold_files_content.txt Normal file
View file

@ -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)
`,
},
];

View file

@ -38,7 +38,7 @@ try {
/\/tests?\//,
],
detectiveOptions: {
es6: { mixedImports: true },
es6: { mixedImports: true, skipAsyncImports: true },
},
});
} catch (err) {

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -1088,6 +1088,12 @@ export async function stopAuto(ctx, pi, reason) {
const rawReason = reason ?? "stop";
const normalizedReason = rawReason.startsWith("Blocked:")
? "blocked"
: 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") ||

View file

@ -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(

View file

@ -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";

View file

@ -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 {

View file

@ -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.

View file

@ -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.

View file

@ -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).

View file

@ -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) {

View file

@ -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);
}

View file

@ -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() {

View file

@ -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")

View file

@ -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([

View file

@ -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<string, unknown>} 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";
}

View file

@ -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";

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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)
`,
},
];

View file

@ -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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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);

View file

@ -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/<name>).
*/
/**
* 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/<name>
const directMarker = "/.sf/worktrees/";
const idx = normalizedPath.indexOf(directMarker);
if (idx !== -1) {
return { sfIdx: idx, afterWorktrees: idx + directMarker.length };
}
// Symlink-resolved layout: /.sf/projects/<hash>/worktrees/<name>
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/<name>/).
*
* 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 };

View file

@ -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/<hash>/worktrees/`)
* layouts. When `.sf` is a symlink to `~/.sf/projects/<hash>`, resolved
* paths contain the intermediate `projects/<hash>/` segment that the old
* single-marker check missed.
*/
function findWorktreeSegment(normalizedPath) {
// Direct layout: /.sf/worktrees/<name>
const directMarker = "/.sf/worktrees/";
const idx = normalizedPath.indexOf(directMarker);
if (idx !== -1) {
return { sfIdx: idx, afterWorktrees: idx + directMarker.length };
}
// Symlink-resolved layout: /.sf/projects/<hash>/worktrees/<name>
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/<name>/).
*/
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