sf snapshot: uncommitted changes after 93m inactivity
This commit is contained in:
parent
a73ea845e7
commit
f655188814
12 changed files with 302 additions and 19 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ Coverage thresholds (enforced by `npm run test:coverage`):
|
|||
- Lines: **40%** minimum
|
||||
- Branches: **20%** minimum
|
||||
- Functions: **20%** minimum
|
||||
- Autonomous path overrides:
|
||||
- `src/resources/extensions/sf/auto/**`: **60%** statements/lines/functions, **40%** branches
|
||||
- `src/resources/extensions/sf/uok/**`: **60%** statements/lines/functions, **40%** branches
|
||||
|
||||
These are floors, not targets. The real quality bar is purposeful tests that assert behavior contracts (see `docs/SPEC_FIRST_TDD.md`).
|
||||
|
||||
|
|
|
|||
74
docs/dev/SETUP.md
Normal file
74
docs/dev/SETUP.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Developer Setup
|
||||
|
||||
This page is the short path for contributors who already have the repository
|
||||
checked out and want a working local SF development environment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24 or newer
|
||||
- npm
|
||||
- Git
|
||||
- Rust toolchain for native engine work
|
||||
- GitHub CLI for label, issue, and PR workflows
|
||||
- Docker or a compatible container runtime for devcontainer verification
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run secret-scan:install-hook
|
||||
node scripts/tech-debt-scan.mjs
|
||||
```
|
||||
|
||||
Optional but recommended:
|
||||
|
||||
```bash
|
||||
devcontainer build --workspace-folder .
|
||||
```
|
||||
|
||||
## Daily Checks
|
||||
|
||||
Use the narrowest command that proves the change:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run typecheck:extensions
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
Before shipping changes that affect the CLI runtime:
|
||||
|
||||
```bash
|
||||
npm run build:core
|
||||
npm run test:smoke
|
||||
```
|
||||
|
||||
For coverage-sensitive changes:
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
The global floor is intentionally modest, but autonomous paths have stricter
|
||||
coverage thresholds in `vitest.config.ts`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
SF-specific environment variables are parsed through `src/env.ts` for typed
|
||||
callers. Add new `SF_*` variables there before introducing new production reads.
|
||||
Document user-facing variables in `docs/user-docs/configuration.md`.
|
||||
|
||||
Common local overrides:
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `SF_HOME` | Redirect global SF runtime state away from `~/.sf`. |
|
||||
| `SF_AGENT_DIR` | Override the managed agent directory used by headless/runtime loaders. |
|
||||
| `SF_PROJECT_ID` | Override the project state key; must be alphanumeric, hyphen, or underscore. |
|
||||
| `SF_BIN_PATH` | Point child processes at a specific SF loader. |
|
||||
|
||||
## Contribution Contract
|
||||
|
||||
Every behavioral change starts with a failing behavior test or executable
|
||||
evidence. Keep commits focused, avoid drive-by formatting, and do not commit
|
||||
transient `.sf/` runtime state.
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
# File Reference — Example Extensions
|
||||
|
||||
|
||||
All paths relative to:
|
||||
Upstream example extensions are copied into the managed agent directory during
|
||||
runtime setup. In source checkouts, use the bundled extension tree under
|
||||
`src/resources/extensions/` for SF-owned examples and tests.
|
||||
|
||||
Common reference roots:
|
||||
|
||||
```
|
||||
/Users/lexchristopherson/.nvm/versions/node/v22.20.0/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/
|
||||
src/resources/extensions/
|
||||
~/.sf/agent/extensions/
|
||||
```
|
||||
|
||||
### Lifecycle & Safety
|
||||
|
|
@ -125,8 +131,6 @@ All paths relative to:
|
|||
|
||||
---
|
||||
|
||||
*This document was generated from the Pi extension documentation and examples. Source docs are at:*
|
||||
```
|
||||
/Users/lexchristopherson/.nvm/versions/node/v22.20.0/lib/node_modules/@mariozechner/pi-coding-agent/docs/
|
||||
/Users/lexchristopherson/.nvm/versions/node/v22.20.0/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/
|
||||
```
|
||||
*This document was generated from the Pi extension documentation and examples.
|
||||
Use repository-relative paths for SF source work and `~/.sf/agent/extensions/`
|
||||
for installed runtime inspection.*
|
||||
|
|
|
|||
88
docs/dev/extending-pi/26-extension-template.md
Normal file
88
docs/dev/extending-pi/26-extension-template.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Extension Development Template
|
||||
|
||||
Use this template for small repo-local extensions before adding packaging,
|
||||
dependencies, or distribution metadata.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```text
|
||||
~/.sf/agent/extensions/my-extension/
|
||||
├── index.ts
|
||||
├── extension-manifest.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
For source-tree development inside this repository, place bundled extensions
|
||||
under `src/resources/extensions/<extension-id>/` and keep tests next to the
|
||||
extension's existing test conventions.
|
||||
|
||||
## `extension-manifest.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-extension",
|
||||
"name": "My Extension",
|
||||
"version": "0.1.0",
|
||||
"description": "Adds one focused command or tool.",
|
||||
"tier": "project",
|
||||
"entry": "index.ts",
|
||||
"provides": {
|
||||
"tools": ["my_tool"],
|
||||
"commands": ["/my-command"],
|
||||
"hooks": []
|
||||
},
|
||||
"dependencies": {
|
||||
"extensions": [],
|
||||
"runtime": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `index.ts`
|
||||
|
||||
```ts
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
/**
|
||||
* Register the extension's command and tool.
|
||||
*
|
||||
* Purpose: expose one project-specific operation through a typed tool and a
|
||||
* human-triggered command.
|
||||
*
|
||||
* Consumer: SF runtime extension loader.
|
||||
*/
|
||||
export default function activate(pi: ExtensionAPI, ctx: ExtensionContext) {
|
||||
pi.registerTool({
|
||||
name: "my_tool",
|
||||
description: "Return a concise project-specific status.",
|
||||
parameters: Type.Object({
|
||||
label: Type.String({ description: "Status label to display." }),
|
||||
}),
|
||||
async execute({ label }) {
|
||||
return { ok: true, label };
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("my-command", {
|
||||
description: "Show the current extension status.",
|
||||
async run() {
|
||||
ctx.ui.notify("My extension is loaded.", "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- The extension has one clear purpose and a named production consumer.
|
||||
- Every tool parameter has a TypeBox schema.
|
||||
- Persistent state is written under the project `.sf/` tree or the managed
|
||||
agent directory, not arbitrary home-directory paths.
|
||||
- Commands and tools degrade with useful messages when dependencies are missing.
|
||||
- Tests cover the behavior contract, not only mocks or call counts.
|
||||
- User-facing environment variables are added to `src/env.ts` and
|
||||
`docs/user-docs/configuration.md`.
|
||||
|
|
@ -29,8 +29,8 @@
|
|||
- [23. File Reference — Documentation](./23-file-reference-documentation.md)
|
||||
- [24. File Reference — Example Extensions](./24-file-reference-example-extensions.md)
|
||||
- [25. Slash Command Subcommand Patterns](./25-slash-command-subcommand-patterns.md)
|
||||
- [26. Extension Development Template](./26-extension-template.md)
|
||||
|
||||
---
|
||||
|
||||
*Split into per-section files for surgical context loading.*
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { getSfEnv } from "./env.js";
|
||||
|
||||
/**
|
||||
* app-paths.ts — central directory and file path constants for the sf runtime.
|
||||
|
|
@ -26,7 +26,7 @@ import { join } from "node:path";
|
|||
* remote-questions-config.ts (global preferences), extension-registry.ts
|
||||
* (registry JSON), and every other derived path in this module.
|
||||
*/
|
||||
export const appRoot = process.env.SF_HOME || join(homedir(), ".sf");
|
||||
export const appRoot = getSfEnv().sfHome;
|
||||
|
||||
/**
|
||||
* Returns the path to the managed agent directory.
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {
|
|||
unwatchFile,
|
||||
watchFile,
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { getSfEnv } from "./env.js";
|
||||
|
||||
export type LogSourceName = "notif" | "session" | "activity" | "audit";
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ function normalizeSource(value: string | undefined): LogSourceName | undefined {
|
|||
}
|
||||
|
||||
function sfHomeFromEnv(): string {
|
||||
return process.env.SF_HOME || join(homedir(), ".sf");
|
||||
return getSfEnv().sfHome;
|
||||
}
|
||||
|
||||
export function getProjectSessionKey(basePath: string): string {
|
||||
|
|
|
|||
65
src/env.ts
Normal file
65
src/env.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
const optionalNonEmptyString = z.string().trim().min(1).optional();
|
||||
|
||||
const booleanOneZero = z
|
||||
.enum(["0", "1"])
|
||||
.optional()
|
||||
.transform((value) => value === "1");
|
||||
|
||||
export const sfEnvSchema = z.object({
|
||||
SF_HOME: optionalNonEmptyString,
|
||||
SF_AGENT_DIR: optionalNonEmptyString,
|
||||
SF_CODING_AGENT_DIR: optionalNonEmptyString,
|
||||
SF_STATE_DIR: optionalNonEmptyString,
|
||||
SF_PROJECT_ID: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[A-Za-z0-9_-]+$/, {
|
||||
message:
|
||||
"SF_PROJECT_ID must contain only letters, numbers, hyphens, and underscores",
|
||||
})
|
||||
.optional(),
|
||||
SF_BIN_PATH: optionalNonEmptyString,
|
||||
SF_VERSION: optionalNonEmptyString,
|
||||
SF_WEB_PROJECT_CWD: optionalNonEmptyString,
|
||||
SF_WEB_DAEMON_MODE: booleanOneZero,
|
||||
});
|
||||
|
||||
export type SfEnv = z.infer<typeof sfEnvSchema>;
|
||||
|
||||
/**
|
||||
* Parse supported SF_* environment variables into a typed object.
|
||||
*
|
||||
* Purpose: give runtime code a shared contract for SF-specific environment
|
||||
* variables instead of scattering ad hoc `process.env` parsing across entry
|
||||
* points.
|
||||
*
|
||||
* Consumer: root CLI/headless modules and web bridge code that need stable SF
|
||||
* path and mode values.
|
||||
*/
|
||||
export function parseSfEnv(env: NodeJS.ProcessEnv = process.env): SfEnv {
|
||||
return sfEnvSchema.parse(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return typed SF environment values with path defaults applied.
|
||||
*
|
||||
* Purpose: centralize default path behavior for SF_HOME and the managed agent
|
||||
* directory while still validating user-provided overrides.
|
||||
*
|
||||
* Consumer: app-paths.ts, cli-logs.ts, headless-query.ts, and future env readers.
|
||||
*/
|
||||
export function getSfEnv(env: NodeJS.ProcessEnv = process.env) {
|
||||
const parsed = parseSfEnv(env);
|
||||
const sfHome = parsed.SF_HOME ?? join(homedir(), ".sf");
|
||||
const agentDir =
|
||||
parsed.SF_AGENT_DIR ?? parsed.SF_CODING_AGENT_DIR ?? join(sfHome, "agent");
|
||||
return {
|
||||
...parsed,
|
||||
sfHome,
|
||||
agentDir,
|
||||
};
|
||||
}
|
||||
|
|
@ -16,10 +16,10 @@
|
|||
*/
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createJiti } from "@mariozechner/jiti";
|
||||
import { resolveBundledSourceResource } from "./bundled-resource-path.js";
|
||||
import { getSfEnv } from "./env.js";
|
||||
import type { SFState } from "./resources/extensions/sf/types.js";
|
||||
|
||||
const jiti = createJiti(import.meta.filename, {
|
||||
|
|
@ -29,11 +29,7 @@ const jiti = createJiti(import.meta.filename, {
|
|||
// Resolve extensions from the synced agent directory so headless-query
|
||||
// loads the same extension copy as interactive/auto modes (#3471).
|
||||
// The synced runtime is compiled .js; source-tree fallback is .ts.
|
||||
const agentExtensionsDir = join(
|
||||
process.env.SF_AGENT_DIR || join(homedir(), ".sf", "agent"),
|
||||
"extensions",
|
||||
"sf",
|
||||
);
|
||||
const agentExtensionsDir = join(getSfEnv().agentDir, "extensions", "sf");
|
||||
const useAgentDir = existsSync(join(agentExtensionsDir, "state.js"));
|
||||
const sfExtensionPath = (moduleName: string) => {
|
||||
if (useAgentDir) return join(agentExtensionsDir, `${moduleName}.js`);
|
||||
|
|
|
|||
41
src/tests/env.test.ts
Normal file
41
src/tests/env.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, test } from "vitest";
|
||||
import { getSfEnv, parseSfEnv } from "../env.js";
|
||||
|
||||
describe("sf env schema", () => {
|
||||
test("parseSfEnv_when_project_id_is_valid_returns_typed_values", () => {
|
||||
const env = parseSfEnv({
|
||||
SF_HOME: "/tmp/sf-home",
|
||||
SF_PROJECT_ID: "repo_123-main",
|
||||
SF_WEB_DAEMON_MODE: "1",
|
||||
});
|
||||
|
||||
assert.equal(env.SF_HOME, "/tmp/sf-home");
|
||||
assert.equal(env.SF_PROJECT_ID, "repo_123-main");
|
||||
assert.equal(env.SF_WEB_DAEMON_MODE, true);
|
||||
});
|
||||
|
||||
test("parseSfEnv_when_project_id_contains_path_separator_rejects", () => {
|
||||
assert.throws(
|
||||
() => parseSfEnv({ SF_PROJECT_ID: "../escape" }),
|
||||
/SF_PROJECT_ID/,
|
||||
);
|
||||
});
|
||||
|
||||
test("getSfEnv_when_agent_dir_unset_uses_sf_home_agent", () => {
|
||||
const env = getSfEnv({ SF_HOME: "/tmp/sf-home" });
|
||||
|
||||
assert.equal(env.sfHome, "/tmp/sf-home");
|
||||
assert.equal(env.agentDir, "/tmp/sf-home/agent");
|
||||
});
|
||||
|
||||
test("getSfEnv_when_agent_dir_set_prefers_explicit_agent_dir", () => {
|
||||
const env = getSfEnv({
|
||||
SF_HOME: "/tmp/sf-home",
|
||||
SF_AGENT_DIR: "/tmp/agent-dir",
|
||||
SF_CODING_AGENT_DIR: "/tmp/coding-agent-dir",
|
||||
});
|
||||
|
||||
assert.equal(env.agentDir, "/tmp/agent-dir");
|
||||
});
|
||||
});
|
||||
|
|
@ -225,6 +225,18 @@ export default defineConfig({
|
|||
lines: 40,
|
||||
branches: 20,
|
||||
functions: 20,
|
||||
"src/resources/extensions/sf/auto/**": {
|
||||
statements: 60,
|
||||
lines: 60,
|
||||
branches: 40,
|
||||
functions: 60,
|
||||
},
|
||||
"src/resources/extensions/sf/uok/**": {
|
||||
statements: 60,
|
||||
lines: 60,
|
||||
branches: 40,
|
||||
functions: 60,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue