feat: add FEATURES.md capability map and generator
Human-oriented documentation of SF capabilities, with a script that keeps it in sync with workflow-tools.ts and extension manifests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
25797129e2
commit
efd5e14e0a
3 changed files with 640 additions and 0 deletions
445
FEATURES.md
Normal file
445
FEATURES.md
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
# FEATURES
|
||||
|
||||
This file is the human-oriented capability map for Singularity Foundry.
|
||||
|
||||
It is intentionally not the source of truth for schemas or tool parameters. Use it to answer:
|
||||
|
||||
- what SF can do today
|
||||
- which surfaces are first-class versus experimental
|
||||
- where a capability lives in the system
|
||||
|
||||
For exact contracts, use:
|
||||
|
||||
- `README.md` for product positioning and user docs
|
||||
- `packages/mcp-server/src/workflow-tools.ts` for workflow tool schemas
|
||||
- `src/resources/extensions/sf/` for planning/state-machine behavior
|
||||
- `src/resources/extensions/*/extension-manifest.json` for extension inventory
|
||||
- `packages/pi-ai/src/` for provider and model registry behavior
|
||||
|
||||
## Core Product Shape
|
||||
|
||||
SF is a coding-agent application built around:
|
||||
|
||||
- a milestone → slice → task planning hierarchy
|
||||
- a DB-backed workflow state machine
|
||||
- MCP-accessible workflow mutations and readers
|
||||
- extension-based capability loading
|
||||
- multi-provider model routing
|
||||
- interactive and autonomous execution modes
|
||||
|
||||
The core planning/runtime loop is:
|
||||
|
||||
1. discuss / research / align
|
||||
2. plan milestone
|
||||
3. plan slice
|
||||
4. execute task-by-task
|
||||
5. verify gates
|
||||
6. summarize and validate
|
||||
7. reassess roadmap and continue
|
||||
|
||||
## Planning And Ceremony Capabilities
|
||||
|
||||
### Milestone planning
|
||||
|
||||
SF supports milestone plans with:
|
||||
|
||||
- milestone title, vision, and slice breakdown
|
||||
- success criteria and definition of done
|
||||
- key risks and proof strategy
|
||||
- verification contract, integration, operational, and UAT sections
|
||||
- requirement coverage and boundary-map support
|
||||
|
||||
### Vision meeting
|
||||
|
||||
Milestones can carry a weighted `visionMeeting` that captures:
|
||||
|
||||
- `pm`
|
||||
- `userAdvocate`
|
||||
- `customerPanel`
|
||||
- `business`
|
||||
- `researcher`
|
||||
- `deliveryLead`
|
||||
- `partner`
|
||||
- `combatant`
|
||||
- `architect`
|
||||
- `moderator`
|
||||
- weighted synthesis
|
||||
- confidence by area
|
||||
- recommended route: `discussing`, `researching`, or `planning`
|
||||
|
||||
This is the top-level roadmap/vision alignment ceremony.
|
||||
|
||||
### Slice planning
|
||||
|
||||
Slices support:
|
||||
|
||||
- goal
|
||||
- success criteria
|
||||
- proof level
|
||||
- integration closure
|
||||
- observability impact
|
||||
- ordered task plans with expected files, verification, inputs, outputs
|
||||
|
||||
### Adversarial review
|
||||
|
||||
Slice planning supports first-class adversarial review with:
|
||||
|
||||
- `partner`
|
||||
- `combatant`
|
||||
- `architect`
|
||||
|
||||
This is treated as required planning structure, not commentary.
|
||||
|
||||
### Planning meeting
|
||||
|
||||
Slices also support a structured planning meeting with:
|
||||
|
||||
- trigger
|
||||
- `pm`
|
||||
- `researcher`
|
||||
- `partner`
|
||||
- `combatant`
|
||||
- `architect`
|
||||
- `moderator`
|
||||
- recommended route
|
||||
- confidence summary
|
||||
|
||||
This is the narrower execution-readiness ceremony.
|
||||
|
||||
### Replanning
|
||||
|
||||
When a blocker invalidates a slice plan, SF supports slice replanning with:
|
||||
|
||||
- blocker task + blocker description
|
||||
- what changed
|
||||
- updated tasks
|
||||
- removed tasks
|
||||
- updated slice-level planning fields
|
||||
- updated adversarial review
|
||||
- updated planning meeting
|
||||
|
||||
Replan state is preserved in DB and re-rendered into plan artifacts.
|
||||
|
||||
## Workflow State Machine
|
||||
|
||||
The SF workflow engine derives and enforces states including:
|
||||
|
||||
- `pre-planning`
|
||||
- `needs-discussion`
|
||||
- `planning`
|
||||
- `evaluating-gates`
|
||||
- `executing`
|
||||
- `summarizing`
|
||||
- `validating-milestone`
|
||||
- `completing-milestone`
|
||||
- `replanning-slice`
|
||||
- `complete`
|
||||
- `blocked`
|
||||
|
||||
Important properties:
|
||||
|
||||
- execution readiness is gated by artifact completeness, not just file existence
|
||||
- meeting/ceremony data participates in readiness
|
||||
- blocked/dependency-aware progression is built in
|
||||
- routed-back plans stay in planning instead of pretending to be ready
|
||||
|
||||
## Artifact And Persistence Capabilities
|
||||
|
||||
SF persists workflow state in multiple synchronized forms:
|
||||
|
||||
- SQLite DB (`.sf/sf.db`)
|
||||
- markdown planning artifacts
|
||||
- state manifest snapshots
|
||||
- worktree DB reconciliation state
|
||||
- workflow events
|
||||
|
||||
Planning/ceremony state now survives across:
|
||||
|
||||
- DB writes
|
||||
- markdown rendering
|
||||
- pure projection rendering
|
||||
- manifest export / restore
|
||||
- worktree reconciliation
|
||||
- state derivation and execution gating
|
||||
- slice replanning
|
||||
|
||||
## Execution Capabilities
|
||||
|
||||
SF can execute work in:
|
||||
|
||||
- interactive mode
|
||||
- headless mode
|
||||
- auto mode
|
||||
- parallel / multi-worker orchestration
|
||||
|
||||
Execution-related features include:
|
||||
|
||||
- task-sized dispatch units
|
||||
- crash recovery and lock-aware state
|
||||
- timeout supervision
|
||||
- worktree isolation
|
||||
- per-unit summaries and milestone completion flow
|
||||
- roadmap reassessment after completed slices
|
||||
|
||||
## MCP And Workflow Tooling
|
||||
|
||||
The workflow layer is exposed over MCP, including mutation/read paths for:
|
||||
|
||||
- milestone planning
|
||||
- slice planning
|
||||
- slice replanning
|
||||
- task completion
|
||||
- slice completion
|
||||
- milestone validation
|
||||
- milestone completion
|
||||
- roadmap reassessment
|
||||
- gate results
|
||||
- summary save/read flows
|
||||
|
||||
This makes SF usable from external clients without relying on slash-command prompt tricks.
|
||||
|
||||
## Search And Research Capabilities
|
||||
|
||||
SF has dedicated web/research support via onboarding, auth storage, and extension flows.
|
||||
|
||||
Currently supported first-class web-search providers include:
|
||||
|
||||
- `brave`
|
||||
- `tavily`
|
||||
- `serper`
|
||||
- `exa`
|
||||
|
||||
Other search/research surfaces include:
|
||||
|
||||
- Ollama native web search / fetch integration
|
||||
- Google search extension
|
||||
- Context7 extension for library/documentation retrieval
|
||||
- Jina-backed content extraction paths where configured
|
||||
|
||||
The search stack is available to automatic workflows, not only slash commands.
|
||||
|
||||
## Subagents And Background Work
|
||||
|
||||
SF includes subagent capabilities via the `subagent` extension, including:
|
||||
|
||||
- delegated agent runs
|
||||
- background subagent jobs
|
||||
- await/join behavior
|
||||
- cancellation
|
||||
- workflow-driven use rather than only interactive commands
|
||||
|
||||
This is useful for automatic coding flows and wave-based task execution.
|
||||
|
||||
## Extension Inventory
|
||||
|
||||
Bundled extension families currently include:
|
||||
|
||||
- `sf` — workflow engine, planning/state/artifacts
|
||||
- `search-the-web`
|
||||
- `subagent`
|
||||
- `async-jobs`
|
||||
- `bg-shell`
|
||||
- `browser-tools`
|
||||
- `context7`
|
||||
- `genai-proxy`
|
||||
- `google-search`
|
||||
- `ollama`
|
||||
- `remote-questions`
|
||||
- `slash-commands`
|
||||
- `mac-tools`
|
||||
- `ttsr`
|
||||
- `universal-config`
|
||||
- `voice`
|
||||
|
||||
These are not all equal in product importance, but they are real shipped extension surfaces.
|
||||
|
||||
## Model And Provider Capabilities
|
||||
|
||||
SF supports multi-provider model routing across built-in and custom providers.
|
||||
|
||||
Notable supported/known providers in the current runtime and registry surface include:
|
||||
|
||||
- `anthropic`
|
||||
- `anthropic-vertex`
|
||||
- `openai`
|
||||
- `azure-openai-responses`
|
||||
- `openai-codex`
|
||||
- `google`
|
||||
- `google-gemini-cli`
|
||||
- `google-vertex`
|
||||
- `mistral`
|
||||
- `amazon-bedrock`
|
||||
- `ollama`
|
||||
- `ollama-cloud`
|
||||
- `openrouter`
|
||||
- `groq`
|
||||
- `xai`
|
||||
- `github-copilot`
|
||||
- `zai`
|
||||
- `minimax`
|
||||
- `minimax-cn`
|
||||
- `kimi-coding`
|
||||
- `xiaomi`
|
||||
- `custom-openai`
|
||||
|
||||
Recent/custom provider support in this tree also includes:
|
||||
|
||||
- `zai` / GLM-family routing
|
||||
- `xiaomi` / MiMo Anthropic-compatible endpoint
|
||||
- `kimi-coding` / dedicated coding endpoint
|
||||
- `minimax` Anthropic-compatible support
|
||||
|
||||
## Onboarding And Auth
|
||||
|
||||
Onboarding currently supports:
|
||||
|
||||
- LLM provider selection
|
||||
- OAuth or API-key based provider setup where applicable
|
||||
- local Ollama detection
|
||||
- web-search provider setup
|
||||
- remote questions setup
|
||||
- tool-key collection for selected extensions
|
||||
|
||||
This is a real product capability, not just a doc path.
|
||||
|
||||
## Recovery, Reliability, And Operational Features
|
||||
|
||||
SF includes real operational hardening around:
|
||||
|
||||
- manifest bootstrapping and restore
|
||||
- worktree/DB reconciliation
|
||||
- cache invalidation around plan parsing
|
||||
- atomic writes and TOCTOU protection
|
||||
- gate-aware progression
|
||||
- idle/timeout handling
|
||||
- scoped recovery for auto mode
|
||||
|
||||
## UI And Interaction Surfaces
|
||||
|
||||
SF is not only a CLI. The repo also carries:
|
||||
|
||||
- TUI support
|
||||
- web interface support
|
||||
- VS Code extension support
|
||||
- MCP server support
|
||||
|
||||
So the product surface is broader than “terminal prompt framework.”
|
||||
|
||||
## What This File Does Not Try To Be
|
||||
|
||||
This file does not list:
|
||||
|
||||
- every MCP tool parameter
|
||||
- every extension command
|
||||
- every model ID
|
||||
- every preference flag
|
||||
- every internal DB column
|
||||
|
||||
Those should stay close to code or generated inventories.
|
||||
|
||||
## Generated Inventory
|
||||
|
||||
The section below is generated from source declarations so this overview can stay concise while exact inventories remain refreshable.
|
||||
|
||||
<!-- GENERATED_FEATURE_INVENTORY_START -->
|
||||
|
||||
### Workflow Tools
|
||||
|
||||
Generated from `packages/mcp-server/src/workflow-tools.ts`.
|
||||
|
||||
- `sf_complete_milestone`
|
||||
- `sf_complete_slice`
|
||||
- `sf_complete_task`
|
||||
- `sf_decision_save`
|
||||
- `sf_generate_milestone_id`
|
||||
- `sf_journal_query`
|
||||
- `sf_milestone_complete`
|
||||
- `sf_milestone_generate_id`
|
||||
- `sf_milestone_status`
|
||||
- `sf_milestone_validate`
|
||||
- `sf_plan_milestone`
|
||||
- `sf_plan_slice`
|
||||
- `sf_plan_task`
|
||||
- `sf_reassess_roadmap`
|
||||
- `sf_replan_slice`
|
||||
- `sf_requirement_save`
|
||||
- `sf_requirement_update`
|
||||
- `sf_roadmap_reassess`
|
||||
- `sf_save_decision`
|
||||
- `sf_save_gate_result`
|
||||
- `sf_save_requirement`
|
||||
- `sf_skip_slice`
|
||||
- `sf_slice_complete`
|
||||
- `sf_slice_replan`
|
||||
- `sf_summary_save`
|
||||
- `sf_task_complete`
|
||||
- `sf_task_plan`
|
||||
- `sf_update_requirement`
|
||||
- `sf_validate_milestone`
|
||||
|
||||
### Bundled Extensions
|
||||
|
||||
Generated from `src/resources/extensions/*/extension-manifest.json`.
|
||||
|
||||
- `async-jobs` — [extension-manifest.json](src/resources/extensions/async-jobs/extension-manifest.json)
|
||||
- `bg-shell` — [extension-manifest.json](src/resources/extensions/bg-shell/extension-manifest.json)
|
||||
- `browser-tools` — [extension-manifest.json](src/resources/extensions/browser-tools/extension-manifest.json)
|
||||
- `context7` — [extension-manifest.json](src/resources/extensions/context7/extension-manifest.json)
|
||||
- `genai-proxy` — [extension-manifest.json](src/resources/extensions/genai-proxy/extension-manifest.json)
|
||||
- `google-search` — [extension-manifest.json](src/resources/extensions/google-search/extension-manifest.json)
|
||||
- `mac-tools` — [extension-manifest.json](src/resources/extensions/mac-tools/extension-manifest.json)
|
||||
- `ollama` — [extension-manifest.json](src/resources/extensions/ollama/extension-manifest.json)
|
||||
- `remote-questions` — [extension-manifest.json](src/resources/extensions/remote-questions/extension-manifest.json)
|
||||
- `search-the-web` — [extension-manifest.json](src/resources/extensions/search-the-web/extension-manifest.json)
|
||||
- `sf` — [extension-manifest.json](src/resources/extensions/sf/extension-manifest.json)
|
||||
- `slash-commands` — [extension-manifest.json](src/resources/extensions/slash-commands/extension-manifest.json)
|
||||
- `subagent` — [extension-manifest.json](src/resources/extensions/subagent/extension-manifest.json)
|
||||
- `ttsr` — [extension-manifest.json](src/resources/extensions/ttsr/extension-manifest.json)
|
||||
- `universal-config` — [extension-manifest.json](src/resources/extensions/universal-config/extension-manifest.json)
|
||||
- `voice` — [extension-manifest.json](src/resources/extensions/voice/extension-manifest.json)
|
||||
|
||||
### Search Providers
|
||||
|
||||
Generated from the `search-the-web` extension provider declarations.
|
||||
|
||||
- `brave`
|
||||
- `exa`
|
||||
- `ollama`
|
||||
- `serper`
|
||||
- `tavily`
|
||||
|
||||
### Known Model Providers
|
||||
|
||||
Generated from `packages/pi-ai/src/types.ts` (`KnownProvider`).
|
||||
|
||||
- `alibaba-coding-plan`
|
||||
- `alibaba-dashscope`
|
||||
- `amazon-bedrock`
|
||||
- `anthropic`
|
||||
- `anthropic-vertex`
|
||||
- `azure-openai-responses`
|
||||
- `cerebras`
|
||||
- `github-copilot`
|
||||
- `google`
|
||||
- `google-gemini-cli`
|
||||
- `google-vertex`
|
||||
- `groq`
|
||||
- `huggingface`
|
||||
- `kimi-coding`
|
||||
- `longcat`
|
||||
- `minimax`
|
||||
- `minimax-cn`
|
||||
- `mistral`
|
||||
- `ollama`
|
||||
- `ollama-cloud`
|
||||
- `openai`
|
||||
- `openai-codex`
|
||||
- `opencode`
|
||||
- `opencode-go`
|
||||
- `openrouter`
|
||||
- `vercel-ai-gateway`
|
||||
- `xai`
|
||||
- `xiaomi`
|
||||
- `zai`
|
||||
|
||||
<!-- GENERATED_FEATURE_INVENTORY_END -->
|
||||
127
scripts/generate-features-inventory.mjs
Normal file
127
scripts/generate-features-inventory.mjs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
|
||||
const featuresPath = join(repoRoot, "FEATURES.md");
|
||||
const workflowToolsPath = join(repoRoot, "packages", "mcp-server", "src", "workflow-tools.ts");
|
||||
const providersPath = join(repoRoot, "packages", "pi-ai", "src", "types.ts");
|
||||
const extensionsRoot = join(repoRoot, "src", "resources", "extensions");
|
||||
const searchToolPath = join(repoRoot, "src", "resources", "extensions", "search-the-web", "tool-search.ts");
|
||||
|
||||
export const START = "<!-- GENERATED_FEATURE_INVENTORY_START -->";
|
||||
export const END = "<!-- GENERATED_FEATURE_INVENTORY_END -->";
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function parseWorkflowToolNames() {
|
||||
const src = readFileSync(workflowToolsPath, "utf8");
|
||||
const matches = [...src.matchAll(/server\.tool\(\s*"([^"]+)"/g)].map((m) => m[1]);
|
||||
return uniqueSorted(matches);
|
||||
}
|
||||
|
||||
export function parseKnownProviders() {
|
||||
const src = readFileSync(providersPath, "utf8");
|
||||
const match = src.match(/export type KnownProvider =([\s\S]*?);/);
|
||||
if (!match) throw new Error("Could not find KnownProvider in packages/pi-ai/src/types.ts");
|
||||
const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
||||
return uniqueSorted(providers);
|
||||
}
|
||||
|
||||
export function parseBundledExtensions() {
|
||||
const entries = readdirSync(extensionsRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => {
|
||||
try {
|
||||
const manifestPath = join(extensionsRoot, name, "extension-manifest.json");
|
||||
readFileSync(manifestPath, "utf8");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return uniqueSorted(entries);
|
||||
}
|
||||
|
||||
export function parseSearchProviders() {
|
||||
const src = readFileSync(searchToolPath, "utf8");
|
||||
const providers = [
|
||||
...src.matchAll(/providers\.push\('([^']+)'\)/g),
|
||||
...src.matchAll(/provider\?: '([^']+)'/g),
|
||||
]
|
||||
.map((m) => m[1])
|
||||
.filter((p) => p !== "combosearch");
|
||||
return uniqueSorted(providers);
|
||||
}
|
||||
|
||||
function formatBullets(values, formatter = (value) => `- \`${value}\``) {
|
||||
return values.map((value) => formatter(value)).join("\n");
|
||||
}
|
||||
|
||||
export function buildSection() {
|
||||
const workflowTools = parseWorkflowToolNames();
|
||||
const extensions = parseBundledExtensions();
|
||||
const searchProviders = parseSearchProviders();
|
||||
const knownProviders = parseKnownProviders();
|
||||
|
||||
return [
|
||||
"### Workflow Tools",
|
||||
"",
|
||||
"Generated from `packages/mcp-server/src/workflow-tools.ts`.",
|
||||
"",
|
||||
formatBullets(workflowTools),
|
||||
"",
|
||||
"### Bundled Extensions",
|
||||
"",
|
||||
"Generated from `src/resources/extensions/*/extension-manifest.json`.",
|
||||
"",
|
||||
formatBullets(
|
||||
extensions,
|
||||
(value) => `- \`${value}\` — [extension-manifest.json](${relative(repoRoot, join(extensionsRoot, value, "extension-manifest.json"))})`,
|
||||
),
|
||||
"",
|
||||
"### Search Providers",
|
||||
"",
|
||||
"Generated from the `search-the-web` extension provider declarations.",
|
||||
"",
|
||||
formatBullets(searchProviders),
|
||||
"",
|
||||
"### Known Model Providers",
|
||||
"",
|
||||
"Generated from `packages/pi-ai/src/types.ts` (`KnownProvider`).",
|
||||
"",
|
||||
formatBullets(knownProviders),
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function updateFeaturesContent(features) {
|
||||
const startIndex = features.indexOf(START);
|
||||
const endIndex = features.indexOf(END);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
||||
throw new Error("FEATURES.md is missing generated inventory markers");
|
||||
}
|
||||
|
||||
const before = features.slice(0, startIndex + START.length);
|
||||
const after = features.slice(endIndex);
|
||||
const section = `\n\n${buildSection()}`;
|
||||
return `${before}${section}\n${after}`;
|
||||
}
|
||||
|
||||
export function main() {
|
||||
const features = readFileSync(featuresPath, "utf8");
|
||||
const updated = updateFeaturesContent(features);
|
||||
writeFileSync(featuresPath, updated);
|
||||
process.stdout.write(`Updated ${relative(repoRoot, featuresPath)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === __filename) {
|
||||
main();
|
||||
}
|
||||
68
src/tests/features-inventory-generator.test.ts
Normal file
68
src/tests/features-inventory-generator.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
START,
|
||||
END,
|
||||
buildSection,
|
||||
parseBundledExtensions,
|
||||
parseKnownProviders,
|
||||
parseSearchProviders,
|
||||
parseWorkflowToolNames,
|
||||
updateFeaturesContent,
|
||||
} from "../../scripts/generate-features-inventory.mjs";
|
||||
|
||||
test("features inventory generator surfaces expected workflow tool, extension, search, and provider inventories", () => {
|
||||
const workflowTools = parseWorkflowToolNames();
|
||||
const extensions = parseBundledExtensions();
|
||||
const searchProviders = parseSearchProviders();
|
||||
const knownProviders = parseKnownProviders();
|
||||
|
||||
assert.ok(workflowTools.includes("sf_plan_milestone"));
|
||||
assert.ok(workflowTools.includes("sf_replan_slice"));
|
||||
assert.ok(workflowTools.includes("sf_task_complete"));
|
||||
|
||||
assert.ok(extensions.includes("sf"));
|
||||
assert.ok(extensions.includes("search-the-web"));
|
||||
assert.ok(extensions.includes("subagent"));
|
||||
|
||||
assert.deepEqual(searchProviders, ["brave", "exa", "ollama", "serper", "tavily"]);
|
||||
|
||||
assert.ok(knownProviders.includes("mistral"));
|
||||
assert.ok(knownProviders.includes("zai"));
|
||||
assert.ok(knownProviders.includes("kimi-coding"));
|
||||
assert.ok(knownProviders.includes("xiaomi"));
|
||||
});
|
||||
|
||||
test("features inventory generator injects a rendered appendix between markers", () => {
|
||||
const original = [
|
||||
"# FEATURES",
|
||||
"",
|
||||
"Intro text.",
|
||||
"",
|
||||
START,
|
||||
END,
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const updated = updateFeaturesContent(original);
|
||||
const generated = buildSection();
|
||||
|
||||
assert.match(updated, new RegExp(`${START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n\\n### Workflow Tools`));
|
||||
assert.match(updated, /### Bundled Extensions/);
|
||||
assert.match(updated, /### Search Providers/);
|
||||
assert.match(updated, /### Known Model Providers/);
|
||||
assert.match(updated, /- `sf_plan_milestone`/);
|
||||
assert.match(updated, /- `search-the-web` — \[extension-manifest\.json]/);
|
||||
assert.match(updated, /- `brave`/);
|
||||
assert.match(updated, /- `xiaomi`/);
|
||||
assert.ok(updated.includes(generated));
|
||||
assert.ok(updated.includes(END));
|
||||
});
|
||||
|
||||
test("features inventory generator fails closed when markers are missing", () => {
|
||||
assert.throws(
|
||||
() => updateFeaturesContent("# FEATURES\n\nNo markers here.\n"),
|
||||
/FEATURES\.md is missing generated inventory markers/,
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue