sf snapshot: uncommitted changes after 61m inactivity
This commit is contained in:
parent
8088489e38
commit
deeb4dbd4e
8 changed files with 152 additions and 8 deletions
55
.sf/PREFERENCES.md
Normal file
55
.sf/PREFERENCES.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
version: 1
|
||||
last_synced_with_sf: 2.75.3
|
||||
sf_template_state: pending
|
||||
sf_template_hash: "sha256:287389de2f7e2bfa1c6043682cde774f8d39e2ed6591dcec633f6c72af8acac2"
|
||||
verification_commands:
|
||||
- "npm run typecheck:extensions"
|
||||
- npm run build
|
||||
- npm run lint
|
||||
- "npm run test:sf-light"
|
||||
- "bash -c 'set -e; for d in \"rust-engine\" \"rust-engine/crates/ast\" \"rust-engine/crates/engine\" \"rust-engine/crates/grep\"; do (cd \"$d\" && cargo fmt --check); done'"
|
||||
- "bash -c 'set -e; for d in \"rust-engine\" \"rust-engine/crates/ast\" \"rust-engine/crates/engine\" \"rust-engine/crates/grep\"; do (cd \"$d\" && cargo check); done'"
|
||||
- "bash -c 'set -e; for d in \"rust-engine\" \"rust-engine/crates/ast\" \"rust-engine/crates/engine\" \"rust-engine/crates/grep\"; do (cd \"$d\" && cargo test -- --test-threads=2); done'"
|
||||
- "bash -c 'set -e; for d in \"rust-engine\" \"rust-engine/crates/ast\" \"rust-engine/crates/engine\" \"rust-engine/crates/grep\"; do (cd \"$d\" && cargo clippy -- -D warnings); done'"
|
||||
always_use_skills: []
|
||||
prefer_skills: []
|
||||
avoid_skills: []
|
||||
skill_rules: []
|
||||
custom_instructions: []
|
||||
models: {}
|
||||
skill_discovery: {}
|
||||
auto_supervisor: {}
|
||||
---
|
||||
|
||||
# SF Skill Preferences
|
||||
|
||||
Project-specific guidance for skill selection and execution preferences.
|
||||
|
||||
See `~/.sf/agent/extensions/sf/docs/preferences-reference.md` for full field documentation and examples.
|
||||
|
||||
## Fields
|
||||
|
||||
- `always_use_skills`: Skills that must be available during all SF operations
|
||||
- `prefer_skills`: Skills to prioritize when multiple options exist
|
||||
- `avoid_skills`: Skills to minimize or avoid (with lower priority than prefer)
|
||||
- `skill_rules`: Context-specific rules (e.g., "use tool X for Y type of work")
|
||||
- `custom_instructions`: Append-only project guidance (do not override system rules)
|
||||
- `models`: Model preferences for specific task types
|
||||
- `skill_discovery`: Automatic skill detection preferences
|
||||
- `auto_supervisor`: Supervision and gating rules for autonomous modes
|
||||
- `git`: Git preferences — `main_branch` (default branch name for new repos, e.g., "main", "master", "trunk"), `auto_push`, `snapshots`, etc.
|
||||
|
||||
## Examples
|
||||
|
||||
```yaml
|
||||
prefer_skills:
|
||||
- playwright
|
||||
- resolve_library
|
||||
avoid_skills:
|
||||
- subagent # prefer direct execution in this project
|
||||
|
||||
custom_instructions:
|
||||
- "Always verify with browser_assert before marking UI work done"
|
||||
- "Use Context7 for all library/framework decisions"
|
||||
```
|
||||
|
|
@ -162,6 +162,21 @@ export function hasMilestones(basePath: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export async function hasProjectMilestones(basePath: string): Promise<boolean> {
|
||||
if (hasMilestones(basePath)) return true;
|
||||
try {
|
||||
const dynamicToolsPath =
|
||||
"./resources/extensions/sf/bootstrap/dynamic-tools.js";
|
||||
const { ensureDbOpen } = await import(dynamicToolsPath);
|
||||
if (!(await ensureDbOpen(basePath))) return false;
|
||||
const sfDbPath = "./resources/extensions/sf/sf-db.js";
|
||||
const { getAllMilestones, isDbAvailable } = await import(sfDbPath);
|
||||
return isDbAvailable() && getAllMilestones().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAutoBootstrapContext(basePath: string): string {
|
||||
const selectedFiles = collectAutoBootstrapFiles(basePath);
|
||||
const sourceFiles = collectSourceFiles(basePath);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
import {
|
||||
bootstrapProject,
|
||||
buildAutoBootstrapContext,
|
||||
hasMilestones,
|
||||
hasProjectMilestones,
|
||||
loadContext,
|
||||
} from "./headless-context.js";
|
||||
|
||||
|
|
@ -571,7 +571,7 @@ async function runHeadlessOnce(
|
|||
}
|
||||
if (options.command === "autonomous" && !options.resumeSession) {
|
||||
bootstrapProject(process.cwd());
|
||||
if (!hasMilestones(process.cwd())) {
|
||||
if (!(await hasProjectMilestones(process.cwd()))) {
|
||||
if (!options.json) {
|
||||
process.stderr.write(
|
||||
"[headless] No milestones found; bootstrapping from repo docs and source inventory...\n",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import { renderAllProjections } from "./workflow-projections.js";
|
|||
|
||||
const LEGACY_MILESTONE_DIR_RE = /^(M\d+)-.+$/;
|
||||
const LEGACY_SLICE_DIR_RE = /^(S\d+)-.+$/;
|
||||
function canonicalMilestonePrefix(id) {
|
||||
return id.match(/^([A-Z]\d{3})/)?.[1] ?? id;
|
||||
}
|
||||
|
||||
function projectionDriftIssues(basePath, milestoneId) {
|
||||
const issues = [];
|
||||
|
|
@ -403,16 +406,19 @@ export async function checkEngineHealth(
|
|||
const msDir = milestonesDir(basePath);
|
||||
if (existsSync(msDir)) {
|
||||
const validMilestoneIds = new Set();
|
||||
const validMilestonePrefixes = new Set();
|
||||
if (isDbAvailable()) {
|
||||
// DB-authoritative: only DB rows count as valid
|
||||
for (const m of getAllMilestones()) {
|
||||
validMilestoneIds.add(m.id);
|
||||
validMilestonePrefixes.add(canonicalMilestonePrefix(m.id));
|
||||
}
|
||||
} else {
|
||||
// No DB: fall back to filesystem registry
|
||||
const state = await deriveState(basePath);
|
||||
for (const m of state.registry) {
|
||||
validMilestoneIds.add(m.id);
|
||||
validMilestonePrefixes.add(canonicalMilestonePrefix(m.id));
|
||||
}
|
||||
}
|
||||
for (const entry of readdirSync(msDir)) {
|
||||
|
|
@ -427,7 +433,8 @@ export async function checkEngineHealth(
|
|||
if (!milestoneId) continue;
|
||||
if (
|
||||
!validMilestoneIds.has(milestoneId) &&
|
||||
!validMilestoneIds.has(entry)
|
||||
!validMilestoneIds.has(entry) &&
|
||||
!validMilestonePrefixes.has(canonicalMilestonePrefix(entry))
|
||||
) {
|
||||
issues.push({
|
||||
severity: "warning",
|
||||
|
|
|
|||
|
|
@ -731,7 +731,7 @@ export async function showHeadlessMilestoneCreation(
|
|||
}
|
||||
/**
|
||||
* Single discuss-dispatch entry point for new milestones.
|
||||
* auto=true → headless prompt, rootFiles seed, plan-milestone workflow, no pendingAutoStartMap
|
||||
* auto=true → headless prompt, rootFiles seed, discuss-milestone workflow, no pendingAutoStartMap
|
||||
* auto=false → discuss prompt with preparation, discuss-milestone workflow, sets pendingAutoStartMap
|
||||
*/
|
||||
export async function dispatchNewMilestoneDiscuss(
|
||||
|
|
@ -785,7 +785,7 @@ export async function dispatchNewMilestoneDiscuss(
|
|||
basePath,
|
||||
);
|
||||
// Do NOT set pendingAutoStartMap — caller (bootstrapAutoSession) manages the loop
|
||||
await dispatchWorkflow(pi, prompt, "sf-run", ctx, "plan-milestone");
|
||||
await dispatchWorkflow(pi, prompt, "sf-run", ctx, "discuss-milestone");
|
||||
} else {
|
||||
pendingAutoStartMap.set(basePath, {
|
||||
ctx,
|
||||
|
|
|
|||
|
|
@ -134,6 +134,13 @@ export function clearPathCache() {
|
|||
export function buildMilestoneFileName(milestoneId, suffix) {
|
||||
return `${milestoneId}-${suffix}.md`;
|
||||
}
|
||||
function canonicalMilestoneIdForDir(milestoneId, milestoneDir) {
|
||||
const dirName = milestoneDir ? milestoneDir.split(/[/\\]/).pop() : null;
|
||||
const canonicalPrefix = milestoneId.match(/^([A-Z]\d{3})-/)?.[1];
|
||||
return canonicalPrefix && dirName === canonicalPrefix
|
||||
? canonicalPrefix
|
||||
: milestoneId;
|
||||
}
|
||||
/**
|
||||
* Build a slice-level file name.
|
||||
* ("S01", "PLAN") → "S01-PLAN.md"
|
||||
|
|
@ -163,6 +170,14 @@ export function resolveDir(parentDir, idPrefix) {
|
|||
// Exact match first (current convention: bare ID)
|
||||
const exact = entries.find((e) => e.isDirectory() && e.name === idPrefix);
|
||||
if (exact) return exact.name;
|
||||
// Unique-ID fallback: M001-abc123 should still resolve bare M001/
|
||||
const canonicalPrefix = idPrefix.match(/^([A-Z]\d{3})-/)?.[1];
|
||||
if (canonicalPrefix) {
|
||||
const canonical = entries.find(
|
||||
(e) => e.isDirectory() && e.name === canonicalPrefix,
|
||||
);
|
||||
if (canonical) return canonical.name;
|
||||
}
|
||||
// Prefix match for legacy descriptor dirs: M001-SOMETHING
|
||||
const prefixed = entries.find(
|
||||
(e) => e.isDirectory() && e.name.startsWith(idPrefix + "-"),
|
||||
|
|
@ -187,10 +202,27 @@ export function resolveFile(dir, idPrefix, suffix) {
|
|||
// Direct match: ID-SUFFIX.md
|
||||
const direct = entries.find((e) => e.toUpperCase() === target);
|
||||
if (direct) return direct;
|
||||
// Unique-ID fallback: M001-abc123-CONTEXT.md should still resolve M001-CONTEXT.md
|
||||
const canonicalPrefix = idPrefix.match(/^([A-Z]\d{3})-/)?.[1];
|
||||
if (canonicalPrefix) {
|
||||
const canonicalTarget = `${canonicalPrefix}-${suffix}.md`.toUpperCase();
|
||||
const canonicalDirect = entries.find(
|
||||
(e) => e.toUpperCase() === canonicalTarget,
|
||||
);
|
||||
if (canonicalDirect) return canonicalDirect;
|
||||
}
|
||||
// Legacy pattern match: ID-DESCRIPTOR-SUFFIX.md
|
||||
const pattern = new RegExp(`^${idPrefix}-.*-${suffix}\\.md$`, "i");
|
||||
const match = entries.find((e) => pattern.test(e));
|
||||
if (match) return match;
|
||||
if (canonicalPrefix) {
|
||||
const canonicalPattern = new RegExp(
|
||||
`^${canonicalPrefix}-.*-${suffix}\\.md$`,
|
||||
"i",
|
||||
);
|
||||
const canonicalMatch = entries.find((e) => canonicalPattern.test(e));
|
||||
if (canonicalMatch) return canonicalMatch;
|
||||
}
|
||||
// Legacy fallback: suffix.md
|
||||
const legacy = entries.find(
|
||||
(e) => e.toLowerCase() === `${suffix.toLowerCase()}.md`,
|
||||
|
|
@ -526,7 +558,8 @@ export function relMilestoneFile(basePath, milestoneId, suffix) {
|
|||
const file = resolveFile(mDir, milestoneId, suffix);
|
||||
if (file) return `${mRel}/${file}`;
|
||||
}
|
||||
return `${mRel}/${buildMilestoneFileName(milestoneId, suffix)}`;
|
||||
const fileId = canonicalMilestoneIdForDir(milestoneId, mDir);
|
||||
return `${mRel}/${buildMilestoneFileName(fileId, suffix)}`;
|
||||
}
|
||||
/**
|
||||
* Build relative .sf/ path to a slice directory.
|
||||
|
|
|
|||
|
|
@ -265,6 +265,9 @@ export async function deriveState(basePath) {
|
|||
function stripMilestonePrefix(title) {
|
||||
return title.replace(/^M\d+(?:-[a-z0-9]{6})?[^:]*:\s*/, "") || title;
|
||||
}
|
||||
function canonicalMilestonePrefix(id) {
|
||||
return id.match(/^([A-Z]\d{3})/)?.[1] ?? id;
|
||||
}
|
||||
function extractContextTitle(content, fallback) {
|
||||
if (!content) return fallback;
|
||||
const h1 = content.split("\n").find((line) => line.startsWith("# "));
|
||||
|
|
@ -290,8 +293,14 @@ function reconcileDiskToDb(basePath) {
|
|||
const diskIds = findMilestoneIds(basePath);
|
||||
if (diskIds.length > 0) {
|
||||
const dbIds = new Set(getAllMilestones().map((m) => m.id));
|
||||
const dbPrefixes = new Set(
|
||||
Array.from(dbIds, (id) => canonicalMilestonePrefix(id)),
|
||||
);
|
||||
const diskOnlyIds = diskIds.filter(
|
||||
(id) => !dbIds.has(id) && !isGhostMilestone(basePath, id),
|
||||
(id) =>
|
||||
!dbIds.has(id) &&
|
||||
!dbPrefixes.has(canonicalMilestonePrefix(id)) &&
|
||||
!isGhostMilestone(basePath, id),
|
||||
);
|
||||
if (diskOnlyIds.length > 0) {
|
||||
logWarning(
|
||||
|
|
|
|||
|
|
@ -2,12 +2,22 @@ import assert from "node:assert/strict";
|
|||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { test } from "vitest";
|
||||
import { afterEach, test } from "vitest";
|
||||
|
||||
import {
|
||||
buildAutoBootstrapContext,
|
||||
hasMilestones,
|
||||
hasProjectMilestones,
|
||||
} from "../headless-context.js";
|
||||
import {
|
||||
closeDatabase,
|
||||
insertMilestone,
|
||||
openDatabase,
|
||||
} from "../resources/extensions/sf/sf-db.js";
|
||||
|
||||
afterEach(() => {
|
||||
closeDatabase();
|
||||
});
|
||||
|
||||
test("buildAutoBootstrapContext includes purpose docs and source inventory", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-headless-bootstrap-"));
|
||||
|
|
@ -44,3 +54,18 @@ test("hasMilestones only reports true when milestone directories exist", () => {
|
|||
mkdirSync(join(root, ".sf", "milestones", "M001"), { recursive: true });
|
||||
assert.equal(hasMilestones(root), true);
|
||||
});
|
||||
|
||||
test("hasProjectMilestones_when_db_contains_milestone_without_disk_dir_reports_true", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-headless-db-milestones-"));
|
||||
mkdirSync(join(root, ".sf", "milestones"), { recursive: true });
|
||||
openDatabase(join(root, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: "M001-6377a4",
|
||||
title: "DB-only milestone",
|
||||
status: "queued",
|
||||
});
|
||||
closeDatabase();
|
||||
|
||||
assert.equal(hasMilestones(root), false);
|
||||
assert.equal(await hasProjectMilestones(root), true);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue