refactor(sf): rename BACKLOG.md → SELF-FEEDBACK.md (matches jsonl SoT)
The forge-local human-readable file was misnamed — it's sf-internal self- reports, not a generic project backlog. The jsonl source-of-truth is already self-feedback.jsonl; the markdown should match. Renames: - File: BACKLOG.md → SELF-FEEDBACK.md - Constant: BACKLOG_HEADER → SELF_FEEDBACK_HEADER - Constant: BACKLOG_MAX_CHARS → SELF_FEEDBACK_MAX_CHARS - Function: appendBacklogRow → appendSelfFeedbackRow - Function: loadBacklogBlock → loadSelfFeedbackBlock (parallel session) - Prompt file: prompts/triage-backlog.md → prompts/triage-self-feedback.md (parallel session) - Module: triage-backlog.ts → triage-self-feedback.ts (parallel session) - Header: "# SF Self-Feedback Backlog" → "# SF Self-Feedback" Doc/text refs across prompts (execute-task, complete-milestone, triage-self-feedback) and helper modules (gap-audit, requirement-promoter, db-tools, system-context) updated to .sf/SELF-FEEDBACK.md. Migration: new exported migrateLegacyBacklogFilename() in self-feedback.ts runs at session_start (wired in register-hooks.ts) — renames the legacy BACKLOG.md → SELF-FEEDBACK.md once, idempotent + non-fatal. system-context's loadSelfFeedbackBlock also reads either name during the transition. system-context.ts: BACKLOG_MAX_CHARS retained but raised earlier from 2000 to 8000 with all-entries-fit-or-truncate-tail (separate commit). The SoT mtime-cache and per-severity rendering remain as before. Tests: 77/77 pass across UOK + upstream-bridge + triage-self-feedback. Not done in this commit (next iteration): - Direct-drain dispatch at session_start for high/critical (subprocess spawn). - Queue promotion for medium severity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a492079b9
commit
983a2e0a44
14 changed files with 120 additions and 92 deletions
|
|
@ -638,8 +638,8 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
|
||||
// ─── sf_self_report ─────────────────────────────────────────────────
|
||||
// Agent-callable bug-report channel. Records anomalies the agent observes
|
||||
// in sf's own behavior so they accumulate in a backlog (forge's own
|
||||
// .sf/BACKLOG.md when running on forge itself, ~/.sf/agent/upstream-feedback.jsonl
|
||||
// in sf's own behavior so they accumulate in self-feedback (forge's own
|
||||
// .sf/SELF-FEEDBACK.md when running on forge itself, ~/.sf/agent/upstream-feedback.jsonl
|
||||
// otherwise). Severity drives whether the originating unit is also blocked
|
||||
// pending an sf version bump.
|
||||
|
||||
|
|
@ -723,15 +723,15 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
"workflows, or speculative improvements. Over-reporting is preferred to under-reporting; " +
|
||||
"dedup happens later. Do NOT use this for bugs in the user's project or for your own task " +
|
||||
"work — only for sf-the-tool observations. Entries route automatically: when working on " +
|
||||
"singularity-forge itself they land in .sf/BACKLOG.md; otherwise they land in a global " +
|
||||
"singularity-forge itself they land in .sf/SELF-FEEDBACK.md; otherwise they land in a global " +
|
||||
"~/.sf/upstream-feedback.jsonl.",
|
||||
promptSnippet:
|
||||
"Report any sf-internal observation: bug, missing feature, prompt issue, idea, friction",
|
||||
promptGuidelines: [
|
||||
"Use sf_self_report for ANY sf-internal observation — not just bugs. Acceptable kinds include: 'prompt-quality-issue' (you found a prompt ambiguous, contradictory, or missing context), 'improvement-idea' (a non-bug enhancement that would help), 'agent-friction' (workflow friction you worked around), 'design-thought' (broader speculation), 'missing-feature' (capability you wished sf had), as well as classic bug kinds like 'brittle-predicate' or 'git-empty-pathspec'.",
|
||||
"Do NOT use this for bugs in the user's project, for your own task work, or to track your task's todo list. ONLY for observations about sf-the-tool itself.",
|
||||
"This tool FILES new entries; it does not address or resolve existing ones. The backlog is a triage inbox awaiting human/triage-agent review — do NOT autonomously pick entries off the backlog and try to fix them. Treat existing entries as out of scope unless your task plan explicitly names a backlog entry id as the work.",
|
||||
"Over-reporting is preferred to under-reporting at this stage. If you noticed it about sf, file it. Dedup and threshold-to-roadmap promotion are tracked as their own backlog items and will eventually clean noise.",
|
||||
"This tool FILES new entries; it does not address or resolve existing ones. Self-feedback is a triage inbox awaiting human/triage-agent review — do NOT autonomously pick entries off self-feedback and try to fix them. Treat existing entries as out of scope unless your task plan explicitly names a self-feedback entry id as the work.",
|
||||
"Over-reporting is preferred to under-reporting at this stage. If you noticed it about sf, file it. Dedup and threshold-to-roadmap promotion are tracked as their own self-feedback items and will eventually clean noise.",
|
||||
"Severity guide: low = cosmetic / nice-to-have / improvement idea. medium = noisy or imperfect or recurring friction. high = blocked the unit (sf-the-tool prevented you from completing the task). critical = needs immediate fix (currently treated as high until inline-fix dispatch lands).",
|
||||
"high/critical entries mark the originating unit as blocked: it will not seal as success, and will be re-queued only after sf is bumped past the recorded version.",
|
||||
"Provide concrete evidence — log excerpt, command, file path, error message, the literal prompt text that confused you, etc. Vague reports are not actionable; specific ones are.",
|
||||
|
|
|
|||
|
|
@ -192,14 +192,17 @@ export function registerHooks(
|
|||
}
|
||||
}
|
||||
loadToolApiKeys();
|
||||
// Drain self-feedback backlog: auto-resolve entries whose blocking
|
||||
// Drain self-feedback: auto-resolve entries whose blocking
|
||||
// sf-version constraint has been satisfied by the current sf bump,
|
||||
// and surface entries that remain blocked to the operator. Done after
|
||||
// other init so notifications appear in the same session-start sweep.
|
||||
try {
|
||||
const { triageBlockedEntries, markResolved } = await import(
|
||||
"../self-feedback.js"
|
||||
);
|
||||
const {
|
||||
markResolved,
|
||||
migrateLegacyBacklogFilename,
|
||||
triageBlockedEntries,
|
||||
} = await import("../self-feedback.js");
|
||||
migrateLegacyBacklogFilename(process.cwd());
|
||||
const triage = triageBlockedEntries(process.cwd());
|
||||
const currentSfVersion = process.env.SF_VERSION || "unknown";
|
||||
for (const e of triage.retry) {
|
||||
|
|
@ -228,7 +231,7 @@ export function registerHooks(
|
|||
}
|
||||
if (triage.stillBlocked.length > 0) {
|
||||
ctx.ui?.notify?.(
|
||||
`${triage.stillBlocked.length} self-feedback entr${triage.stillBlocked.length === 1 ? "y" : "ies"} still blocked on prior sf versions. See .sf/BACKLOG.md or ~/.sf/agent/upstream-feedback.jsonl.`,
|
||||
`${triage.stillBlocked.length} self-feedback entr${triage.stillBlocked.length === 1 ? "y" : "ies"} still blocked on prior sf versions. See .sf/SELF-FEEDBACK.md or ~/.sf/agent/upstream-feedback.jsonl.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
|
@ -241,7 +244,7 @@ export function registerHooks(
|
|||
if (highBlocked.length > 0) {
|
||||
const ids = highBlocked.map((e) => `${e.id} (${e.kind})`).join(", ");
|
||||
ctx.ui?.notify?.(
|
||||
`${highBlocked.length} inline-fix candidate${highBlocked.length === 1 ? "" : "s"} pending in .sf/BACKLOG.md: ${ids}`,
|
||||
`${highBlocked.length} inline-fix candidate${highBlocked.length === 1 ? "" : "s"} pending in .sf/SELF-FEEDBACK.md: ${ids}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
|
@ -254,7 +257,7 @@ export function registerHooks(
|
|||
const filed = runGapAudit(process.cwd());
|
||||
if (filed > 0) {
|
||||
ctx.ui?.notify?.(
|
||||
`Gap audit filed ${filed} new finding${filed === 1 ? "" : "s"} in .sf/BACKLOG.md`,
|
||||
`Gap audit filed ${filed} new finding${filed === 1 ? "" : "s"} in .sf/SELF-FEEDBACK.md`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
|
|
@ -269,13 +272,13 @@ export function registerHooks(
|
|||
} catch {
|
||||
/* non-fatal — parity summary must never block session start */
|
||||
}
|
||||
// Bridge upstream feedback into forge-local backlog
|
||||
// Bridge upstream feedback into forge-local self-feedback
|
||||
try {
|
||||
const { bridgeUpstreamFeedback } = await import("../upstream-bridge.js");
|
||||
const filed = bridgeUpstreamFeedback(process.cwd());
|
||||
if (filed > 0) {
|
||||
ctx.ui?.notify?.(
|
||||
`Upstream bridge filed ${filed} rollup${filed === 1 ? "" : "s"} in .sf/BACKLOG.md`,
|
||||
`Upstream bridge filed ${filed} rollup${filed === 1 ? "" : "s"} in .sf/SELF-FEEDBACK.md`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,9 +364,9 @@ export async function buildBeforeAgentStartResult(
|
|||
? `\n\n[JUDGMENT LOG — autonomous mode]\nWhen you make a judgment call between alternatives at an ambiguous point, call sf_log_judgment with: decision, alternatives, reasoning, confidence. This lets the user review your reasoning at milestone close. It does NOT delay or block the work.`
|
||||
: "";
|
||||
|
||||
const backlogBlock = loadBacklogBlock(process.cwd());
|
||||
const selfFeedbackBlock = loadSelfFeedbackBlock(process.cwd());
|
||||
|
||||
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — SF]\n\n${escalationPolicyBlock}${systemContent}${preferenceBlock}${knowledgeBlock}${architectureBlock}${tacitKnowledgeBlock}${codebaseBlock}${codeIntelligenceBlock}${memoryBlock}${newSkillsBlock}${backlogBlock}${worktreeBlock}${repositoryVcsBlock}${modelIdentityBlock}${subagentModelBlock}${judgmentLogBlock}`;
|
||||
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — SF]\n\n${escalationPolicyBlock}${systemContent}${preferenceBlock}${knowledgeBlock}${architectureBlock}${tacitKnowledgeBlock}${codebaseBlock}${codeIntelligenceBlock}${memoryBlock}${newSkillsBlock}${selfFeedbackBlock}${worktreeBlock}${repositoryVcsBlock}${modelIdentityBlock}${subagentModelBlock}${judgmentLogBlock}`;
|
||||
|
||||
stopContextTimer({
|
||||
systemPromptSize: fullSystem.length,
|
||||
|
|
@ -435,16 +435,20 @@ export function loadKnowledgeBlock(
|
|||
}
|
||||
|
||||
const TACIT_SECTION_MAX_BYTES = 4096;
|
||||
// No entry-count cap — the backlog must flow into work in full. The only
|
||||
// No entry-count cap — self-feedback must flow into work in full. The only
|
||||
// guard is char length: if the rendered block would exceed this budget,
|
||||
// truncate from the lowest-priority tail (oldest medium/low first) until
|
||||
// it fits. High/critical entries are never truncated.
|
||||
const BACKLOG_MAX_CHARS = 8000;
|
||||
const SELF_FEEDBACK_MAX_CHARS = 8000;
|
||||
|
||||
function loadBacklogBlock(cwd: string): string {
|
||||
const backlogPath = join(cwd, ".sf", "BACKLOG.md");
|
||||
if (!existsSync(backlogPath)) return "";
|
||||
const raw = cachedReadFile(backlogPath)?.trim() ?? "";
|
||||
function loadSelfFeedbackBlock(cwd: string): string {
|
||||
const selfFeedbackPath = join(cwd, ".sf", "SELF-FEEDBACK.md");
|
||||
const legacyBacklogPath = join(cwd, ".sf", "BACKLOG.md");
|
||||
const sourcePath = existsSync(selfFeedbackPath)
|
||||
? selfFeedbackPath
|
||||
: legacyBacklogPath;
|
||||
if (!existsSync(sourcePath)) return "";
|
||||
const raw = cachedReadFile(sourcePath)?.trim() ?? "";
|
||||
if (!raw) return "";
|
||||
|
||||
// Parse the table rows — skip header lines
|
||||
|
|
@ -478,20 +482,20 @@ function loadBacklogBlock(cwd: string): string {
|
|||
|
||||
// Render all entries; sort already put high/critical first.
|
||||
const rows = entries.map((e) => `- **${e.severity}** \`${e.kind}\` — ${e.summary}`).join("\n");
|
||||
let block = `## Self-Feedback Entries (from .sf/BACKLOG.md, ordered by severity)\n\n${rows}`;
|
||||
let block = `## Self-Feedback Entries (from .sf/SELF-FEEDBACK.md, ordered by severity)\n\n${rows}`;
|
||||
// If over the char budget, drop entries from the tail (lowest priority,
|
||||
// oldest) one at a time until it fits. High/critical never get truncated
|
||||
// because severity sort puts them at the front.
|
||||
if (block.length > BACKLOG_MAX_CHARS) {
|
||||
if (block.length > SELF_FEEDBACK_MAX_CHARS) {
|
||||
let kept = entries.slice();
|
||||
while (kept.length > 1 && block.length > BACKLOG_MAX_CHARS) {
|
||||
while (kept.length > 1 && block.length > SELF_FEEDBACK_MAX_CHARS) {
|
||||
kept = kept.slice(0, -1);
|
||||
block =
|
||||
`## Self-Feedback Entries (from .sf/BACKLOG.md, ordered by severity, truncated)\n\n` +
|
||||
`## Self-Feedback Entries (from .sf/SELF-FEEDBACK.md, ordered by severity, truncated)\n\n` +
|
||||
kept.map((e) => `- **${e.severity}** \`${e.kind}\` — ${e.summary}`).join("\n");
|
||||
}
|
||||
}
|
||||
return `\n\n[BACKLOG — Recent sf-internal anomalies]\n\n${block}`;
|
||||
return `\n\n[SELF-FEEDBACK — Recent sf-internal anomalies]\n\n${block}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Purpose: automatically find dead code, unreferenced prompts, undispatched
|
||||
* command handlers, and shipped-but-unimported native modules. Results are
|
||||
* written to self-feedback so they surface in BACKLOG.md and can be triaged.
|
||||
* written to self-feedback so they surface in SELF-FEEDBACK.md and can be triaged.
|
||||
*
|
||||
* Consumer: session_start drain hook in register-hooks.ts.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ If work falls into the second bucket, do not fail the milestone just because it
|
|||
- `deviations` (string) — Deviations from the original plan
|
||||
12. Update `.sf/PROJECT.md`: use the `write` tool with `path: ".sf/PROJECT.md"` and `content` containing the full updated document reflecting milestone completion and current project state. Do NOT use the `edit` tool for this — PROJECT.md is a full-document refresh.
|
||||
13. Review all slice summaries for cross-cutting lessons, patterns, or gotchas that emerged during this milestone. Append any non-obvious, reusable insights to `.sf/KNOWLEDGE.md`.
|
||||
13b. Review `.sf/BACKLOG.md` (if present — it lives only when sf is dogfooded on forge) and the global `~/.sf/agent/upstream-feedback.jsonl`. For any sf-internal anomaly that recurred across multiple slices in this milestone but is not yet captured in either log, file it now via `sf_self_report`. The milestone-close agent is the last line of defense for systemic sf bugs that single-task agents missed.
|
||||
13b. Review `.sf/SELF-FEEDBACK.md` (if present — it lives only when sf is dogfooded on forge) and the global `~/.sf/agent/upstream-feedback.jsonl`. For any sf-internal anomaly that recurred across multiple slices in this milestone but is not yet captured in either log, file it now via `sf_self_report`. The milestone-close agent is the last line of defense for systemic sf bugs that single-task agents missed.
|
||||
14. Do not commit manually — the system auto-commits your changes after this unit completes.
|
||||
- Say: "Milestone {{milestoneId}} complete."
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ Then:
|
|||
- After a compile-repair edit, rerun the narrow failing command immediately before more feature work. If two repair attempts leave the same unknown-symbol class, stop broad edits and write a precise handoff/blocker summary.
|
||||
17. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
|
||||
17b. **sf-internal anomalies and observations:** If during execution you observe sf-the-tool misbehaving (empty `git add --` pathspecs, brittle gate predicates, advisory-downgrade hiding real failures, false safety floods), find a prompt ambiguous or contradictory, hit workflow friction, or have an idea that would make sf better — call `sf_self_report`. Use `prompt-quality-issue`, `improvement-idea`, `agent-friction`, or `design-thought` kinds for non-bug observations alongside the classic bug kinds. Severity guide: `low`/`medium` for cosmetic / noisy / nice-to-have (sf continues); `high`/`critical` only when the sf issue actually prevents the task from sealing correctly (this blocks the unit). For high/critical, include `acceptance_criteria` so a future resolver has a falsifiable bar. This is distinct from `blocker_discovered` (which is about the user's plan, not about sf). Over-reporting is preferred to under-reporting at this stage.
|
||||
17c. **The self-feedback backlog is a TRIAGE inbox, not a work queue.** Do NOT autonomously pick up entries from `.sf/BACKLOG.md` or `~/.sf/agent/upstream-feedback.jsonl` and try to fix them — those are open observations awaiting human/triage-agent review to decide which become scheduled work, duplicates, or wontfix. Your scope is the task plan you were dispatched with. The only interaction your task should have with the backlog is FILING new entries (via `sf_self_report`) when you observe sf-internal anomalies. The exception: if a backlog entry id is *explicitly named* in your task plan as the work to be done, treat it as you would any other planned item — read its `acceptanceCriteria`, satisfy each, and cite the entry id + criteria met in your task summary's `narrative` so the resolution is traceable.
|
||||
17c. **Self-feedback is a TRIAGE inbox, not a work queue.** Do NOT autonomously pick up entries from `.sf/SELF-FEEDBACK.md` or `~/.sf/agent/upstream-feedback.jsonl` and try to fix them — those are open observations awaiting human/triage-agent review to decide which become scheduled work, duplicates, or wontfix. Your scope is the task plan you were dispatched with. The only interaction your task should have with self-feedback is FILING new entries (via `sf_self_report`) when you observe sf-internal anomalies. The exception: if a self-feedback entry id is *explicitly named* in your task plan as the work to be done, treat it as you would any other planned item — read its `acceptanceCriteria`, satisfy each, and cite the entry id + criteria met in your task summary's `narrative` so the resolution is traceable.
|
||||
18. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.sf/DECISIONS.md` (read the template at `~/.sf/agent/extensions/sf/templates/decisions.md` if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
|
||||
19. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.sf/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
|
||||
20. Read the template at `~/.sf/agent/extensions/sf/templates/task-summary.md`
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
You are executing SF triage-backlog mode.
|
||||
You are executing SF triage-self-feedback mode.
|
||||
|
||||
## UNIT: Triage Backlog
|
||||
## UNIT: Triage Self-Feedback
|
||||
|
||||
You are the **triage agent**. Your sole job is to read the open feedback backlog,
|
||||
You are the **triage agent**. Your sole job is to read the open self-feedback,
|
||||
cluster related entries, and produce a structured triage report. You are explicitly
|
||||
authorized to act on backlog entries in this context — this overrides the
|
||||
authorized to act on self-feedback entries in this context — this overrides the
|
||||
execute-task prohibition that says "Do NOT autonomously pick up entries from
|
||||
.sf/BACKLOG.md or ~/.sf/agent/upstream-feedback.jsonl and try to fix them."
|
||||
.sf/SELF-FEEDBACK.md or ~/.sf/agent/upstream-feedback.jsonl and try to fix them."
|
||||
|
||||
That prohibition applies to execute-task agents because they have tight scope.
|
||||
You do not. Reading and triaging the backlog is your entire scope.
|
||||
You do not. Reading and triaging self-feedback is your entire scope.
|
||||
|
||||
## Inputs
|
||||
|
||||
|
|
@ -18,12 +18,12 @@ The following data has been preloaded for you — do not re-read files from disk
|
|||
### Open Forge-Local Feedback Entries
|
||||
|
||||
```json
|
||||
{{forgeBacklogJson}}
|
||||
{{forgeSelfFeedbackJson}}
|
||||
```
|
||||
|
||||
### Open Upstream-Rollup Entries
|
||||
|
||||
These are entries from the forge-local backlog that were originally filed while
|
||||
These are entries from forge-local self-feedback that were originally filed while
|
||||
SF was running on an external project (repoIdentity = "external"), aggregated
|
||||
here for cross-project signal.
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ here for cross-project signal.
|
|||
|
||||
## Your Task
|
||||
|
||||
Work through the backlog entries above. For each cluster of related entries:
|
||||
Work through the self-feedback entries above. For each cluster of related entries:
|
||||
|
||||
1. Decide on a recommendation using one of these values exactly:
|
||||
- `schedule-as-slice` — enough signal to warrant a new slice in an existing milestone
|
||||
|
|
@ -73,8 +73,8 @@ Work through the backlog entries above. For each cluster of related entries:
|
|||
existing requirement's description already covers the failure mode.
|
||||
- Do not schedule work that is already covered by an in-flight slice.
|
||||
- Use `sf_self_report` only if you observe something NEW during this triage session
|
||||
(e.g. a systematic gap in the backlog structure itself). Do not re-report entries
|
||||
already in the backlog.
|
||||
(e.g. a systematic gap in the self-feedback structure itself). Do not re-report
|
||||
entries already in self-feedback.
|
||||
|
||||
## Output Format
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* auto-promotes to a row in `.sf/REQUIREMENTS.md`.
|
||||
*
|
||||
* Requirements flow into prompt context via the existing planning pipeline,
|
||||
* so promotion turns "noise that piles up in BACKLOG.md" into "something
|
||||
* so promotion turns "noise that piles up in SELF-FEEDBACK.md" into "something
|
||||
* the next planning round naturally addresses."
|
||||
*
|
||||
* Consumer: session_start drain hook in register-hooks.ts (wired separately).
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* Routing:
|
||||
* - When the current project IS singularity-forge itself (detected via
|
||||
* package.json `name`), entries land in two places:
|
||||
* • `<basePath>/.sf/BACKLOG.md` — human-readable summary
|
||||
* • `<basePath>/.sf/SELF-FEEDBACK.md` — human-readable summary
|
||||
* • `<basePath>/.sf/self-feedback.jsonl` — structured source of truth
|
||||
* The jsonl is what reads use. The markdown is for humans browsing the dir.
|
||||
* - For any other project, entries land in
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* third-party codebases.
|
||||
*
|
||||
* Severity → blocking semantics:
|
||||
* - low/medium: log-and-continue. Bug accumulates in the backlog.
|
||||
* - low/medium: log-and-continue. Bug accumulates in self-feedback.
|
||||
* - high: blocking — the unit that produced the report must not seal
|
||||
* successfully. On next auto session-start, getBlockedEntries()
|
||||
* returns this entry; the dispatcher checks whether sfVersion
|
||||
|
|
@ -33,6 +33,7 @@ import {
|
|||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
|
|
@ -41,8 +42,8 @@ import { sfRuntimeRoot } from "./paths.js";
|
|||
|
||||
const SF_HOME = process.env.SF_HOME || join(homedir(), ".sf");
|
||||
const UPSTREAM_LOG = join(SF_HOME, "agent", "upstream-feedback.jsonl");
|
||||
const BACKLOG_HEADER =
|
||||
"# SF Self-Feedback Backlog\n\n" +
|
||||
const SELF_FEEDBACK_HEADER =
|
||||
"# SF Self-Feedback\n\n" +
|
||||
"Anomalies caught during auto runs (by runtime detectors or via the\n" +
|
||||
"`sf_self_report` tool). Each row is a candidate work item for sf to\n" +
|
||||
"address in itself. Source-of-truth records live in `self-feedback.jsonl`.\n\n" +
|
||||
|
|
@ -93,7 +94,7 @@ export interface SelfFeedbackEntry {
|
|||
* - `kind: "agent-fix"` — agent landed a code fix. Cite commitSha and
|
||||
* optionally testPath / summaryNarrative.
|
||||
* - `kind: "human-clear"` — operator manually cleared via doctor or by
|
||||
* editing BACKLOG.md. reason should explain why.
|
||||
* editing SELF-FEEDBACK.md. reason should explain why.
|
||||
* - `kind: "promoted-to-requirement"` — entry was promoted to a REQUIREMENTS
|
||||
* row by the threshold-promotion sweeper.
|
||||
*/
|
||||
|
|
@ -176,7 +177,25 @@ function projectJsonlPath(basePath: string): string {
|
|||
}
|
||||
|
||||
function projectMarkdownPath(basePath: string): string {
|
||||
return join(sfRuntimeRoot(basePath), "BACKLOG.md");
|
||||
return join(sfRuntimeRoot(basePath), "SELF-FEEDBACK.md");
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the legacy filename. Older sf versions wrote `BACKLOG.md`; the
|
||||
* canonical name is now `SELF-FEEDBACK.md` (matches `self-feedback.jsonl`).
|
||||
* Idempotent + non-fatal: silent if the new file already exists or rename
|
||||
* fails. Called from session_start before any write/read.
|
||||
*/
|
||||
export function migrateLegacyBacklogFilename(basePath: string): void {
|
||||
try {
|
||||
const newPath = projectMarkdownPath(basePath);
|
||||
const legacyPath = join(sfRuntimeRoot(basePath), "BACKLOG.md");
|
||||
if (existsSync(legacyPath) && !existsSync(newPath)) {
|
||||
renameSync(legacyPath, newPath);
|
||||
}
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDir(path: string): void {
|
||||
|
|
@ -191,13 +210,13 @@ function appendJsonl(path: string, entry: PersistedSelfFeedbackEntry): void {
|
|||
appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf-8");
|
||||
}
|
||||
|
||||
function appendBacklogRow(
|
||||
function appendSelfFeedbackRow(
|
||||
basePath: string,
|
||||
entry: PersistedSelfFeedbackEntry,
|
||||
): void {
|
||||
const path = projectMarkdownPath(basePath);
|
||||
ensureDir(path);
|
||||
if (!existsSync(path)) writeFileSync(path, BACKLOG_HEADER, "utf-8");
|
||||
if (!existsSync(path)) writeFileSync(path, SELF_FEEDBACK_HEADER, "utf-8");
|
||||
const unit = formatUnitCell(entry.occurredIn);
|
||||
const summary = escapeCell(entry.summary);
|
||||
const blocking = entry.blocking ? "yes" : "no";
|
||||
|
|
@ -272,7 +291,7 @@ export function recordSelfFeedback(
|
|||
};
|
||||
if (persisted.repoIdentity === "forge") {
|
||||
appendJsonl(projectJsonlPath(basePath), persisted);
|
||||
appendBacklogRow(basePath, persisted);
|
||||
appendSelfFeedbackRow(basePath, persisted);
|
||||
} else {
|
||||
appendJsonl(UPSTREAM_LOG, persisted);
|
||||
}
|
||||
|
|
@ -345,7 +364,7 @@ export interface ResolutionInput {
|
|||
* naming which criteria were satisfied. (Not enforced — entries without
|
||||
* acceptanceCriteria are common during the bootstrap of this channel.)
|
||||
*
|
||||
* The corresponding BACKLOG.md row is *not* mutated — markdown is human-
|
||||
* The corresponding SELF-FEEDBACK.md row is *not* mutated — markdown is human-
|
||||
* authored space; humans can strike-through resolved rows or trim them.
|
||||
*/
|
||||
export function markResolved(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ test("pr-branch: all .sf/ files returns empty", () => {
|
|||
const files = [
|
||||
".sf/milestones/M001/ROADMAP.md",
|
||||
".sf/metrics.json",
|
||||
".sf/BACKLOG.md",
|
||||
".sf/SELF-FEEDBACK.md",
|
||||
];
|
||||
|
||||
const codeFiles = files.filter(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Unit tests for the triage-backlog agent persona.
|
||||
* Unit tests for the triage-self-feedback agent persona.
|
||||
*
|
||||
* Tests:
|
||||
* 1. loadTriageBacklogVars — returns correct shape for a tmpdir with sample feedback
|
||||
* 1. loadTriageSelfFeedbackVars — returns correct shape for a tmpdir with sample feedback
|
||||
* 2. applyTriageReport — writes new REQUIREMENTS rows and resolves entries
|
||||
* 3. Idempotency — applying the same report twice mutates only once
|
||||
* 4. Resolution evidence kind — agent-fix / human-clear, never auto-version-bump
|
||||
* 5. Prompt contract — loadPrompt("triage-backlog", vars) succeeds without throwing
|
||||
* 5. Prompt contract — loadPrompt("triage-self-feedback", vars) succeeds without throwing
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
|
|
@ -28,11 +28,11 @@ import {
|
|||
} from "../self-feedback.ts";
|
||||
import {
|
||||
applyTriageReport,
|
||||
buildTriageBacklogPrompt,
|
||||
loadTriageBacklogVars,
|
||||
buildTriageSelfFeedbackPrompt,
|
||||
loadTriageSelfFeedbackVars,
|
||||
parseTriageReport,
|
||||
type TriageReport,
|
||||
} from "../triage-backlog.ts";
|
||||
} from "../triage-self-feedback.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const promptsDir = join(__dirname, "..", "prompts");
|
||||
|
|
@ -40,7 +40,7 @@ const promptsDir = join(__dirname, "..", "prompts");
|
|||
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function makeForgeProject(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-triage-backlog-"));
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-triage-self-feedback-"));
|
||||
mkdirSync(join(root, ".sf"), { recursive: true });
|
||||
// Give it a forge identity so entries land in <root>/.sf/self-feedback.jsonl
|
||||
writeFileSync(
|
||||
|
|
@ -76,7 +76,7 @@ function sampleReport(entryId: string): TriageReport {
|
|||
whyItMatters: "Avoids silent data loss",
|
||||
source: "execution",
|
||||
validation: "unmapped",
|
||||
notes: "Surfaced by triage-backlog agent",
|
||||
notes: "Surfaced by triage-self-feedback agent",
|
||||
},
|
||||
],
|
||||
resolutions: [
|
||||
|
|
@ -98,9 +98,9 @@ afterAll(() => {
|
|||
}
|
||||
});
|
||||
|
||||
// ─── Test 1: loadTriageBacklogVars shape ──────────────────────────────────────
|
||||
// ─── Test 1: loadTriageSelfFeedbackVars shape ──────────────────────────────────────
|
||||
|
||||
test("loadTriageBacklogVars: returns correct shape for a tmpdir with sample feedback", () => {
|
||||
test("loadTriageSelfFeedbackVars: returns correct shape for a tmpdir with sample feedback", () => {
|
||||
const root = makeForgeProject();
|
||||
roots.push(root);
|
||||
|
||||
|
|
@ -124,11 +124,11 @@ test("loadTriageBacklogVars: returns correct shape for a tmpdir with sample feed
|
|||
root,
|
||||
);
|
||||
|
||||
const vars = loadTriageBacklogVars(root);
|
||||
const vars = loadTriageSelfFeedbackVars(root);
|
||||
|
||||
assert.ok(
|
||||
"forgeBacklogJson" in vars,
|
||||
"vars must have forgeBacklogJson",
|
||||
"forgeSelfFeedbackJson" in vars,
|
||||
"vars must have forgeSelfFeedbackJson",
|
||||
);
|
||||
assert.ok(
|
||||
"upstreamRollups" in vars,
|
||||
|
|
@ -143,7 +143,7 @@ test("loadTriageBacklogVars: returns correct shape for a tmpdir with sample feed
|
|||
"vars must have existingRoadmapSummary",
|
||||
);
|
||||
|
||||
const forgeEntries = JSON.parse(vars.forgeBacklogJson) as unknown[];
|
||||
const forgeEntries = JSON.parse(vars.forgeSelfFeedbackJson) as unknown[];
|
||||
assert.equal(forgeEntries.length, 2, "should have 2 forge-local entries");
|
||||
|
||||
const upstreamEntries = JSON.parse(vars.upstreamRollups) as unknown[];
|
||||
|
|
@ -363,35 +363,35 @@ test("applyTriageReport: human-clear evidence is stored correctly", () => {
|
|||
|
||||
// ─── Test 5: Prompt contract ──────────────────────────────────────────────────
|
||||
|
||||
test("loadPrompt(triage-backlog, vars) parses without throwing", () => {
|
||||
test("loadPrompt(triage-self-feedback, vars) parses without throwing", () => {
|
||||
const root = makeForgeProject();
|
||||
roots.push(root);
|
||||
|
||||
// Should not throw even with empty feedback and missing files
|
||||
assert.doesNotThrow(() => {
|
||||
buildTriageBacklogPrompt(root);
|
||||
}, "buildTriageBacklogPrompt must not throw for a valid base path");
|
||||
buildTriageSelfFeedbackPrompt(root);
|
||||
}, "buildTriageSelfFeedbackPrompt must not throw for a valid base path");
|
||||
});
|
||||
|
||||
test("triage-backlog prompt: all declared {{vars}} are satisfied by loadTriageBacklogVars", () => {
|
||||
test("triage-self-feedback prompt: all declared {{vars}} are satisfied by loadTriageSelfFeedbackVars", () => {
|
||||
const root = makeForgeProject();
|
||||
roots.push(root);
|
||||
|
||||
// Read the prompt template to check what vars it declares
|
||||
const promptPath = join(promptsDir, "triage-backlog.md");
|
||||
const promptPath = join(promptsDir, "triage-self-feedback.md");
|
||||
const promptContent = readFileSync(promptPath, "utf-8");
|
||||
const declared = promptContent.match(/\{\{[a-zA-Z][a-zA-Z0-9_]*\}\}/g) ?? [];
|
||||
const declaredKeys = [...new Set(declared.map((m) => m.slice(2, -2)))];
|
||||
|
||||
// skillActivation is injected by loadPrompt itself, not by loadTriageBacklogVars
|
||||
// skillActivation is injected by loadPrompt itself, not by loadTriageSelfFeedbackVars
|
||||
const autoInjected = new Set(["skillActivation"]);
|
||||
const vars = loadTriageBacklogVars(root);
|
||||
const vars = loadTriageSelfFeedbackVars(root);
|
||||
|
||||
for (const key of declaredKeys) {
|
||||
if (autoInjected.has(key)) continue;
|
||||
assert.ok(
|
||||
key in vars,
|
||||
`Template var {{${key}}} must be present in loadTriageBacklogVars output`,
|
||||
`Template var {{${key}}} must be present in loadTriageSelfFeedbackVars output`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -134,8 +134,8 @@ test("files a rollup when ≥3 entries of same kind from ≥2 distinct repos", (
|
|||
assert.match(rollup.summary, /3 external-repo entries/);
|
||||
assert.match(rollup.summary, /3 repos/);
|
||||
|
||||
// Rollup appears in BACKLOG.md
|
||||
const backlog = readFileSync(join(forgeDir, ".sf", "BACKLOG.md"), "utf-8");
|
||||
// Rollup appears in SELF-FEEDBACK.md
|
||||
const backlog = readFileSync(join(forgeDir, ".sf", "SELF-FEEDBACK.md"), "utf-8");
|
||||
assert.match(backlog, /upstream-rollup:runaway-guard-hard-pause/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Triage-Backlog agent persona — vars loader and report applier.
|
||||
* Triage-Self-Feedback agent persona — vars loader and report applier.
|
||||
*
|
||||
* loadTriageBacklogVars: builds the vars object for loadPrompt("triage-backlog", vars).
|
||||
* loadTriageSelfFeedbackVars: builds the vars object for loadPrompt("triage-self-feedback", vars).
|
||||
* applyTriageReport: applies a parsed triage report: writes new REQUIREMENTS rows,
|
||||
* resolves entries via markResolved. Idempotent.
|
||||
*/
|
||||
|
|
@ -68,8 +68,8 @@ export interface TriageReport {
|
|||
|
||||
// ─── Vars loader ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface TriageBacklogVars {
|
||||
forgeBacklogJson: string;
|
||||
export interface TriageSelfFeedbackVars {
|
||||
forgeSelfFeedbackJson: string;
|
||||
upstreamRollups: string;
|
||||
existingRequirementsTable: string;
|
||||
existingRoadmapSummary: string;
|
||||
|
|
@ -182,11 +182,13 @@ function buildRoadmapSummary(basePath: string): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Build the vars object for loadPrompt("triage-backlog", vars).
|
||||
* Build the vars object for loadPrompt("triage-self-feedback", vars).
|
||||
*
|
||||
* @param basePath - project root (the directory containing package.json and .sf/)
|
||||
*/
|
||||
export function loadTriageBacklogVars(basePath: string): TriageBacklogVars {
|
||||
export function loadTriageSelfFeedbackVars(
|
||||
basePath: string,
|
||||
): TriageSelfFeedbackVars {
|
||||
const allOpen = readOpenEntries(basePath);
|
||||
const forgeEntries = allOpen.filter((e) => e.repoIdentity === "forge");
|
||||
const upstreamEntries = allOpen.filter(
|
||||
|
|
@ -194,7 +196,7 @@ export function loadTriageBacklogVars(basePath: string): TriageBacklogVars {
|
|||
);
|
||||
|
||||
return {
|
||||
forgeBacklogJson: JSON.stringify(forgeEntries, null, 2),
|
||||
forgeSelfFeedbackJson: JSON.stringify(forgeEntries, null, 2),
|
||||
upstreamRollups: JSON.stringify(upstreamEntries, null, 2),
|
||||
existingRequirementsTable: readRequirementsContent(basePath),
|
||||
existingRoadmapSummary: buildRoadmapSummary(basePath),
|
||||
|
|
@ -202,12 +204,12 @@ export function loadTriageBacklogVars(basePath: string): TriageBacklogVars {
|
|||
}
|
||||
|
||||
/**
|
||||
* Build the full prompt string for a triage-backlog agent run.
|
||||
* Convenience wrapper around loadPrompt + loadTriageBacklogVars.
|
||||
* Build the full prompt string for a triage-self-feedback agent run.
|
||||
* Convenience wrapper around loadPrompt + loadTriageSelfFeedbackVars.
|
||||
*/
|
||||
export function buildTriageBacklogPrompt(basePath: string): string {
|
||||
const vars = loadTriageBacklogVars(basePath);
|
||||
return loadPrompt("triage-backlog", { ...vars });
|
||||
export function buildTriageSelfFeedbackPrompt(basePath: string): string {
|
||||
const vars = loadTriageSelfFeedbackVars(basePath);
|
||||
return loadPrompt("triage-self-feedback", { ...vars });
|
||||
}
|
||||
|
||||
// ─── Report parser ────────────────────────────────────────────────────────────
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Upstream-feedback → forge-backlog bridge.
|
||||
* Upstream-feedback → forge-self-feedback bridge.
|
||||
*
|
||||
* Rolls up recurring upstream anomalies (observed while sf runs on external
|
||||
* repos) into the forge-local self-feedback backlog so they can be triaged and
|
||||
* repos) into the forge-local self-self-feedback so they can be triaged and
|
||||
* addressed as forge-side fixes.
|
||||
*
|
||||
* Called from register-hooks.ts session_start drain (wired externally).
|
||||
|
|
@ -82,7 +82,7 @@ function maxSeverity(entries: PersistedSelfFeedbackEntry[]): SelfFeedbackSeverit
|
|||
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Roll up upstream feedback entries into the forge-local backlog.
|
||||
* Roll up upstream feedback entries into the forge-local self-feedback.
|
||||
* Only runs when basePath is the singularity-forge repo itself.
|
||||
*
|
||||
* @returns count of new rollup entries filed (0 on bail/failure)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue