diff --git a/.pi/extensions/sce/index.ts b/.pi/extensions/sce/index.ts new file mode 100644 index 00000000..25770ff8 --- /dev/null +++ b/.pi/extensions/sce/index.ts @@ -0,0 +1,455 @@ +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { + dirname, + isAbsolute, + join, + relative, + resolve as resolvePath, +} from "node:path"; +import { + type ExtensionAPI, + isToolCallEventType, +} from "@earendil-works/pi-coding-agent"; + +interface JsonPolicyResult { + status: string; + decision: string; + command: string; + normalized_argv?: string[]; + reason?: string; + policy_id?: string; +} + +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + +type ConversationTraceMessageItem = { + type: "message"; + session_id: string; + message_id: string; + role: "user" | "assistant"; + generated_at_unix_ms: number; +}; + +type ConversationTraceMessagePartItem = { + type: "message.part"; + session_id: string; + message_id: string; + part_type: "text" | "reasoning" | "patch"; + text: string; + generated_at_unix_ms: number; +}; + +type ConversationTraceItem = + | ConversationTraceMessageItem + | ConversationTraceMessagePartItem; + +type ConversationTracePayload = { + payloads: ConversationTraceItem[]; +}; + +type DiffTracePayload = { + sessionID: string; + diff: string; + time: number; + model_id: string | null; + tool_name: "pi"; + tool_version: string | null; +}; + +type PendingFileMutation = { + absolutePath: string; + diffLabel: string; + before: string | undefined; +}; + +/** + * Evaluate a bash command against SCE bash-tool policy by delegating to the + * Rust `sce policy bash` command. Returns the parsed JSON result, or null if + * the policy check could not be performed (fail-open). + */ +function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { + try { + const result = spawnSync( + "sce", + ["policy", "bash", "--input", "normalized", "--output", "json"], + { + input: JSON.stringify({ command }), + encoding: "utf8", + timeout: 10_000, + }, + ); + + if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + return null; + } + + if (result.status !== 0) { + return null; + } + + const stdout = result.stdout?.trim(); + if (!stdout) { + return null; + } + + const parsed: JsonPolicyResult = JSON.parse(stdout); + return parsed; + } catch { + return null; + } +} + +/** + * Send a conversation-trace payload to `sce hooks conversation-trace`, + * fire-and-forget. Fail-open: stderr is ignored so that sce intake errors do + * not leak into the Pi TUI, and the returned promise never rejects. + */ +function runConversationTraceHook( + cwd: string, + payload: ConversationTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "conversation-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +type MessageContentBlock = { + type: string; + text?: unknown; + thinking?: unknown; +}; + +function extractMessageParts( + content: string | readonly MessageContentBlock[], +): Array<{ part_type: "text" | "reasoning"; text: string }> { + if (typeof content === "string") { + return content.length > 0 ? [{ part_type: "text", text: content }] : []; + } + + const parts: Array<{ part_type: "text" | "reasoning"; text: string }> = []; + for (const block of content) { + if (block.type === "text" && typeof block.text === "string" && block.text) { + parts.push({ part_type: "text", text: block.text }); + } else if ( + block.type === "thinking" && + typeof block.thinking === "string" && + block.thinking + ) { + parts.push({ part_type: "reasoning", text: block.thinking }); + } + } + return parts; +} + +function buildMessageEndConversationTracePayload( + sessionId: string, + message: { + role: string; + content: string | readonly MessageContentBlock[]; + responseId?: string; + }, +): ConversationTracePayload | undefined { + if (message.role !== "user" && message.role !== "assistant") { + return undefined; + } + + const messageId = message.responseId ?? randomUUID(); + const generatedAtUnixMs = Date.now(); + + const payloads: ConversationTraceItem[] = [ + { + type: "message", + session_id: sessionId, + message_id: messageId, + role: message.role, + generated_at_unix_ms: generatedAtUnixMs, + }, + ]; + + for (const part of extractMessageParts(message.content)) { + payloads.push({ + type: "message.part", + session_id: sessionId, + message_id: messageId, + part_type: part.part_type, + text: part.text, + generated_at_unix_ms: generatedAtUnixMs, + }); + } + + return { payloads }; +} + +/** + * Resolve the installed Pi package version for diff-trace `tool_version`. + * The package's `exports` map does not expose `package.json`, so resolve the + * package entry point and read `package.json` from the package root instead. + * Returns null when resolution fails (normalized diff traces permit it). + */ +async function resolvePiToolVersion(): Promise { + try { + const require_ = createRequire(import.meta.url); + const entryPath = require_.resolve("@earendil-works/pi-coding-agent"); + const packageJsonPath = join(dirname(entryPath), "..", "package.json"); + const parsed: { version?: unknown } = JSON.parse( + await readFile(packageJsonPath, "utf8"), + ); + return typeof parsed.version === "string" && parsed.version.length > 0 + ? parsed.version + : null; + } catch { + return null; + } +} + +/** + * Send a diff-trace payload to `sce hooks diff-trace`, fire-and-forget. + * Fail-open: stderr is ignored and the returned promise never rejects. + */ +function runDiffTraceHook( + cwd: string, + payload: DiffTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "diff-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +async function readFileOrUndefined(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch { + return undefined; + } +} + +function diffLabelFor(cwd: string, absolutePath: string): string { + const relPath = relative(cwd, absolutePath); + return relPath.length > 0 && !relPath.startsWith("..") && !isAbsolute(relPath) + ? relPath + : absolutePath; +} + +/** + * Rewrite temp-file path labels in git diff header lines to the repo-relative + * target path. Only header lines before the first `@@` hunk marker are + * touched so that content lines starting with `--- ` / `+++ ` are preserved. + */ +function rewriteDiffLabels( + diff: string, + label: string, + isCreate: boolean, +): string { + const lines = diff.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("@@")) { + break; + } + if (line.startsWith("diff --git ")) { + lines[i] = `diff --git a/${label} b/${label}`; + } else if (line.startsWith("--- ")) { + lines[i] = isCreate ? "--- /dev/null" : `--- a/${label}`; + } else if (line.startsWith("+++ ")) { + lines[i] = `+++ b/${label}`; + } + } + return lines.join("\n"); +} + +/** + * Produce a unified diff between before/after contents by writing them to + * temp files and spawning `git diff --no-index --no-ext-diff` (exit status 1 + * means "files differ"). Returns undefined for no-op diffs or any failure; + * temp files are always cleaned up. + */ +async function buildUnifiedDiff( + label: string, + before: string | undefined, + after: string, +): Promise { + const tempDir = await mkdtemp(join(tmpdir(), "sce-pi-diff-")); + try { + const beforePath = join(tempDir, "before"); + const afterPath = join(tempDir, "after"); + await writeFile(beforePath, before ?? "", "utf8"); + await writeFile(afterPath, after, "utf8"); + + const result = spawnSync( + "git", + ["diff", "--no-index", "--no-ext-diff", "--", beforePath, afterPath], + { encoding: "utf8", timeout: 10_000 }, + ); + + if (result.error || result.status !== 1) { + return undefined; + } + const stdout = result.stdout; + if (!stdout) { + return undefined; + } + return rewriteDiffLabels(stdout, label, before === undefined); + } catch { + return undefined; + } finally { + await rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +} + +export default function sceExtension(pi: ExtensionAPI): void { + const pendingFileMutations = new Map(); + const piToolVersionPromise = resolvePiToolVersion(); + + pi.on("tool_call", (event) => { + if (!isToolCallEventType("bash", event)) { + return undefined; + } + + const command = event.input.command; + if (typeof command !== "string" || command.length === 0) { + return undefined; + } + + const policyResult = evaluateBashCommandPolicy(command); + if (!policyResult) { + // Fail open: if the policy check cannot be performed, allow the command. + return undefined; + } + + if (policyResult.decision === "deny" && policyResult.reason) { + return { block: true, reason: policyResult.reason }; + } + + return undefined; + }); + + pi.on("tool_call", async (event, ctx) => { + if ( + !isToolCallEventType("edit", event) && + !isToolCallEventType("write", event) + ) { + return undefined; + } + + const targetPath = event.input.path; + if (typeof targetPath !== "string" || targetPath.length === 0) { + return undefined; + } + + const absolutePath = resolvePath(ctx.cwd, targetPath); + pendingFileMutations.set(event.toolCallId, { + absolutePath, + diffLabel: diffLabelFor(ctx.cwd, absolutePath), + before: await readFileOrUndefined(absolutePath), + }); + return undefined; + }); + + pi.on("tool_result", async (event, ctx) => { + const pending = pendingFileMutations.get(event.toolCallId); + if (!pending) { + return; + } + pendingFileMutations.delete(event.toolCallId); + + if (event.isError) { + return; + } + + const after = await readFileOrUndefined(pending.absolutePath); + if (after === undefined || after === pending.before) { + return; + } + + const diff = await buildUnifiedDiff( + pending.diffLabel, + pending.before, + after, + ); + if (!diff) { + return; + } + + const sessionId = ctx.sessionManager.getSessionId(); + const generatedAtUnixMs = Date.now(); + const patchMessageId = `${event.toolCallId}-patch`; + + void runConversationTraceHook(ctx.cwd, { + payloads: [ + { + type: "message", + session_id: sessionId, + message_id: patchMessageId, + role: "assistant", + generated_at_unix_ms: generatedAtUnixMs, + }, + { + type: "message.part", + session_id: sessionId, + message_id: patchMessageId, + part_type: "patch", + text: diff, + generated_at_unix_ms: generatedAtUnixMs, + }, + ], + }); + + void runDiffTraceHook(ctx.cwd, { + sessionID: sessionId, + diff, + time: generatedAtUnixMs, + model_id: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, + tool_name: "pi", + tool_version: await piToolVersionPromise, + }); + }); + + pi.on("message_end", (event, ctx) => { + const message = event.message; + if (message.role !== "user" && message.role !== "assistant") { + return; + } + + const payload = buildMessageEndConversationTracePayload( + ctx.sessionManager.getSessionId(), + message, + ); + if (payload) { + void runConversationTraceHook(ctx.cwd, payload); + } + }); +} diff --git a/.pi/prompts/agent-shared-context-code.md b/.pi/prompts/agent-shared-context-code.md new file mode 100644 index 00000000..9d3abdd5 --- /dev/null +++ b/.pi/prompts/agent-shared-context-code.md @@ -0,0 +1,62 @@ +--- +description: "Act in the shared-context-code role to execute one approved SCE task and sync context." +argument-hint: " [T0X]" +--- + +Act as the Shared Context Code agent for the rest of this session. Load and follow the `sce-task-execution` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +You are the Shared Context Code agent. + +Mission +- Implement exactly one approved task from an existing plan. +- Validate behavior and keep `context/` aligned with the resulting code. + +Core principles +- The human owns architecture, risk, and final decisions. +- `context/` is durable AI-first memory and must stay current-state oriented. +- If context and code diverge, code is source of truth and context must be repaired. + +Hard boundaries +- One task per session unless the human explicitly approves multi-task execution. +- Do not change plan structure or reorder tasks without approval. +- If scope expansion is required, stop and ask. + +Authority inside `context/` +- You may create, update, rename, move, or delete files under `context/` as needed. +- You may create new top-level folders under `context/` when needed. +- Delete a file only if it exists and has no uncommitted changes. +- Use Mermaid when a diagram is needed. + +Startup +1) Confirm this session targets one approved plan task. +2) Proceed using the Procedure below. + +Procedure +- Load `sce-plan-review` and follow it exactly. +- Ask for explicit user confirmation that the reviewed task is ready for implementation. +- After confirmation, load `sce-task-execution` and follow it exactly. +- After implementation, load `sce-context-sync` and follow it. +- Wait for user feedback. +- If feedback requires in-scope fixes, apply the fixes, rerun light task-level checks/lints, run a build if it is light/fast, and run `sce-context-sync` again. +- If this is the final plan task, load `sce-validation` and follow it. + +Important behaviors +- Keep context optimized for future AI sessions, not prose-heavy narration. +- Do not leave completed-work summaries in core context files; represent resulting current state. +- After accepted implementation changes, context synchronization is part of done. +- Long-term quality is measured by code quality and context accuracy. + +Natural nudges to use +- "I will run `sce-plan-review` first to confirm the next task and clarify acceptance criteria." +- "Please confirm this task is ready for implementation, then I will execute it." +- "I will run light, task-level checks and lints first, and run a build too if it is light/fast." +- "After implementation, I will sync `context/`, wait for feedback, and resync if we apply fixes." + +Definition of done +- Code changes satisfy task acceptance checks. +- Relevant tests/checks are executed with evidence. +- Plan task status is updated. +- Context and code have no unresolved drift for this task. diff --git a/.pi/prompts/agent-shared-context-plan.md b/.pi/prompts/agent-shared-context-plan.md new file mode 100644 index 00000000..5dbad4e1 --- /dev/null +++ b/.pi/prompts/agent-shared-context-plan.md @@ -0,0 +1,68 @@ +--- +description: "Act in the shared-context-plan role to create or update an SCE plan before implementation." +argument-hint: "" +--- + +Act as the Shared Context Plan agent for the rest of this session. Load and follow the `sce-plan-authoring` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +You are the Shared Context Plan agent. + +Mission +- Convert a human change request into an implementation plan in `context/plans/`. +- Keep planning deterministic and reviewable. + +Core principles +- The human owns architecture, risk, and final decisions. +- `context/` is durable AI-first memory and must stay current-state oriented. +- If context and code diverge, code is source of truth and context must be repaired. + +Hard boundaries +- Never modify application code. +- Never run shell commands. +- Only write planning and context artifacts. +- Planning does not imply execution approval. + +Authority inside `context/` +- You may create, update, rename, move, or delete files under `context/` as needed. +- You may create new top-level folders under `context/` when needed. +- Delete a file only if it exists and has no uncommitted changes. +- Use Mermaid when a diagram is needed. + +Startup +1) Check for `context/`. +2) If missing, ask once for approval to bootstrap. +3) If approved, load `sce-bootstrap-context` and follow it. +4) If not approved, stop. +5) Read `context/context-map.md`, `context/overview.md`, and `context/glossary.md` if present. +6) Before broad exploration, consult `context/context-map.md` for relevant context files. +7) If context is partial or stale, continue with code truth and propose focused context repairs. + +Procedure +- Load `sce-plan-authoring` and follow it exactly. +- Ask targeted clarifying questions when requirements, boundaries, dependencies, or acceptance criteria are unclear. +- Write or update `context/plans/{plan_name}.md`. +- Confirm plan creation with `plan_name` and exact file path. +- Present the full ordered task list in chat, if it's written to a subagent print it in the main agent. +- Prompt the user to start a new session to implement `T01`. +- Provide one canonical next command: `/next-task {plan_name} T01`. + +Important behaviors +- Keep context optimized for future AI sessions, not prose-heavy narration. +- Do not leave completed-work summaries in core context files; represent resulting current state. +- Treat `context/plans/` as active execution artifacts; completed plans are disposable and not durable history. +- Promote durable outcomes into current-state context files and `context/decisions/` when needed. +- Long-term quality is measured by code quality and context accuracy. + +Natural nudges to use +- "Let me pull relevant files from `context/` before implementation." +- "Per SCE, chat-mode first, then implementation mode." +- "I will propose a plan with trade-offs first, then implement." +- "Now that this is settled, I will sync `context/` so future sessions stay aligned." + +Definition of done +- Plan has stable task IDs (`T01..T0N`). +- Each task has boundaries, done checks, and verification notes. +- Final task is always validation and cleanup. diff --git a/.pi/prompts/change-to-plan.md b/.pi/prompts/change-to-plan.md new file mode 100644 index 00000000..8a782aea --- /dev/null +++ b/.pi/prompts/change-to-plan.md @@ -0,0 +1,17 @@ +--- +description: "Use `sce-plan-authoring` to turn a change request into a scoped SCE plan" +argument-hint: "" +--- + +Load and follow the `sce-plan-authoring` skill. + +Input change request: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; detailed clarification handling, plan-shape rules, and task-writing behavior stay owned by `sce-plan-authoring`. +- Run `sce-plan-authoring` to resolve whether the input targets a new or existing plan, normalize goals/constraints/success criteria, and produce an implementation-ready task stack. +- Preserve the clarification gate from `sce-plan-authoring`: if blockers, ambiguity, or missing acceptance criteria remain, stop and ask the focused user questions needed to finish the plan safely. +- Require one-task/one-atomic-commit slicing through `sce-plan-authoring` before any task is considered ready for implementation. +- When the plan is ready, write or update `context/plans/{plan_name}.md`, confirm the resolved `{plan_name}` and exact path, and return the ordered task list. +- Stop after the planning handoff by providing the exact next-session command `/next-task {plan_name} T01`. diff --git a/.pi/prompts/commit.md b/.pi/prompts/commit.md new file mode 100644 index 00000000..b868096a --- /dev/null +++ b/.pi/prompts/commit.md @@ -0,0 +1,42 @@ +--- +description: "Use `sce-atomic-commit` to propose atomic commit message(s) from staged changes" +argument-hint: "[oneshot|skip]" +--- + +Load and follow the `sce-atomic-commit` skill. + +Input: +`$ARGUMENTS` + +## Bypass path (`/commit oneshot` or `/commit skip`) + +If `$ARGUMENTS` starts with `oneshot` or `skip` (case-insensitive, first token only): + +- **Skip the staging confirmation prompt.** Do not ask the user to stage files or confirm staging. +- **Validate staged content exists.** Check that `git diff --cached` is non-empty. If no staged changes exist, stop with the error: "No staged changes. Stage changes before commit." Do not proceed. +- **Skip context-guidance gate classification.** Do not classify staged diff scope as `context/`-only vs mixed. Do not apply context-file guidance gating. +- **Produce exactly one commit message.** Run `sce-atomic-commit` with these overrides: + - Produce exactly one commit message. Do not propose splits. Do not emit split guidance. + - When staged changes include `context/plans/*.md`, make a best-effort inference to cite affected plan slug(s) and updated task ID(s). If ambiguous, omit the citation rather than stopping for clarification. +- **Auto-execute `git commit`.** Use the produced commit message to run `git commit -m ""`. + - If `git commit` succeeds, report the commit hash and stop. + - If `git commit` fails, stop and report the failure. Do not invent fallback commits, retry, or amend. + +## Regular path (no arguments or non-bypass arguments) + +If `$ARGUMENTS` does not start with `oneshot` or `skip`: + +Behavior: +- If arguments are empty, treat input as unstated and infer commit intent from staged changes only. +- If arguments are provided, treat them as optional commit context to refine message proposals. +- Keep this command as thin orchestration; staged-diff analysis, atomic split decisions, and message wording stay owned by `sce-atomic-commit`. +- Before running `sce-atomic-commit`, explicitly stop and prompt the user: + + "Please run `git add ` for all changes you want included in this commit. + Atomic commits should only include intentionally staged changes. + Confirm once staging is complete." + +- After confirmation: + - Classify staged diff scope (`context/`-only vs mixed `context/` + non-`context/`) and apply the context-guidance gate from `sce-atomic-commit`. + - Run `sce-atomic-commit` to produce commit-message proposals and any needed split guidance. +- Do not create commits automatically; stop after returning proposed commit message(s) and split guidance when needed. diff --git a/.pi/prompts/handover.md b/.pi/prompts/handover.md new file mode 100644 index 00000000..af3666ff --- /dev/null +++ b/.pi/prompts/handover.md @@ -0,0 +1,15 @@ +--- +description: "Run `sce-handover-writer` to capture the current task for handoff" +argument-hint: "[task context]" +--- + +Load and follow the `sce-handover-writer` skill. + +Input: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; handover structure, naming, and content decisions stay owned by `sce-handover-writer`. +- Run `sce-handover-writer` to gather current task state, decisions made and rationale, open questions or blockers, and the next recommended step. +- Let `sce-handover-writer` create the handover in `context/handovers/`, using task-aligned naming such as `context/handovers/{plan_name}-{task_id}-{timestamp}.md` when the inputs support it. +- If required details are missing, infer only from current repo state, label assumptions clearly, then stop after reporting the exact handover path. diff --git a/.pi/prompts/next-task.md b/.pi/prompts/next-task.md new file mode 100644 index 00000000..3cfa8607 --- /dev/null +++ b/.pi/prompts/next-task.md @@ -0,0 +1,24 @@ +--- +description: "Run `sce-plan-review` -> `sce-task-execution` -> `sce-context-sync` for one approved SCE task" +argument-hint: " [T0X]" +--- + +Load and follow `sce-plan-review`, then `sce-task-execution`, then `sce-context-sync`. + +Input: +`$ARGUMENTS` + +Expected arguments: +- plan name or plan path (required) +- task ID (`T0X`) (optional) + +Behavior: +- Keep this command as thin orchestration; skill-owned review, implementation, validation, and context-sync details stay in the referenced skills. +- Run `sce-plan-review` first to resolve the plan target, choose the task, and report readiness. +- Apply the readiness confirmation gate from `sce-plan-review` before implementation: + - auto-pass only when both plan + task ID are provided and review reports no blockers, ambiguity, or missing acceptance criteria + - otherwise resolve the open points and ask the user to confirm the task is ready before continuing +- Run `sce-task-execution` next; keep the mandatory implementation stop, scoped edits, light checks/lints/build, and plan status updates skill-owned. +- After implementation, run `sce-context-sync` as the required done gate and wait for user feedback. +- If feedback requires in-scope fixes, apply the fixes, rerun light checks (and a light/fast build when applicable), then run `sce-context-sync` again. +- If this was the final plan task, run `sce-validation`; otherwise stop after prompting a new session with `/next-task {plan_name} T0X`. diff --git a/.pi/prompts/validate.md b/.pi/prompts/validate.md new file mode 100644 index 00000000..e2c32ffe --- /dev/null +++ b/.pi/prompts/validate.md @@ -0,0 +1,15 @@ +--- +description: "Run `sce-validation` to finish an SCE plan with validation and cleanup" +argument-hint: "" +--- + +Load and follow the `sce-validation` skill. + +Input: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; validation scope, command selection, cleanup, and evidence formatting stay owned by `sce-validation`. +- Run `sce-validation` to execute the full validation phase for the targeted plan or change, including required checks, evidence capture, and cleanup expected by the skill. +- Let `sce-validation` decide pass/fail status and record any residual risks or unmet criteria. +- Stop after reporting the validation outcome and the location of any written validation evidence. diff --git a/.pi/skills/sce-atomic-commit/SKILL.md b/.pi/skills/sce-atomic-commit/SKILL.md new file mode 100644 index 00000000..0d7fe5bd --- /dev/null +++ b/.pi/skills/sce-atomic-commit/SKILL.md @@ -0,0 +1,102 @@ +--- +name: sce-atomic-commit +description: Write atomic, repo-style git commits from a change summary or diff. Use when preparing commit messages, splitting work into coherent commits, or reviewing whether a commit is too broad. +--- + +## Goal + +Turn the current staged changes into atomic repository-style commit message proposals. + +For this workflow: +- analyze the staged diff to identify coherent change units +- propose one or more commit messages when staged changes mix unrelated goals +- keep each proposed message focused on a single coherent change +- stay proposal-only: do not create commits automatically +- in bypass mode (command-invoked), relax proposal-only, split guidance, context-guidance gate, and plan-citation ambiguity rules + +## Inputs + +Accept any of: +- staged diff (preferred) +- changed file list with notes +- PR/task summary +- before/after behavior notes + +## Output format + +Produce commit message proposals that follow: +- `scope: Subject` +- imperative verb (Fix/Add/Remove/Implement/Refactor/Simplify/Rename/Update/Ensure/Allow) +- no trailing period in subject +- body when context is needed (why/what changed/impact) +- issue references on their own lines (for example `Fixes #123`) + +When staged changes include `context/plans/*.md`, each commit body must also include: +- affected plan slug(s) +- updated task ID(s) (`T0X`) + +If staged `context/plans/*.md` changes do not expose the plan slug or updated task ID clearly enough to cite faithfully, stop and ask for clarification instead of inventing references. + +## Bypass mode + +When this skill is invoked by the `/commit` command in bypass mode (`/commit oneshot` or `/commit skip`), the command passes overrides that relax the standard rules below: + +- **Proposal-only → auto-commit allowed.** Do not block auto-commit; the command will execute `git commit` with the produced message. +- **Single message only.** Produce exactly one commit message. Do not propose splits. Do not emit split guidance. +- **Context-guidance gate skipped.** Do not classify staged diff scope as `context/`-only vs mixed. Do not apply context-file guidance gating. +- **Plan citations: best-effort only.** When staged changes include `context/plans/*.md`, make a best-effort inference to cite affected plan slug(s) and updated task ID(s). If ambiguous, omit the citation rather than stopping for clarification. + +When NOT in bypass mode, follow the standard rules in the Procedure and Context-file guidance gating sections below. + +## Procedure + +1) Analyze the staged diff for coherent units +- Infer the main reason(s) for the staged change from the diff first. +- Use optional notes only to refine wording, not to override the staged truth. +- Identify whether staged changes represent one coherent unit or multiple unrelated goals. + +2) Choose scope for each unit +- Use the smallest stable subsystem/module name recognizable in the repo. +- If unclear, use the primary directory/package of the change. + +3) Write subject for each unit +- Pattern: `: ` +- Keep concrete and targeted. + +4) Add body when needed +- Explain what was wrong/missing, why it matters, what changed conceptually, and impact. +- Add issue references on separate lines. + +5) In regular mode: apply the plan-update body rule when needed +- Check whether staged changes include `context/plans/*.md`. +- If yes, cite the affected plan slug(s) and updated task ID(s) in the body. +- If the staged plan diff is ambiguous, stop with actionable guidance asking the user to stage or clarify the plan/task reference explicitly. + +6) In regular mode: propose split guidance when appropriate +- If staged changes mix unrelated goals (for example: a feature change plus unrelated refactoring), propose separate commit messages for each coherent unit. +- Explain why the split is recommended and which files belong to each proposed commit. +- If staged changes represent one coherent unit, propose a single commit message. + +7) In regular mode: validate each proposed message +- Each message should describe its intended change faithfully. +- The subject should stay concise and technical. +- The body should add useful why/impact context instead of repeating the subject. +- Do not invent plan or task references. + +## Context-file guidance gating + +In regular mode: +- Check staged diff scope before proposing commit messaging guidance. +- If staged changes are context-only (`context/**`), context-file-focused guidance is allowed. +- If staged changes are mixed (`context/**` + non-`context/**`), avoid default context-file commit reminders and prioritize guidance that reflects the full staged scope. + +## Anti-patterns + +- vague subjects ("cleanup", "updates") +- body repeats subject without adding why +- playful tone in serious fixes/architecture changes +- mention `context/` sync activity in commit messages +- inventing plan slugs or task IDs for staged plan edits +- proposing splits for changes that are already coherent +- forcing unrelated changes into a single commit +- applying split guidance or plan-citation ambiguity stops when the command is in bypass mode diff --git a/.pi/skills/sce-bootstrap-context/SKILL.md b/.pi/skills/sce-bootstrap-context/SKILL.md new file mode 100644 index 00000000..69610b01 --- /dev/null +++ b/.pi/skills/sce-bootstrap-context/SKILL.md @@ -0,0 +1,55 @@ +--- +name: sce-bootstrap-context +description: Use when user wants to Bootstrap SCE baseline context directory when missing. +--- + +## When to use +- Use only when `context/` is missing. +- Ask for human approval before creating files. + +## Required baseline +Create these paths: +- `context/overview.md` +- `context/architecture.md` +- `context/patterns.md` +- `context/glossary.md` +- `context/context-map.md` +- `context/plans/` +- `context/handovers/` +- `context/decisions/` +- `context/tmp/` +- `context/tmp/.gitignore` + +Use the following commands to create the directory structure: +```bash +mkdir -p context/plans context/handovers context/decisions context/tmp +touch context/overview.md context/architecture.md context/patterns.md context/glossary.md context/context-map.md +``` + +`context/tmp/.gitignore` content: +``` +* +!.gitignore +``` + +## Validation +After running the commands, verify all expected paths exist before proceeding: +```bash +ls context/overview.md context/architecture.md context/patterns.md context/glossary.md context/context-map.md context/plans context/handovers context/decisions context/tmp context/tmp/.gitignore +``` +If any path is missing, re-create it before moving on. + +## No-code bootstrap rule +- If the repository has no application code, keep `overview.md`, `architecture.md`, `patterns.md`, and `glossary.md` empty or placeholder-only. +- Do not invent implementation details. + +Example placeholder content for empty files in a no-code repo: +```markdown +# Overview + +> This section has not been populated yet. Add a high-level description of the project here. +``` + +## After bootstrapping +- Add baseline links in `context/context-map.md`. +- Tell the user that `context/` should be committed as shared memory. diff --git a/.pi/skills/sce-context-sync/SKILL.md b/.pi/skills/sce-context-sync/SKILL.md new file mode 100644 index 00000000..f051a981 --- /dev/null +++ b/.pi/skills/sce-context-sync/SKILL.md @@ -0,0 +1,91 @@ +--- +name: sce-context-sync +description: Use when user wants to Synchronize context files to match current code behavior after task execution. +--- + +## Principle +- Context is durable AI memory and must reflect current-state truth. +- If context and code diverge, code is source of truth. + +## Mandatory sync pass (important-change gated) +For every completed implementation task, run a sync pass over these shared files: +- `context/overview.md` +- `context/architecture.md` +- `context/glossary.md` +- `context/patterns.md` +- `context/context-map.md` + +Classify whether the task is an important change before deciding to edit or verify root context files. + +## Root context significance gating +- **Root edits required** - task introduces cross-cutting behavior, repository-wide policy/contracts, architecture boundaries, or canonical terminology changes. +- **Verify-only** - task is localized to a single feature/domain with no root-level behavior, architecture, or terminology impact. Keep root files unchanged; capture details in domain files instead. +- Even when verify-only, still verify `context/overview.md`, `context/architecture.md`, and `context/glossary.md` against code truth before declaring done. + +## Step-by-step sync pass workflow + +1. **Classify the change** - Important change or verify-only (see [Classification Reference](#classification-reference) below). +2. **Read the affected code** - Review modified files to understand what actually changed. +3. **Verify root files** - Open `context/overview.md`, `context/architecture.md`, and `context/glossary.md`; confirm they match code truth. +4. **Edit or skip root files** - Important change: update relevant root files. Verify-only: leave root files unchanged. +5. **Create or update domain files** - Write or revise `context/{domain}/` files for feature-specific detail (see [Domain File Policy](#domain-file-creation-policy) below). +6. **Ensure feature existence** - Every newly implemented feature must have at least one durable canonical description discoverable from context (domain file or `context/overview.md` for cross-cutting features). +7. **Update `context/context-map.md`** - Add or refresh discoverability links to any new or changed context files. +8. **Add glossary entries** - For any new domain language introduced by the task. +9. **Final check** - Confirm all updated files are <= 250 lines, diagrams are present where needed, and links use relative paths. + +### Before/after example +A task adds a new `PaymentGateway` abstraction used only in the payments domain (verify-only - domain-local). + +**`context/glossary.md`** - unchanged (no new root-level terminology). + +**New file: `context/payments/payment-gateway.md`:** +```markdown +# PaymentGateway + +Abstraction over external payment processors (Stripe, Adyen). +Defined in `src/payments/gateway/`. + +## Contract +- `charge(amount, token): Result` +- `refund(chargeId): Result` + +See also: [overview.md](../overview.md), [context-map.md](../context-map.md) +``` + +**`context/context-map.md`** - updated with a link to `context/payments/payment-gateway.md`. + +--- + +## Classification Reference + +| Important change (root edits required) | Verify-only (root files unchanged) | +|---|---| +| New auth strategy replacing existing one - architecture + terminology | New field on an existing API response - localized, no architecture impact | +| Background job queue used across multiple domains - cross-cutting | Bug fix in a single service's retry logic - no new root-level behavior | +| Renaming a core concept (e.g., `Order` -> `Purchase`) - canonical terminology | New UI component added to an existing feature - no cross-cutting impact | + +--- + +## Domain File Creation Policy + +- Use `context/{domain}/` for detailed feature behavior. +- If a feature does not cleanly fit an existing domain file, create a new one - do not defer documentation. +- If the feature appears to be part of a larger future domain, document the implemented slice now in a focused file and link it to related context. +- Prefer a small, precise domain file over overloading `overview.md` with detail. +- If updates for the current feature/domain outgrow shared files, migrate detail into `context/{domain}/` files, keep concise pointers in shared files, and add discoverability links in `context/context-map.md`. + +--- + +## Final-task requirement +- In the final plan task (validation/cleanup), confirm feature existence documentation is present and linked. +- If a feature was implemented but not represented in context, add the missing entry before declaring the task done. + +## Quality constraints +- One topic per file. +- Prefer concise current-state documentation over narrative changelogs. +- Link related context files with relative paths. +- Include concrete code examples when needed to clarify non-trivial behavior. +- Every context file must stay at or below 250 lines; if it would exceed 250, split into focused files and link them. +- Add a Mermaid diagram when structure, boundaries, or flows are complex. +- Ensure major code areas have matching context coverage. diff --git a/.pi/skills/sce-handover-writer/SKILL.md b/.pi/skills/sce-handover-writer/SKILL.md new file mode 100644 index 00000000..73292812 --- /dev/null +++ b/.pi/skills/sce-handover-writer/SKILL.md @@ -0,0 +1,46 @@ +--- +name: sce-handover-writer +description: Use when user wants to create a structured SCE handover for the current task. +--- + +## What I do +- Create a new handover file in `context/handovers/`. +- Capture: + - current task state + - decisions made and rationale + - open questions or blockers + - next recommended step + +## How to run this + +1. **Gather context** - review the current task, recent changes, and repo state. +2. **Create the file** - use task-aligned naming: `context/handovers/{plan_name}-{task_id}.md`. +3. **Fill each section** - follow the template below, labelling any inferred details as assumptions. +4. **Verify completeness** - confirm all four sections are populated before finishing. + +If key details are missing, infer from repo state and clearly label assumptions. + +## Handover document template + +```markdown +# Handover: {plan_name} - {task_id} + +## Current Task State +- What was being worked on and how far along it is. +- e.g. "Implementing OAuth login flow; token generation complete, redirect handling in progress." + +## Decisions Made +- Key choices and their rationale. +- e.g. "Chose JWT over session cookies for statelessness. Rejected library X due to licence constraints." + +## Open Questions / Blockers +- Unresolved issues or outstanding dependencies. +- e.g. "Awaiting confirmation on token expiry policy from product team." + +## Next Recommended Step +- The single most important action for whoever picks this up. +- e.g. "Complete the redirect handler in `src/auth/callback.ts`, then run the auth integration tests." +``` + +## Expected output +- A complete handover document in `context/handovers/` using task-aligned naming when possible. diff --git a/.pi/skills/sce-plan-authoring/SKILL.md b/.pi/skills/sce-plan-authoring/SKILL.md new file mode 100644 index 00000000..02c177e4 --- /dev/null +++ b/.pi/skills/sce-plan-authoring/SKILL.md @@ -0,0 +1,87 @@ +--- +name: sce-plan-authoring +description: Use when user wants to Create or update an SCE implementation plan with scoped atomic tasks. +--- + +## Goal +Turn a human change request into `context/plans/{plan_name}.md`. + +## Intake trigger +- If a request includes both a change description and success criteria, planning is mandatory before implementation. +- Planning does not imply execution approval. + +## Clarification gate (blocking) +- Before writing or updating any plan, run an ambiguity check. +- If any critical detail is unclear, ask 1-3 targeted questions and stop. +- Do not write or update `context/plans/{plan_name}.md` until the user answers. +- Critical details that must be resolved before planning include: + - scope boundaries and out-of-scope items + - success criteria and acceptance signals + - constraints and non-goals + - dependency choices (new libs/services, versions, and integration approach) + - domain ambiguity (unclear business rules, terminology, or ownership) + - architecture concerns (patterns, interfaces, data flow, migration strategy, and risk tradeoffs) + - task ordering assumptions and prerequisite sequencing +- Do not silently invent missing requirements. +- If the user explicitly allows assumptions, record them in an `Assumptions` section. +- Incorporate user answers into the plan before handoff. + +Example clarification questions (use this style - specific, blocking, targeted): +> 1. Should the new endpoint authenticate via the existing JWT middleware, or is a separate auth flow in scope? +> 2. Is database migration rollback a hard requirement, or is forward-only acceptable for this change? +> 3. Which service owns the `UserProfile` type - should this task modify that definition or only consume it? + +## Plan format +1) Change summary +2) Success criteria +3) Constraints and non-goals +4) Task stack (`T01..T0N`) +5) Open questions (if any) + +## Task format (required) +For each task include: +- Task ID +- Goal +- Boundaries (in/out of scope) +- Done when +- Verification notes (commands or checks) + +## Atomic task slicing contract (required) +- Author each executable task as one atomic commit unit by default. +- Every task must be scoped so one contributor can complete it and land it as one coherent commit without bundling unrelated changes. +- If a candidate task would require multiple independent commits (for example: refactor + behavior change + docs), split it into separate sequential tasks before finalizing the plan. +- Keep broad wrappers (`polish`, `finalize`, `misc updates`) out of executable tasks; convert them into specific outcomes with concrete acceptance checks. + +Example compliant skeleton: +- [ ] T0X: `[single intent title]` (status:todo) + - Task ID: T0X + - Goal: `[one outcome]` + - Boundaries (in/out of scope): `[tight scope]` + - Done when: `[clear acceptance for one coherent change]` + - Verification notes (commands or checks): `[targeted checks for this change]` + +Example filled-in task entry: +- [ ] T02: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T02 + - Goal: Implement a POST `/auth/refresh` endpoint that exchanges a valid refresh token for a new access token. + - Boundaries (in/out of scope): In - route handler, token validation logic, response schema. Out - refresh token rotation policy (covered in T03), client-side storage changes. + - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired/invalid token; unit tests pass; OpenAPI spec updated. + - Verification notes (commands or checks): `pnpm test src/auth/refresh.test.ts`; `curl -X POST localhost:3000/auth/refresh -d '{"token":"..."}' -w "%{http_code}"`. + +Use checkbox lines for machine-friendly progress tracking: +- `- [ ] T01: ... (status:todo)` + +## Complete plan example + +See `context/plans/PLAN_EXAMPLE.md` for a full annotated reference plan (JWT auth walkthrough covering all required sections and four task entries). + +## Required final task +- Final task is always validation and cleanup. +- It must include full checks and context sync verification. + +## Output contract +- Save plan under `context/plans/`. +- Confirm plan creation with `plan_name` and exact file path. +- Present the full ordered task list in chat. +- Prompt the user to start a new session with Shared Context Code agent to implement `T01`. +- Provide one canonical next command: `/next-task {plan_name} T01`. diff --git a/.pi/skills/sce-plan-review/SKILL.md b/.pi/skills/sce-plan-review/SKILL.md new file mode 100644 index 00000000..471baf7b --- /dev/null +++ b/.pi/skills/sce-plan-review/SKILL.md @@ -0,0 +1,89 @@ +--- +name: sce-plan-review +description: Use when user wants to review an existing plan and prepare the next task safely. +--- + +## What I do +- Continue execution from an existing plan in `context/plans/`. +- Read the selected plan and identify the next task from the first unchecked checkbox. +- Ask focused questions for anything not clear enough to execute safely. + +## How to run this +- Use this skill when the user asks to continue a plan or pick the next task. +- If `context/` is missing, ask once: "`context/` is missing. Bootstrap SCE baseline now?" + - If yes, create baseline with `sce-bootstrap-context` and continue. + - If no, stop and explain SCE workflows require `context/`. +- Read `context/context-map.md`, `context/overview.md`, and `context/glossary.md` before broad exploration. +- Resolve plan target: + - If plan path argument exists, use it. + - If multiple plans exist and no explicit path is provided, ask user to choose. +- Collect: + - completed tasks + - next task + - blockers, ambiguity, and missing acceptance criteria +- Prompt user to resolve unclear points before implementation. +- Confirm scope explicitly for this session: one task by default unless user requests multi-task execution. + +## Plan file format +SCE plans are Markdown files stored in `context/plans/`. Tasks are tracked as checkboxes: + +```markdown +# Plan: Add user authentication + +## Tasks +- [x] Scaffold auth module +- [x] Add password hashing utility +- [ ] Implement login endpoint <- next task (first unchecked) +- [ ] Write integration tests +- [ ] Update context/current-state.md +``` + +The first unchecked `- [ ]` item is the next task to review and prepare. + +## Rules +- Do not auto-mark tasks complete during review. +- Keep continuation state in the plan markdown itself. +- Treat `context/plans/` as active execution artifacts; completed plans are disposable and not a durable context source. +- If durable history is needed, record it in current-state context files and/or `context/decisions/` instead of completed plan files. +- Keep implementation blocked until decision alignment on unclear points. +- If plan context is stale or partial, continue with code truth and flag context updates. + +## Expected output + +Produce a structured readiness summary after review: + +``` +## Plan Review - [plan filename] + +**Completed tasks:** 2 of 5 +**Next task:** Implement login endpoint + +**Acceptance criteria:** +- POST /auth/login returns JWT on success +- Returns 401 on invalid credentials + +**Issues found:** +- Blocker: JWT secret source not specified (env var? config file?) +- Ambiguity: Should failed attempts be rate-limited in this task or a later one? + +**ready_for_implementation: no** + +**Required decisions before proceeding:** +1. Confirm JWT secret source +2. Confirm rate-limiting scope +``` + +When all issues are resolved: + +``` +**ready_for_implementation: yes** +Proceeding with: Implement login endpoint +``` + +- Explicit readiness verdict: `ready_for_implementation: yes|no`. +- If not ready, explicit issue categories: blockers, ambiguity, missing acceptance criteria. +- Explicit user-aligned decisions needed to proceed to implementation. +- Explicit user confirmation request that the task is ready for implementation when unresolved issues remain. + +## Related skills +- `sce-bootstrap-context` - creates the `context/` baseline required by this skill diff --git a/.pi/skills/sce-task-execution/SKILL.md b/.pi/skills/sce-task-execution/SKILL.md new file mode 100644 index 00000000..36a78e5b --- /dev/null +++ b/.pi/skills/sce-task-execution/SKILL.md @@ -0,0 +1,56 @@ +--- +name: sce-task-execution +description: Use when user wants to Execute one approved task with explicit scope, evidence, and status updates. +--- + +## Scope rule +- Execute exactly one task per session by default. +- If multi-task execution is requested, confirm explicit human approval. + +## Mandatory implementation stop +- Before writing or modifying any code, pause and prompt the user. +- The prompt must explain: + - task goal + - boundaries (in/out of scope) + - done checks + - expected files/components to change + - key approach, trade-offs, and risks +- Then ask explicitly whether to continue. +- Do not edit files, generate code, or apply patches until the user confirms. + +**Example mandatory stop prompt:** +``` +Task goal: Add input validation to the user registration endpoint. +In scope: src/routes/register.ts, src/validators/user.ts +Out of scope: Auth logic, database schema, frontend forms +Done checks: All existing tests pass; new validation tests cover empty/invalid email and short passwords +Expected changes: ~2 files modified, no new dependencies +Approach: Use the existing `validateSchema` helper; add a Zod schema for registration payload +Trade-offs: Zod adds minor overhead; keeps validation consistent with other routes +Risks: Existing callers that omit optional fields may start failing validation + +Continue with implementation now? (yes/no) +``` + +## Required sequence +1) Restate task goal, boundaries, done checks, and expected file touch scope. +2) Propose approach, trade-offs, and risks. +3) Stop and ask: "Continue with implementation now?" (yes/no). +4) Implement minimal in-scope changes. +5) Run light task-level tests/checks and lints first, and run a build when the build is light/fast (targeted over full-suite unless requested), then capture evidence. +6) Record whether the implementation is an important change for context sync (root-edit required) or verify-only (no root edits expected). +7) Keep session-only scraps in `context/tmp/`. +8) Update task status in `context/plans/{plan_id}.md`. + +**Example task status update (`context/plans/{plan_id}.md`):** +```markdown +## Task: Add input validation to registration endpoint +- **Status:** done +- **Completed:** 2025-06-10 +- **Files changed:** src/routes/register.ts, src/validators/user.ts +- **Evidence:** 14/14 tests passed, lint clean, build succeeded (12s) +- **Notes:** Zod schema added; no breaking changes to existing callers +``` + +## Scope expansion rule +- If out-of-scope edits are needed, stop and ask for approval. diff --git a/.pi/skills/sce-validation/SKILL.md b/.pi/skills/sce-validation/SKILL.md new file mode 100644 index 00000000..efb7a23e --- /dev/null +++ b/.pi/skills/sce-validation/SKILL.md @@ -0,0 +1,45 @@ +--- +name: sce-validation +description: Use when user wants to Run final plan validation and cleanup with evidence capture. +--- + +## When to use +- Use for the plan's final validation task after implementation is complete. +- Triggered by requests like "validate the plan", "run final checks", "confirm everything passes", "wrap up the task", or "sign off on this change". + +## Validation checklist +1) **Run full test suite** - discover and run the project's primary test command (e.g., `pytest`, `npm test`, `go test ./...`, `cargo test`, `make test`). Check `package.json`, `Makefile`, `pyproject.toml`, or CI config files to find the right command. +2) **Run lint/format checks** - discover and run the project's lint and format tools (e.g., `eslint`, `ruff`, `golangci-lint`, `cargo clippy`, `make lint`). Check project config files such as `.eslintrc`, `pyproject.toml`, or `.golangci.yml`. +3) **Remove temporary scaffolding** - delete any debug code, temporary files, or intermediate artifacts introduced during the change. +4) **Verify context reflects final implemented behavior** - confirm that plan context and notes match the actual final state of the implementation. +5) **Confirm each success criterion has evidence** - for every success criterion defined in the plan, record concrete evidence (command output, exit code, screenshot reference, or file path). + +### If checks fail +- **Fixable failures**: fix the issue, re-run the failing check, and update the report with the corrected output. +- **Non-trivial failures**: document the failure, the attempted fix, and the blocker in the report under "Failed checks and follow-ups". Escalate to the user before closing out. +- **Lint/format auto-fixes**: if the tool supports auto-fix (e.g., `ruff --fix`, `eslint --fix`), apply it, then re-run to confirm clean output. + +## Validation report +Write to `context/plans/{plan_name}.md` including: +- Commands run +- Exit codes and key outputs +- Failed checks and follow-ups +- Success-criteria verification summary +- Residual risks, if any + +### Example report entry +``` +## Validation Report + +### Commands run +- `npm test` -> exit 0 (42 tests passed, 0 failed) +- `eslint src/` -> exit 0 (no warnings) +- Removed: `src/debug_patch.js` (temporary scaffolding) + +### Success-criteria verification +- [x] All API endpoints return 200 for valid input -> confirmed via test output line 34 +- [x] Error responses include structured JSON -> confirmed via `test_error_format.js` + +### Residual risks +- None identified. +``` diff --git a/.sce/config.json b/.sce/config.json index dcf5092c..a756fd03 100644 --- a/.sce/config.json +++ b/.sce/config.json @@ -3,7 +3,8 @@ "integrations": { "target": [ "claude", - "opencode" + "opencode", + "pi" ] }, "log_file": "context/tmp/sce.log", diff --git a/README.md b/README.md index fd4618e8..66f6bbbf 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ SCE treats the *why* behind your code, architecture, decisions, constraints, as a versioned, shared artifact that both your team and your AI agents work from. - a repo-owned `context/` directory holding the architecture, decisions, and constraints AI agents otherwise re-derive every session -- generated configs that make OpenCode and Claude Code actually read it +- generated configs that make OpenCode, Claude Code, and Pi actually read it - a Bash policy that keeps agents inside your repo's rules - hooks that capture agent activity at the commit boundary - a local Agent Trace SQLite database linking each commit to the session that produced it @@ -36,7 +36,7 @@ sce setup # install generated assistant config, hooks, and bash policy sce doctor # verify the install is healthy ``` -`sce setup` writes the OpenCode and/or Claude config into your repo, installs the required git hooks, and initializes the per-repo Agent Trace database. `sce doctor` is read-only by default; `sce doctor --fix` will repair the issues it knows how to repair (missing or stale hooks, missing canonical DB parent directories) and report the rest for manual follow-up. +`sce setup` writes OpenCode, Claude Code, and/or Pi config into your repo, installs the required git hooks, and initializes the per-repo Agent Trace database. Use `sce setup --pi` for Pi only, or `sce setup --all` for OpenCode + Claude Code + Pi. `sce doctor` is read-only by default; `sce doctor --fix` will repair the issues it knows how to repair (missing or stale hooks, missing canonical DB parent directories) and report the rest for manual follow-up. ## Bash policy @@ -54,15 +54,15 @@ SCE writes a local, per-repo audit trail conforming to the [Agent Trace](https:/ ## Supported integrations -| Feature | OpenCode | Claude Code | -|---|---|---| -| Generated config | ✓ | ✓ | -| Hooks + Bash policy | ✓ | ✓ | -| Conversation + diff trace | ✓ | ✓ | -| Model / session attribution | full | full | -| Shared `context/` | ✓ | ✓ | +| Feature | OpenCode | Claude Code | Pi | +|---|---|---|---| +| Generated config | ✓ | ✓ | ✓ | +| Hooks + Bash policy | ✓ | ✓ | ✓ | +| Conversation + diff trace | ✓ | ✓ | ✓ | +| Model / session attribution | full | full | `pi_` session prefix | +| Shared `context/` | ✓ | ✓ | ✓ | -OpenCode and Claude Code are first-class. Other agents can read `context/` but don't get generated config, hooks, or Bash policy enforcement. +OpenCode, Claude Code, and Pi are first-class generated config targets. Pi receives `.pi/prompts/` command and agent-role prompt templates, `.pi/skills/` packages, and a project-local `.pi/extensions/sce/index.ts` extension. The Pi extension delegates Bash policy checks to `sce policy bash`, sends conversation text and reasoning to `sce hooks conversation-trace`, and records edit/write diffs through `sce hooks diff-trace` with `pi_`-prefixed session IDs. Pi user-shell `!`/`!!` policy enforcement and bash-mutation diff tracing are deferred gaps. ## Why this exists diff --git a/cli/assets/generated/config/pi/extensions/sce/index.ts b/cli/assets/generated/config/pi/extensions/sce/index.ts new file mode 100644 index 00000000..25770ff8 --- /dev/null +++ b/cli/assets/generated/config/pi/extensions/sce/index.ts @@ -0,0 +1,455 @@ +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { + dirname, + isAbsolute, + join, + relative, + resolve as resolvePath, +} from "node:path"; +import { + type ExtensionAPI, + isToolCallEventType, +} from "@earendil-works/pi-coding-agent"; + +interface JsonPolicyResult { + status: string; + decision: string; + command: string; + normalized_argv?: string[]; + reason?: string; + policy_id?: string; +} + +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + +type ConversationTraceMessageItem = { + type: "message"; + session_id: string; + message_id: string; + role: "user" | "assistant"; + generated_at_unix_ms: number; +}; + +type ConversationTraceMessagePartItem = { + type: "message.part"; + session_id: string; + message_id: string; + part_type: "text" | "reasoning" | "patch"; + text: string; + generated_at_unix_ms: number; +}; + +type ConversationTraceItem = + | ConversationTraceMessageItem + | ConversationTraceMessagePartItem; + +type ConversationTracePayload = { + payloads: ConversationTraceItem[]; +}; + +type DiffTracePayload = { + sessionID: string; + diff: string; + time: number; + model_id: string | null; + tool_name: "pi"; + tool_version: string | null; +}; + +type PendingFileMutation = { + absolutePath: string; + diffLabel: string; + before: string | undefined; +}; + +/** + * Evaluate a bash command against SCE bash-tool policy by delegating to the + * Rust `sce policy bash` command. Returns the parsed JSON result, or null if + * the policy check could not be performed (fail-open). + */ +function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { + try { + const result = spawnSync( + "sce", + ["policy", "bash", "--input", "normalized", "--output", "json"], + { + input: JSON.stringify({ command }), + encoding: "utf8", + timeout: 10_000, + }, + ); + + if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + return null; + } + + if (result.status !== 0) { + return null; + } + + const stdout = result.stdout?.trim(); + if (!stdout) { + return null; + } + + const parsed: JsonPolicyResult = JSON.parse(stdout); + return parsed; + } catch { + return null; + } +} + +/** + * Send a conversation-trace payload to `sce hooks conversation-trace`, + * fire-and-forget. Fail-open: stderr is ignored so that sce intake errors do + * not leak into the Pi TUI, and the returned promise never rejects. + */ +function runConversationTraceHook( + cwd: string, + payload: ConversationTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "conversation-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +type MessageContentBlock = { + type: string; + text?: unknown; + thinking?: unknown; +}; + +function extractMessageParts( + content: string | readonly MessageContentBlock[], +): Array<{ part_type: "text" | "reasoning"; text: string }> { + if (typeof content === "string") { + return content.length > 0 ? [{ part_type: "text", text: content }] : []; + } + + const parts: Array<{ part_type: "text" | "reasoning"; text: string }> = []; + for (const block of content) { + if (block.type === "text" && typeof block.text === "string" && block.text) { + parts.push({ part_type: "text", text: block.text }); + } else if ( + block.type === "thinking" && + typeof block.thinking === "string" && + block.thinking + ) { + parts.push({ part_type: "reasoning", text: block.thinking }); + } + } + return parts; +} + +function buildMessageEndConversationTracePayload( + sessionId: string, + message: { + role: string; + content: string | readonly MessageContentBlock[]; + responseId?: string; + }, +): ConversationTracePayload | undefined { + if (message.role !== "user" && message.role !== "assistant") { + return undefined; + } + + const messageId = message.responseId ?? randomUUID(); + const generatedAtUnixMs = Date.now(); + + const payloads: ConversationTraceItem[] = [ + { + type: "message", + session_id: sessionId, + message_id: messageId, + role: message.role, + generated_at_unix_ms: generatedAtUnixMs, + }, + ]; + + for (const part of extractMessageParts(message.content)) { + payloads.push({ + type: "message.part", + session_id: sessionId, + message_id: messageId, + part_type: part.part_type, + text: part.text, + generated_at_unix_ms: generatedAtUnixMs, + }); + } + + return { payloads }; +} + +/** + * Resolve the installed Pi package version for diff-trace `tool_version`. + * The package's `exports` map does not expose `package.json`, so resolve the + * package entry point and read `package.json` from the package root instead. + * Returns null when resolution fails (normalized diff traces permit it). + */ +async function resolvePiToolVersion(): Promise { + try { + const require_ = createRequire(import.meta.url); + const entryPath = require_.resolve("@earendil-works/pi-coding-agent"); + const packageJsonPath = join(dirname(entryPath), "..", "package.json"); + const parsed: { version?: unknown } = JSON.parse( + await readFile(packageJsonPath, "utf8"), + ); + return typeof parsed.version === "string" && parsed.version.length > 0 + ? parsed.version + : null; + } catch { + return null; + } +} + +/** + * Send a diff-trace payload to `sce hooks diff-trace`, fire-and-forget. + * Fail-open: stderr is ignored and the returned promise never rejects. + */ +function runDiffTraceHook( + cwd: string, + payload: DiffTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "diff-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +async function readFileOrUndefined(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch { + return undefined; + } +} + +function diffLabelFor(cwd: string, absolutePath: string): string { + const relPath = relative(cwd, absolutePath); + return relPath.length > 0 && !relPath.startsWith("..") && !isAbsolute(relPath) + ? relPath + : absolutePath; +} + +/** + * Rewrite temp-file path labels in git diff header lines to the repo-relative + * target path. Only header lines before the first `@@` hunk marker are + * touched so that content lines starting with `--- ` / `+++ ` are preserved. + */ +function rewriteDiffLabels( + diff: string, + label: string, + isCreate: boolean, +): string { + const lines = diff.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("@@")) { + break; + } + if (line.startsWith("diff --git ")) { + lines[i] = `diff --git a/${label} b/${label}`; + } else if (line.startsWith("--- ")) { + lines[i] = isCreate ? "--- /dev/null" : `--- a/${label}`; + } else if (line.startsWith("+++ ")) { + lines[i] = `+++ b/${label}`; + } + } + return lines.join("\n"); +} + +/** + * Produce a unified diff between before/after contents by writing them to + * temp files and spawning `git diff --no-index --no-ext-diff` (exit status 1 + * means "files differ"). Returns undefined for no-op diffs or any failure; + * temp files are always cleaned up. + */ +async function buildUnifiedDiff( + label: string, + before: string | undefined, + after: string, +): Promise { + const tempDir = await mkdtemp(join(tmpdir(), "sce-pi-diff-")); + try { + const beforePath = join(tempDir, "before"); + const afterPath = join(tempDir, "after"); + await writeFile(beforePath, before ?? "", "utf8"); + await writeFile(afterPath, after, "utf8"); + + const result = spawnSync( + "git", + ["diff", "--no-index", "--no-ext-diff", "--", beforePath, afterPath], + { encoding: "utf8", timeout: 10_000 }, + ); + + if (result.error || result.status !== 1) { + return undefined; + } + const stdout = result.stdout; + if (!stdout) { + return undefined; + } + return rewriteDiffLabels(stdout, label, before === undefined); + } catch { + return undefined; + } finally { + await rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +} + +export default function sceExtension(pi: ExtensionAPI): void { + const pendingFileMutations = new Map(); + const piToolVersionPromise = resolvePiToolVersion(); + + pi.on("tool_call", (event) => { + if (!isToolCallEventType("bash", event)) { + return undefined; + } + + const command = event.input.command; + if (typeof command !== "string" || command.length === 0) { + return undefined; + } + + const policyResult = evaluateBashCommandPolicy(command); + if (!policyResult) { + // Fail open: if the policy check cannot be performed, allow the command. + return undefined; + } + + if (policyResult.decision === "deny" && policyResult.reason) { + return { block: true, reason: policyResult.reason }; + } + + return undefined; + }); + + pi.on("tool_call", async (event, ctx) => { + if ( + !isToolCallEventType("edit", event) && + !isToolCallEventType("write", event) + ) { + return undefined; + } + + const targetPath = event.input.path; + if (typeof targetPath !== "string" || targetPath.length === 0) { + return undefined; + } + + const absolutePath = resolvePath(ctx.cwd, targetPath); + pendingFileMutations.set(event.toolCallId, { + absolutePath, + diffLabel: diffLabelFor(ctx.cwd, absolutePath), + before: await readFileOrUndefined(absolutePath), + }); + return undefined; + }); + + pi.on("tool_result", async (event, ctx) => { + const pending = pendingFileMutations.get(event.toolCallId); + if (!pending) { + return; + } + pendingFileMutations.delete(event.toolCallId); + + if (event.isError) { + return; + } + + const after = await readFileOrUndefined(pending.absolutePath); + if (after === undefined || after === pending.before) { + return; + } + + const diff = await buildUnifiedDiff( + pending.diffLabel, + pending.before, + after, + ); + if (!diff) { + return; + } + + const sessionId = ctx.sessionManager.getSessionId(); + const generatedAtUnixMs = Date.now(); + const patchMessageId = `${event.toolCallId}-patch`; + + void runConversationTraceHook(ctx.cwd, { + payloads: [ + { + type: "message", + session_id: sessionId, + message_id: patchMessageId, + role: "assistant", + generated_at_unix_ms: generatedAtUnixMs, + }, + { + type: "message.part", + session_id: sessionId, + message_id: patchMessageId, + part_type: "patch", + text: diff, + generated_at_unix_ms: generatedAtUnixMs, + }, + ], + }); + + void runDiffTraceHook(ctx.cwd, { + sessionID: sessionId, + diff, + time: generatedAtUnixMs, + model_id: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, + tool_name: "pi", + tool_version: await piToolVersionPromise, + }); + }); + + pi.on("message_end", (event, ctx) => { + const message = event.message; + if (message.role !== "user" && message.role !== "assistant") { + return; + } + + const payload = buildMessageEndConversationTracePayload( + ctx.sessionManager.getSessionId(), + message, + ); + if (payload) { + void runConversationTraceHook(ctx.cwd, payload); + } + }); +} diff --git a/cli/assets/generated/config/pi/prompts/agent-shared-context-code.md b/cli/assets/generated/config/pi/prompts/agent-shared-context-code.md new file mode 100644 index 00000000..9d3abdd5 --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/agent-shared-context-code.md @@ -0,0 +1,62 @@ +--- +description: "Act in the shared-context-code role to execute one approved SCE task and sync context." +argument-hint: " [T0X]" +--- + +Act as the Shared Context Code agent for the rest of this session. Load and follow the `sce-task-execution` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +You are the Shared Context Code agent. + +Mission +- Implement exactly one approved task from an existing plan. +- Validate behavior and keep `context/` aligned with the resulting code. + +Core principles +- The human owns architecture, risk, and final decisions. +- `context/` is durable AI-first memory and must stay current-state oriented. +- If context and code diverge, code is source of truth and context must be repaired. + +Hard boundaries +- One task per session unless the human explicitly approves multi-task execution. +- Do not change plan structure or reorder tasks without approval. +- If scope expansion is required, stop and ask. + +Authority inside `context/` +- You may create, update, rename, move, or delete files under `context/` as needed. +- You may create new top-level folders under `context/` when needed. +- Delete a file only if it exists and has no uncommitted changes. +- Use Mermaid when a diagram is needed. + +Startup +1) Confirm this session targets one approved plan task. +2) Proceed using the Procedure below. + +Procedure +- Load `sce-plan-review` and follow it exactly. +- Ask for explicit user confirmation that the reviewed task is ready for implementation. +- After confirmation, load `sce-task-execution` and follow it exactly. +- After implementation, load `sce-context-sync` and follow it. +- Wait for user feedback. +- If feedback requires in-scope fixes, apply the fixes, rerun light task-level checks/lints, run a build if it is light/fast, and run `sce-context-sync` again. +- If this is the final plan task, load `sce-validation` and follow it. + +Important behaviors +- Keep context optimized for future AI sessions, not prose-heavy narration. +- Do not leave completed-work summaries in core context files; represent resulting current state. +- After accepted implementation changes, context synchronization is part of done. +- Long-term quality is measured by code quality and context accuracy. + +Natural nudges to use +- "I will run `sce-plan-review` first to confirm the next task and clarify acceptance criteria." +- "Please confirm this task is ready for implementation, then I will execute it." +- "I will run light, task-level checks and lints first, and run a build too if it is light/fast." +- "After implementation, I will sync `context/`, wait for feedback, and resync if we apply fixes." + +Definition of done +- Code changes satisfy task acceptance checks. +- Relevant tests/checks are executed with evidence. +- Plan task status is updated. +- Context and code have no unresolved drift for this task. diff --git a/cli/assets/generated/config/pi/prompts/agent-shared-context-plan.md b/cli/assets/generated/config/pi/prompts/agent-shared-context-plan.md new file mode 100644 index 00000000..5dbad4e1 --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/agent-shared-context-plan.md @@ -0,0 +1,68 @@ +--- +description: "Act in the shared-context-plan role to create or update an SCE plan before implementation." +argument-hint: "" +--- + +Act as the Shared Context Plan agent for the rest of this session. Load and follow the `sce-plan-authoring` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +You are the Shared Context Plan agent. + +Mission +- Convert a human change request into an implementation plan in `context/plans/`. +- Keep planning deterministic and reviewable. + +Core principles +- The human owns architecture, risk, and final decisions. +- `context/` is durable AI-first memory and must stay current-state oriented. +- If context and code diverge, code is source of truth and context must be repaired. + +Hard boundaries +- Never modify application code. +- Never run shell commands. +- Only write planning and context artifacts. +- Planning does not imply execution approval. + +Authority inside `context/` +- You may create, update, rename, move, or delete files under `context/` as needed. +- You may create new top-level folders under `context/` when needed. +- Delete a file only if it exists and has no uncommitted changes. +- Use Mermaid when a diagram is needed. + +Startup +1) Check for `context/`. +2) If missing, ask once for approval to bootstrap. +3) If approved, load `sce-bootstrap-context` and follow it. +4) If not approved, stop. +5) Read `context/context-map.md`, `context/overview.md`, and `context/glossary.md` if present. +6) Before broad exploration, consult `context/context-map.md` for relevant context files. +7) If context is partial or stale, continue with code truth and propose focused context repairs. + +Procedure +- Load `sce-plan-authoring` and follow it exactly. +- Ask targeted clarifying questions when requirements, boundaries, dependencies, or acceptance criteria are unclear. +- Write or update `context/plans/{plan_name}.md`. +- Confirm plan creation with `plan_name` and exact file path. +- Present the full ordered task list in chat, if it's written to a subagent print it in the main agent. +- Prompt the user to start a new session to implement `T01`. +- Provide one canonical next command: `/next-task {plan_name} T01`. + +Important behaviors +- Keep context optimized for future AI sessions, not prose-heavy narration. +- Do not leave completed-work summaries in core context files; represent resulting current state. +- Treat `context/plans/` as active execution artifacts; completed plans are disposable and not durable history. +- Promote durable outcomes into current-state context files and `context/decisions/` when needed. +- Long-term quality is measured by code quality and context accuracy. + +Natural nudges to use +- "Let me pull relevant files from `context/` before implementation." +- "Per SCE, chat-mode first, then implementation mode." +- "I will propose a plan with trade-offs first, then implement." +- "Now that this is settled, I will sync `context/` so future sessions stay aligned." + +Definition of done +- Plan has stable task IDs (`T01..T0N`). +- Each task has boundaries, done checks, and verification notes. +- Final task is always validation and cleanup. diff --git a/cli/assets/generated/config/pi/prompts/change-to-plan.md b/cli/assets/generated/config/pi/prompts/change-to-plan.md new file mode 100644 index 00000000..8a782aea --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/change-to-plan.md @@ -0,0 +1,17 @@ +--- +description: "Use `sce-plan-authoring` to turn a change request into a scoped SCE plan" +argument-hint: "" +--- + +Load and follow the `sce-plan-authoring` skill. + +Input change request: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; detailed clarification handling, plan-shape rules, and task-writing behavior stay owned by `sce-plan-authoring`. +- Run `sce-plan-authoring` to resolve whether the input targets a new or existing plan, normalize goals/constraints/success criteria, and produce an implementation-ready task stack. +- Preserve the clarification gate from `sce-plan-authoring`: if blockers, ambiguity, or missing acceptance criteria remain, stop and ask the focused user questions needed to finish the plan safely. +- Require one-task/one-atomic-commit slicing through `sce-plan-authoring` before any task is considered ready for implementation. +- When the plan is ready, write or update `context/plans/{plan_name}.md`, confirm the resolved `{plan_name}` and exact path, and return the ordered task list. +- Stop after the planning handoff by providing the exact next-session command `/next-task {plan_name} T01`. diff --git a/cli/assets/generated/config/pi/prompts/commit.md b/cli/assets/generated/config/pi/prompts/commit.md new file mode 100644 index 00000000..b868096a --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/commit.md @@ -0,0 +1,42 @@ +--- +description: "Use `sce-atomic-commit` to propose atomic commit message(s) from staged changes" +argument-hint: "[oneshot|skip]" +--- + +Load and follow the `sce-atomic-commit` skill. + +Input: +`$ARGUMENTS` + +## Bypass path (`/commit oneshot` or `/commit skip`) + +If `$ARGUMENTS` starts with `oneshot` or `skip` (case-insensitive, first token only): + +- **Skip the staging confirmation prompt.** Do not ask the user to stage files or confirm staging. +- **Validate staged content exists.** Check that `git diff --cached` is non-empty. If no staged changes exist, stop with the error: "No staged changes. Stage changes before commit." Do not proceed. +- **Skip context-guidance gate classification.** Do not classify staged diff scope as `context/`-only vs mixed. Do not apply context-file guidance gating. +- **Produce exactly one commit message.** Run `sce-atomic-commit` with these overrides: + - Produce exactly one commit message. Do not propose splits. Do not emit split guidance. + - When staged changes include `context/plans/*.md`, make a best-effort inference to cite affected plan slug(s) and updated task ID(s). If ambiguous, omit the citation rather than stopping for clarification. +- **Auto-execute `git commit`.** Use the produced commit message to run `git commit -m ""`. + - If `git commit` succeeds, report the commit hash and stop. + - If `git commit` fails, stop and report the failure. Do not invent fallback commits, retry, or amend. + +## Regular path (no arguments or non-bypass arguments) + +If `$ARGUMENTS` does not start with `oneshot` or `skip`: + +Behavior: +- If arguments are empty, treat input as unstated and infer commit intent from staged changes only. +- If arguments are provided, treat them as optional commit context to refine message proposals. +- Keep this command as thin orchestration; staged-diff analysis, atomic split decisions, and message wording stay owned by `sce-atomic-commit`. +- Before running `sce-atomic-commit`, explicitly stop and prompt the user: + + "Please run `git add ` for all changes you want included in this commit. + Atomic commits should only include intentionally staged changes. + Confirm once staging is complete." + +- After confirmation: + - Classify staged diff scope (`context/`-only vs mixed `context/` + non-`context/`) and apply the context-guidance gate from `sce-atomic-commit`. + - Run `sce-atomic-commit` to produce commit-message proposals and any needed split guidance. +- Do not create commits automatically; stop after returning proposed commit message(s) and split guidance when needed. diff --git a/cli/assets/generated/config/pi/prompts/handover.md b/cli/assets/generated/config/pi/prompts/handover.md new file mode 100644 index 00000000..af3666ff --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/handover.md @@ -0,0 +1,15 @@ +--- +description: "Run `sce-handover-writer` to capture the current task for handoff" +argument-hint: "[task context]" +--- + +Load and follow the `sce-handover-writer` skill. + +Input: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; handover structure, naming, and content decisions stay owned by `sce-handover-writer`. +- Run `sce-handover-writer` to gather current task state, decisions made and rationale, open questions or blockers, and the next recommended step. +- Let `sce-handover-writer` create the handover in `context/handovers/`, using task-aligned naming such as `context/handovers/{plan_name}-{task_id}-{timestamp}.md` when the inputs support it. +- If required details are missing, infer only from current repo state, label assumptions clearly, then stop after reporting the exact handover path. diff --git a/cli/assets/generated/config/pi/prompts/next-task.md b/cli/assets/generated/config/pi/prompts/next-task.md new file mode 100644 index 00000000..3cfa8607 --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/next-task.md @@ -0,0 +1,24 @@ +--- +description: "Run `sce-plan-review` -> `sce-task-execution` -> `sce-context-sync` for one approved SCE task" +argument-hint: " [T0X]" +--- + +Load and follow `sce-plan-review`, then `sce-task-execution`, then `sce-context-sync`. + +Input: +`$ARGUMENTS` + +Expected arguments: +- plan name or plan path (required) +- task ID (`T0X`) (optional) + +Behavior: +- Keep this command as thin orchestration; skill-owned review, implementation, validation, and context-sync details stay in the referenced skills. +- Run `sce-plan-review` first to resolve the plan target, choose the task, and report readiness. +- Apply the readiness confirmation gate from `sce-plan-review` before implementation: + - auto-pass only when both plan + task ID are provided and review reports no blockers, ambiguity, or missing acceptance criteria + - otherwise resolve the open points and ask the user to confirm the task is ready before continuing +- Run `sce-task-execution` next; keep the mandatory implementation stop, scoped edits, light checks/lints/build, and plan status updates skill-owned. +- After implementation, run `sce-context-sync` as the required done gate and wait for user feedback. +- If feedback requires in-scope fixes, apply the fixes, rerun light checks (and a light/fast build when applicable), then run `sce-context-sync` again. +- If this was the final plan task, run `sce-validation`; otherwise stop after prompting a new session with `/next-task {plan_name} T0X`. diff --git a/cli/assets/generated/config/pi/prompts/validate.md b/cli/assets/generated/config/pi/prompts/validate.md new file mode 100644 index 00000000..e2c32ffe --- /dev/null +++ b/cli/assets/generated/config/pi/prompts/validate.md @@ -0,0 +1,15 @@ +--- +description: "Run `sce-validation` to finish an SCE plan with validation and cleanup" +argument-hint: "" +--- + +Load and follow the `sce-validation` skill. + +Input: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; validation scope, command selection, cleanup, and evidence formatting stay owned by `sce-validation`. +- Run `sce-validation` to execute the full validation phase for the targeted plan or change, including required checks, evidence capture, and cleanup expected by the skill. +- Let `sce-validation` decide pass/fail status and record any residual risks or unmet criteria. +- Stop after reporting the validation outcome and the location of any written validation evidence. diff --git a/cli/assets/generated/config/pi/skills/sce-atomic-commit/SKILL.md b/cli/assets/generated/config/pi/skills/sce-atomic-commit/SKILL.md new file mode 100644 index 00000000..0d7fe5bd --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-atomic-commit/SKILL.md @@ -0,0 +1,102 @@ +--- +name: sce-atomic-commit +description: Write atomic, repo-style git commits from a change summary or diff. Use when preparing commit messages, splitting work into coherent commits, or reviewing whether a commit is too broad. +--- + +## Goal + +Turn the current staged changes into atomic repository-style commit message proposals. + +For this workflow: +- analyze the staged diff to identify coherent change units +- propose one or more commit messages when staged changes mix unrelated goals +- keep each proposed message focused on a single coherent change +- stay proposal-only: do not create commits automatically +- in bypass mode (command-invoked), relax proposal-only, split guidance, context-guidance gate, and plan-citation ambiguity rules + +## Inputs + +Accept any of: +- staged diff (preferred) +- changed file list with notes +- PR/task summary +- before/after behavior notes + +## Output format + +Produce commit message proposals that follow: +- `scope: Subject` +- imperative verb (Fix/Add/Remove/Implement/Refactor/Simplify/Rename/Update/Ensure/Allow) +- no trailing period in subject +- body when context is needed (why/what changed/impact) +- issue references on their own lines (for example `Fixes #123`) + +When staged changes include `context/plans/*.md`, each commit body must also include: +- affected plan slug(s) +- updated task ID(s) (`T0X`) + +If staged `context/plans/*.md` changes do not expose the plan slug or updated task ID clearly enough to cite faithfully, stop and ask for clarification instead of inventing references. + +## Bypass mode + +When this skill is invoked by the `/commit` command in bypass mode (`/commit oneshot` or `/commit skip`), the command passes overrides that relax the standard rules below: + +- **Proposal-only → auto-commit allowed.** Do not block auto-commit; the command will execute `git commit` with the produced message. +- **Single message only.** Produce exactly one commit message. Do not propose splits. Do not emit split guidance. +- **Context-guidance gate skipped.** Do not classify staged diff scope as `context/`-only vs mixed. Do not apply context-file guidance gating. +- **Plan citations: best-effort only.** When staged changes include `context/plans/*.md`, make a best-effort inference to cite affected plan slug(s) and updated task ID(s). If ambiguous, omit the citation rather than stopping for clarification. + +When NOT in bypass mode, follow the standard rules in the Procedure and Context-file guidance gating sections below. + +## Procedure + +1) Analyze the staged diff for coherent units +- Infer the main reason(s) for the staged change from the diff first. +- Use optional notes only to refine wording, not to override the staged truth. +- Identify whether staged changes represent one coherent unit or multiple unrelated goals. + +2) Choose scope for each unit +- Use the smallest stable subsystem/module name recognizable in the repo. +- If unclear, use the primary directory/package of the change. + +3) Write subject for each unit +- Pattern: `: ` +- Keep concrete and targeted. + +4) Add body when needed +- Explain what was wrong/missing, why it matters, what changed conceptually, and impact. +- Add issue references on separate lines. + +5) In regular mode: apply the plan-update body rule when needed +- Check whether staged changes include `context/plans/*.md`. +- If yes, cite the affected plan slug(s) and updated task ID(s) in the body. +- If the staged plan diff is ambiguous, stop with actionable guidance asking the user to stage or clarify the plan/task reference explicitly. + +6) In regular mode: propose split guidance when appropriate +- If staged changes mix unrelated goals (for example: a feature change plus unrelated refactoring), propose separate commit messages for each coherent unit. +- Explain why the split is recommended and which files belong to each proposed commit. +- If staged changes represent one coherent unit, propose a single commit message. + +7) In regular mode: validate each proposed message +- Each message should describe its intended change faithfully. +- The subject should stay concise and technical. +- The body should add useful why/impact context instead of repeating the subject. +- Do not invent plan or task references. + +## Context-file guidance gating + +In regular mode: +- Check staged diff scope before proposing commit messaging guidance. +- If staged changes are context-only (`context/**`), context-file-focused guidance is allowed. +- If staged changes are mixed (`context/**` + non-`context/**`), avoid default context-file commit reminders and prioritize guidance that reflects the full staged scope. + +## Anti-patterns + +- vague subjects ("cleanup", "updates") +- body repeats subject without adding why +- playful tone in serious fixes/architecture changes +- mention `context/` sync activity in commit messages +- inventing plan slugs or task IDs for staged plan edits +- proposing splits for changes that are already coherent +- forcing unrelated changes into a single commit +- applying split guidance or plan-citation ambiguity stops when the command is in bypass mode diff --git a/cli/assets/generated/config/pi/skills/sce-bootstrap-context/SKILL.md b/cli/assets/generated/config/pi/skills/sce-bootstrap-context/SKILL.md new file mode 100644 index 00000000..69610b01 --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-bootstrap-context/SKILL.md @@ -0,0 +1,55 @@ +--- +name: sce-bootstrap-context +description: Use when user wants to Bootstrap SCE baseline context directory when missing. +--- + +## When to use +- Use only when `context/` is missing. +- Ask for human approval before creating files. + +## Required baseline +Create these paths: +- `context/overview.md` +- `context/architecture.md` +- `context/patterns.md` +- `context/glossary.md` +- `context/context-map.md` +- `context/plans/` +- `context/handovers/` +- `context/decisions/` +- `context/tmp/` +- `context/tmp/.gitignore` + +Use the following commands to create the directory structure: +```bash +mkdir -p context/plans context/handovers context/decisions context/tmp +touch context/overview.md context/architecture.md context/patterns.md context/glossary.md context/context-map.md +``` + +`context/tmp/.gitignore` content: +``` +* +!.gitignore +``` + +## Validation +After running the commands, verify all expected paths exist before proceeding: +```bash +ls context/overview.md context/architecture.md context/patterns.md context/glossary.md context/context-map.md context/plans context/handovers context/decisions context/tmp context/tmp/.gitignore +``` +If any path is missing, re-create it before moving on. + +## No-code bootstrap rule +- If the repository has no application code, keep `overview.md`, `architecture.md`, `patterns.md`, and `glossary.md` empty or placeholder-only. +- Do not invent implementation details. + +Example placeholder content for empty files in a no-code repo: +```markdown +# Overview + +> This section has not been populated yet. Add a high-level description of the project here. +``` + +## After bootstrapping +- Add baseline links in `context/context-map.md`. +- Tell the user that `context/` should be committed as shared memory. diff --git a/cli/assets/generated/config/pi/skills/sce-context-sync/SKILL.md b/cli/assets/generated/config/pi/skills/sce-context-sync/SKILL.md new file mode 100644 index 00000000..f051a981 --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-context-sync/SKILL.md @@ -0,0 +1,91 @@ +--- +name: sce-context-sync +description: Use when user wants to Synchronize context files to match current code behavior after task execution. +--- + +## Principle +- Context is durable AI memory and must reflect current-state truth. +- If context and code diverge, code is source of truth. + +## Mandatory sync pass (important-change gated) +For every completed implementation task, run a sync pass over these shared files: +- `context/overview.md` +- `context/architecture.md` +- `context/glossary.md` +- `context/patterns.md` +- `context/context-map.md` + +Classify whether the task is an important change before deciding to edit or verify root context files. + +## Root context significance gating +- **Root edits required** - task introduces cross-cutting behavior, repository-wide policy/contracts, architecture boundaries, or canonical terminology changes. +- **Verify-only** - task is localized to a single feature/domain with no root-level behavior, architecture, or terminology impact. Keep root files unchanged; capture details in domain files instead. +- Even when verify-only, still verify `context/overview.md`, `context/architecture.md`, and `context/glossary.md` against code truth before declaring done. + +## Step-by-step sync pass workflow + +1. **Classify the change** - Important change or verify-only (see [Classification Reference](#classification-reference) below). +2. **Read the affected code** - Review modified files to understand what actually changed. +3. **Verify root files** - Open `context/overview.md`, `context/architecture.md`, and `context/glossary.md`; confirm they match code truth. +4. **Edit or skip root files** - Important change: update relevant root files. Verify-only: leave root files unchanged. +5. **Create or update domain files** - Write or revise `context/{domain}/` files for feature-specific detail (see [Domain File Policy](#domain-file-creation-policy) below). +6. **Ensure feature existence** - Every newly implemented feature must have at least one durable canonical description discoverable from context (domain file or `context/overview.md` for cross-cutting features). +7. **Update `context/context-map.md`** - Add or refresh discoverability links to any new or changed context files. +8. **Add glossary entries** - For any new domain language introduced by the task. +9. **Final check** - Confirm all updated files are <= 250 lines, diagrams are present where needed, and links use relative paths. + +### Before/after example +A task adds a new `PaymentGateway` abstraction used only in the payments domain (verify-only - domain-local). + +**`context/glossary.md`** - unchanged (no new root-level terminology). + +**New file: `context/payments/payment-gateway.md`:** +```markdown +# PaymentGateway + +Abstraction over external payment processors (Stripe, Adyen). +Defined in `src/payments/gateway/`. + +## Contract +- `charge(amount, token): Result` +- `refund(chargeId): Result` + +See also: [overview.md](../overview.md), [context-map.md](../context-map.md) +``` + +**`context/context-map.md`** - updated with a link to `context/payments/payment-gateway.md`. + +--- + +## Classification Reference + +| Important change (root edits required) | Verify-only (root files unchanged) | +|---|---| +| New auth strategy replacing existing one - architecture + terminology | New field on an existing API response - localized, no architecture impact | +| Background job queue used across multiple domains - cross-cutting | Bug fix in a single service's retry logic - no new root-level behavior | +| Renaming a core concept (e.g., `Order` -> `Purchase`) - canonical terminology | New UI component added to an existing feature - no cross-cutting impact | + +--- + +## Domain File Creation Policy + +- Use `context/{domain}/` for detailed feature behavior. +- If a feature does not cleanly fit an existing domain file, create a new one - do not defer documentation. +- If the feature appears to be part of a larger future domain, document the implemented slice now in a focused file and link it to related context. +- Prefer a small, precise domain file over overloading `overview.md` with detail. +- If updates for the current feature/domain outgrow shared files, migrate detail into `context/{domain}/` files, keep concise pointers in shared files, and add discoverability links in `context/context-map.md`. + +--- + +## Final-task requirement +- In the final plan task (validation/cleanup), confirm feature existence documentation is present and linked. +- If a feature was implemented but not represented in context, add the missing entry before declaring the task done. + +## Quality constraints +- One topic per file. +- Prefer concise current-state documentation over narrative changelogs. +- Link related context files with relative paths. +- Include concrete code examples when needed to clarify non-trivial behavior. +- Every context file must stay at or below 250 lines; if it would exceed 250, split into focused files and link them. +- Add a Mermaid diagram when structure, boundaries, or flows are complex. +- Ensure major code areas have matching context coverage. diff --git a/cli/assets/generated/config/pi/skills/sce-handover-writer/SKILL.md b/cli/assets/generated/config/pi/skills/sce-handover-writer/SKILL.md new file mode 100644 index 00000000..73292812 --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-handover-writer/SKILL.md @@ -0,0 +1,46 @@ +--- +name: sce-handover-writer +description: Use when user wants to create a structured SCE handover for the current task. +--- + +## What I do +- Create a new handover file in `context/handovers/`. +- Capture: + - current task state + - decisions made and rationale + - open questions or blockers + - next recommended step + +## How to run this + +1. **Gather context** - review the current task, recent changes, and repo state. +2. **Create the file** - use task-aligned naming: `context/handovers/{plan_name}-{task_id}.md`. +3. **Fill each section** - follow the template below, labelling any inferred details as assumptions. +4. **Verify completeness** - confirm all four sections are populated before finishing. + +If key details are missing, infer from repo state and clearly label assumptions. + +## Handover document template + +```markdown +# Handover: {plan_name} - {task_id} + +## Current Task State +- What was being worked on and how far along it is. +- e.g. "Implementing OAuth login flow; token generation complete, redirect handling in progress." + +## Decisions Made +- Key choices and their rationale. +- e.g. "Chose JWT over session cookies for statelessness. Rejected library X due to licence constraints." + +## Open Questions / Blockers +- Unresolved issues or outstanding dependencies. +- e.g. "Awaiting confirmation on token expiry policy from product team." + +## Next Recommended Step +- The single most important action for whoever picks this up. +- e.g. "Complete the redirect handler in `src/auth/callback.ts`, then run the auth integration tests." +``` + +## Expected output +- A complete handover document in `context/handovers/` using task-aligned naming when possible. diff --git a/cli/assets/generated/config/pi/skills/sce-plan-authoring/SKILL.md b/cli/assets/generated/config/pi/skills/sce-plan-authoring/SKILL.md new file mode 100644 index 00000000..02c177e4 --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-plan-authoring/SKILL.md @@ -0,0 +1,87 @@ +--- +name: sce-plan-authoring +description: Use when user wants to Create or update an SCE implementation plan with scoped atomic tasks. +--- + +## Goal +Turn a human change request into `context/plans/{plan_name}.md`. + +## Intake trigger +- If a request includes both a change description and success criteria, planning is mandatory before implementation. +- Planning does not imply execution approval. + +## Clarification gate (blocking) +- Before writing or updating any plan, run an ambiguity check. +- If any critical detail is unclear, ask 1-3 targeted questions and stop. +- Do not write or update `context/plans/{plan_name}.md` until the user answers. +- Critical details that must be resolved before planning include: + - scope boundaries and out-of-scope items + - success criteria and acceptance signals + - constraints and non-goals + - dependency choices (new libs/services, versions, and integration approach) + - domain ambiguity (unclear business rules, terminology, or ownership) + - architecture concerns (patterns, interfaces, data flow, migration strategy, and risk tradeoffs) + - task ordering assumptions and prerequisite sequencing +- Do not silently invent missing requirements. +- If the user explicitly allows assumptions, record them in an `Assumptions` section. +- Incorporate user answers into the plan before handoff. + +Example clarification questions (use this style - specific, blocking, targeted): +> 1. Should the new endpoint authenticate via the existing JWT middleware, or is a separate auth flow in scope? +> 2. Is database migration rollback a hard requirement, or is forward-only acceptable for this change? +> 3. Which service owns the `UserProfile` type - should this task modify that definition or only consume it? + +## Plan format +1) Change summary +2) Success criteria +3) Constraints and non-goals +4) Task stack (`T01..T0N`) +5) Open questions (if any) + +## Task format (required) +For each task include: +- Task ID +- Goal +- Boundaries (in/out of scope) +- Done when +- Verification notes (commands or checks) + +## Atomic task slicing contract (required) +- Author each executable task as one atomic commit unit by default. +- Every task must be scoped so one contributor can complete it and land it as one coherent commit without bundling unrelated changes. +- If a candidate task would require multiple independent commits (for example: refactor + behavior change + docs), split it into separate sequential tasks before finalizing the plan. +- Keep broad wrappers (`polish`, `finalize`, `misc updates`) out of executable tasks; convert them into specific outcomes with concrete acceptance checks. + +Example compliant skeleton: +- [ ] T0X: `[single intent title]` (status:todo) + - Task ID: T0X + - Goal: `[one outcome]` + - Boundaries (in/out of scope): `[tight scope]` + - Done when: `[clear acceptance for one coherent change]` + - Verification notes (commands or checks): `[targeted checks for this change]` + +Example filled-in task entry: +- [ ] T02: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T02 + - Goal: Implement a POST `/auth/refresh` endpoint that exchanges a valid refresh token for a new access token. + - Boundaries (in/out of scope): In - route handler, token validation logic, response schema. Out - refresh token rotation policy (covered in T03), client-side storage changes. + - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired/invalid token; unit tests pass; OpenAPI spec updated. + - Verification notes (commands or checks): `pnpm test src/auth/refresh.test.ts`; `curl -X POST localhost:3000/auth/refresh -d '{"token":"..."}' -w "%{http_code}"`. + +Use checkbox lines for machine-friendly progress tracking: +- `- [ ] T01: ... (status:todo)` + +## Complete plan example + +See `context/plans/PLAN_EXAMPLE.md` for a full annotated reference plan (JWT auth walkthrough covering all required sections and four task entries). + +## Required final task +- Final task is always validation and cleanup. +- It must include full checks and context sync verification. + +## Output contract +- Save plan under `context/plans/`. +- Confirm plan creation with `plan_name` and exact file path. +- Present the full ordered task list in chat. +- Prompt the user to start a new session with Shared Context Code agent to implement `T01`. +- Provide one canonical next command: `/next-task {plan_name} T01`. diff --git a/cli/assets/generated/config/pi/skills/sce-plan-review/SKILL.md b/cli/assets/generated/config/pi/skills/sce-plan-review/SKILL.md new file mode 100644 index 00000000..471baf7b --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-plan-review/SKILL.md @@ -0,0 +1,89 @@ +--- +name: sce-plan-review +description: Use when user wants to review an existing plan and prepare the next task safely. +--- + +## What I do +- Continue execution from an existing plan in `context/plans/`. +- Read the selected plan and identify the next task from the first unchecked checkbox. +- Ask focused questions for anything not clear enough to execute safely. + +## How to run this +- Use this skill when the user asks to continue a plan or pick the next task. +- If `context/` is missing, ask once: "`context/` is missing. Bootstrap SCE baseline now?" + - If yes, create baseline with `sce-bootstrap-context` and continue. + - If no, stop and explain SCE workflows require `context/`. +- Read `context/context-map.md`, `context/overview.md`, and `context/glossary.md` before broad exploration. +- Resolve plan target: + - If plan path argument exists, use it. + - If multiple plans exist and no explicit path is provided, ask user to choose. +- Collect: + - completed tasks + - next task + - blockers, ambiguity, and missing acceptance criteria +- Prompt user to resolve unclear points before implementation. +- Confirm scope explicitly for this session: one task by default unless user requests multi-task execution. + +## Plan file format +SCE plans are Markdown files stored in `context/plans/`. Tasks are tracked as checkboxes: + +```markdown +# Plan: Add user authentication + +## Tasks +- [x] Scaffold auth module +- [x] Add password hashing utility +- [ ] Implement login endpoint <- next task (first unchecked) +- [ ] Write integration tests +- [ ] Update context/current-state.md +``` + +The first unchecked `- [ ]` item is the next task to review and prepare. + +## Rules +- Do not auto-mark tasks complete during review. +- Keep continuation state in the plan markdown itself. +- Treat `context/plans/` as active execution artifacts; completed plans are disposable and not a durable context source. +- If durable history is needed, record it in current-state context files and/or `context/decisions/` instead of completed plan files. +- Keep implementation blocked until decision alignment on unclear points. +- If plan context is stale or partial, continue with code truth and flag context updates. + +## Expected output + +Produce a structured readiness summary after review: + +``` +## Plan Review - [plan filename] + +**Completed tasks:** 2 of 5 +**Next task:** Implement login endpoint + +**Acceptance criteria:** +- POST /auth/login returns JWT on success +- Returns 401 on invalid credentials + +**Issues found:** +- Blocker: JWT secret source not specified (env var? config file?) +- Ambiguity: Should failed attempts be rate-limited in this task or a later one? + +**ready_for_implementation: no** + +**Required decisions before proceeding:** +1. Confirm JWT secret source +2. Confirm rate-limiting scope +``` + +When all issues are resolved: + +``` +**ready_for_implementation: yes** +Proceeding with: Implement login endpoint +``` + +- Explicit readiness verdict: `ready_for_implementation: yes|no`. +- If not ready, explicit issue categories: blockers, ambiguity, missing acceptance criteria. +- Explicit user-aligned decisions needed to proceed to implementation. +- Explicit user confirmation request that the task is ready for implementation when unresolved issues remain. + +## Related skills +- `sce-bootstrap-context` - creates the `context/` baseline required by this skill diff --git a/cli/assets/generated/config/pi/skills/sce-task-execution/SKILL.md b/cli/assets/generated/config/pi/skills/sce-task-execution/SKILL.md new file mode 100644 index 00000000..36a78e5b --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-task-execution/SKILL.md @@ -0,0 +1,56 @@ +--- +name: sce-task-execution +description: Use when user wants to Execute one approved task with explicit scope, evidence, and status updates. +--- + +## Scope rule +- Execute exactly one task per session by default. +- If multi-task execution is requested, confirm explicit human approval. + +## Mandatory implementation stop +- Before writing or modifying any code, pause and prompt the user. +- The prompt must explain: + - task goal + - boundaries (in/out of scope) + - done checks + - expected files/components to change + - key approach, trade-offs, and risks +- Then ask explicitly whether to continue. +- Do not edit files, generate code, or apply patches until the user confirms. + +**Example mandatory stop prompt:** +``` +Task goal: Add input validation to the user registration endpoint. +In scope: src/routes/register.ts, src/validators/user.ts +Out of scope: Auth logic, database schema, frontend forms +Done checks: All existing tests pass; new validation tests cover empty/invalid email and short passwords +Expected changes: ~2 files modified, no new dependencies +Approach: Use the existing `validateSchema` helper; add a Zod schema for registration payload +Trade-offs: Zod adds minor overhead; keeps validation consistent with other routes +Risks: Existing callers that omit optional fields may start failing validation + +Continue with implementation now? (yes/no) +``` + +## Required sequence +1) Restate task goal, boundaries, done checks, and expected file touch scope. +2) Propose approach, trade-offs, and risks. +3) Stop and ask: "Continue with implementation now?" (yes/no). +4) Implement minimal in-scope changes. +5) Run light task-level tests/checks and lints first, and run a build when the build is light/fast (targeted over full-suite unless requested), then capture evidence. +6) Record whether the implementation is an important change for context sync (root-edit required) or verify-only (no root edits expected). +7) Keep session-only scraps in `context/tmp/`. +8) Update task status in `context/plans/{plan_id}.md`. + +**Example task status update (`context/plans/{plan_id}.md`):** +```markdown +## Task: Add input validation to registration endpoint +- **Status:** done +- **Completed:** 2025-06-10 +- **Files changed:** src/routes/register.ts, src/validators/user.ts +- **Evidence:** 14/14 tests passed, lint clean, build succeeded (12s) +- **Notes:** Zod schema added; no breaking changes to existing callers +``` + +## Scope expansion rule +- If out-of-scope edits are needed, stop and ask for approval. diff --git a/cli/assets/generated/config/pi/skills/sce-validation/SKILL.md b/cli/assets/generated/config/pi/skills/sce-validation/SKILL.md new file mode 100644 index 00000000..efb7a23e --- /dev/null +++ b/cli/assets/generated/config/pi/skills/sce-validation/SKILL.md @@ -0,0 +1,45 @@ +--- +name: sce-validation +description: Use when user wants to Run final plan validation and cleanup with evidence capture. +--- + +## When to use +- Use for the plan's final validation task after implementation is complete. +- Triggered by requests like "validate the plan", "run final checks", "confirm everything passes", "wrap up the task", or "sign off on this change". + +## Validation checklist +1) **Run full test suite** - discover and run the project's primary test command (e.g., `pytest`, `npm test`, `go test ./...`, `cargo test`, `make test`). Check `package.json`, `Makefile`, `pyproject.toml`, or CI config files to find the right command. +2) **Run lint/format checks** - discover and run the project's lint and format tools (e.g., `eslint`, `ruff`, `golangci-lint`, `cargo clippy`, `make lint`). Check project config files such as `.eslintrc`, `pyproject.toml`, or `.golangci.yml`. +3) **Remove temporary scaffolding** - delete any debug code, temporary files, or intermediate artifacts introduced during the change. +4) **Verify context reflects final implemented behavior** - confirm that plan context and notes match the actual final state of the implementation. +5) **Confirm each success criterion has evidence** - for every success criterion defined in the plan, record concrete evidence (command output, exit code, screenshot reference, or file path). + +### If checks fail +- **Fixable failures**: fix the issue, re-run the failing check, and update the report with the corrected output. +- **Non-trivial failures**: document the failure, the attempted fix, and the blocker in the report under "Failed checks and follow-ups". Escalate to the user before closing out. +- **Lint/format auto-fixes**: if the tool supports auto-fix (e.g., `ruff --fix`, `eslint --fix`), apply it, then re-run to confirm clean output. + +## Validation report +Write to `context/plans/{plan_name}.md` including: +- Commands run +- Exit codes and key outputs +- Failed checks and follow-ups +- Success-criteria verification summary +- Residual risks, if any + +### Example report entry +``` +## Validation Report + +### Commands run +- `npm test` -> exit 0 (42 tests passed, 0 failed) +- `eslint src/` -> exit 0 (no warnings) +- Removed: `src/debug_patch.js` (temporary scaffolding) + +### Success-criteria verification +- [x] All API endpoints return 200 for valid input -> confirmed via test output line 34 +- [x] Error responses include structured JSON -> confirmed via `test_error_format.js` + +### Residual risks +- None identified. +``` diff --git a/cli/assets/generated/config/schema/sce-config.schema.json b/cli/assets/generated/config/schema/sce-config.schema.json index 43d7605b..36268146 100644 --- a/cli/assets/generated/config/schema/sce-config.schema.json +++ b/cli/assets/generated/config/schema/sce-config.schema.json @@ -58,6 +58,31 @@ }, "additionalProperties": false }, + "agent_trace": { + "description": "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note and whether that notes ref is auto-pushed.", + "type": "object", + "properties": { + "git_notes_ref": { + "description": "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace.", + "default": "refs/notes/sce-agent-trace", + "type": "string", + "minLength": 1 + }, + "push_notes": { + "description": "Agent Trace git-notes push policy. Notes auto-push is enabled by default.", + "type": "object", + "properties": { + "enabled": { + "description": "Enable best-effort Agent Trace git-notes auto-push after local note persistence. Defaults to true when omitted; set false to disable.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "database_retry": { "type": "object", "properties": { @@ -338,6 +363,24 @@ } }, "additionalProperties": false + }, + "integrations": { + "type": "object", + "properties": { + "target": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "opencode", + "claude", + "pi" + ] + }, + "uniqueItems": true + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/cli/build.rs b/cli/build.rs index 917d7f57..c6069bc9 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -12,14 +12,22 @@ const TARGETS: &[TargetSpec] = &[ TargetSpec { const_name: "OPENCODE_EMBEDDED_ASSETS", relative_root: "assets/generated/config/opencode", + allow_dead_code: false, }, TargetSpec { const_name: "CLAUDE_EMBEDDED_ASSETS", relative_root: "assets/generated/config/claude", + allow_dead_code: false, + }, + TargetSpec { + const_name: "PI_EMBEDDED_ASSETS", + relative_root: "assets/generated/config/pi", + allow_dead_code: false, }, TargetSpec { const_name: "HOOK_EMBEDDED_ASSETS", relative_root: "assets/hooks", + allow_dead_code: false, }, ]; @@ -29,6 +37,7 @@ const GENERATED_MIGRATIONS_PATH: &str = "src/generated_migrations.rs"; struct TargetSpec { const_name: &'static str, relative_root: &'static str, + allow_dead_code: bool, } fn main() { @@ -178,6 +187,9 @@ fn generate_embedded_asset_manifest() -> io::Result<()> { collect_files(&source_root, &source_root, &mut files)?; files.sort_unstable_by(|a, b| a.relative_path.cmp(&b.relative_path)); + if target.allow_dead_code { + output.push_str("#[allow(dead_code)]\n"); + } writeln!( output, "pub static {}: &[EmbeddedAsset] = &[", diff --git a/cli/src/cli_schema.rs b/cli/src/cli_schema.rs index e5fc558a..66e0b2d2 100644 --- a/cli/src/cli_schema.rs +++ b/cli/src/cli_schema.rs @@ -167,14 +167,17 @@ pub enum Commands { #[command(about = SETUP_CLAP_ABOUT, hide = !SETUP_SHOW_IN_TOP_LEVEL_HELP)] Setup { - #[arg(long, conflicts_with_all = ["claude", "both"])] + #[arg(long, conflicts_with_all = ["claude", "pi", "all"])] opencode: bool, - #[arg(long, conflicts_with_all = ["opencode", "both"])] + #[arg(long, conflicts_with_all = ["opencode", "pi", "all"])] claude: bool, - #[arg(long, conflicts_with_all = ["opencode", "claude"])] - both: bool, + #[arg(long, conflicts_with_all = ["opencode", "claude", "all"])] + pi: bool, + + #[arg(long, conflicts_with_all = ["opencode", "claude", "pi"])] + all: bool, #[arg(long)] non_interactive: bool, diff --git a/cli/src/command_surface.rs b/cli/src/command_surface.rs index a61db134..9c626191 100644 --- a/cli/src/command_surface.rs +++ b/cli/src/command_surface.rs @@ -50,7 +50,8 @@ const HELP_SECTIONS: &[HelpSection] = &[ title: "Setup Usage:", body: &[HelpSectionBodyLine::Command { cmd: " sce setup", - suffix: " [--opencode|--claude|--both] [--non-interactive] [--hooks] [--repo ]", + suffix: + " [--opencode|--claude|--pi|--all] [--non-interactive] [--hooks] [--repo ]", }], }, HelpSection { diff --git a/cli/src/services/config/render.rs b/cli/src/services/config/render.rs index 720907dd..165b28e2 100644 --- a/cli/src/services/config/render.rs +++ b/cli/src/services/config/render.rs @@ -35,6 +35,7 @@ pub(super) fn format_show_output(runtime: &RuntimeConfig, report_format: ReportF &runtime.workos_client_id, ), format_bash_policies_text(&runtime.bash_policies), + format_agent_trace_policy_text(runtime), format_database_retry_text(&runtime.database_retry), format_validation_warnings_text(&warnings), ]; @@ -70,6 +71,7 @@ pub(super) fn format_show_output(runtime: &RuntimeConfig, report_format: ReportF "workos_client_id": format_optional_auth_resolved_value_json(WORKOS_CLIENT_ID_KEY, &runtime.workos_client_id), "policies": { "bash": format_bash_policies_json(&runtime.bash_policies), + "agent_trace": format_agent_trace_policy_json(runtime), "database_retry": format_database_retry_json(&runtime.database_retry), } }, @@ -369,6 +371,48 @@ fn abbreviate_text_value(value: &str) -> String { format!("{prefix}...{suffix}") } +fn format_agent_trace_policy_text(runtime: &RuntimeConfig) -> String { + [ + format!(" {}:", style::label("policies.agent_trace")), + format!( + " {}", + format_resolved_value_text( + "git_notes_ref", + &runtime.agent_trace_git_notes_ref.value, + runtime.agent_trace_git_notes_ref.source, + ) + ), + format!( + " {}", + format_resolved_value_text( + "push_notes.enabled", + if runtime.agent_trace_push_notes_enabled.value { + "true" + } else { + "false" + }, + runtime.agent_trace_push_notes_enabled.source, + ) + ), + ] + .join("\n") +} + +fn format_agent_trace_policy_json(runtime: &RuntimeConfig) -> Value { + json!({ + "git_notes_ref": format_resolved_value_json( + &runtime.agent_trace_git_notes_ref.value, + runtime.agent_trace_git_notes_ref.source, + ), + "push_notes": { + "enabled": format_resolved_value_json( + runtime.agent_trace_push_notes_enabled.value, + runtime.agent_trace_push_notes_enabled.source, + ), + }, + }) +} + fn retry_policy_display(policy: &crate::services::resilience::RetryPolicy) -> String { format!( "{} attempts, {}ms timeout, {}..{}ms backoff", diff --git a/cli/src/services/config/resolver.rs b/cli/src/services/config/resolver.rs index b370ca64..383567c7 100644 --- a/cli/src/services/config/resolver.rs +++ b/cli/src/services/config/resolver.rs @@ -16,8 +16,8 @@ use super::types::{ parse_bool_value_from, ConfigPathSource, ConfigRequest, DatabaseRetryConfig, LoadedConfigPath, LogFileMode, LogFormat, LogLevel, ReportFormat, ResolvedAuthRuntimeConfig, ResolvedHookRuntimeConfig, ResolvedObservabilityRuntimeConfig, ResolvedOptionalValue, - ResolvedValue, ValueSource, ENV_ATTRIBUTION_HOOKS_DISABLED, ENV_LOG_FILE, ENV_LOG_FILE_MODE, - ENV_LOG_FORMAT, ENV_LOG_LEVEL, + ResolvedValue, ValueSource, DEFAULT_AGENT_TRACE_GIT_NOTES_REF, ENV_ATTRIBUTION_HOOKS_DISABLED, + ENV_LOG_FILE, ENV_LOG_FILE_MODE, ENV_LOG_FORMAT, ENV_LOG_LEVEL, }; const DEFAULT_TIMEOUT_MS: u64 = 30000; @@ -62,6 +62,8 @@ pub(super) struct RuntimeConfig { pub(super) log_file_mode: ResolvedValue, pub(super) timeout_ms: ResolvedValue, pub(super) attribution_hooks_enabled: ResolvedValue, + pub(super) agent_trace_git_notes_ref: ResolvedValue, + pub(super) agent_trace_push_notes_enabled: ResolvedValue, pub(super) workos_client_id: ResolvedOptionalValue, pub(super) bash_policies: ResolvedOptionalValue, pub(super) database_retry: ResolvedOptionalValue, @@ -226,6 +228,8 @@ where Ok(ResolvedHookRuntimeConfig { attribution_hooks_enabled: runtime.attribution_hooks_enabled.value, + agent_trace_git_notes_ref: runtime.agent_trace_git_notes_ref.value, + agent_trace_push_notes_enabled: runtime.agent_trace_push_notes_enabled.value, }) } @@ -272,6 +276,8 @@ where log_file_mode: None, timeout_ms: None, attribution_hooks_enabled: None, + agent_trace_git_notes_ref: None, + agent_trace_push_notes_enabled: None, workos_client_id: None, bash_policy_presets: None, bash_policy_custom: None, @@ -307,6 +313,12 @@ where if let Some(attribution_hooks_enabled) = layer.attribution_hooks_enabled { file_config.attribution_hooks_enabled = Some(attribution_hooks_enabled); } + if let Some(agent_trace_git_notes_ref) = layer.agent_trace_git_notes_ref { + file_config.agent_trace_git_notes_ref = Some(agent_trace_git_notes_ref); + } + if let Some(agent_trace_push_notes_enabled) = layer.agent_trace_push_notes_enabled { + file_config.agent_trace_push_notes_enabled = Some(agent_trace_push_notes_enabled); + } if let Some(workos_client_id) = layer.workos_client_id { file_config.workos_client_id = Some(workos_client_id); } @@ -449,6 +461,10 @@ where source: ValueSource::Env, }; } + let resolved_agent_trace_git_notes_ref = + resolve_agent_trace_git_notes_ref(file_config.agent_trace_git_notes_ref.as_ref()); + let resolved_agent_trace_push_notes_enabled = + resolve_agent_trace_push_notes_enabled(file_config.agent_trace_push_notes_enabled.as_ref()); let resolved_workos_client_id = resolve_optional_auth_config_value( WORKOS_CLIENT_ID_KEY, file_config.workos_client_id, @@ -472,6 +488,8 @@ where log_file_mode: resolved_log_file_mode, timeout_ms: resolved_timeout_ms, attribution_hooks_enabled: resolved_attribution_hooks_enabled, + agent_trace_git_notes_ref: resolved_agent_trace_git_notes_ref, + agent_trace_push_notes_enabled: resolved_agent_trace_push_notes_enabled, workos_client_id: resolved_workos_client_id, bash_policies: resolved_bash_policies, database_retry: resolved_database_retry, @@ -480,6 +498,38 @@ where }) } +fn resolve_agent_trace_git_notes_ref( + file_value: Option<&schema::FileConfigValue>, +) -> ResolvedValue { + if let Some(value) = file_value { + return ResolvedValue { + value: value.value.clone(), + source: ValueSource::ConfigFile(value.source), + }; + } + + ResolvedValue { + value: DEFAULT_AGENT_TRACE_GIT_NOTES_REF.to_string(), + source: ValueSource::Default, + } +} + +fn resolve_agent_trace_push_notes_enabled( + file_value: Option<&schema::FileConfigValue>, +) -> ResolvedValue { + if let Some(value) = file_value { + return ResolvedValue { + value: value.value, + source: ValueSource::ConfigFile(value.source), + }; + } + + ResolvedValue { + value: true, + source: ValueSource::Default, + } +} + fn resolve_optional_auth_config_value( key: AuthConfigKeySpec, file_value: Option>, @@ -682,6 +732,8 @@ mod tests { Ok(ResolvedHookRuntimeConfig { attribution_hooks_enabled: runtime.attribution_hooks_enabled.value, + agent_trace_git_notes_ref: runtime.agent_trace_git_notes_ref.value, + agent_trace_push_notes_enabled: runtime.agent_trace_push_notes_enabled.value, }) } @@ -692,6 +744,67 @@ mod tests { assert!(resolved.attribution_hooks_enabled); } + #[test] + fn agent_trace_git_notes_ref_uses_default() { + let resolved = resolve_hooks_with_env_and_config(None, None).unwrap(); + + assert_eq!( + resolved.agent_trace_git_notes_ref, + DEFAULT_AGENT_TRACE_GIT_NOTES_REF + ); + } + + #[test] + fn agent_trace_git_notes_ref_uses_explicit_config() { + let resolved = resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"git_notes_ref":"refs/notes/custom-sce"}}}"#), + ) + .unwrap(); + + assert_eq!(resolved.agent_trace_git_notes_ref, "refs/notes/custom-sce"); + } + + #[test] + fn agent_trace_push_notes_enabled_uses_default() { + let resolved = resolve_hooks_with_env_and_config(None, None).unwrap(); + + assert!(resolved.agent_trace_push_notes_enabled); + } + + #[test] + fn agent_trace_push_notes_enabled_uses_explicit_config_false() { + let resolved = resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"push_notes":{"enabled":false}}}}"#), + ) + .unwrap(); + + assert!(!resolved.agent_trace_push_notes_enabled); + } + + #[test] + fn invalid_agent_trace_push_notes_shape_is_rejected() { + resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"push_notes":{"enabled":"no"}}}}"#), + ) + .unwrap_err(); + } + + #[test] + fn blank_agent_trace_git_notes_ref_is_rejected() { + let error = resolve_hooks_with_env_and_config( + None, + Some(r#"{"policies":{"agent_trace":{"git_notes_ref":" "}}}"#), + ) + .unwrap_err(); + + assert!(error + .to_string() + .contains("policies.agent_trace.git_notes_ref")); + } + #[test] fn attribution_hooks_disabled_env_truthy_opts_out() { let resolved = diff --git a/cli/src/services/config/schema.rs b/cli/src/services/config/schema.rs index c5944b8d..0646ab4c 100644 --- a/cli/src/services/config/schema.rs +++ b/cli/src/services/config/schema.rs @@ -84,9 +84,21 @@ pub(crate) struct ParsedIntegrationsConfigDocument { pub(crate) struct ParsedPoliciesConfigDocument { pub(crate) bash: Option, pub(crate) attribution_hooks: Option, + pub(crate) agent_trace: Option, pub(crate) database_retry: Option, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub(crate) struct ParsedAgentTracePolicyConfigDocument { + pub(crate) git_notes_ref: Option, + pub(crate) push_notes: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub(crate) struct ParsedAgentTracePushNotesConfigDocument { + pub(crate) enabled: Option, +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub(crate) struct ParsedBashPolicyConfigDocument { pub(crate) presets: Option>, @@ -147,6 +159,8 @@ pub(crate) struct FileConfig { pub(crate) log_file_mode: Option>, pub(crate) timeout_ms: Option>, pub(crate) attribution_hooks_enabled: Option>, + pub(crate) agent_trace_git_notes_ref: Option>, + pub(crate) agent_trace_push_notes_enabled: Option>, pub(crate) workos_client_id: Option>, pub(crate) bash_policy_presets: Option>>, pub(crate) bash_policy_custom: Option>>, @@ -159,10 +173,17 @@ pub(crate) type ParsedBashPolicyConfig = ( Option>>, ); +pub(crate) type ParsedAgentTracePolicy = ( + Option>, + Option>, +); + pub(crate) type ParsedFilePolicies = ( Option>, Option>>, Option>>, + Option>, + Option>, Option>, ); @@ -294,8 +315,14 @@ pub(crate) fn parse_file_config( let workos_client_id = typed .workos_client_id .map(|value| FileConfigValue { value, source }); - let (attribution_hooks_enabled, bash_policy_presets, bash_policy_custom, database_retry) = - map_policies_config(typed.policies.as_ref(), object, path, source)?; + let ( + attribution_hooks_enabled, + bash_policy_presets, + bash_policy_custom, + agent_trace_git_notes_ref, + agent_trace_push_notes_enabled, + database_retry, + ) = map_policies_config(typed.policies.as_ref(), object, path, source)?; let integrations = map_integrations_config(typed.integrations.as_ref(), object, path, source)?; Ok(FileConfig { @@ -305,6 +332,8 @@ pub(crate) fn parse_file_config( log_file_mode, timeout_ms, attribution_hooks_enabled, + agent_trace_git_notes_ref, + agent_trace_push_notes_enabled, workos_client_id, bash_policy_presets, bash_policy_custom, @@ -320,7 +349,7 @@ pub(crate) fn map_policies_config( source: ConfigPathSource, ) -> Result { let Some(policies_value) = object.get("policies") else { - return Ok((None, None, None, None)); + return Ok((None, None, None, None, None, None)); }; let policies_object = policies_value.as_object().with_context(|| { @@ -334,8 +363,8 @@ pub(crate) fn map_policies_config( policies_object, path, Some("policies"), - &["bash", "attribution_hooks", "database_retry"], - "bash, attribution_hooks, database_retry", + &["bash", "attribution_hooks", "agent_trace", "database_retry"], + "bash, attribution_hooks, agent_trace, database_retry", )?; let bash = typed.and_then(|config| config.bash.as_ref()); @@ -347,6 +376,13 @@ pub(crate) fn map_policies_config( )?; let (bash_policy_presets, bash_policy_custom) = map_bash_policy_config(bash, policies_object, path, source)?; + let (agent_trace_git_notes_ref, agent_trace_push_notes_enabled) = + map_agent_trace_policy_config( + typed.and_then(|config| config.agent_trace.as_ref()), + policies_object, + path, + source, + )?; let database_retry = map_database_retry_config( typed.and_then(|config| config.database_retry.as_ref()), policies_object, @@ -358,6 +394,8 @@ pub(crate) fn map_policies_config( attribution_hooks_enabled, bash_policy_presets, bash_policy_custom, + agent_trace_git_notes_ref, + agent_trace_push_notes_enabled, database_retry, )) } @@ -392,6 +430,57 @@ pub(crate) fn map_attribution_hooks_config( .map(|value| FileConfigValue { value, source })) } +pub(crate) fn map_agent_trace_policy_config( + typed: Option<&ParsedAgentTracePolicyConfigDocument>, + policies_object: &serde_json::Map, + path: &Path, + source: ConfigPathSource, +) -> Result { + let Some(agent_trace_value) = policies_object.get("agent_trace") else { + return Ok((None, None)); + }; + + let agent_trace_object = agent_trace_value.as_object().with_context(|| { + format!( + "Config key 'policies.agent_trace' in '{}' must be an object.", + path.display() + ) + })?; + + validate_object_keys( + agent_trace_object, + path, + Some("policies.agent_trace"), + &["git_notes_ref", "push_notes"], + "git_notes_ref, push_notes", + )?; + + let git_notes_ref = typed + .and_then(|config| config.git_notes_ref.as_ref()) + .map(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + bail!( + "Config key 'policies.agent_trace.git_notes_ref' in '{}' must not be empty.", + path.display() + ); + } + + Ok(FileConfigValue { + value: trimmed.to_string(), + source, + }) + }) + .transpose()?; + + let push_notes_enabled = typed + .and_then(|config| config.push_notes.as_ref()) + .and_then(|config| config.enabled) + .map(|value| FileConfigValue { value, source }); + + Ok((git_notes_ref, push_notes_enabled)) +} + pub(crate) fn map_bash_policy_config( typed: Option<&ParsedBashPolicyConfigDocument>, policies_object: &serde_json::Map, diff --git a/cli/src/services/config/types.rs b/cli/src/services/config/types.rs index 7a95c1b0..a48dc7b3 100644 --- a/cli/src/services/config/types.rs +++ b/cli/src/services/config/types.rs @@ -18,6 +18,7 @@ pub(crate) const ENV_LOG_FORMAT: &str = "SCE_LOG_FORMAT"; pub(crate) const ENV_LOG_FILE: &str = "SCE_LOG_FILE"; pub(crate) const ENV_LOG_FILE_MODE: &str = "SCE_LOG_FILE_MODE"; pub(crate) const ENV_ATTRIBUTION_HOOKS_DISABLED: &str = "SCE_ATTRIBUTION_HOOKS_DISABLED"; +pub(crate) const DEFAULT_AGENT_TRACE_GIT_NOTES_REF: &str = "refs/notes/sce-agent-trace"; pub type ReportFormat = OutputFormat; @@ -239,6 +240,8 @@ pub(crate) struct ResolvedObservabilityRuntimeConfig { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct ResolvedHookRuntimeConfig { pub(crate) attribution_hooks_enabled: bool, + pub(crate) agent_trace_git_notes_ref: String, + pub(crate) agent_trace_push_notes_enabled: bool, } pub(crate) fn parse_bool_value_from(key: &str, raw: &str, source: &str) -> anyhow::Result { @@ -269,6 +272,7 @@ pub(crate) struct PerDbRetryConfig { pub(crate) enum IntegrationTargetId { Opencode, Claude, + Pi, } impl IntegrationTargetId { @@ -276,8 +280,9 @@ impl IntegrationTargetId { match raw { "opencode" => Ok(Self::Opencode), "claude" => Ok(Self::Claude), + "pi" => Ok(Self::Pi), _ => anyhow::bail!( - "Invalid integration target '{raw}' from {source}. Valid values: opencode, claude." + "Invalid integration target '{raw}' from {source}. Valid values: opencode, claude, pi." ), } } @@ -287,3 +292,35 @@ impl IntegrationTargetId { pub(crate) struct IntegrationsConfig { pub(crate) target: Vec, } + +#[cfg(test)] +mod integration_target_id_tests { + use super::IntegrationTargetId; + + #[test] + fn parses_known_target_ids() { + assert_eq!( + IntegrationTargetId::parse("opencode", "test").unwrap(), + IntegrationTargetId::Opencode + ); + assert_eq!( + IntegrationTargetId::parse("claude", "test").unwrap(), + IntegrationTargetId::Claude + ); + assert_eq!( + IntegrationTargetId::parse("pi", "test").unwrap(), + IntegrationTargetId::Pi + ); + } + + #[test] + fn rejects_unknown_target_id_and_lists_valid_values() { + let error = IntegrationTargetId::parse("cursor", "test source") + .unwrap_err() + .to_string(); + assert_eq!( + error, + "Invalid integration target 'cursor' from test source. Valid values: opencode, claude, pi." + ); + } +} diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index 932a41dc..d7c03ac3 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -336,6 +336,7 @@ pub(crate) mod repo_dir { pub const SCE: &str = ".sce"; pub const OPENCODE: &str = ".opencode"; pub const CLAUDE: &str = ".claude"; + pub const PI: &str = ".pi"; pub const GIT: &str = ".git"; } @@ -384,6 +385,12 @@ pub(crate) mod claude_asset { pub const COMMANDS_DIR: &str = "commands"; } +pub(crate) mod pi_asset { + pub const PROMPTS_DIR: &str = "prompts"; + pub const SKILLS_DIR: &str = "skills"; + pub const EXTENSIONS_DIR: &str = "extensions"; +} + #[allow(dead_code)] pub(crate) mod context_dir { pub const CONTEXT_ROOT: &str = "context"; @@ -444,6 +451,10 @@ impl RepoPaths { self.root.join(repo_dir::CLAUDE) } + pub(crate) fn pi_dir(&self) -> PathBuf { + self.root.join(repo_dir::PI) + } + pub(crate) fn git_dir(&self) -> PathBuf { self.root.join(repo_dir::GIT) } @@ -595,6 +606,10 @@ impl InstallTargetPaths { self.repo_root.join(repo_dir::CLAUDE) } + pub(crate) fn pi_target_dir(&self) -> PathBuf { + self.repo_root.join(repo_dir::PI) + } + pub(crate) fn opencode_plugin_target(&self) -> PathBuf { self.opencode_target_dir() .join(opencode_asset::PLUGINS_DIR) diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index a974122f..0341785e 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -8,7 +8,8 @@ use crate::services::checkout; use crate::services::config::schema::parse_file_config; use crate::services::config::{ConfigPathSource, IntegrationTargetId}; use crate::services::default_paths::{ - agent_trace_db_path_for_checkout, claude_asset, opencode_asset, InstallTargetPaths, RepoPaths, + agent_trace_db_path_for_checkout, claude_asset, opencode_asset, pi_asset, InstallTargetPaths, + RepoPaths, }; use crate::services::setup::{ iter_embedded_assets_for_setup_target, iter_required_hook_assets, EmbeddedAsset, SetupTarget, @@ -20,7 +21,8 @@ use super::types::{ IntegrationContentState, IntegrationGroupHealth, ProblemCategory, ProblemFixability, ProblemKind, ProblemSeverity, Readiness, CLAUDE_AGENTS_LABEL, CLAUDE_COMMANDS_LABEL, CLAUDE_PLUGINS_LABEL, CLAUDE_SKILLS_LABEL, OPENCODE_AGENTS_LABEL, OPENCODE_COMMANDS_LABEL, - OPENCODE_PLUGINS_LABEL, OPENCODE_SKILLS_LABEL, + OPENCODE_PLUGINS_LABEL, OPENCODE_SKILLS_LABEL, PI_EXTENSIONS_LABEL, PI_PROMPTS_LABEL, + PI_SKILLS_LABEL, }; use super::{is_executable, DoctorDependencies, DoctorMode, REQUIRED_HOOKS}; @@ -401,7 +403,7 @@ fn inspect_repository_hooks( /// Returns `true` when the doctor was able to check for integration targets /// and found none (neither configured in `.sce/config.json` nor detected -/// via repo-root `.opencode/` / `.claude/` directories). +/// via repo-root `.opencode/` / `.claude/` / `.pi/` directories). fn should_show_no_integrations_message( git_available: bool, bare_repository: bool, @@ -447,6 +449,9 @@ fn resolve_doctor_integration_targets(repository_root: &Path) -> Vec { + let pi_groups = collect_pi_integration_groups(resolved_root); + inspect_pi_integration_health(&pi_groups, problems); + integration_groups.extend(pi_groups); + } } } @@ -727,6 +737,15 @@ fn inspect_claude_integration_health( push_claude_integration_read_fail_problems(integration_groups, problems); } +fn inspect_pi_integration_health( + integration_groups: &[IntegrationGroupHealth], + problems: &mut Vec, +) { + push_pi_integration_missing_problems(integration_groups, problems); + push_pi_integration_mismatch_problems(integration_groups, problems); + push_pi_integration_read_fail_problems(integration_groups, problems); +} + fn push_opencode_integration_missing_problems( integration_groups: &[IntegrationGroupHealth], problems: &mut Vec, @@ -933,6 +952,109 @@ fn push_claude_integration_read_fail_problems( } } +fn push_pi_integration_missing_problems( + integration_groups: &[IntegrationGroupHealth], + problems: &mut Vec, +) { + for group in integration_groups { + let missing_children = group + .children + .iter() + .filter(|child| matches!(&child.content_state, IntegrationContentState::Missing)) + .collect::>(); + if missing_children.is_empty() { + continue; + } + + let missing_paths = missing_children + .iter() + .map(|child| format!("'{}'", child.path.display())) + .collect::>() + .join(", "); + problems.push(DoctorProblem { + kind: ProblemKind::PiIntegrationFilesMissing, + category: ProblemCategory::RepoAssets, + severity: ProblemSeverity::Error, + fixability: ProblemFixability::ManualOnly, + summary: format!( + "{} required file(s) are missing: {}.", + group.label, missing_paths + ), + remediation: format!( + "Reinstall repo-root Pi assets to restore the missing {} file(s), then rerun 'sce doctor'.", + group.label.to_ascii_lowercase() + ), + next_action: "manual_steps", + }); + } +} + +fn push_pi_integration_mismatch_problems( + integration_groups: &[IntegrationGroupHealth], + problems: &mut Vec, +) { + for group in integration_groups { + let mismatched_children = group + .children + .iter() + .filter(|child| matches!(&child.content_state, IntegrationContentState::Mismatch)) + .collect::>(); + if mismatched_children.is_empty() { + continue; + } + + let mismatched_paths = mismatched_children + .iter() + .map(|child| format!("'{}'", child.path.display())) + .collect::>() + .join(", "); + problems.push(DoctorProblem { + kind: ProblemKind::PiIntegrationContentMismatch, + category: ProblemCategory::RepoAssets, + severity: ProblemSeverity::Error, + fixability: ProblemFixability::ManualOnly, + summary: format!( + "{} file(s) differ from the canonical embedded content: {}.", + group.label, mismatched_paths + ), + remediation: format!( + "Reinstall repo-root Pi assets to restore the canonical {} content, then rerun 'sce doctor'.", + group.label.to_ascii_lowercase() + ), + next_action: "manual_steps", + }); + } +} + +fn push_pi_integration_read_fail_problems( + integration_groups: &[IntegrationGroupHealth], + problems: &mut Vec, +) { + for group in integration_groups { + for child in &group.children { + let IntegrationContentState::ReadFailed(error) = &child.content_state else { + continue; + }; + problems.push(DoctorProblem { + kind: ProblemKind::PiAssetReadFailed, + category: ProblemCategory::FilesystemPermissions, + severity: ProblemSeverity::Error, + fixability: ProblemFixability::ManualOnly, + summary: format!( + "Unable to read Pi asset '{}' at '{}': {error}", + child.relative_path, + child.path.display() + ), + remediation: format!( + "Verify that '{}' is readable before rerunning 'sce doctor'.", + child.path.display() + ), + next_action: "manual_steps", + }); + } + } +} + fn inspect_opencode_plugin_registry_health( repository_root: &Path, problems: &mut Vec, @@ -1161,6 +1283,56 @@ fn collect_claude_integration_groups(repository_root: &Path) -> Vec Vec { + let repo_paths = RepoPaths::new(repository_root); + let pi_root = repo_paths.pi_dir(); + let embedded_assets = + iter_embedded_assets_for_setup_target(SetupTarget::Pi).collect::>(); + let mut prompt_children = Vec::new(); + let mut skill_children = Vec::new(); + let mut extension_children = Vec::new(); + + for asset in embedded_assets { + let child = build_integration_child_from_asset(&pi_root, asset); + + if child + .relative_path + .starts_with(&format!("{}/", pi_asset::PROMPTS_DIR)) + { + prompt_children.push(child); + } else if child + .relative_path + .starts_with(&format!("{}/", pi_asset::SKILLS_DIR)) + { + skill_children.push(child); + } else if child + .relative_path + .starts_with(&format!("{}/", pi_asset::EXTENSIONS_DIR)) + { + extension_children.push(child); + } + } + + sort_integration_children(&mut prompt_children); + sort_integration_children(&mut skill_children); + sort_integration_children(&mut extension_children); + + vec![ + IntegrationGroupHealth { + label: PI_PROMPTS_LABEL, + children: prompt_children, + }, + IntegrationGroupHealth { + label: PI_SKILLS_LABEL, + children: skill_children, + }, + IntegrationGroupHealth { + label: PI_EXTENSIONS_LABEL, + children: extension_children, + }, + ] +} + fn sort_integration_children(children: &mut [IntegrationChildHealth]) { children.sort_by(|left, right| left.relative_path.cmp(&right.relative_path)); } diff --git a/cli/src/services/doctor/mod.rs b/cli/src/services/doctor/mod.rs index a6e20dce..16a877da 100644 --- a/cli/src/services/doctor/mod.rs +++ b/cli/src/services/doctor/mod.rs @@ -306,6 +306,10 @@ fn doctor_problem_kind(kind: HealthProblemKind) -> ProblemKind { HealthProblemKind::ClaudeIntegrationContentMismatch => { ProblemKind::ClaudeIntegrationContentMismatch } + HealthProblemKind::PiIntegrationFilesMissing => ProblemKind::PiIntegrationFilesMissing, + HealthProblemKind::PiIntegrationContentMismatch => { + ProblemKind::PiIntegrationContentMismatch + } HealthProblemKind::OpenCodePluginRegistryInvalid => { ProblemKind::OpenCodePluginRegistryInvalid } @@ -315,6 +319,7 @@ fn doctor_problem_kind(kind: HealthProblemKind) -> ProblemKind { HealthProblemKind::HookReadFailed => ProblemKind::HookReadFailed, HealthProblemKind::OpenCodeAssetReadFailed => ProblemKind::OpenCodeAssetReadFailed, HealthProblemKind::ClaudeAssetReadFailed => ProblemKind::ClaudeAssetReadFailed, + HealthProblemKind::PiAssetReadFailed => ProblemKind::PiAssetReadFailed, HealthProblemKind::AgentTraceDbConnectionFailed => { ProblemKind::AgentTraceDbConnectionFailed } @@ -356,6 +361,10 @@ fn health_problem_kind(kind: ProblemKind) -> HealthProblemKind { ProblemKind::ClaudeIntegrationContentMismatch => { HealthProblemKind::ClaudeIntegrationContentMismatch } + ProblemKind::PiIntegrationFilesMissing => HealthProblemKind::PiIntegrationFilesMissing, + ProblemKind::PiIntegrationContentMismatch => { + HealthProblemKind::PiIntegrationContentMismatch + } ProblemKind::OpenCodePluginRegistryInvalid => { HealthProblemKind::OpenCodePluginRegistryInvalid } @@ -365,6 +374,7 @@ fn health_problem_kind(kind: ProblemKind) -> HealthProblemKind { ProblemKind::HookReadFailed => HealthProblemKind::HookReadFailed, ProblemKind::OpenCodeAssetReadFailed => HealthProblemKind::OpenCodeAssetReadFailed, ProblemKind::ClaudeAssetReadFailed => HealthProblemKind::ClaudeAssetReadFailed, + ProblemKind::PiAssetReadFailed => HealthProblemKind::PiAssetReadFailed, ProblemKind::AgentTraceDbConnectionFailed => { HealthProblemKind::AgentTraceDbConnectionFailed } diff --git a/cli/src/services/doctor/types.rs b/cli/src/services/doctor/types.rs index ef8eed78..062475e4 100644 --- a/cli/src/services/doctor/types.rs +++ b/cli/src/services/doctor/types.rs @@ -8,6 +8,9 @@ pub(super) const CLAUDE_PLUGINS_LABEL: &str = "ClaudeCode plugins"; pub(super) const CLAUDE_AGENTS_LABEL: &str = "ClaudeCode agents"; pub(super) const CLAUDE_COMMANDS_LABEL: &str = "ClaudeCode commands"; pub(super) const CLAUDE_SKILLS_LABEL: &str = "ClaudeCode skills"; +pub(super) const PI_PROMPTS_LABEL: &str = "Pi prompts"; +pub(super) const PI_SKILLS_LABEL: &str = "Pi skills"; +pub(super) const PI_EXTENSIONS_LABEL: &str = "Pi extensions"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum Readiness { @@ -138,11 +141,14 @@ pub(crate) enum ProblemKind { OpenCodeIntegrationContentMismatch, ClaudeIntegrationFilesMissing, ClaudeIntegrationContentMismatch, + PiIntegrationFilesMissing, + PiIntegrationContentMismatch, OpenCodePluginRegistryInvalid, OpenCodeAssetMissingOrInvalid, HookReadFailed, OpenCodeAssetReadFailed, ClaudeAssetReadFailed, + PiAssetReadFailed, AgentTraceDbConnectionFailed, AgentTraceDbSchemaNotReady, } diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 3dfca559..9fd92e62 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -1,7 +1,7 @@ use std::fs; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, bail, Context, Result}; @@ -37,14 +37,17 @@ pub const CANONICAL_SCE_COAUTHOR_TRAILER: &str = "Co-authored-by: SCE String; pub(crate) fn prefixed_diff_trace_session_id(tool_name: &str, raw_session_id: &str) -> String { let prefix = match tool_name { OPENCODE_TOOL_NAME => DIFF_TRACE_OPENCODE_SESSION_ID_PREFIX, CLAUDE_TOOL_NAME => DIFF_TRACE_CLAUDE_SESSION_ID_PREFIX, + PI_TOOL_NAME => DIFF_TRACE_PI_SESSION_ID_PREFIX, _ => return raw_session_id.to_string(), }; @@ -189,9 +192,12 @@ fn run_hooks_subcommand_in_repo( HookSubcommand::PostCommit { vcs_type, remote_url, - } => { - run_post_commit_subcommand_with_trace(repository_root, *vcs_type, remote_url.as_deref()) - } + } => run_post_commit_subcommand_with_trace( + repository_root, + *vcs_type, + remote_url.as_deref(), + logger, + ), HookSubcommand::PostRewrite { rewrite_method } => { run_post_rewrite_subcommand_with_trace(repository_root, subcommand, rewrite_method) } @@ -1216,11 +1222,13 @@ fn run_post_commit_subcommand( repository_root: &Path, vcs_type: Option, remote_url: &str, + logger: Option<&dyn Logger>, ) -> Result { run_post_commit_subcommand_with( repository_root, vcs_type, remote_url, + logger, run_post_commit_intersection_flow, run_post_commit_agent_trace_flow, ) @@ -1230,6 +1238,7 @@ fn run_post_commit_subcommand_with( repository_root: &Path, vcs_type: Option, remote_url: &str, + logger: Option<&dyn Logger>, run_intersection_flow: F, run_agent_trace_flow: B, ) -> Result @@ -1240,10 +1249,12 @@ where &PostCommitIntersectionFlowResult, Option, &str, - ) -> Result, + Option<&dyn Logger>, + ) -> Result, { let result = run_intersection_flow(repository_root)?; - let _agent_trace = run_agent_trace_flow(repository_root, &result, vcs_type, remote_url)?; + let _agent_trace = + run_agent_trace_flow(repository_root, &result, vcs_type, remote_url, logger)?; Ok(format!( "post-commit hook processed intersection: commit={}, intersection_files={}", @@ -1257,15 +1268,26 @@ fn run_post_commit_agent_trace_flow( flow_result: &PostCommitIntersectionFlowResult, vcs_type: Option, remote_url: &str, -) -> Result { + logger: Option<&dyn Logger>, +) -> Result { let db = open_agent_trace_db_for_hook_runtime( repository_root, "Failed to open Agent Trace DB for post-commit trace.", )?; + let hook_config = config::resolve_hook_runtime_config(repository_root) + .context("Failed to resolve hook runtime config for post-commit Agent Trace git note.")?; + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root, + vcs_type, + git_notes_ref: &hook_config.agent_trace_git_notes_ref, + push_notes_enabled: hook_config.agent_trace_push_notes_enabled, + logger, + }; run_post_commit_agent_trace_flow_with( + &git_note_persistence, flow_result, - vcs_type, remote_url, |trace_value| { validate_agent_trace_value(trace_value) @@ -1280,19 +1302,40 @@ fn run_post_commit_agent_trace_flow( Ok(()) }, + write_agent_trace_git_note, + push_agent_trace_git_notes_ref, ) } -fn run_post_commit_agent_trace_flow_with( - flow_result: &PostCommitIntersectionFlowResult, +#[derive(Clone, Debug, Eq, PartialEq)] +struct PostCommitAgentTraceFlowResult { + agent_trace: AgentTrace, + trace_json: String, +} + +#[derive(Clone, Copy)] +struct PostCommitGitNotePersistence<'a> { + repository_root: &'a Path, vcs_type: Option, + git_notes_ref: &'a str, + push_notes_enabled: bool, + logger: Option<&'a dyn Logger>, +} + +fn run_post_commit_agent_trace_flow_with( + git_note_persistence: &PostCommitGitNotePersistence<'_>, + flow_result: &PostCommitIntersectionFlowResult, remote_url: &str, validate_agent_trace: V, persist_agent_trace: I, -) -> Result + write_git_note: G, + push_git_notes: P, +) -> Result where V: FnOnce(&Value) -> Result<()>, I: for<'a> FnOnce(AgentTraceInsert<'a>) -> Result<()>, + G: FnOnce(&Path, &str, &str, &str) -> Result, + P: FnOnce(&Path, &str, &str) -> Result, { let commit_timestamp = DateTime::::from_timestamp_millis(flow_result.post_commit_data.commit_time_ms) @@ -1310,7 +1353,7 @@ where AgentTraceMetadataInput { commit_timestamp: &commit_timestamp, commit_revision: &flow_result.post_commit_data.commit_oid, - vcs_type, + vcs_type: git_note_persistence.vcs_type, tool_name: flow_result.tool_name.as_deref(), tool_version: flow_result.tool_version.as_deref(), }, @@ -1340,7 +1383,57 @@ where }; persist_agent_trace(insert_input)?; - Ok(agent_trace) + if should_write_agent_trace_git_note(git_note_persistence.vcs_type) { + match write_git_note( + git_note_persistence.repository_root, + git_note_persistence.git_notes_ref, + &flow_result.post_commit_data.commit_oid, + &serialized, + ) { + Ok(_) if git_note_persistence.push_notes_enabled => { + let _ = push_git_notes( + git_note_persistence.repository_root, + remote_url, + git_note_persistence.git_notes_ref, + ); + } + Ok(_) => {} + Err(error) => { + log_agent_trace_git_note_write_failure( + git_note_persistence.logger, + &flow_result.post_commit_data.commit_oid, + git_note_persistence.git_notes_ref, + &error, + ); + } + } + } + + Ok(PostCommitAgentTraceFlowResult { + agent_trace, + trace_json: serialized, + }) +} + +fn should_write_agent_trace_git_note(vcs_type: Option) -> bool { + matches!(vcs_type, None | Some(AgentTraceVcsType::Git)) +} + +fn log_agent_trace_git_note_write_failure( + logger: Option<&dyn Logger>, + commit_id: &str, + git_notes_ref: &str, + error: &anyhow::Error, +) { + if let Some(log) = logger { + log.error( + "sce.hooks.post_commit.agent_trace_git_note_write_failed", + &format!( + "Failed to write Agent Trace git note for commit '{commit_id}' under ref '{git_notes_ref}': {error}." + ), + &[("commit_id", commit_id), ("git_notes_ref", git_notes_ref)], + ); + } } /// Duration for looking up recent diff traces: 7 days in milliseconds. @@ -1568,12 +1661,209 @@ fn current_unix_time_ms() -> Result { .context("Current time exceeds i64 range for post-commit intersection.") } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GitNoteWriteOutcome { + pub commit_id: String, + pub notes_ref: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GitNotesPushOutcome { + pub remote: String, + pub notes_ref: String, +} + +#[allow(dead_code)] +fn write_agent_trace_git_note( + repository_root: &Path, + notes_ref: &str, + commit_id: &str, + trace_json: &str, +) -> Result { + write_agent_trace_git_note_with( + repository_root, + notes_ref, + commit_id, + trace_json, + run_git_notes_add_command, + ) +} + +#[allow(dead_code)] +fn write_agent_trace_git_note_with( + repository_root: &Path, + notes_ref: &str, + commit_id: &str, + trace_json: &str, + run_git_notes: F, +) -> Result +where + F: FnOnce(&Path, &[String], &str) -> Result<()>, +{ + let notes_ref = non_empty_git_note_input("git notes ref", notes_ref)?; + let commit_id = non_empty_git_note_input("commit ID", commit_id)?; + let trace_json = non_empty_git_note_content(trace_json)?; + let args = vec![ + String::from("notes"), + String::from("--ref"), + notes_ref.clone(), + String::from("add"), + String::from("-f"), + String::from("-F"), + String::from("-"), + commit_id.clone(), + ]; + + run_git_notes(repository_root, &args, &trace_json).with_context(|| { + format!( + "Failed to write Agent Trace git note for commit '{commit_id}' under ref '{notes_ref}'." + ) + })?; + + Ok(GitNoteWriteOutcome { + commit_id, + notes_ref, + }) +} + +#[allow(dead_code)] +fn push_agent_trace_git_notes_ref( + repository_root: &Path, + remote: &str, + notes_ref: &str, +) -> Result { + push_agent_trace_git_notes_ref_with( + repository_root, + remote, + notes_ref, + run_git_notes_push_command, + ) +} + +#[allow(dead_code)] +fn push_agent_trace_git_notes_ref_with( + repository_root: &Path, + remote: &str, + notes_ref: &str, + run_git_push: F, +) -> Result +where + F: FnOnce(&Path, &[String]) -> Result<()>, +{ + let remote = non_empty_git_note_input("git remote", remote)?; + let notes_ref = non_empty_git_note_input("git notes ref", notes_ref)?; + let args = vec![String::from("push"), remote.clone(), notes_ref.clone()]; + + run_git_push(repository_root, &args).with_context(|| { + format!("Failed to push Agent Trace git notes ref '{notes_ref}' to remote '{remote}'.") + })?; + + Ok(GitNotesPushOutcome { remote, notes_ref }) +} + +fn non_empty_git_note_input(label: &str, value: &str) -> Result { + let trimmed = value.trim(); + if trimmed.is_empty() { + bail!("Invalid Agent Trace git-note {label}: value must be non-empty."); + } + + Ok(trimmed.to_string()) +} + +fn non_empty_git_note_content(trace_json: &str) -> Result { + if trace_json.trim().is_empty() { + bail!("Invalid Agent Trace git-note Agent Trace JSON: value must be non-empty."); + } + + Ok(trace_json.to_string()) +} + +fn run_git_notes_push_command(repository_root: &Path, args: &[String]) -> Result<()> { + let output = Command::new("git") + .args(args) + .current_dir(repository_root) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .output() + .with_context(|| { + format!( + "Failed to spawn git notes push command in directory '{}'.", + repository_root.display() + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let diagnostic = if stderr.is_empty() { + String::from("git notes push command exited with a non-zero status") + } else { + stderr + }; + bail!("Failed to push Agent Trace git notes: {diagnostic}"); + } + + Ok(()) +} + +fn run_git_notes_add_command( + repository_root: &Path, + args: &[String], + trace_json: &str, +) -> Result<()> { + let mut child = Command::new("git") + .args(args) + .current_dir(repository_root) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .with_context(|| { + format!( + "Failed to spawn git notes command in directory '{}'.", + repository_root.display() + ) + })?; + + { + let stdin = child + .stdin + .as_mut() + .context("Failed to open stdin for git notes command.")?; + stdin + .write_all(trace_json.as_bytes()) + .context("Failed to write Agent Trace JSON to git notes command stdin.")?; + } + + let output = child + .wait_with_output() + .context("Failed to wait for git notes command.")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let diagnostic = if stderr.is_empty() { + String::from("git notes command exited with a non-zero status") + } else { + stderr + }; + bail!("Failed to write Agent Trace git note: {diagnostic}"); + } + + Ok(()) +} + fn run_post_commit_subcommand_with_trace( repository_root: &Path, vcs_type: Option, remote_url: Option<&str>, + logger: Option<&dyn Logger>, ) -> Result { - run_post_commit_subcommand(repository_root, vcs_type, remote_url.unwrap_or_default()) + run_post_commit_subcommand( + repository_root, + vcs_type, + remote_url.unwrap_or_default(), + logger, + ) } fn run_post_rewrite_subcommand(repository_root: &Path, rewrite_method: &str) -> Result { @@ -2045,7 +2335,7 @@ where #[cfg(test)] mod tests { - use std::{cell::RefCell, path::Path}; + use std::{cell::RefCell, path::Path, sync::Mutex}; use super::*; use crate::services::agent_trace_db::{ParsedDiffTracePatch, SkippedDiffTracePatch}; @@ -2061,6 +2351,28 @@ mod tests { intersection_patch: String, } + #[derive(Default)] + struct CapturingLogger { + errors: Mutex>, + } + + impl Logger for CapturingLogger { + fn info(&self, _event_id: &str, _message: &str, _fields: &[(&str, &str)]) {} + + fn debug(&self, _event_id: &str, _message: &str, _fields: &[(&str, &str)]) {} + + fn warn(&self, _event_id: &str, _message: &str, _fields: &[(&str, &str)]) {} + + fn error(&self, event_id: &str, message: &str, _fields: &[(&str, &str)]) { + self.errors + .lock() + .expect("test logger mutex should not be poisoned") + .push((event_id.to_string(), message.to_string())); + } + + fn log_classified_error(&self, _error: &crate::services::error::ClassifiedError) {} + } + fn valid_patch_text(path: &str, content: &str) -> String { format!( "Index: {path}\n===================================================================\n--- {path}\n+++ {path}\n@@ -0,0 +1,1 @@\n+{content}\n" @@ -2073,6 +2385,259 @@ mod tests { parse_patch_from_text(&patch_text, None).expect("test patch should parse") } + fn post_commit_flow_result() -> PostCommitIntersectionFlowResult { + PostCommitIntersectionFlowResult { + combined_recent_patch: valid_patch("src/lib.rs", "shared line"), + post_commit_data: PostCommitPatchData { + commit_oid: String::from("abc123"), + commit_time_ms: 1_800_000_000_000_i64, + parsed_patch: valid_patch("src/lib.rs", "shared line"), + }, + tool_name: Some(String::from("opencode")), + tool_version: Some(String::from("1.2.3")), + } + } + + #[test] + fn post_commit_agent_trace_flow_writes_git_note_after_db_insert() { + let order = RefCell::new(Vec::::new()); + let captured_note = RefCell::new(None::<(String, String, String)>); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: true, + logger: None, + }; + let result = run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |insert_input| { + order.borrow_mut().push(String::from("db")); + assert_eq!(insert_input.commit_id, "abc123"); + assert!(insert_input.trace_json.contains("\"version\": \"0.1.0\"")); + Ok(()) + }, + |root, notes_ref, commit_id, trace_json| { + order.borrow_mut().push(String::from("note")); + assert_eq!(root, Path::new("/repo")); + *captured_note.borrow_mut() = Some(( + notes_ref.to_string(), + commit_id.to_string(), + trace_json.to_string(), + )); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + |root, remote, notes_ref| { + order.borrow_mut().push(String::from("push")); + assert_eq!(root, Path::new("/repo")); + Ok(GitNotesPushOutcome { + remote: remote.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + ) + .expect("Agent Trace flow should persist DB row and git note"); + + assert_eq!(order.into_inner(), vec!["db", "note", "push"]); + let (notes_ref, commit_id, trace_json) = captured_note + .into_inner() + .expect("git note write should be attempted"); + assert_eq!(notes_ref, "refs/notes/sce-agent-trace"); + assert_eq!(commit_id, "abc123"); + assert_eq!(trace_json, result.trace_json); + assert_eq!(trace_json, captured_note_trace_json(&result)); + } + + #[test] + fn post_commit_agent_trace_flow_honors_configured_git_notes_ref() { + let captured_ref = RefCell::new(String::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/custom-sce", + push_notes_enabled: true, + logger: None, + }; + run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| Ok(()), + |_, notes_ref, commit_id, trace_json| { + *captured_ref.borrow_mut() = notes_ref.to_string(); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: trace_json + .contains("\"version\": \"0.1.0\"") + .then(|| notes_ref.to_string()) + .expect("git-note content should be full Agent Trace JSON"), + }) + }, + |_, remote, notes_ref| { + assert_eq!(remote, "https://example.test/repo.git"); + assert_eq!(notes_ref, "refs/notes/custom-sce"); + Ok(GitNotesPushOutcome { + remote: remote.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + ) + .expect("Agent Trace flow should succeed with configured git-notes ref"); + + assert_eq!(captured_ref.into_inner(), "refs/notes/custom-sce"); + } + + #[test] + fn post_commit_agent_trace_flow_skips_push_when_config_disabled() { + let order = RefCell::new(Vec::::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: false, + logger: None, + }; + run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| { + order.borrow_mut().push(String::from("db")); + Ok(()) + }, + |_, notes_ref, commit_id, _| { + order.borrow_mut().push(String::from("note")); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + |_, _, _| { + order.borrow_mut().push(String::from("push")); + Ok(GitNotesPushOutcome { + remote: String::from("https://example.test/repo.git"), + notes_ref: String::from("refs/notes/sce-agent-trace"), + }) + }, + ) + .expect("Agent Trace flow should succeed with notes push disabled"); + + assert_eq!(order.into_inner(), vec!["db", "note"]); + } + + #[test] + fn post_commit_agent_trace_flow_swallows_git_notes_push_failure() { + let logger = CapturingLogger::default(); + let order = RefCell::new(Vec::::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: true, + logger: Some(&logger), + }; + let result = run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| { + order.borrow_mut().push(String::from("db")); + Ok(()) + }, + |_, notes_ref, commit_id, _| { + order.borrow_mut().push(String::from("note")); + Ok(GitNoteWriteOutcome { + commit_id: commit_id.to_string(), + notes_ref: notes_ref.to_string(), + }) + }, + |_, remote, notes_ref| { + order.borrow_mut().push(String::from("push")); + assert_eq!(remote, "https://example.test/repo.git"); + assert_eq!(notes_ref, "refs/notes/sce-agent-trace"); + bail!("simulated git push failure") + }, + ) + .expect("git-notes push failure should not fail post-commit Agent Trace flow"); + + assert_eq!(order.into_inner(), vec!["db", "note", "push"]); + assert!(result.trace_json.contains("\"version\": \"0.1.0\"")); + assert!(logger + .errors + .lock() + .expect("test logger mutex should not be poisoned") + .is_empty()); + } + + #[test] + fn post_commit_agent_trace_flow_logs_git_note_failure_without_failing() { + let logger = CapturingLogger::default(); + let order = RefCell::new(Vec::::new()); + + let git_note_persistence = PostCommitGitNotePersistence { + repository_root: Path::new("/repo"), + vcs_type: Some(AgentTraceVcsType::Git), + git_notes_ref: "refs/notes/sce-agent-trace", + push_notes_enabled: true, + logger: Some(&logger), + }; + let result = run_post_commit_agent_trace_flow_with( + &git_note_persistence, + &post_commit_flow_result(), + "https://example.test/repo.git", + |_| Ok(()), + |_| { + order.borrow_mut().push(String::from("db")); + Ok(()) + }, + |_, _, _, _| { + order.borrow_mut().push(String::from("note")); + bail!("simulated git notes failure") + }, + |_, _, _| { + order.borrow_mut().push(String::from("push")); + Ok(GitNotesPushOutcome { + remote: String::from("https://example.test/repo.git"), + notes_ref: String::from("refs/notes/sce-agent-trace"), + }) + }, + ) + .expect("git-note failure should not fail post-commit Agent Trace flow"); + + assert_eq!(order.into_inner(), vec!["db", "note"]); + assert!(result.trace_json.contains("\"version\": \"0.1.0\"")); + + let errors = logger + .errors + .lock() + .expect("test logger mutex should not be poisoned"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].0, + "sce.hooks.post_commit.agent_trace_git_note_write_failed" + ); + assert!(errors[0].1.contains("simulated git notes failure")); + } + + fn captured_note_trace_json(result: &PostCommitAgentTraceFlowResult) -> String { + serde_json::to_string_pretty(&result.agent_trace) + .map(|value| format!("{value}\n")) + .expect("test Agent Trace should serialize") + } + #[test] fn conversation_trace_mixed_payload_maps_to_message_and_part_insert_inputs() { let patch_text = valid_patch_text("src/lib.rs", "let answer = 42;"); @@ -2284,6 +2849,103 @@ mod tests { } } + #[test] + fn prefixed_diff_trace_session_id_prefixes_fresh_pi_session_id() { + assert_eq!( + prefixed_diff_trace_session_id("pi", "session-123"), + "pi_session-123" + ); + } + + #[test] + fn prefixed_diff_trace_session_id_keeps_already_prefixed_pi_session_id() { + assert_eq!( + prefixed_diff_trace_session_id("pi", "pi_session-123"), + "pi_session-123" + ); + } + + #[test] + fn pi_normalized_diff_trace_payload_persists_with_pi_prefixed_session_id() { + let stdin_payload = serde_json::json!({ + "sessionID": "session-123", + "diff": "diff text", + "time": 1_800_000_000_000_u64, + "model_id": "anthropic/claude-opus-4", + "tool_name": "pi", + "tool_version": null + }) + .to_string(); + + let parsed = parse_diff_trace_payload(&stdin_payload) + .expect("normalized Pi diff-trace payload should parse"); + let payload = match parsed { + DiffTraceParseResult::Persist(payload) => payload, + DiffTraceParseResult::NoOp(message) => { + panic!("Pi payload should persist, got no-op: {message}") + } + }; + + assert_eq!(payload.tool_name, "pi"); + assert_eq!(payload.model_id.as_deref(), Some("anthropic/claude-opus-4")); + assert_eq!(payload.tool_version, None); + + persist_diff_trace_payload_to_agent_trace_db_with( + &payload, + payload.model_id.as_deref(), + payload.tool_version.as_deref(), + |input| { + assert_eq!(input.time_ms, 1_800_000_000_000_i64); + assert_eq!(input.session_id, "pi_session-123"); + assert_eq!(input.model_id, Some("anthropic/claude-opus-4")); + assert_eq!(input.tool_name, "pi"); + assert_eq!(input.tool_version, None); + assert_eq!(input.payload_type, PAYLOAD_TYPE_PATCH); + + Ok(()) + }, + ) + .expect("Pi diff-trace payload should be persisted"); + } + + #[test] + fn post_commit_intersection_flow_preserves_pi_provenance() { + let now_ms = 1_800_000_000_000_i64; + let commit_time_ms = now_ms - 1_000; + + let output = run_post_commit_intersection_flow_with( + Path::new("/repo"), + |_| { + Ok(PostCommitPatchData { + commit_oid: String::from("def456"), + commit_time_ms, + parsed_patch: valid_patch("src/lib.rs", "shared line"), + }) + }, + || Ok(now_ms), + |_, _| { + Ok(RecentDiffTracePatches { + patches: vec![ParsedDiffTracePatch { + id: 9, + time_ms: now_ms - 500, + session_id: String::from("pi_valid-session"), + patch: valid_patch("src/lib.rs", "shared line"), + tool_name: Some(String::from("pi")), + tool_version: None, + payload_type: String::from(PAYLOAD_TYPE_PATCH), + }], + skipped: vec![], + }) + }, + |_| Ok(()), + ) + .expect("post-commit intersection flow should succeed"); + + assert_eq!(output.combined_recent_patch.files.len(), 1); + assert_eq!(output.tool_name, Some(String::from("pi"))); + assert_eq!(output.tool_version, None); + } + #[test] fn diff_trace_db_persistence_uses_direct_payload_model_and_tool_version() { let payload = diff_trace_payload(Some("direct-model"), None); @@ -2393,4 +3055,205 @@ mod tests { assert_eq!(output.tool_name, Some(String::from("opencode"))); assert_eq!(output.tool_version, Some(String::from("1.2.3"))); } + + #[test] + fn git_note_writer_builds_replace_command_and_pipes_json() { + let captured_args = RefCell::new(Vec::::new()); + let captured_content = RefCell::new(String::new()); + + let outcome = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "abc123", + "{\n \"version\": \"0.1.0\"\n}\n", + |root, args, content| { + assert_eq!(root, Path::new("/repo")); + *captured_args.borrow_mut() = args.to_vec(); + *captured_content.borrow_mut() = content.to_string(); + Ok(()) + }, + ) + .expect("git-note writer should succeed"); + + assert_eq!(outcome.commit_id, "abc123"); + assert_eq!(outcome.notes_ref, "refs/notes/sce-agent-trace"); + assert_eq!( + captured_args.into_inner(), + vec![ + "notes", + "--ref", + "refs/notes/sce-agent-trace", + "add", + "-f", + "-F", + "-", + "abc123", + ] + ); + assert_eq!( + captured_content.into_inner(), + "{\n \"version\": \"0.1.0\"\n}\n" + ); + } + + #[test] + fn git_notes_push_helper_builds_push_command() { + let captured_args = RefCell::new(Vec::::new()); + + let outcome = push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + "origin", + "refs/notes/sce-agent-trace", + |root, args| { + assert_eq!(root, Path::new("/repo")); + *captured_args.borrow_mut() = args.to_vec(); + Ok(()) + }, + ) + .expect("git-notes push helper should succeed"); + + assert_eq!(outcome.remote, "origin"); + assert_eq!(outcome.notes_ref, "refs/notes/sce-agent-trace"); + assert_eq!( + captured_args.into_inner(), + vec!["push", "origin", "refs/notes/sce-agent-trace"] + ); + } + + #[test] + fn git_notes_push_helper_honors_configured_ref() { + let captured_args = RefCell::new(Vec::::new()); + + push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + "origin", + "refs/notes/custom-agent-trace", + |_, args| { + *captured_args.borrow_mut() = args.to_vec(); + Ok(()) + }, + ) + .expect("git-notes push helper should succeed"); + + assert_eq!( + captured_args.into_inner(), + vec!["push", "origin", "refs/notes/custom-agent-trace"] + ); + } + + #[test] + fn git_notes_push_helper_rejects_blank_inputs() { + let error = push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + " ", + "refs/notes/sce-agent-trace", + |_, _| panic!("runner should not be called for invalid input"), + ) + .expect_err("blank remote should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note git remote")); + + let error = + push_agent_trace_git_notes_ref_with(Path::new("/repo"), "origin", "\t", |_, _| { + panic!("runner should not be called for invalid input") + }) + .expect_err("blank ref should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note git notes ref")); + } + + #[test] + fn git_notes_push_helper_returns_command_failure_with_context() { + let error = push_agent_trace_git_notes_ref_with( + Path::new("/repo"), + "origin", + "refs/notes/sce-agent-trace", + |_, _| bail!("simulated git push failure"), + ) + .expect_err("git push failure should fail helper"); + + let rendered = format!("{error:#}"); + assert!(rendered.contains( + "Failed to push Agent Trace git notes ref 'refs/notes/sce-agent-trace' to remote 'origin'." + )); + assert!(rendered.contains("simulated git push failure")); + } + + #[test] + fn git_note_writer_honors_configured_ref() { + let captured_args = RefCell::new(Vec::::new()); + + write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/custom-agent-trace", + "def456", + "{}", + |_, args, _| { + *captured_args.borrow_mut() = args.to_vec(); + Ok(()) + }, + ) + .expect("git-note writer should succeed"); + + assert_eq!( + captured_args.into_inner()[2], + "refs/notes/custom-agent-trace" + ); + } + + #[test] + fn git_note_writer_rejects_blank_inputs() { + let error = + write_agent_trace_git_note_with(Path::new("/repo"), " ", "abc123", "{}", |_, _, _| { + panic!("runner should not be called for invalid input") + }) + .expect_err("blank ref should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note git notes ref")); + + let error = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "\t", + "{}", + |_, _, _| panic!("runner should not be called for invalid input"), + ) + .expect_err("blank commit should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note commit ID")); + + let error = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "abc123", + "\n", + |_, _, _| panic!("runner should not be called for invalid input"), + ) + .expect_err("blank content should fail"); + assert!(error + .to_string() + .contains("Invalid Agent Trace git-note Agent Trace JSON")); + } + + #[test] + fn git_note_writer_returns_command_failure_with_context() { + let error = write_agent_trace_git_note_with( + Path::new("/repo"), + "refs/notes/sce-agent-trace", + "abc123", + "{}", + |_, _, _| bail!("simulated git failure"), + ) + .expect_err("git failure should fail helper"); + + let rendered = format!("{error:#}"); + assert!(rendered.contains( + "Failed to write Agent Trace git note for commit 'abc123' under ref 'refs/notes/sce-agent-trace'." + )); + assert!(rendered.contains("simulated git failure")); + } } diff --git a/cli/src/services/lifecycle.rs b/cli/src/services/lifecycle.rs index dc2c93d4..df335874 100644 --- a/cli/src/services/lifecycle.rs +++ b/cli/src/services/lifecycle.rs @@ -54,11 +54,14 @@ pub enum HealthProblemKind { OpenCodeIntegrationContentMismatch, ClaudeIntegrationFilesMissing, ClaudeIntegrationContentMismatch, + PiIntegrationFilesMissing, + PiIntegrationContentMismatch, OpenCodePluginRegistryInvalid, OpenCodeAssetMissingOrInvalid, HookReadFailed, OpenCodeAssetReadFailed, ClaudeAssetReadFailed, + PiAssetReadFailed, AgentTraceDbConnectionFailed, AgentTraceDbSchemaNotReady, } diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 01c8d804..6fe07f9c 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -217,11 +217,12 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result convert_setup_command(opencode, claude, both, non_interactive, hooks, repo), + } => convert_setup_command(opencode, claude, pi, all, non_interactive, hooks, repo), cli_schema::Commands::Doctor { fix, format } => Ok(convert_doctor_command(fix, format)), cli_schema::Commands::Hooks { subcommand } => convert_hooks_subcommand(subcommand), cli_schema::Commands::Policy { subcommand } => Ok(convert_policy_subcommand(&subcommand)), @@ -395,7 +396,8 @@ fn convert_config_subcommand( fn convert_setup_command( opencode: bool, claude: bool, - both: bool, + pi: bool, + all: bool, non_interactive: bool, hooks: bool, repo: Option, @@ -405,7 +407,8 @@ fn convert_setup_command( non_interactive, opencode, claude, - both, + pi, + all, hooks, repo_path: repo, }; diff --git a/cli/src/services/setup/mod.rs b/cli/src/services/setup/mod.rs index 721227a1..5254c47d 100644 --- a/cli/src/services/setup/mod.rs +++ b/cli/src/services/setup/mod.rs @@ -25,7 +25,8 @@ pub const NAME: &str = "setup"; pub enum SetupTarget { OpenCode, Claude, - Both, + Pi, + All, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -62,37 +63,47 @@ pub fn get_required_hook_asset(hook: RequiredHookAsset) -> Option<&'static Embed .find(|asset| asset.relative_path == hook_name) } -pub enum EmbeddedAssetSelectionIter { - One(std::slice::Iter<'static, EmbeddedAsset>), - Both( - std::iter::Chain< - std::slice::Iter<'static, EmbeddedAsset>, - std::slice::Iter<'static, EmbeddedAsset>, - >, - ), +pub struct EmbeddedAssetSelectionIter { + asset_slices: std::vec::IntoIter<&'static [EmbeddedAsset]>, + current: std::slice::Iter<'static, EmbeddedAsset>, +} + +impl EmbeddedAssetSelectionIter { + fn new(asset_slices: Vec<&'static [EmbeddedAsset]>) -> Self { + Self { + asset_slices: asset_slices.into_iter(), + current: [].iter(), + } + } } impl Iterator for EmbeddedAssetSelectionIter { type Item = &'static EmbeddedAsset; fn next(&mut self) -> Option { - match self { - Self::One(iter) => iter.next(), - Self::Both(iter) => iter.next(), + loop { + if let Some(asset) = self.current.next() { + return Some(asset); + } + self.current = self.asset_slices.next()?.iter(); } } } pub fn iter_embedded_assets_for_setup_target(target: SetupTarget) -> EmbeddedAssetSelectionIter { - match target { - SetupTarget::OpenCode => EmbeddedAssetSelectionIter::One(OPENCODE_EMBEDDED_ASSETS.iter()), - SetupTarget::Claude => EmbeddedAssetSelectionIter::One(CLAUDE_EMBEDDED_ASSETS.iter()), - SetupTarget::Both => EmbeddedAssetSelectionIter::Both( - OPENCODE_EMBEDDED_ASSETS - .iter() - .chain(CLAUDE_EMBEDDED_ASSETS.iter()), - ), - } + let asset_slices = concrete_targets_for(target) + .iter() + .map(|concrete| match concrete { + SetupTarget::OpenCode => OPENCODE_EMBEDDED_ASSETS, + SetupTarget::Claude => CLAUDE_EMBEDDED_ASSETS, + SetupTarget::Pi => PI_EMBEDDED_ASSETS, + SetupTarget::All => { + unreachable!("meta targets are expanded into concrete targets") + } + }) + .collect(); + + EmbeddedAssetSelectionIter::new(asset_slices) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -114,7 +125,8 @@ pub struct SetupCliOptions { pub non_interactive: bool, pub opencode: bool, pub claude: bool, - pub both: bool, + pub pi: bool, + pub all: bool, pub hooks: bool, pub repo_path: Option, } @@ -141,19 +153,22 @@ pub fn resolve_setup_request(options: SetupCliOptions) -> Result { if options.claude { selected_targets.push(SetupTarget::Claude); } - if options.both { - selected_targets.push(SetupTarget::Both); + if options.pi { + selected_targets.push(SetupTarget::Pi); + } + if options.all { + selected_targets.push(SetupTarget::All); } if selected_targets.len() > 1 { bail!( - "Options '--opencode', '--claude', and '--both' are mutually exclusive. Try: choose exactly one target flag (for example 'sce setup --opencode --non-interactive') or omit all target flags for interactive mode." + "Options '--opencode', '--claude', '--pi', and '--all' are mutually exclusive. Try: choose exactly one target flag (for example 'sce setup --opencode --non-interactive') or omit all target flags for interactive mode." ); } if options.non_interactive && selected_targets.is_empty() && !options.hooks { bail!( - "Option '--non-interactive' requires a target flag. Try: 'sce setup --opencode --non-interactive', 'sce setup --claude --non-interactive', or 'sce setup --both --non-interactive'." + "Option '--non-interactive' requires a target flag. Try: 'sce setup --opencode --non-interactive', 'sce setup --claude --non-interactive', 'sce setup --pi --non-interactive', or 'sce setup --all --non-interactive'." ); } @@ -316,7 +331,8 @@ fn setup_target_label(target: SetupTarget) -> &'static str { match target { SetupTarget::OpenCode => "OpenCode", SetupTarget::Claude => "Claude", - SetupTarget::Both => "Both", + SetupTarget::Pi => "Pi", + SetupTarget::All => "All", } } @@ -403,18 +419,20 @@ pub(crate) fn concrete_targets_for(target: SetupTarget) -> &'static [SetupTarget match target { SetupTarget::OpenCode => &[SetupTarget::OpenCode], SetupTarget::Claude => &[SetupTarget::Claude], - SetupTarget::Both => &[SetupTarget::OpenCode, SetupTarget::Claude], + SetupTarget::Pi => &[SetupTarget::Pi], + SetupTarget::All => &[SetupTarget::OpenCode, SetupTarget::Claude, SetupTarget::Pi], } } -/// Convert a concrete [`SetupTarget`] (not `Both`) to its canonical +/// Convert a concrete [`SetupTarget`] (not `All`) to its canonical /// `integrations.target` string representation. fn integration_target_id_str(target: SetupTarget) -> &'static str { match target { SetupTarget::OpenCode => "opencode", SetupTarget::Claude => "claude", - SetupTarget::Both => { - unreachable!("integration_target_id_str must not be called with SetupTarget::Both") + SetupTarget::Pi => "pi", + SetupTarget::All => { + unreachable!("integration_target_id_str must not be called with meta targets") } } } @@ -465,7 +483,7 @@ pub fn persist_integration_targets(repository_root: &Path, target: SetupTarget) }) .unwrap_or_default(); - // Add new concrete targets (expanding Both), deduping as we go. + // Add new concrete targets (expanding All), deduping as we go. let new_targets = concrete_targets_for(target); for concrete in new_targets { let id_str = integration_target_id_str(*concrete); @@ -899,7 +917,10 @@ mod install { let destination_root = match target { SetupTarget::OpenCode => install_targets.opencode_target_dir(), SetupTarget::Claude => install_targets.claude_target_dir(), - SetupTarget::Both => unreachable!("both is expanded into concrete targets"), + SetupTarget::Pi => install_targets.pi_target_dir(), + SetupTarget::All => { + unreachable!("meta targets are expanded into concrete targets") + } }; let staging_root = create_staging_root(repository_root, target)?; @@ -1015,7 +1036,10 @@ mod install { let target_dir = match target { SetupTarget::OpenCode => install_targets.opencode_target_dir(), SetupTarget::Claude => install_targets.claude_target_dir(), - SetupTarget::Both => unreachable!("both is expanded into concrete targets"), + SetupTarget::Pi => install_targets.pi_target_dir(), + SetupTarget::All => { + unreachable!("meta targets are expanded into concrete targets") + } }; let target_label = target_dir .file_name() @@ -1071,7 +1095,8 @@ impl SetupTargetPrompter for InquireSetupTargetPrompter { enum SetupPromptTarget { OpenCode, Claude, - Both, + Pi, + All, } impl std::fmt::Display for SetupPromptTarget { @@ -1111,7 +1136,8 @@ mod prompt { let options = vec![ SetupPromptTarget::OpenCode, SetupPromptTarget::Claude, - SetupPromptTarget::Both, + SetupPromptTarget::Pi, + SetupPromptTarget::All, ]; let selection = Select::new(&setup_prompt_title(), options).prompt(); @@ -1123,14 +1149,17 @@ mod prompt { Ok(SetupPromptTarget::Claude) => { Ok(SetupDispatch::Proceed(SetupMode::NonInteractive(SetupTarget::Claude))) } - Ok(SetupPromptTarget::Both) => { - Ok(SetupDispatch::Proceed(SetupMode::NonInteractive(SetupTarget::Both))) + Ok(SetupPromptTarget::Pi) => { + Ok(SetupDispatch::Proceed(SetupMode::NonInteractive(SetupTarget::Pi))) + } + Ok(SetupPromptTarget::All) => { + Ok(SetupDispatch::Proceed(SetupMode::NonInteractive(SetupTarget::All))) } Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => { Ok(SetupDispatch::Cancelled) } Err(InquireError::NotTTY) => bail!( - "Interactive setup requires a TTY. Re-run with '--non-interactive' and one of '--opencode', '--claude', or '--both'." + "Interactive setup requires a TTY. Re-run with '--non-interactive' and one of '--opencode', '--claude', '--pi', or '--all'." ), Err(error) => Err(error.into()), } @@ -1154,7 +1183,8 @@ mod prompt { let label = match target { SetupPromptTarget::OpenCode => "OpenCode", SetupPromptTarget::Claude => "Claude", - SetupPromptTarget::Both => "Both", + SetupPromptTarget::Pi => "Pi", + SetupPromptTarget::All => "All (OpenCode + Claude + Pi)", }; prompt_value_with_color_policy(label, color_enabled) @@ -1181,3 +1211,89 @@ where pub fn setup_cancelled_text() -> String { value("Setup cancelled. No files were changed.") } + +#[cfg(test)] +mod tests { + use super::*; + + fn options_with(mutate: impl FnOnce(&mut SetupCliOptions)) -> SetupCliOptions { + let mut options = SetupCliOptions::default(); + mutate(&mut options); + options + } + + #[test] + fn resolve_setup_request_accepts_pi_target() { + let request = resolve_setup_request(options_with(|options| { + options.pi = true; + options.non_interactive = true; + })) + .expect("pi target should resolve"); + + assert_eq!( + request.config_mode, + Some(SetupMode::NonInteractive(SetupTarget::Pi)) + ); + } + + #[test] + fn resolve_setup_request_accepts_all_target() { + let request = resolve_setup_request(options_with(|options| { + options.all = true; + options.non_interactive = true; + })) + .expect("all target should resolve"); + + assert_eq!( + request.config_mode, + Some(SetupMode::NonInteractive(SetupTarget::All)) + ); + } + + #[test] + fn resolve_setup_request_rejects_combined_target_flags() { + let error = resolve_setup_request(options_with(|options| { + options.pi = true; + options.all = true; + })) + .expect_err("combined target flags must be rejected"); + + assert!(error.to_string().contains("mutually exclusive")); + } + + #[test] + fn resolve_setup_request_non_interactive_error_lists_pi_and_all() { + let error = resolve_setup_request(options_with(|options| { + options.non_interactive = true; + })) + .expect_err("non-interactive without target must be rejected"); + + let message = error.to_string(); + assert!(message.contains("--pi")); + assert!(message.contains("--all")); + } + + #[test] + fn concrete_targets_for_all_expands_to_three_targets() { + assert_eq!( + concrete_targets_for(SetupTarget::All), + &[SetupTarget::OpenCode, SetupTarget::Claude, SetupTarget::Pi] + ); + } + + #[test] + fn integration_target_id_str_maps_pi() { + assert_eq!(integration_target_id_str(SetupTarget::Pi), "pi"); + } + + #[test] + fn iter_embedded_assets_for_all_covers_each_concrete_target() { + let all_count = iter_embedded_assets_for_setup_target(SetupTarget::All).count(); + let concrete_sum = iter_embedded_assets_for_setup_target(SetupTarget::OpenCode).count() + + iter_embedded_assets_for_setup_target(SetupTarget::Claude).count() + + iter_embedded_assets_for_setup_target(SetupTarget::Pi).count(); + + assert!(iter_embedded_assets_for_setup_target(SetupTarget::Pi).count() > 0); + assert_eq!(all_count, concrete_sum); + } +} diff --git a/config/.pi/extensions/sce/index.ts b/config/.pi/extensions/sce/index.ts new file mode 100644 index 00000000..25770ff8 --- /dev/null +++ b/config/.pi/extensions/sce/index.ts @@ -0,0 +1,455 @@ +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { + dirname, + isAbsolute, + join, + relative, + resolve as resolvePath, +} from "node:path"; +import { + type ExtensionAPI, + isToolCallEventType, +} from "@earendil-works/pi-coding-agent"; + +interface JsonPolicyResult { + status: string; + decision: string; + command: string; + normalized_argv?: string[]; + reason?: string; + policy_id?: string; +} + +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + +type ConversationTraceMessageItem = { + type: "message"; + session_id: string; + message_id: string; + role: "user" | "assistant"; + generated_at_unix_ms: number; +}; + +type ConversationTraceMessagePartItem = { + type: "message.part"; + session_id: string; + message_id: string; + part_type: "text" | "reasoning" | "patch"; + text: string; + generated_at_unix_ms: number; +}; + +type ConversationTraceItem = + | ConversationTraceMessageItem + | ConversationTraceMessagePartItem; + +type ConversationTracePayload = { + payloads: ConversationTraceItem[]; +}; + +type DiffTracePayload = { + sessionID: string; + diff: string; + time: number; + model_id: string | null; + tool_name: "pi"; + tool_version: string | null; +}; + +type PendingFileMutation = { + absolutePath: string; + diffLabel: string; + before: string | undefined; +}; + +/** + * Evaluate a bash command against SCE bash-tool policy by delegating to the + * Rust `sce policy bash` command. Returns the parsed JSON result, or null if + * the policy check could not be performed (fail-open). + */ +function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { + try { + const result = spawnSync( + "sce", + ["policy", "bash", "--input", "normalized", "--output", "json"], + { + input: JSON.stringify({ command }), + encoding: "utf8", + timeout: 10_000, + }, + ); + + if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + return null; + } + + if (result.status !== 0) { + return null; + } + + const stdout = result.stdout?.trim(); + if (!stdout) { + return null; + } + + const parsed: JsonPolicyResult = JSON.parse(stdout); + return parsed; + } catch { + return null; + } +} + +/** + * Send a conversation-trace payload to `sce hooks conversation-trace`, + * fire-and-forget. Fail-open: stderr is ignored so that sce intake errors do + * not leak into the Pi TUI, and the returned promise never rejects. + */ +function runConversationTraceHook( + cwd: string, + payload: ConversationTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "conversation-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +type MessageContentBlock = { + type: string; + text?: unknown; + thinking?: unknown; +}; + +function extractMessageParts( + content: string | readonly MessageContentBlock[], +): Array<{ part_type: "text" | "reasoning"; text: string }> { + if (typeof content === "string") { + return content.length > 0 ? [{ part_type: "text", text: content }] : []; + } + + const parts: Array<{ part_type: "text" | "reasoning"; text: string }> = []; + for (const block of content) { + if (block.type === "text" && typeof block.text === "string" && block.text) { + parts.push({ part_type: "text", text: block.text }); + } else if ( + block.type === "thinking" && + typeof block.thinking === "string" && + block.thinking + ) { + parts.push({ part_type: "reasoning", text: block.thinking }); + } + } + return parts; +} + +function buildMessageEndConversationTracePayload( + sessionId: string, + message: { + role: string; + content: string | readonly MessageContentBlock[]; + responseId?: string; + }, +): ConversationTracePayload | undefined { + if (message.role !== "user" && message.role !== "assistant") { + return undefined; + } + + const messageId = message.responseId ?? randomUUID(); + const generatedAtUnixMs = Date.now(); + + const payloads: ConversationTraceItem[] = [ + { + type: "message", + session_id: sessionId, + message_id: messageId, + role: message.role, + generated_at_unix_ms: generatedAtUnixMs, + }, + ]; + + for (const part of extractMessageParts(message.content)) { + payloads.push({ + type: "message.part", + session_id: sessionId, + message_id: messageId, + part_type: part.part_type, + text: part.text, + generated_at_unix_ms: generatedAtUnixMs, + }); + } + + return { payloads }; +} + +/** + * Resolve the installed Pi package version for diff-trace `tool_version`. + * The package's `exports` map does not expose `package.json`, so resolve the + * package entry point and read `package.json` from the package root instead. + * Returns null when resolution fails (normalized diff traces permit it). + */ +async function resolvePiToolVersion(): Promise { + try { + const require_ = createRequire(import.meta.url); + const entryPath = require_.resolve("@earendil-works/pi-coding-agent"); + const packageJsonPath = join(dirname(entryPath), "..", "package.json"); + const parsed: { version?: unknown } = JSON.parse( + await readFile(packageJsonPath, "utf8"), + ); + return typeof parsed.version === "string" && parsed.version.length > 0 + ? parsed.version + : null; + } catch { + return null; + } +} + +/** + * Send a diff-trace payload to `sce hooks diff-trace`, fire-and-forget. + * Fail-open: stderr is ignored and the returned promise never rejects. + */ +function runDiffTraceHook( + cwd: string, + payload: DiffTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "diff-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +async function readFileOrUndefined(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch { + return undefined; + } +} + +function diffLabelFor(cwd: string, absolutePath: string): string { + const relPath = relative(cwd, absolutePath); + return relPath.length > 0 && !relPath.startsWith("..") && !isAbsolute(relPath) + ? relPath + : absolutePath; +} + +/** + * Rewrite temp-file path labels in git diff header lines to the repo-relative + * target path. Only header lines before the first `@@` hunk marker are + * touched so that content lines starting with `--- ` / `+++ ` are preserved. + */ +function rewriteDiffLabels( + diff: string, + label: string, + isCreate: boolean, +): string { + const lines = diff.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("@@")) { + break; + } + if (line.startsWith("diff --git ")) { + lines[i] = `diff --git a/${label} b/${label}`; + } else if (line.startsWith("--- ")) { + lines[i] = isCreate ? "--- /dev/null" : `--- a/${label}`; + } else if (line.startsWith("+++ ")) { + lines[i] = `+++ b/${label}`; + } + } + return lines.join("\n"); +} + +/** + * Produce a unified diff between before/after contents by writing them to + * temp files and spawning `git diff --no-index --no-ext-diff` (exit status 1 + * means "files differ"). Returns undefined for no-op diffs or any failure; + * temp files are always cleaned up. + */ +async function buildUnifiedDiff( + label: string, + before: string | undefined, + after: string, +): Promise { + const tempDir = await mkdtemp(join(tmpdir(), "sce-pi-diff-")); + try { + const beforePath = join(tempDir, "before"); + const afterPath = join(tempDir, "after"); + await writeFile(beforePath, before ?? "", "utf8"); + await writeFile(afterPath, after, "utf8"); + + const result = spawnSync( + "git", + ["diff", "--no-index", "--no-ext-diff", "--", beforePath, afterPath], + { encoding: "utf8", timeout: 10_000 }, + ); + + if (result.error || result.status !== 1) { + return undefined; + } + const stdout = result.stdout; + if (!stdout) { + return undefined; + } + return rewriteDiffLabels(stdout, label, before === undefined); + } catch { + return undefined; + } finally { + await rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +} + +export default function sceExtension(pi: ExtensionAPI): void { + const pendingFileMutations = new Map(); + const piToolVersionPromise = resolvePiToolVersion(); + + pi.on("tool_call", (event) => { + if (!isToolCallEventType("bash", event)) { + return undefined; + } + + const command = event.input.command; + if (typeof command !== "string" || command.length === 0) { + return undefined; + } + + const policyResult = evaluateBashCommandPolicy(command); + if (!policyResult) { + // Fail open: if the policy check cannot be performed, allow the command. + return undefined; + } + + if (policyResult.decision === "deny" && policyResult.reason) { + return { block: true, reason: policyResult.reason }; + } + + return undefined; + }); + + pi.on("tool_call", async (event, ctx) => { + if ( + !isToolCallEventType("edit", event) && + !isToolCallEventType("write", event) + ) { + return undefined; + } + + const targetPath = event.input.path; + if (typeof targetPath !== "string" || targetPath.length === 0) { + return undefined; + } + + const absolutePath = resolvePath(ctx.cwd, targetPath); + pendingFileMutations.set(event.toolCallId, { + absolutePath, + diffLabel: diffLabelFor(ctx.cwd, absolutePath), + before: await readFileOrUndefined(absolutePath), + }); + return undefined; + }); + + pi.on("tool_result", async (event, ctx) => { + const pending = pendingFileMutations.get(event.toolCallId); + if (!pending) { + return; + } + pendingFileMutations.delete(event.toolCallId); + + if (event.isError) { + return; + } + + const after = await readFileOrUndefined(pending.absolutePath); + if (after === undefined || after === pending.before) { + return; + } + + const diff = await buildUnifiedDiff( + pending.diffLabel, + pending.before, + after, + ); + if (!diff) { + return; + } + + const sessionId = ctx.sessionManager.getSessionId(); + const generatedAtUnixMs = Date.now(); + const patchMessageId = `${event.toolCallId}-patch`; + + void runConversationTraceHook(ctx.cwd, { + payloads: [ + { + type: "message", + session_id: sessionId, + message_id: patchMessageId, + role: "assistant", + generated_at_unix_ms: generatedAtUnixMs, + }, + { + type: "message.part", + session_id: sessionId, + message_id: patchMessageId, + part_type: "patch", + text: diff, + generated_at_unix_ms: generatedAtUnixMs, + }, + ], + }); + + void runDiffTraceHook(ctx.cwd, { + sessionID: sessionId, + diff, + time: generatedAtUnixMs, + model_id: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, + tool_name: "pi", + tool_version: await piToolVersionPromise, + }); + }); + + pi.on("message_end", (event, ctx) => { + const message = event.message; + if (message.role !== "user" && message.role !== "assistant") { + return; + } + + const payload = buildMessageEndConversationTracePayload( + ctx.sessionManager.getSessionId(), + message, + ); + if (payload) { + void runConversationTraceHook(ctx.cwd, payload); + } + }); +} diff --git a/config/.pi/prompts/agent-shared-context-code.md b/config/.pi/prompts/agent-shared-context-code.md new file mode 100644 index 00000000..9d3abdd5 --- /dev/null +++ b/config/.pi/prompts/agent-shared-context-code.md @@ -0,0 +1,62 @@ +--- +description: "Act in the shared-context-code role to execute one approved SCE task and sync context." +argument-hint: " [T0X]" +--- + +Act as the Shared Context Code agent for the rest of this session. Load and follow the `sce-task-execution` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +You are the Shared Context Code agent. + +Mission +- Implement exactly one approved task from an existing plan. +- Validate behavior and keep `context/` aligned with the resulting code. + +Core principles +- The human owns architecture, risk, and final decisions. +- `context/` is durable AI-first memory and must stay current-state oriented. +- If context and code diverge, code is source of truth and context must be repaired. + +Hard boundaries +- One task per session unless the human explicitly approves multi-task execution. +- Do not change plan structure or reorder tasks without approval. +- If scope expansion is required, stop and ask. + +Authority inside `context/` +- You may create, update, rename, move, or delete files under `context/` as needed. +- You may create new top-level folders under `context/` when needed. +- Delete a file only if it exists and has no uncommitted changes. +- Use Mermaid when a diagram is needed. + +Startup +1) Confirm this session targets one approved plan task. +2) Proceed using the Procedure below. + +Procedure +- Load `sce-plan-review` and follow it exactly. +- Ask for explicit user confirmation that the reviewed task is ready for implementation. +- After confirmation, load `sce-task-execution` and follow it exactly. +- After implementation, load `sce-context-sync` and follow it. +- Wait for user feedback. +- If feedback requires in-scope fixes, apply the fixes, rerun light task-level checks/lints, run a build if it is light/fast, and run `sce-context-sync` again. +- If this is the final plan task, load `sce-validation` and follow it. + +Important behaviors +- Keep context optimized for future AI sessions, not prose-heavy narration. +- Do not leave completed-work summaries in core context files; represent resulting current state. +- After accepted implementation changes, context synchronization is part of done. +- Long-term quality is measured by code quality and context accuracy. + +Natural nudges to use +- "I will run `sce-plan-review` first to confirm the next task and clarify acceptance criteria." +- "Please confirm this task is ready for implementation, then I will execute it." +- "I will run light, task-level checks and lints first, and run a build too if it is light/fast." +- "After implementation, I will sync `context/`, wait for feedback, and resync if we apply fixes." + +Definition of done +- Code changes satisfy task acceptance checks. +- Relevant tests/checks are executed with evidence. +- Plan task status is updated. +- Context and code have no unresolved drift for this task. diff --git a/config/.pi/prompts/agent-shared-context-plan.md b/config/.pi/prompts/agent-shared-context-plan.md new file mode 100644 index 00000000..5dbad4e1 --- /dev/null +++ b/config/.pi/prompts/agent-shared-context-plan.md @@ -0,0 +1,68 @@ +--- +description: "Act in the shared-context-plan role to create or update an SCE plan before implementation." +argument-hint: "" +--- + +Act as the Shared Context Plan agent for the rest of this session. Load and follow the `sce-plan-authoring` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +You are the Shared Context Plan agent. + +Mission +- Convert a human change request into an implementation plan in `context/plans/`. +- Keep planning deterministic and reviewable. + +Core principles +- The human owns architecture, risk, and final decisions. +- `context/` is durable AI-first memory and must stay current-state oriented. +- If context and code diverge, code is source of truth and context must be repaired. + +Hard boundaries +- Never modify application code. +- Never run shell commands. +- Only write planning and context artifacts. +- Planning does not imply execution approval. + +Authority inside `context/` +- You may create, update, rename, move, or delete files under `context/` as needed. +- You may create new top-level folders under `context/` when needed. +- Delete a file only if it exists and has no uncommitted changes. +- Use Mermaid when a diagram is needed. + +Startup +1) Check for `context/`. +2) If missing, ask once for approval to bootstrap. +3) If approved, load `sce-bootstrap-context` and follow it. +4) If not approved, stop. +5) Read `context/context-map.md`, `context/overview.md`, and `context/glossary.md` if present. +6) Before broad exploration, consult `context/context-map.md` for relevant context files. +7) If context is partial or stale, continue with code truth and propose focused context repairs. + +Procedure +- Load `sce-plan-authoring` and follow it exactly. +- Ask targeted clarifying questions when requirements, boundaries, dependencies, or acceptance criteria are unclear. +- Write or update `context/plans/{plan_name}.md`. +- Confirm plan creation with `plan_name` and exact file path. +- Present the full ordered task list in chat, if it's written to a subagent print it in the main agent. +- Prompt the user to start a new session to implement `T01`. +- Provide one canonical next command: `/next-task {plan_name} T01`. + +Important behaviors +- Keep context optimized for future AI sessions, not prose-heavy narration. +- Do not leave completed-work summaries in core context files; represent resulting current state. +- Treat `context/plans/` as active execution artifacts; completed plans are disposable and not durable history. +- Promote durable outcomes into current-state context files and `context/decisions/` when needed. +- Long-term quality is measured by code quality and context accuracy. + +Natural nudges to use +- "Let me pull relevant files from `context/` before implementation." +- "Per SCE, chat-mode first, then implementation mode." +- "I will propose a plan with trade-offs first, then implement." +- "Now that this is settled, I will sync `context/` so future sessions stay aligned." + +Definition of done +- Plan has stable task IDs (`T01..T0N`). +- Each task has boundaries, done checks, and verification notes. +- Final task is always validation and cleanup. diff --git a/config/.pi/prompts/change-to-plan.md b/config/.pi/prompts/change-to-plan.md new file mode 100644 index 00000000..8a782aea --- /dev/null +++ b/config/.pi/prompts/change-to-plan.md @@ -0,0 +1,17 @@ +--- +description: "Use `sce-plan-authoring` to turn a change request into a scoped SCE plan" +argument-hint: "" +--- + +Load and follow the `sce-plan-authoring` skill. + +Input change request: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; detailed clarification handling, plan-shape rules, and task-writing behavior stay owned by `sce-plan-authoring`. +- Run `sce-plan-authoring` to resolve whether the input targets a new or existing plan, normalize goals/constraints/success criteria, and produce an implementation-ready task stack. +- Preserve the clarification gate from `sce-plan-authoring`: if blockers, ambiguity, or missing acceptance criteria remain, stop and ask the focused user questions needed to finish the plan safely. +- Require one-task/one-atomic-commit slicing through `sce-plan-authoring` before any task is considered ready for implementation. +- When the plan is ready, write or update `context/plans/{plan_name}.md`, confirm the resolved `{plan_name}` and exact path, and return the ordered task list. +- Stop after the planning handoff by providing the exact next-session command `/next-task {plan_name} T01`. diff --git a/config/.pi/prompts/commit.md b/config/.pi/prompts/commit.md new file mode 100644 index 00000000..b868096a --- /dev/null +++ b/config/.pi/prompts/commit.md @@ -0,0 +1,42 @@ +--- +description: "Use `sce-atomic-commit` to propose atomic commit message(s) from staged changes" +argument-hint: "[oneshot|skip]" +--- + +Load and follow the `sce-atomic-commit` skill. + +Input: +`$ARGUMENTS` + +## Bypass path (`/commit oneshot` or `/commit skip`) + +If `$ARGUMENTS` starts with `oneshot` or `skip` (case-insensitive, first token only): + +- **Skip the staging confirmation prompt.** Do not ask the user to stage files or confirm staging. +- **Validate staged content exists.** Check that `git diff --cached` is non-empty. If no staged changes exist, stop with the error: "No staged changes. Stage changes before commit." Do not proceed. +- **Skip context-guidance gate classification.** Do not classify staged diff scope as `context/`-only vs mixed. Do not apply context-file guidance gating. +- **Produce exactly one commit message.** Run `sce-atomic-commit` with these overrides: + - Produce exactly one commit message. Do not propose splits. Do not emit split guidance. + - When staged changes include `context/plans/*.md`, make a best-effort inference to cite affected plan slug(s) and updated task ID(s). If ambiguous, omit the citation rather than stopping for clarification. +- **Auto-execute `git commit`.** Use the produced commit message to run `git commit -m ""`. + - If `git commit` succeeds, report the commit hash and stop. + - If `git commit` fails, stop and report the failure. Do not invent fallback commits, retry, or amend. + +## Regular path (no arguments or non-bypass arguments) + +If `$ARGUMENTS` does not start with `oneshot` or `skip`: + +Behavior: +- If arguments are empty, treat input as unstated and infer commit intent from staged changes only. +- If arguments are provided, treat them as optional commit context to refine message proposals. +- Keep this command as thin orchestration; staged-diff analysis, atomic split decisions, and message wording stay owned by `sce-atomic-commit`. +- Before running `sce-atomic-commit`, explicitly stop and prompt the user: + + "Please run `git add ` for all changes you want included in this commit. + Atomic commits should only include intentionally staged changes. + Confirm once staging is complete." + +- After confirmation: + - Classify staged diff scope (`context/`-only vs mixed `context/` + non-`context/`) and apply the context-guidance gate from `sce-atomic-commit`. + - Run `sce-atomic-commit` to produce commit-message proposals and any needed split guidance. +- Do not create commits automatically; stop after returning proposed commit message(s) and split guidance when needed. diff --git a/config/.pi/prompts/handover.md b/config/.pi/prompts/handover.md new file mode 100644 index 00000000..af3666ff --- /dev/null +++ b/config/.pi/prompts/handover.md @@ -0,0 +1,15 @@ +--- +description: "Run `sce-handover-writer` to capture the current task for handoff" +argument-hint: "[task context]" +--- + +Load and follow the `sce-handover-writer` skill. + +Input: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; handover structure, naming, and content decisions stay owned by `sce-handover-writer`. +- Run `sce-handover-writer` to gather current task state, decisions made and rationale, open questions or blockers, and the next recommended step. +- Let `sce-handover-writer` create the handover in `context/handovers/`, using task-aligned naming such as `context/handovers/{plan_name}-{task_id}-{timestamp}.md` when the inputs support it. +- If required details are missing, infer only from current repo state, label assumptions clearly, then stop after reporting the exact handover path. diff --git a/config/.pi/prompts/next-task.md b/config/.pi/prompts/next-task.md new file mode 100644 index 00000000..3cfa8607 --- /dev/null +++ b/config/.pi/prompts/next-task.md @@ -0,0 +1,24 @@ +--- +description: "Run `sce-plan-review` -> `sce-task-execution` -> `sce-context-sync` for one approved SCE task" +argument-hint: " [T0X]" +--- + +Load and follow `sce-plan-review`, then `sce-task-execution`, then `sce-context-sync`. + +Input: +`$ARGUMENTS` + +Expected arguments: +- plan name or plan path (required) +- task ID (`T0X`) (optional) + +Behavior: +- Keep this command as thin orchestration; skill-owned review, implementation, validation, and context-sync details stay in the referenced skills. +- Run `sce-plan-review` first to resolve the plan target, choose the task, and report readiness. +- Apply the readiness confirmation gate from `sce-plan-review` before implementation: + - auto-pass only when both plan + task ID are provided and review reports no blockers, ambiguity, or missing acceptance criteria + - otherwise resolve the open points and ask the user to confirm the task is ready before continuing +- Run `sce-task-execution` next; keep the mandatory implementation stop, scoped edits, light checks/lints/build, and plan status updates skill-owned. +- After implementation, run `sce-context-sync` as the required done gate and wait for user feedback. +- If feedback requires in-scope fixes, apply the fixes, rerun light checks (and a light/fast build when applicable), then run `sce-context-sync` again. +- If this was the final plan task, run `sce-validation`; otherwise stop after prompting a new session with `/next-task {plan_name} T0X`. diff --git a/config/.pi/prompts/validate.md b/config/.pi/prompts/validate.md new file mode 100644 index 00000000..e2c32ffe --- /dev/null +++ b/config/.pi/prompts/validate.md @@ -0,0 +1,15 @@ +--- +description: "Run `sce-validation` to finish an SCE plan with validation and cleanup" +argument-hint: "" +--- + +Load and follow the `sce-validation` skill. + +Input: +`$ARGUMENTS` + +Behavior: +- Keep this command as thin orchestration; validation scope, command selection, cleanup, and evidence formatting stay owned by `sce-validation`. +- Run `sce-validation` to execute the full validation phase for the targeted plan or change, including required checks, evidence capture, and cleanup expected by the skill. +- Let `sce-validation` decide pass/fail status and record any residual risks or unmet criteria. +- Stop after reporting the validation outcome and the location of any written validation evidence. diff --git a/config/.pi/skills/sce-atomic-commit/SKILL.md b/config/.pi/skills/sce-atomic-commit/SKILL.md new file mode 100644 index 00000000..0d7fe5bd --- /dev/null +++ b/config/.pi/skills/sce-atomic-commit/SKILL.md @@ -0,0 +1,102 @@ +--- +name: sce-atomic-commit +description: Write atomic, repo-style git commits from a change summary or diff. Use when preparing commit messages, splitting work into coherent commits, or reviewing whether a commit is too broad. +--- + +## Goal + +Turn the current staged changes into atomic repository-style commit message proposals. + +For this workflow: +- analyze the staged diff to identify coherent change units +- propose one or more commit messages when staged changes mix unrelated goals +- keep each proposed message focused on a single coherent change +- stay proposal-only: do not create commits automatically +- in bypass mode (command-invoked), relax proposal-only, split guidance, context-guidance gate, and plan-citation ambiguity rules + +## Inputs + +Accept any of: +- staged diff (preferred) +- changed file list with notes +- PR/task summary +- before/after behavior notes + +## Output format + +Produce commit message proposals that follow: +- `scope: Subject` +- imperative verb (Fix/Add/Remove/Implement/Refactor/Simplify/Rename/Update/Ensure/Allow) +- no trailing period in subject +- body when context is needed (why/what changed/impact) +- issue references on their own lines (for example `Fixes #123`) + +When staged changes include `context/plans/*.md`, each commit body must also include: +- affected plan slug(s) +- updated task ID(s) (`T0X`) + +If staged `context/plans/*.md` changes do not expose the plan slug or updated task ID clearly enough to cite faithfully, stop and ask for clarification instead of inventing references. + +## Bypass mode + +When this skill is invoked by the `/commit` command in bypass mode (`/commit oneshot` or `/commit skip`), the command passes overrides that relax the standard rules below: + +- **Proposal-only → auto-commit allowed.** Do not block auto-commit; the command will execute `git commit` with the produced message. +- **Single message only.** Produce exactly one commit message. Do not propose splits. Do not emit split guidance. +- **Context-guidance gate skipped.** Do not classify staged diff scope as `context/`-only vs mixed. Do not apply context-file guidance gating. +- **Plan citations: best-effort only.** When staged changes include `context/plans/*.md`, make a best-effort inference to cite affected plan slug(s) and updated task ID(s). If ambiguous, omit the citation rather than stopping for clarification. + +When NOT in bypass mode, follow the standard rules in the Procedure and Context-file guidance gating sections below. + +## Procedure + +1) Analyze the staged diff for coherent units +- Infer the main reason(s) for the staged change from the diff first. +- Use optional notes only to refine wording, not to override the staged truth. +- Identify whether staged changes represent one coherent unit or multiple unrelated goals. + +2) Choose scope for each unit +- Use the smallest stable subsystem/module name recognizable in the repo. +- If unclear, use the primary directory/package of the change. + +3) Write subject for each unit +- Pattern: `: ` +- Keep concrete and targeted. + +4) Add body when needed +- Explain what was wrong/missing, why it matters, what changed conceptually, and impact. +- Add issue references on separate lines. + +5) In regular mode: apply the plan-update body rule when needed +- Check whether staged changes include `context/plans/*.md`. +- If yes, cite the affected plan slug(s) and updated task ID(s) in the body. +- If the staged plan diff is ambiguous, stop with actionable guidance asking the user to stage or clarify the plan/task reference explicitly. + +6) In regular mode: propose split guidance when appropriate +- If staged changes mix unrelated goals (for example: a feature change plus unrelated refactoring), propose separate commit messages for each coherent unit. +- Explain why the split is recommended and which files belong to each proposed commit. +- If staged changes represent one coherent unit, propose a single commit message. + +7) In regular mode: validate each proposed message +- Each message should describe its intended change faithfully. +- The subject should stay concise and technical. +- The body should add useful why/impact context instead of repeating the subject. +- Do not invent plan or task references. + +## Context-file guidance gating + +In regular mode: +- Check staged diff scope before proposing commit messaging guidance. +- If staged changes are context-only (`context/**`), context-file-focused guidance is allowed. +- If staged changes are mixed (`context/**` + non-`context/**`), avoid default context-file commit reminders and prioritize guidance that reflects the full staged scope. + +## Anti-patterns + +- vague subjects ("cleanup", "updates") +- body repeats subject without adding why +- playful tone in serious fixes/architecture changes +- mention `context/` sync activity in commit messages +- inventing plan slugs or task IDs for staged plan edits +- proposing splits for changes that are already coherent +- forcing unrelated changes into a single commit +- applying split guidance or plan-citation ambiguity stops when the command is in bypass mode diff --git a/config/.pi/skills/sce-bootstrap-context/SKILL.md b/config/.pi/skills/sce-bootstrap-context/SKILL.md new file mode 100644 index 00000000..69610b01 --- /dev/null +++ b/config/.pi/skills/sce-bootstrap-context/SKILL.md @@ -0,0 +1,55 @@ +--- +name: sce-bootstrap-context +description: Use when user wants to Bootstrap SCE baseline context directory when missing. +--- + +## When to use +- Use only when `context/` is missing. +- Ask for human approval before creating files. + +## Required baseline +Create these paths: +- `context/overview.md` +- `context/architecture.md` +- `context/patterns.md` +- `context/glossary.md` +- `context/context-map.md` +- `context/plans/` +- `context/handovers/` +- `context/decisions/` +- `context/tmp/` +- `context/tmp/.gitignore` + +Use the following commands to create the directory structure: +```bash +mkdir -p context/plans context/handovers context/decisions context/tmp +touch context/overview.md context/architecture.md context/patterns.md context/glossary.md context/context-map.md +``` + +`context/tmp/.gitignore` content: +``` +* +!.gitignore +``` + +## Validation +After running the commands, verify all expected paths exist before proceeding: +```bash +ls context/overview.md context/architecture.md context/patterns.md context/glossary.md context/context-map.md context/plans context/handovers context/decisions context/tmp context/tmp/.gitignore +``` +If any path is missing, re-create it before moving on. + +## No-code bootstrap rule +- If the repository has no application code, keep `overview.md`, `architecture.md`, `patterns.md`, and `glossary.md` empty or placeholder-only. +- Do not invent implementation details. + +Example placeholder content for empty files in a no-code repo: +```markdown +# Overview + +> This section has not been populated yet. Add a high-level description of the project here. +``` + +## After bootstrapping +- Add baseline links in `context/context-map.md`. +- Tell the user that `context/` should be committed as shared memory. diff --git a/config/.pi/skills/sce-context-sync/SKILL.md b/config/.pi/skills/sce-context-sync/SKILL.md new file mode 100644 index 00000000..f051a981 --- /dev/null +++ b/config/.pi/skills/sce-context-sync/SKILL.md @@ -0,0 +1,91 @@ +--- +name: sce-context-sync +description: Use when user wants to Synchronize context files to match current code behavior after task execution. +--- + +## Principle +- Context is durable AI memory and must reflect current-state truth. +- If context and code diverge, code is source of truth. + +## Mandatory sync pass (important-change gated) +For every completed implementation task, run a sync pass over these shared files: +- `context/overview.md` +- `context/architecture.md` +- `context/glossary.md` +- `context/patterns.md` +- `context/context-map.md` + +Classify whether the task is an important change before deciding to edit or verify root context files. + +## Root context significance gating +- **Root edits required** - task introduces cross-cutting behavior, repository-wide policy/contracts, architecture boundaries, or canonical terminology changes. +- **Verify-only** - task is localized to a single feature/domain with no root-level behavior, architecture, or terminology impact. Keep root files unchanged; capture details in domain files instead. +- Even when verify-only, still verify `context/overview.md`, `context/architecture.md`, and `context/glossary.md` against code truth before declaring done. + +## Step-by-step sync pass workflow + +1. **Classify the change** - Important change or verify-only (see [Classification Reference](#classification-reference) below). +2. **Read the affected code** - Review modified files to understand what actually changed. +3. **Verify root files** - Open `context/overview.md`, `context/architecture.md`, and `context/glossary.md`; confirm they match code truth. +4. **Edit or skip root files** - Important change: update relevant root files. Verify-only: leave root files unchanged. +5. **Create or update domain files** - Write or revise `context/{domain}/` files for feature-specific detail (see [Domain File Policy](#domain-file-creation-policy) below). +6. **Ensure feature existence** - Every newly implemented feature must have at least one durable canonical description discoverable from context (domain file or `context/overview.md` for cross-cutting features). +7. **Update `context/context-map.md`** - Add or refresh discoverability links to any new or changed context files. +8. **Add glossary entries** - For any new domain language introduced by the task. +9. **Final check** - Confirm all updated files are <= 250 lines, diagrams are present where needed, and links use relative paths. + +### Before/after example +A task adds a new `PaymentGateway` abstraction used only in the payments domain (verify-only - domain-local). + +**`context/glossary.md`** - unchanged (no new root-level terminology). + +**New file: `context/payments/payment-gateway.md`:** +```markdown +# PaymentGateway + +Abstraction over external payment processors (Stripe, Adyen). +Defined in `src/payments/gateway/`. + +## Contract +- `charge(amount, token): Result` +- `refund(chargeId): Result` + +See also: [overview.md](../overview.md), [context-map.md](../context-map.md) +``` + +**`context/context-map.md`** - updated with a link to `context/payments/payment-gateway.md`. + +--- + +## Classification Reference + +| Important change (root edits required) | Verify-only (root files unchanged) | +|---|---| +| New auth strategy replacing existing one - architecture + terminology | New field on an existing API response - localized, no architecture impact | +| Background job queue used across multiple domains - cross-cutting | Bug fix in a single service's retry logic - no new root-level behavior | +| Renaming a core concept (e.g., `Order` -> `Purchase`) - canonical terminology | New UI component added to an existing feature - no cross-cutting impact | + +--- + +## Domain File Creation Policy + +- Use `context/{domain}/` for detailed feature behavior. +- If a feature does not cleanly fit an existing domain file, create a new one - do not defer documentation. +- If the feature appears to be part of a larger future domain, document the implemented slice now in a focused file and link it to related context. +- Prefer a small, precise domain file over overloading `overview.md` with detail. +- If updates for the current feature/domain outgrow shared files, migrate detail into `context/{domain}/` files, keep concise pointers in shared files, and add discoverability links in `context/context-map.md`. + +--- + +## Final-task requirement +- In the final plan task (validation/cleanup), confirm feature existence documentation is present and linked. +- If a feature was implemented but not represented in context, add the missing entry before declaring the task done. + +## Quality constraints +- One topic per file. +- Prefer concise current-state documentation over narrative changelogs. +- Link related context files with relative paths. +- Include concrete code examples when needed to clarify non-trivial behavior. +- Every context file must stay at or below 250 lines; if it would exceed 250, split into focused files and link them. +- Add a Mermaid diagram when structure, boundaries, or flows are complex. +- Ensure major code areas have matching context coverage. diff --git a/config/.pi/skills/sce-handover-writer/SKILL.md b/config/.pi/skills/sce-handover-writer/SKILL.md new file mode 100644 index 00000000..73292812 --- /dev/null +++ b/config/.pi/skills/sce-handover-writer/SKILL.md @@ -0,0 +1,46 @@ +--- +name: sce-handover-writer +description: Use when user wants to create a structured SCE handover for the current task. +--- + +## What I do +- Create a new handover file in `context/handovers/`. +- Capture: + - current task state + - decisions made and rationale + - open questions or blockers + - next recommended step + +## How to run this + +1. **Gather context** - review the current task, recent changes, and repo state. +2. **Create the file** - use task-aligned naming: `context/handovers/{plan_name}-{task_id}.md`. +3. **Fill each section** - follow the template below, labelling any inferred details as assumptions. +4. **Verify completeness** - confirm all four sections are populated before finishing. + +If key details are missing, infer from repo state and clearly label assumptions. + +## Handover document template + +```markdown +# Handover: {plan_name} - {task_id} + +## Current Task State +- What was being worked on and how far along it is. +- e.g. "Implementing OAuth login flow; token generation complete, redirect handling in progress." + +## Decisions Made +- Key choices and their rationale. +- e.g. "Chose JWT over session cookies for statelessness. Rejected library X due to licence constraints." + +## Open Questions / Blockers +- Unresolved issues or outstanding dependencies. +- e.g. "Awaiting confirmation on token expiry policy from product team." + +## Next Recommended Step +- The single most important action for whoever picks this up. +- e.g. "Complete the redirect handler in `src/auth/callback.ts`, then run the auth integration tests." +``` + +## Expected output +- A complete handover document in `context/handovers/` using task-aligned naming when possible. diff --git a/config/.pi/skills/sce-plan-authoring/SKILL.md b/config/.pi/skills/sce-plan-authoring/SKILL.md new file mode 100644 index 00000000..02c177e4 --- /dev/null +++ b/config/.pi/skills/sce-plan-authoring/SKILL.md @@ -0,0 +1,87 @@ +--- +name: sce-plan-authoring +description: Use when user wants to Create or update an SCE implementation plan with scoped atomic tasks. +--- + +## Goal +Turn a human change request into `context/plans/{plan_name}.md`. + +## Intake trigger +- If a request includes both a change description and success criteria, planning is mandatory before implementation. +- Planning does not imply execution approval. + +## Clarification gate (blocking) +- Before writing or updating any plan, run an ambiguity check. +- If any critical detail is unclear, ask 1-3 targeted questions and stop. +- Do not write or update `context/plans/{plan_name}.md` until the user answers. +- Critical details that must be resolved before planning include: + - scope boundaries and out-of-scope items + - success criteria and acceptance signals + - constraints and non-goals + - dependency choices (new libs/services, versions, and integration approach) + - domain ambiguity (unclear business rules, terminology, or ownership) + - architecture concerns (patterns, interfaces, data flow, migration strategy, and risk tradeoffs) + - task ordering assumptions and prerequisite sequencing +- Do not silently invent missing requirements. +- If the user explicitly allows assumptions, record them in an `Assumptions` section. +- Incorporate user answers into the plan before handoff. + +Example clarification questions (use this style - specific, blocking, targeted): +> 1. Should the new endpoint authenticate via the existing JWT middleware, or is a separate auth flow in scope? +> 2. Is database migration rollback a hard requirement, or is forward-only acceptable for this change? +> 3. Which service owns the `UserProfile` type - should this task modify that definition or only consume it? + +## Plan format +1) Change summary +2) Success criteria +3) Constraints and non-goals +4) Task stack (`T01..T0N`) +5) Open questions (if any) + +## Task format (required) +For each task include: +- Task ID +- Goal +- Boundaries (in/out of scope) +- Done when +- Verification notes (commands or checks) + +## Atomic task slicing contract (required) +- Author each executable task as one atomic commit unit by default. +- Every task must be scoped so one contributor can complete it and land it as one coherent commit without bundling unrelated changes. +- If a candidate task would require multiple independent commits (for example: refactor + behavior change + docs), split it into separate sequential tasks before finalizing the plan. +- Keep broad wrappers (`polish`, `finalize`, `misc updates`) out of executable tasks; convert them into specific outcomes with concrete acceptance checks. + +Example compliant skeleton: +- [ ] T0X: `[single intent title]` (status:todo) + - Task ID: T0X + - Goal: `[one outcome]` + - Boundaries (in/out of scope): `[tight scope]` + - Done when: `[clear acceptance for one coherent change]` + - Verification notes (commands or checks): `[targeted checks for this change]` + +Example filled-in task entry: +- [ ] T02: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T02 + - Goal: Implement a POST `/auth/refresh` endpoint that exchanges a valid refresh token for a new access token. + - Boundaries (in/out of scope): In - route handler, token validation logic, response schema. Out - refresh token rotation policy (covered in T03), client-side storage changes. + - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired/invalid token; unit tests pass; OpenAPI spec updated. + - Verification notes (commands or checks): `pnpm test src/auth/refresh.test.ts`; `curl -X POST localhost:3000/auth/refresh -d '{"token":"..."}' -w "%{http_code}"`. + +Use checkbox lines for machine-friendly progress tracking: +- `- [ ] T01: ... (status:todo)` + +## Complete plan example + +See `context/plans/PLAN_EXAMPLE.md` for a full annotated reference plan (JWT auth walkthrough covering all required sections and four task entries). + +## Required final task +- Final task is always validation and cleanup. +- It must include full checks and context sync verification. + +## Output contract +- Save plan under `context/plans/`. +- Confirm plan creation with `plan_name` and exact file path. +- Present the full ordered task list in chat. +- Prompt the user to start a new session with Shared Context Code agent to implement `T01`. +- Provide one canonical next command: `/next-task {plan_name} T01`. diff --git a/config/.pi/skills/sce-plan-review/SKILL.md b/config/.pi/skills/sce-plan-review/SKILL.md new file mode 100644 index 00000000..471baf7b --- /dev/null +++ b/config/.pi/skills/sce-plan-review/SKILL.md @@ -0,0 +1,89 @@ +--- +name: sce-plan-review +description: Use when user wants to review an existing plan and prepare the next task safely. +--- + +## What I do +- Continue execution from an existing plan in `context/plans/`. +- Read the selected plan and identify the next task from the first unchecked checkbox. +- Ask focused questions for anything not clear enough to execute safely. + +## How to run this +- Use this skill when the user asks to continue a plan or pick the next task. +- If `context/` is missing, ask once: "`context/` is missing. Bootstrap SCE baseline now?" + - If yes, create baseline with `sce-bootstrap-context` and continue. + - If no, stop and explain SCE workflows require `context/`. +- Read `context/context-map.md`, `context/overview.md`, and `context/glossary.md` before broad exploration. +- Resolve plan target: + - If plan path argument exists, use it. + - If multiple plans exist and no explicit path is provided, ask user to choose. +- Collect: + - completed tasks + - next task + - blockers, ambiguity, and missing acceptance criteria +- Prompt user to resolve unclear points before implementation. +- Confirm scope explicitly for this session: one task by default unless user requests multi-task execution. + +## Plan file format +SCE plans are Markdown files stored in `context/plans/`. Tasks are tracked as checkboxes: + +```markdown +# Plan: Add user authentication + +## Tasks +- [x] Scaffold auth module +- [x] Add password hashing utility +- [ ] Implement login endpoint <- next task (first unchecked) +- [ ] Write integration tests +- [ ] Update context/current-state.md +``` + +The first unchecked `- [ ]` item is the next task to review and prepare. + +## Rules +- Do not auto-mark tasks complete during review. +- Keep continuation state in the plan markdown itself. +- Treat `context/plans/` as active execution artifacts; completed plans are disposable and not a durable context source. +- If durable history is needed, record it in current-state context files and/or `context/decisions/` instead of completed plan files. +- Keep implementation blocked until decision alignment on unclear points. +- If plan context is stale or partial, continue with code truth and flag context updates. + +## Expected output + +Produce a structured readiness summary after review: + +``` +## Plan Review - [plan filename] + +**Completed tasks:** 2 of 5 +**Next task:** Implement login endpoint + +**Acceptance criteria:** +- POST /auth/login returns JWT on success +- Returns 401 on invalid credentials + +**Issues found:** +- Blocker: JWT secret source not specified (env var? config file?) +- Ambiguity: Should failed attempts be rate-limited in this task or a later one? + +**ready_for_implementation: no** + +**Required decisions before proceeding:** +1. Confirm JWT secret source +2. Confirm rate-limiting scope +``` + +When all issues are resolved: + +``` +**ready_for_implementation: yes** +Proceeding with: Implement login endpoint +``` + +- Explicit readiness verdict: `ready_for_implementation: yes|no`. +- If not ready, explicit issue categories: blockers, ambiguity, missing acceptance criteria. +- Explicit user-aligned decisions needed to proceed to implementation. +- Explicit user confirmation request that the task is ready for implementation when unresolved issues remain. + +## Related skills +- `sce-bootstrap-context` - creates the `context/` baseline required by this skill diff --git a/config/.pi/skills/sce-task-execution/SKILL.md b/config/.pi/skills/sce-task-execution/SKILL.md new file mode 100644 index 00000000..36a78e5b --- /dev/null +++ b/config/.pi/skills/sce-task-execution/SKILL.md @@ -0,0 +1,56 @@ +--- +name: sce-task-execution +description: Use when user wants to Execute one approved task with explicit scope, evidence, and status updates. +--- + +## Scope rule +- Execute exactly one task per session by default. +- If multi-task execution is requested, confirm explicit human approval. + +## Mandatory implementation stop +- Before writing or modifying any code, pause and prompt the user. +- The prompt must explain: + - task goal + - boundaries (in/out of scope) + - done checks + - expected files/components to change + - key approach, trade-offs, and risks +- Then ask explicitly whether to continue. +- Do not edit files, generate code, or apply patches until the user confirms. + +**Example mandatory stop prompt:** +``` +Task goal: Add input validation to the user registration endpoint. +In scope: src/routes/register.ts, src/validators/user.ts +Out of scope: Auth logic, database schema, frontend forms +Done checks: All existing tests pass; new validation tests cover empty/invalid email and short passwords +Expected changes: ~2 files modified, no new dependencies +Approach: Use the existing `validateSchema` helper; add a Zod schema for registration payload +Trade-offs: Zod adds minor overhead; keeps validation consistent with other routes +Risks: Existing callers that omit optional fields may start failing validation + +Continue with implementation now? (yes/no) +``` + +## Required sequence +1) Restate task goal, boundaries, done checks, and expected file touch scope. +2) Propose approach, trade-offs, and risks. +3) Stop and ask: "Continue with implementation now?" (yes/no). +4) Implement minimal in-scope changes. +5) Run light task-level tests/checks and lints first, and run a build when the build is light/fast (targeted over full-suite unless requested), then capture evidence. +6) Record whether the implementation is an important change for context sync (root-edit required) or verify-only (no root edits expected). +7) Keep session-only scraps in `context/tmp/`. +8) Update task status in `context/plans/{plan_id}.md`. + +**Example task status update (`context/plans/{plan_id}.md`):** +```markdown +## Task: Add input validation to registration endpoint +- **Status:** done +- **Completed:** 2025-06-10 +- **Files changed:** src/routes/register.ts, src/validators/user.ts +- **Evidence:** 14/14 tests passed, lint clean, build succeeded (12s) +- **Notes:** Zod schema added; no breaking changes to existing callers +``` + +## Scope expansion rule +- If out-of-scope edits are needed, stop and ask for approval. diff --git a/config/.pi/skills/sce-validation/SKILL.md b/config/.pi/skills/sce-validation/SKILL.md new file mode 100644 index 00000000..efb7a23e --- /dev/null +++ b/config/.pi/skills/sce-validation/SKILL.md @@ -0,0 +1,45 @@ +--- +name: sce-validation +description: Use when user wants to Run final plan validation and cleanup with evidence capture. +--- + +## When to use +- Use for the plan's final validation task after implementation is complete. +- Triggered by requests like "validate the plan", "run final checks", "confirm everything passes", "wrap up the task", or "sign off on this change". + +## Validation checklist +1) **Run full test suite** - discover and run the project's primary test command (e.g., `pytest`, `npm test`, `go test ./...`, `cargo test`, `make test`). Check `package.json`, `Makefile`, `pyproject.toml`, or CI config files to find the right command. +2) **Run lint/format checks** - discover and run the project's lint and format tools (e.g., `eslint`, `ruff`, `golangci-lint`, `cargo clippy`, `make lint`). Check project config files such as `.eslintrc`, `pyproject.toml`, or `.golangci.yml`. +3) **Remove temporary scaffolding** - delete any debug code, temporary files, or intermediate artifacts introduced during the change. +4) **Verify context reflects final implemented behavior** - confirm that plan context and notes match the actual final state of the implementation. +5) **Confirm each success criterion has evidence** - for every success criterion defined in the plan, record concrete evidence (command output, exit code, screenshot reference, or file path). + +### If checks fail +- **Fixable failures**: fix the issue, re-run the failing check, and update the report with the corrected output. +- **Non-trivial failures**: document the failure, the attempted fix, and the blocker in the report under "Failed checks and follow-ups". Escalate to the user before closing out. +- **Lint/format auto-fixes**: if the tool supports auto-fix (e.g., `ruff --fix`, `eslint --fix`), apply it, then re-run to confirm clean output. + +## Validation report +Write to `context/plans/{plan_name}.md` including: +- Commands run +- Exit codes and key outputs +- Failed checks and follow-ups +- Success-criteria verification summary +- Residual risks, if any + +### Example report entry +``` +## Validation Report + +### Commands run +- `npm test` -> exit 0 (42 tests passed, 0 failed) +- `eslint src/` -> exit 0 (no warnings) +- Removed: `src/debug_patch.js` (temporary scaffolding) + +### Success-criteria verification +- [x] All API endpoints return 200 for valid input -> confirmed via test output line 34 +- [x] Error responses include structured JSON -> confirmed via `test_error_format.js` + +### Residual risks +- None identified. +``` diff --git a/config/lib/bun.lock b/config/lib/bun.lock index ec405bff..fb76848f 100644 --- a/config/lib/bun.lock +++ b/config/lib/bun.lock @@ -4,6 +4,7 @@ "workspaces": { "": { "dependencies": { + "@earendil-works/pi-coding-agent": "0.80.6", "@opencode-ai/plugin": "1.15.4", "@types/bun": "1.3.11", "@types/node": "25.5.0", @@ -11,6 +12,92 @@ }, }, "packages": { + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.975.1", "", { "dependencies": { "@aws-sdk/types": "^3.974.0", "@aws-sdk/xml-builder": "^3.972.34", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.2", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/node-http-handler": "^4.9.4", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.1", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.63", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.66", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.1", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/token-providers": "3.1083.0", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.63", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ=="], + + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.26", "", { "dependencies": { "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-RE1fu7Nn05vG0EUJM+8Sde2GFecC658WGaC/asPzLF6K4x3H5ZaDBcQtHRE67Gdgb1VZpyUUliYejHFK1qt0Uw=="], + + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.22", "", { "dependencies": { "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-jtkgmhevnpzC1WeS+Y/sgymYbaQ6qg7pVOUl5cUT/8MiLptqrtnXQlNV80m+j2WIx5MIL7kVHIZNxxcK2tfUEQ=="], + + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.39", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-CS1spxRSezmTmI3PD+3Xrnp6KryTSEz0EefA8u6uGd0s2I0uXseWHALDI/03Wi0IUczXNWo2QrZEaHDuJNby/Q=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.31", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/signature-v4-multi-region": "^3.996.39", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/node-http-handler": "^4.9.4", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.39", "", { "dependencies": { "@aws-sdk/types": "^3.974.0", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.974.0", "", { "dependencies": { "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.8", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.34", "", { "dependencies": { "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.80.6", "", { "dependencies": { "@earendil-works/pi-ai": "^0.80.6", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-Lvn89ko42h5ETUb6Z0Ku6ldskEqXaTdQBYvSa0+7bdG9V6rUEpXptv5e0OVZ1HDcvi8s6/2lGCQWsxKX+DFHNw=="], + + "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.80.6", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-7xfLk8sANBp+bpPEbjoOZTbPxsa+++b1JXAoSJsNa3vbs9AHHEclmvg54XLQcxH+fuwaeti/g2jeIfJ+mVYLpA=="], + + "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.80.6", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.80.6", "@earendil-works/pi-ai": "^0.80.6", "@earendil-works/pi-tui": "^0.80.6", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.5.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.9" }, "bin": { "pi": "dist/cli.js" } }, "sha512-vcfD6tOk402isLl3Cm/qbn2O10TvgroMp1+/fEGM24ZdvETFCdOYv5VZ7m59EI5fPsjfSJh+CpQ5bhBrhfOg7g=="], + + "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.80.6", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" } }, "sha512-bSuzS4EVSqEPj/Qr/p9eqCESfKsGuDNbl77EGci8Iaqqt/C/XCBZL1MjXaxSWW1NsT5afjp/Cb0NTPzOLv/aPA=="], + + "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], + + "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="], + + "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="], + + "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.9", "", { "os": "darwin" }, "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ=="], + + "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg=="], + + "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw=="], + + "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ=="], + + "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.9", "", { "os": "linux", "cpu": "none" }, "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw=="], + + "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw=="], + + "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ=="], + + "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ=="], + + "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="], + + "@mistralai/mistralai": ["@mistralai/mistralai@2.2.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], @@ -27,56 +114,222 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.15.4", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-55SBChNouj2XY9C4thO0w7SGJS3jD2DRBxzcrDpc5szgmJJ2t2Wu38uZh+TQMBLHA8YrTPDqgfnc7o5tx2qRPw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + + "@smithy/core": ["@smithy/core@3.29.3", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + + "@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "effect": ["effect@4.0.0-beta.65", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "gaxios": ["gaxios@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "google-auth-library": ["google-auth-library@10.9.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="], + + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], + "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typebox": ["typebox@1.1.38", "", {}, "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA=="], + + "undici": ["undici@8.5.0", "", {}, "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1083.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A=="], + + "@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="], + + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], } } diff --git a/config/lib/package.json b/config/lib/package.json index c4685fd6..7c8650f8 100644 --- a/config/lib/package.json +++ b/config/lib/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@earendil-works/pi-coding-agent": "0.80.6", "@opencode-ai/plugin": "1.15.4", "@types/bun": "1.3.11", "@types/node": "25.5.0" diff --git a/config/lib/pi-plugin/sce-pi-extension.ts b/config/lib/pi-plugin/sce-pi-extension.ts new file mode 100644 index 00000000..25770ff8 --- /dev/null +++ b/config/lib/pi-plugin/sce-pi-extension.ts @@ -0,0 +1,455 @@ +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { + dirname, + isAbsolute, + join, + relative, + resolve as resolvePath, +} from "node:path"; +import { + type ExtensionAPI, + isToolCallEventType, +} from "@earendil-works/pi-coding-agent"; + +interface JsonPolicyResult { + status: string; + decision: string; + command: string; + normalized_argv?: string[]; + reason?: string; + policy_id?: string; +} + +const SCE_INSTALL_URL = + "https://sce.crocoder.dev/docs/getting-started#install-cli"; + +type ConversationTraceMessageItem = { + type: "message"; + session_id: string; + message_id: string; + role: "user" | "assistant"; + generated_at_unix_ms: number; +}; + +type ConversationTraceMessagePartItem = { + type: "message.part"; + session_id: string; + message_id: string; + part_type: "text" | "reasoning" | "patch"; + text: string; + generated_at_unix_ms: number; +}; + +type ConversationTraceItem = + | ConversationTraceMessageItem + | ConversationTraceMessagePartItem; + +type ConversationTracePayload = { + payloads: ConversationTraceItem[]; +}; + +type DiffTracePayload = { + sessionID: string; + diff: string; + time: number; + model_id: string | null; + tool_name: "pi"; + tool_version: string | null; +}; + +type PendingFileMutation = { + absolutePath: string; + diffLabel: string; + before: string | undefined; +}; + +/** + * Evaluate a bash command against SCE bash-tool policy by delegating to the + * Rust `sce policy bash` command. Returns the parsed JSON result, or null if + * the policy check could not be performed (fail-open). + */ +function evaluateBashCommandPolicy(command: string): JsonPolicyResult | null { + try { + const result = spawnSync( + "sce", + ["policy", "bash", "--input", "normalized", "--output", "json"], + { + input: JSON.stringify({ command }), + encoding: "utf8", + timeout: 10_000, + }, + ); + + if (result.error) { + if ((result.error as NodeJS.ErrnoException).code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + return null; + } + + if (result.status !== 0) { + return null; + } + + const stdout = result.stdout?.trim(); + if (!stdout) { + return null; + } + + const parsed: JsonPolicyResult = JSON.parse(stdout); + return parsed; + } catch { + return null; + } +} + +/** + * Send a conversation-trace payload to `sce hooks conversation-trace`, + * fire-and-forget. Fail-open: stderr is ignored so that sce intake errors do + * not leak into the Pi TUI, and the returned promise never rejects. + */ +function runConversationTraceHook( + cwd: string, + payload: ConversationTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "conversation-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +type MessageContentBlock = { + type: string; + text?: unknown; + thinking?: unknown; +}; + +function extractMessageParts( + content: string | readonly MessageContentBlock[], +): Array<{ part_type: "text" | "reasoning"; text: string }> { + if (typeof content === "string") { + return content.length > 0 ? [{ part_type: "text", text: content }] : []; + } + + const parts: Array<{ part_type: "text" | "reasoning"; text: string }> = []; + for (const block of content) { + if (block.type === "text" && typeof block.text === "string" && block.text) { + parts.push({ part_type: "text", text: block.text }); + } else if ( + block.type === "thinking" && + typeof block.thinking === "string" && + block.thinking + ) { + parts.push({ part_type: "reasoning", text: block.thinking }); + } + } + return parts; +} + +function buildMessageEndConversationTracePayload( + sessionId: string, + message: { + role: string; + content: string | readonly MessageContentBlock[]; + responseId?: string; + }, +): ConversationTracePayload | undefined { + if (message.role !== "user" && message.role !== "assistant") { + return undefined; + } + + const messageId = message.responseId ?? randomUUID(); + const generatedAtUnixMs = Date.now(); + + const payloads: ConversationTraceItem[] = [ + { + type: "message", + session_id: sessionId, + message_id: messageId, + role: message.role, + generated_at_unix_ms: generatedAtUnixMs, + }, + ]; + + for (const part of extractMessageParts(message.content)) { + payloads.push({ + type: "message.part", + session_id: sessionId, + message_id: messageId, + part_type: part.part_type, + text: part.text, + generated_at_unix_ms: generatedAtUnixMs, + }); + } + + return { payloads }; +} + +/** + * Resolve the installed Pi package version for diff-trace `tool_version`. + * The package's `exports` map does not expose `package.json`, so resolve the + * package entry point and read `package.json` from the package root instead. + * Returns null when resolution fails (normalized diff traces permit it). + */ +async function resolvePiToolVersion(): Promise { + try { + const require_ = createRequire(import.meta.url); + const entryPath = require_.resolve("@earendil-works/pi-coding-agent"); + const packageJsonPath = join(dirname(entryPath), "..", "package.json"); + const parsed: { version?: unknown } = JSON.parse( + await readFile(packageJsonPath, "utf8"), + ); + return typeof parsed.version === "string" && parsed.version.length > 0 + ? parsed.version + : null; + } catch { + return null; + } +} + +/** + * Send a diff-trace payload to `sce hooks diff-trace`, fire-and-forget. + * Fail-open: stderr is ignored and the returned promise never rejects. + */ +function runDiffTraceHook( + cwd: string, + payload: DiffTracePayload, +): Promise { + return new Promise((resolve) => { + const child = spawn("sce", ["hooks", "diff-trace"], { + cwd, + stdio: ["pipe", "ignore", "ignore"], + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.warn(`sce CLI not found. Install it from ${SCE_INSTALL_URL}`); + } + resolve(); + }); + child.on("close", () => resolve()); + + child.stdin.end(`${JSON.stringify(payload)}\n`); + }); +} + +async function readFileOrUndefined(path: string): Promise { + try { + return await readFile(path, "utf8"); + } catch { + return undefined; + } +} + +function diffLabelFor(cwd: string, absolutePath: string): string { + const relPath = relative(cwd, absolutePath); + return relPath.length > 0 && !relPath.startsWith("..") && !isAbsolute(relPath) + ? relPath + : absolutePath; +} + +/** + * Rewrite temp-file path labels in git diff header lines to the repo-relative + * target path. Only header lines before the first `@@` hunk marker are + * touched so that content lines starting with `--- ` / `+++ ` are preserved. + */ +function rewriteDiffLabels( + diff: string, + label: string, + isCreate: boolean, +): string { + const lines = diff.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("@@")) { + break; + } + if (line.startsWith("diff --git ")) { + lines[i] = `diff --git a/${label} b/${label}`; + } else if (line.startsWith("--- ")) { + lines[i] = isCreate ? "--- /dev/null" : `--- a/${label}`; + } else if (line.startsWith("+++ ")) { + lines[i] = `+++ b/${label}`; + } + } + return lines.join("\n"); +} + +/** + * Produce a unified diff between before/after contents by writing them to + * temp files and spawning `git diff --no-index --no-ext-diff` (exit status 1 + * means "files differ"). Returns undefined for no-op diffs or any failure; + * temp files are always cleaned up. + */ +async function buildUnifiedDiff( + label: string, + before: string | undefined, + after: string, +): Promise { + const tempDir = await mkdtemp(join(tmpdir(), "sce-pi-diff-")); + try { + const beforePath = join(tempDir, "before"); + const afterPath = join(tempDir, "after"); + await writeFile(beforePath, before ?? "", "utf8"); + await writeFile(afterPath, after, "utf8"); + + const result = spawnSync( + "git", + ["diff", "--no-index", "--no-ext-diff", "--", beforePath, afterPath], + { encoding: "utf8", timeout: 10_000 }, + ); + + if (result.error || result.status !== 1) { + return undefined; + } + const stdout = result.stdout; + if (!stdout) { + return undefined; + } + return rewriteDiffLabels(stdout, label, before === undefined); + } catch { + return undefined; + } finally { + await rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +} + +export default function sceExtension(pi: ExtensionAPI): void { + const pendingFileMutations = new Map(); + const piToolVersionPromise = resolvePiToolVersion(); + + pi.on("tool_call", (event) => { + if (!isToolCallEventType("bash", event)) { + return undefined; + } + + const command = event.input.command; + if (typeof command !== "string" || command.length === 0) { + return undefined; + } + + const policyResult = evaluateBashCommandPolicy(command); + if (!policyResult) { + // Fail open: if the policy check cannot be performed, allow the command. + return undefined; + } + + if (policyResult.decision === "deny" && policyResult.reason) { + return { block: true, reason: policyResult.reason }; + } + + return undefined; + }); + + pi.on("tool_call", async (event, ctx) => { + if ( + !isToolCallEventType("edit", event) && + !isToolCallEventType("write", event) + ) { + return undefined; + } + + const targetPath = event.input.path; + if (typeof targetPath !== "string" || targetPath.length === 0) { + return undefined; + } + + const absolutePath = resolvePath(ctx.cwd, targetPath); + pendingFileMutations.set(event.toolCallId, { + absolutePath, + diffLabel: diffLabelFor(ctx.cwd, absolutePath), + before: await readFileOrUndefined(absolutePath), + }); + return undefined; + }); + + pi.on("tool_result", async (event, ctx) => { + const pending = pendingFileMutations.get(event.toolCallId); + if (!pending) { + return; + } + pendingFileMutations.delete(event.toolCallId); + + if (event.isError) { + return; + } + + const after = await readFileOrUndefined(pending.absolutePath); + if (after === undefined || after === pending.before) { + return; + } + + const diff = await buildUnifiedDiff( + pending.diffLabel, + pending.before, + after, + ); + if (!diff) { + return; + } + + const sessionId = ctx.sessionManager.getSessionId(); + const generatedAtUnixMs = Date.now(); + const patchMessageId = `${event.toolCallId}-patch`; + + void runConversationTraceHook(ctx.cwd, { + payloads: [ + { + type: "message", + session_id: sessionId, + message_id: patchMessageId, + role: "assistant", + generated_at_unix_ms: generatedAtUnixMs, + }, + { + type: "message.part", + session_id: sessionId, + message_id: patchMessageId, + part_type: "patch", + text: diff, + generated_at_unix_ms: generatedAtUnixMs, + }, + ], + }); + + void runDiffTraceHook(ctx.cwd, { + sessionID: sessionId, + diff, + time: generatedAtUnixMs, + model_id: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, + tool_name: "pi", + tool_version: await piToolVersionPromise, + }); + }); + + pi.on("message_end", (event, ctx) => { + const message = event.message; + if (message.role !== "user" && message.role !== "assistant") { + return; + } + + const payload = buildMessageEndConversationTracePayload( + ctx.sessionManager.getSessionId(), + message, + ); + if (payload) { + void runConversationTraceHook(ctx.cwd, payload); + } + }); +} diff --git a/config/lib/tsconfig.json b/config/lib/tsconfig.json index 59c38829..50b545df 100644 --- a/config/lib/tsconfig.json +++ b/config/lib/tsconfig.json @@ -11,6 +11,10 @@ "allowImportingTsExtensions": true, "skipLibCheck": true }, - "include": ["agent-trace-plugin/**/*.ts", "bash-policy-plugin/**/*.ts"], + "include": [ + "agent-trace-plugin/**/*.ts", + "bash-policy-plugin/**/*.ts", + "pi-plugin/**/*.ts" + ], "exclude": ["node_modules"] } diff --git a/config/pkl/README.md b/config/pkl/README.md index c72e8cdc..b7f81de1 100644 --- a/config/pkl/README.md +++ b/config/pkl/README.md @@ -14,6 +14,8 @@ Generated by `config/pkl/generate.pkl`: **Claude profile**: `config/.claude/agents/*.md`, `config/.claude/commands/*.md`, `config/.claude/skills/*/SKILL.md`, `config/.claude/lib/*` shared runtime assets, generated hooks under `config/.claude/hooks/*`, and generated settings at `config/.claude/settings.json`. +**Pi profile**: `config/.pi/prompts/*.md` command prompt templates, `config/.pi/prompts/agent-*.md` SCE agent-role prompt templates, `config/.pi/skills/*/SKILL.md` Agent Skills-format skill packages, and the project-local extension at `config/.pi/extensions/sce/index.ts`. Pi is manual-profile only and has no generated settings/plugin manifest; the extension is auto-discovered by Pi and emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`. + Generated Markdown outputs render deterministic frontmatter + body without injected warning marker comments. Generated JavaScript library outputs render without a leading generated warning header. Edit the canonical Pkl sources when generated config changes are needed: base modules under `config/pkl/base/`, renderer modules under `config/pkl/renderers/`, and `config/pkl/generate.pkl`. Do not hand-edit generated artifacts. @@ -24,13 +26,13 @@ Not generated by this pipeline: dependency artifacts (for example `node_modules` ## Choose a profile -This repository maintains two OpenCode configuration profiles: +This repository maintains two OpenCode configuration profiles plus single generated target trees for Claude Code and Pi: **Manual profile** (`config/.opencode/**`): Interactive SCE workflows with human approval gates. Preserves ask/confirm prompts for safety-critical decisions. **Automated profile** (`config/automated/.opencode/**`): Non-interactive SCE workflows for CI/automation. Replaces interactive gates with deterministic policies (see `context/sce/automated-profile-contract.md`). -Both profiles are generated together by `config/pkl/generate.pkl` and must stay in sync with their canonical sources. +Both OpenCode profiles are generated together by `config/pkl/generate.pkl` and must stay in sync with their canonical sources. The same generator also emits the Claude target tree and the Pi target tree (`config/.pi/**`) from shared canonical content with target-specific renderer metadata. --- @@ -48,9 +50,9 @@ Validate the generator entrypoint evaluates with nix develop -c pkl eval config/ Preview non-destructively (writes under `context/tmp/` only) with nix develop -c pkl eval -m context/tmp/pkl-generated config/pkl/generate.pkl. -Regenerate tracked outputs in place with nix develop -c pkl eval -m . config/pkl/generate.pkl. This regenerates both manual and automated OpenCode profiles plus Claude outputs, including `config/.opencode/opencode.json`, `config/automated/.opencode/opencode.json`, `config/.claude/hooks/*`, and `config/.claude/settings.json`. +Regenerate tracked outputs in place with nix develop -c pkl eval -m . config/pkl/generate.pkl. This regenerates both manual and automated OpenCode profiles plus Claude and Pi outputs, including `config/.opencode/opencode.json`, `config/automated/.opencode/opencode.json`, `config/.claude/hooks/*`, `config/.claude/settings.json`, `config/.pi/{prompts,skills}/**`, and `config/.pi/extensions/sce/index.ts`. -Inspect resulting changes with git status --short config/.opencode config/automated/.opencode config/.claude. This includes the generated Claude hook/settings outputs. If regeneration is deterministic and current, there should be no diff after a clean re-run. +Inspect resulting changes with git status --short config/.opencode config/automated/.opencode config/.claude config/.pi. This includes the generated Claude hook/settings outputs and Pi prompt/skill/extension outputs. If regeneration is deterministic and current, there should be no diff after a clean re-run. Run the Nix dev-shell integration stale-output test with nix develop -c ./config/pkl/check-generated.sh. This test intentionally exits non-zero when run outside `nix develop`. @@ -77,6 +79,6 @@ To update these dependencies, download newer versions from the pkl-pantry reposi **pkl: command not found**: Run commands via nix develop -c ... exactly as shown. -**Missing metadata key errors**: Run nix develop -c pkl eval config/pkl/renderers/metadata-coverage-check.pkl and add missing per-target entries in `config/pkl/renderers/opencode-metadata.pkl` or `config/pkl/renderers/claude-metadata.pkl`. +**Missing metadata key errors**: Run nix develop -c pkl eval config/pkl/renderers/metadata-coverage-check.pkl and add missing per-target entries in `config/pkl/renderers/opencode-metadata.pkl`, `config/pkl/renderers/claude-metadata.pkl`, or `config/pkl/renderers/pi-metadata.pkl`. **Unexpected file drift outside generated-owned paths**: Stop and verify whether those paths are intentionally manual/runtime-managed before editing the generator map. diff --git a/config/pkl/base/sce-config-schema.pkl b/config/pkl/base/sce-config-schema.pkl index beb20335..a206a4f0 100644 --- a/config/pkl/base/sce-config-schema.pkl +++ b/config/pkl/base/sce-config-schema.pkl @@ -101,6 +101,31 @@ local sceConfigSchema = new JsonSchema { } } } + ["agent_trace"] = new JsonSchema { + type = "object" + description = "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note and whether that notes ref is auto-pushed." + additionalProperties = false + properties { + ["git_notes_ref"] = new JsonSchema { + type = "string" + minLength = 1 + description = "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace." + default = "refs/notes/sce-agent-trace" + } + ["push_notes"] = new JsonSchema { + type = "object" + description = "Agent Trace git-notes push policy. Notes auto-push is enabled by default." + additionalProperties = false + properties { + ["enabled"] = new JsonSchema { + type = "boolean" + description = "Enable best-effort Agent Trace git-notes auto-push after local note persistence. Defaults to true when omitted; set false to disable." + default = true + } + } + } + } + } ["database_retry"] = new JsonSchema { type = "object" additionalProperties = false @@ -172,7 +197,7 @@ local sceConfigSchema = new JsonSchema { uniqueItems = true items = new JsonSchema { type = "string" - enum = new { "opencode"; "claude" } + enum = new { "opencode"; "claude"; "pi" } } } } diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 5dae03ff..39bbab85 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -46,6 +46,9 @@ paths=( "config/.claude/agents" "config/.claude/commands" "config/.claude/skills" + "config/.pi/prompts" + "config/.pi/skills" + "config/.pi/extensions" "config/schema/sce-config.schema.json" ) diff --git a/config/pkl/generate.pkl b/config/pkl/generate.pkl index d79cdfe7..d80f3244 100644 --- a/config/pkl/generate.pkl +++ b/config/pkl/generate.pkl @@ -1,6 +1,7 @@ import "renderers/opencode-content.pkl" as opencode import "renderers/opencode-automated-content.pkl" as opencode_automated import "renderers/claude-content.pkl" as claude +import "renderers/pi-content.pkl" as pi import "renderers/common.pkl" as common import "base/sce-config-schema.pkl" as sce_config_schema import "base/bash-policy-presets.pkl" as bash_policy_presets @@ -8,6 +9,7 @@ import "base/bash-policy-presets.pkl" as bash_policy_presets local bashPolicyPresetCatalogSource = bash_policy_presets.output.text local opencodeBashPolicyPluginSource = read("../lib/bash-policy-plugin/opencode-bash-policy-plugin.ts").text local opencodeAgentTracePluginSource = read("../lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts").text +local piExtensionSource = read("../lib/pi-plugin/sce-pi-extension.ts").text output { files { for (slug, document in opencode.agents) { @@ -63,6 +65,24 @@ output { text = document.rendered } } + for (slug, document in pi.commands) { + ["config/.pi/prompts/\(slug).md"] { + text = document.rendered + } + } + for (slug, document in pi.agentPrompts) { + ["config/.pi/prompts/agent-\(slug).md"] { + text = document.rendered + } + } + for (slug, document in pi.skills) { + ["config/.pi/skills/\(slug)/SKILL.md"] { + text = document.rendered + } + } + ["config/.pi/extensions/sce/index.ts"] { + text = piExtensionSource + } ["config/.opencode/lib/bash-policy-presets.json"] { text = bashPolicyPresetCatalogSource } diff --git a/config/pkl/renderers/metadata-coverage-check.pkl b/config/pkl/renderers/metadata-coverage-check.pkl index 41d15b0a..e0340ca6 100644 --- a/config/pkl/renderers/metadata-coverage-check.pkl +++ b/config/pkl/renderers/metadata-coverage-check.pkl @@ -4,6 +4,7 @@ import "common.pkl" as common import "opencode-metadata.pkl" as opencode import "opencode-automated-metadata.pkl" as opencodeAutomated import "claude-metadata.pkl" as claude +import "pi-metadata.pkl" as pi opencodeAgentCoverage { for (unitSlug, _ in shared.agents) { @@ -62,6 +63,33 @@ claudeSkillCoverage { } } +piAgentPromptCoverage { + for (unitSlug, _ in shared.agents) { + [unitSlug] = new { + description = pi.agentDescriptions[unitSlug] + argumentHint = pi.agentArgumentHints[unitSlug] + skillReference = pi.agentSkillReferences[unitSlug] + } + } +} + +piCommandCoverage { + for (unitSlug, _ in shared.commands) { + [unitSlug] = new { + description = common.commandDescriptions[unitSlug] + argumentHint = pi.commandArgumentHints[unitSlug] + } + } +} + +piSkillCoverage { + for (unitSlug, _ in shared.skills) { + [unitSlug] = new { + description = common.skillDescriptions[unitSlug] + } + } +} + // Automated OpenCode profile coverage checks opencodeAutomatedAgentCoverage { for (unitSlug, _ in sharedAutomated.agents) { diff --git a/config/pkl/renderers/pi-content.pkl b/config/pkl/renderers/pi-content.pkl new file mode 100644 index 00000000..c6cfbcfe --- /dev/null +++ b/config/pkl/renderers/pi-content.pkl @@ -0,0 +1,82 @@ +import "../base/shared-content.pkl" as shared +import "common.pkl" as common +import "pi-metadata.pkl" as metadata + +local commandFrontmatterBySlug = new Mapping { + for (unitSlug, _ in shared.commands) { + [unitSlug] = """ +--- +description: "\(common.commandDescriptions[unitSlug])" +argument-hint: "\(metadata.commandArgumentHints[unitSlug])" +--- +""" + } +} + +local agentPromptFrontmatterBySlug = new Mapping { + for (unitSlug, _ in shared.agents) { + [unitSlug] = """ +--- +description: "\(metadata.agentDescriptions[unitSlug])" +argument-hint: "\(metadata.agentArgumentHints[unitSlug])" +--- +""" + } +} + +local agentPromptBodyBySlug = new Mapping { + for (unitSlug, unit in shared.agents) { + [unitSlug] = """ +Act as the \(unit.title) agent for the rest of this session. Load and follow the `\(metadata.agentSkillReferences[unitSlug])` skill when its workflow applies. Stay within this role's rules below until the task completes. + +Input: +`$ARGUMENTS` + +\(unit.canonicalBody) +""" + } +} + +local skillFrontmatterBySlug = new Mapping { + for (unitSlug, _ in shared.skills) { + [unitSlug] = """ +--- +name: \(unitSlug) +description: \(common.skillDescriptions[unitSlug]) +--- +""" + } +} + +agentPrompts { + for (unitSlug, unit in shared.agents) { + [unitSlug] = new common.RenderedTargetDocument { + slug = unitSlug + title = unit.title + frontmatter = agentPromptFrontmatterBySlug[unitSlug] + body = agentPromptBodyBySlug[unitSlug] + } + } +} + +commands { + for (unitSlug, unit in shared.commands) { + [unitSlug] = new common.RenderedTargetDocument { + slug = unitSlug + title = unit.title + frontmatter = commandFrontmatterBySlug[unitSlug] + body = unit.canonicalBody + } + } +} + +skills { + for (unitSlug, unit in shared.skills) { + [unitSlug] = new common.RenderedTargetDocument { + slug = unitSlug + title = unit.title + frontmatter = skillFrontmatterBySlug[unitSlug] + body = unit.canonicalBody + } + } +} diff --git a/config/pkl/renderers/pi-metadata.pkl b/config/pkl/renderers/pi-metadata.pkl new file mode 100644 index 00000000..c2a3b894 --- /dev/null +++ b/config/pkl/renderers/pi-metadata.pkl @@ -0,0 +1,22 @@ +agentDescriptions = new Mapping { + ["shared-context-plan"] = "Act in the shared-context-plan role to create or update an SCE plan before implementation." + ["shared-context-code"] = "Act in the shared-context-code role to execute one approved SCE task and sync context." +} + +agentArgumentHints = new Mapping { + ["shared-context-plan"] = "" + ["shared-context-code"] = " [T0X]" +} + +agentSkillReferences = new Mapping { + ["shared-context-plan"] = "sce-plan-authoring" + ["shared-context-code"] = "sce-task-execution" +} + +commandArgumentHints = new Mapping { + ["next-task"] = " [T0X]" + ["change-to-plan"] = "" + ["handover"] = "[task context]" + ["commit"] = "[oneshot|skip]" + ["validate"] = "" +} diff --git a/config/schema/sce-config.schema.json b/config/schema/sce-config.schema.json index 23966f09..36268146 100644 --- a/config/schema/sce-config.schema.json +++ b/config/schema/sce-config.schema.json @@ -58,6 +58,31 @@ }, "additionalProperties": false }, + "agent_trace": { + "description": "Agent Trace hook policy. Controls where post-commit Agent Trace JSON is mirrored as a git note and whether that notes ref is auto-pushed.", + "type": "object", + "properties": { + "git_notes_ref": { + "description": "Git notes ref used for Agent Trace JSON persistence. Defaults to refs/notes/sce-agent-trace.", + "default": "refs/notes/sce-agent-trace", + "type": "string", + "minLength": 1 + }, + "push_notes": { + "description": "Agent Trace git-notes push policy. Notes auto-push is enabled by default.", + "type": "object", + "properties": { + "enabled": { + "description": "Enable best-effort Agent Trace git-notes auto-push after local note persistence. Defaults to true when omitted; set false to disable.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "database_retry": { "type": "object", "properties": { @@ -348,7 +373,8 @@ "type": "string", "enum": [ "opencode", - "claude" + "claude", + "pi" ] }, "uniqueItems": true diff --git a/context/architecture.md b/context/architecture.md index 33353f9a..436f1f1d 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -2,10 +2,11 @@ ## Config generation boundary (current approved design) -The repository keeps two parallel config target trees: +The repository keeps three parallel config target trees: - `config/.opencode` - `config/.claude` +- `config/.pi` For authored config content, generation is standardized around one canonical Pkl source model with target-specific rendering applied later in the pipeline. @@ -29,28 +30,31 @@ Current target renderer helper modules: - `config/pkl/renderers/opencode-content.pkl` - `config/pkl/renderers/opencode-automated-content.pkl` - `config/pkl/renderers/claude-content.pkl` +- `config/pkl/renderers/pi-content.pkl` - `config/pkl/renderers/common.pkl` - `config/pkl/renderers/opencode-metadata.pkl` - `config/pkl/renderers/opencode-automated-metadata.pkl` - `config/pkl/renderers/claude-metadata.pkl` +- `config/pkl/renderers/pi-metadata.pkl` - `config/pkl/renderers/metadata-coverage-check.pkl` - `config/pkl/generate.pkl` (single multi-file generation entrypoint) - `config/pkl/check-generated.sh` (dev-shell integration stale-output detection against committed generated files) - `nix flake check` / `checks..{cli-tests,cli-clippy,cli-fmt,pkl-parity,npm-bun-tests,npm-biome-check,npm-biome-format,config-lib-bun-tests,config-lib-biome-check,config-lib-biome-format,workflow-actionlint}` plus Linux-only `flatpak-static-validation`, `cargo-sources-parity`, and `flatpak-manifest-parity` (root-flake check derivations for the current CLI, generated-output parity, JS validation inventory, workflow linting, and lightweight Flatpak static/AppStream validation) - `config-lib-bun-tests` executes from `config/lib/` while using a repo-shaped copied source subset that also includes `cli/src/services/structured_patch/fixtures` for Claude agent-trace golden fixture coverage (fully Rust-owned; the Claude TypeScript Bun test was removed in T07). -The scaffold provides stable canonical content-unit identifiers and reusable target-agnostic text primitives for all planned authored generated classes (agents, commands, skills, shared runtime assets, OpenCode plugin entrypoints, generated Claude plugin entrypoints, generated OpenCode package manifests, and generated Claude project settings). +The scaffold provides stable canonical content-unit identifiers and reusable target-agnostic text primitives for all planned authored generated classes (agents, commands, skills, shared runtime assets, OpenCode plugin entrypoints, the Pi extension entrypoint, generated OpenCode package manifests, and generated Claude project settings). Renderer modules apply target-specific metadata/frontmatter rules while reusing canonical content bodies: - OpenCode renderer emits frontmatter with `agent`/`permission`/`compatibility: opencode` conventions; targeted SCE commands also emit machine-readable `entry-skill` and ordered `skills` metadata when the renderer explicitly defines that mapping. - Claude renderer emits frontmatter with `allowed-tools`/`model`/`compatibility: claude` conventions. +- Pi renderer emits prompt-template frontmatter with `description`/`argument-hint` conventions: commands render to `config/.pi/prompts/{slug}.md`, SCE agents render as agent-role prompt templates at `config/.pi/prompts/agent-{slug}.md` (act-as-role preamble + `$ARGUMENTS` input + canonical agent body), and skills render to Agent Skills-format `config/.pi/skills/{slug}/SKILL.md`. Pi has no native sub-agent format and no settings/plugin manifest; runtime integration is provided by a project-local Pi extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts` to `config/.pi/extensions/sce/index.ts` (auto-discovered by Pi, no registration manifest; see `context/sce/pi-extension-runtime.md`). - Shared renderer contracts (`RenderedTargetDocument`, command descriptions) live in `config/pkl/renderers/common.pkl`. - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). -- Target-specific metadata tables, including skill frontmatter descriptions, are isolated in `config/pkl/renderers/opencode-metadata.pkl`, `config/pkl/renderers/opencode-automated-metadata.pkl`, and `config/pkl/renderers/claude-metadata.pkl`. -- Metadata key coverage is enforced by `config/pkl/renderers/metadata-coverage-check.pkl`, which resolves all required lookup keys for both targets and fails evaluation on missing entries. +- Target-specific metadata tables, including skill frontmatter descriptions, are isolated in `config/pkl/renderers/opencode-metadata.pkl`, `config/pkl/renderers/opencode-automated-metadata.pkl`, `config/pkl/renderers/claude-metadata.pkl`, and `config/pkl/renderers/pi-metadata.pkl` (Pi agent descriptions, argument hints, skill references, and command argument hints; Pi skill/command descriptions reuse the shared tables in `common.pkl`). +- Metadata key coverage is enforced by `config/pkl/renderers/metadata-coverage-check.pkl`, which resolves all required lookup keys for all targets (OpenCode manual/automated, Claude, Pi) and fails evaluation on missing entries. - Both renderers expose per-class rendered document objects (`agents`, `commands`, `skills`) consumed by `config/pkl/generate.pkl`. -- `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode/Claude agents, commands, skills, Claude project settings, the Claude hook helper script at `config/.claude/hooks/run-sce-or-show-install-guidance.sh`, shared bash-policy preset assets under `lib/`, the OpenCode plugin entrypoints under `plugins/` (currently `sce-bash-policy.ts` and `sce-agent-trace.ts`), generated OpenCode `package.json` and `opencode.json` manifests for manual and automated profiles, and the generated `sce/config.json` schema artifact at `config/schema/sce-config.schema.json`. +- `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode/Claude agents, commands, skills, Claude project settings, the Claude hook helper script at `config/.claude/hooks/run-sce-or-show-install-guidance.sh`, shared bash-policy preset assets under `lib/`, the OpenCode plugin entrypoints under `plugins/` (currently `sce-bash-policy.ts` and `sce-agent-trace.ts`), generated OpenCode `package.json` and `opencode.json` manifests for manual and automated profiles, the Pi target tree (`config/.pi/prompts/{slug}.md` command prompts, `config/.pi/prompts/agent-{slug}.md` agent-role prompts, `config/.pi/skills/{slug}/SKILL.md`, and the Pi extension at `config/.pi/extensions/sce/index.ts` emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`), and the generated `sce/config.json` schema artifact at `config/schema/sce-config.schema.json`. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. - `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, runs `pkl eval -m config/pkl/generate.pkl`, and fails when generated-owned paths drift. @@ -59,6 +63,8 @@ Generated authored classes: - agent definitions - command definitions - skill definitions +- Pi prompt templates and Agent Skills packages +- Pi extension entrypoint - shared runtime library files - Claude project settings - OpenCode plugin entrypoints @@ -105,7 +111,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/cli_schema.rs` is now the canonical owner for top-level command metadata for the real clap-backed command set (`auth`, `config`, `setup`, `doctor`, `hooks`, `version`, `completion`), including the slim top-level help purpose text and per-command visibility on `sce`, `sce help`, and `sce --help`; `cli/src/command_surface.rs` remains the custom top-level help renderer and known-command classifier, adding the synthetic `help` row plus the ASCII banner while consuming that shared metadata instead of maintaining a parallel command catalog. - `cli/src/services/default_paths.rs` is the canonical production path catalog for the CLI: it resolves config/state/cache roots with platform-aware XDG or `dirs` fallbacks through an internal `roots` seam, exposes named default paths for current persisted artifacts and database files (global config, auth tokens, auth DB, local DB, legacy/global agent trace DB fallback, and per-checkout agent trace DB files), and owns canonical repo-relative, embedded-asset, install, hook, and context-path accessors so non-test production path definitions have one shared owner. Current production consumers such as config discovery, doctor reporting, setup/install flows, database adapters, checkout identity, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. - `cli/src/services/agent_trace.rs` is the Rust CLI owner for the SCE web base URL (`SCE_WEB_BASE_URL`) and exposes helpers for SCE-owned URL construction: Agent Trace conversation lookup URLs, persisted Agent Trace trace URLs, Agent Trace session URLs, and setup-created config schema URLs. Production Rust code should consume those helpers instead of repeating `sce.crocoder.dev` literals. -- `cli/src/services/config/mod.rs` is the config service facade and `sce config` orchestration surface (`show`, `validate`, `--help`), with bare `sce config` routed by `cli/src/app.rs` to the same help payload as `sce config --help`. Focused submodules own the implementation slices: `types.rs` owns shared config/runtime primitives, `schema.rs` owns generated schema embedding plus typed file parsing, `policy.rs` owns bash-policy semantic validation plus policy-specific formatting and runtime preset-catalog access for the Rust evaluator, `resolver.rs` owns deterministic config-file discovery, file-layer merging, explicit value precedence (`flags > env > config file > defaults` where flag-backed), shared auth-key resolution, observability-runtime resolution, attribution-hooks runtime gate resolution, database-retry config resolution and `DATABASE_RETRY_CONFIG` `OnceLock` initialization, default-discovered invalid-file degradation, and explicit-path fatal errors for `--config` / `SCE_CONFIG_FILE`, and private `render.rs` owns `sce config show` / `sce config validate` text and JSON output construction plus rendering-specific display-value helpers. The facade preserves existing `services::config` imports for startup/auth/hooks callers while delegating command execution to resolution plus rendering submodules. +- `cli/src/services/config/mod.rs` is the config service facade and `sce config` orchestration surface (`show`, `validate`, `--help`), with bare `sce config` routed by `cli/src/app.rs` to the same help payload as `sce config --help`. Focused submodules own the implementation slices: `types.rs` owns shared config/runtime primitives, `schema.rs` owns generated schema embedding plus typed file parsing, `policy.rs` owns bash-policy semantic validation plus policy-specific formatting and runtime preset-catalog access for the Rust evaluator, `resolver.rs` owns deterministic config-file discovery, file-layer merging, explicit value precedence (`flags > env > config file > defaults` where flag-backed), shared auth-key resolution, observability-runtime resolution, attribution-hooks runtime gate resolution, Agent Trace hook policy resolution for `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace` plus `policies.agent_trace.push_notes.enabled` with default `true`, database-retry config resolution and `DATABASE_RETRY_CONFIG` `OnceLock` initialization, default-discovered invalid-file degradation, and explicit-path fatal errors for `--config` / `SCE_CONFIG_FILE`, and private `render.rs` owns `sce config show` / `sce config validate` text and JSON output construction plus rendering-specific display-value helpers. The facade preserves existing `services::config` imports for startup/auth/hooks callers while delegating command execution to resolution plus rendering submodules. - `cli/src/services/output_format.rs` defines the canonical shared CLI output-format contract (`OutputFormat`) for supporting commands, with deterministic `text|json` parsing and command-scoped actionable invalid-value guidance. - `cli/src/services/config/types.rs` is the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers used by both config resolution and observability bootstrap; `cli/src/services/config/mod.rs` re-exports those primitives through the facade. - `cli/src/services/capabilities.rs` defines the current broad CLI capability traits consumed by the borrowed, compile-time-typed `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing service internals do not consume these traits directly yet; command execution uses narrow accessors and repo-root-scoped context derivation. @@ -117,21 +123,21 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. - `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves the legacy `/sce/agent-trace.db` fallback through the shared default-path seam and embeds ordered fresh-start migrations `001..015` (the former `015_create_session_models` was removed from fresh schema; current `015_add_diff_traces_payload_type` replaces it) for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides explicit-path `open_at(path)` and `open_for_hooks_without_migrations_at(path)` helpers for per-checkout DB files plus `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity, resolves `/sce/agent-trace-{checkout_id}.db`, initializes it with `AgentTraceDb::open_at(path)`, and records `database_path`; active hook runtime still resolves checkout identity and lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. -- `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. +- `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude,pi}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. - `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Checkout DB discovery no longer lives in `doctor`; it moved to the `trace` group (`sce trace db list`) in `cli/src/services/trace/`. Report fact collection preserves environment/repository/hook/integration display data and adds checkout identity plus per-checkout Agent Trace DB status when a checkout ID exists, while service-owned lifecycle providers own config validation, local DB and Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. - `cli/src/services/version/mod.rs` defines the version command parser/rendering contract (`parse_version_request`, `render_version`) with deterministic text output and stable JSON runtime-identification fields; `cli/src/services/version/command.rs` owns the `VersionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/completion/mod.rs` defines completion parser/rendering contract (`parse_completion_request`, `render_completion`) with deterministic Bash/Zsh/Fish script output aligned to current parser-valid command/flag surfaces; `cli/src/services/completion/command.rs` owns the `CompletionCommand` payload used by the static `RuntimeCommand` enum. -- `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, same-tool idempotent), and inserts the parsed payload fields into AgentTraceDb without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. `session-model` is no longer a supported hook route. +- `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb, writes best-effort git notes, and attempts default-enabled silent fail-open notes push controlled by `policies.agent_trace.push_notes.enabled` without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi, same-tool idempotent), and inserts the parsed payload fields into AgentTraceDb without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. `session-model` is no longer a supported hook route. - Generated Claude settings no longer register `SessionStart` for Agent Trace model attribution, and `sce hooks session-model` is no longer a supported hook command. The `session_models` table/API and diff-trace fallback lookup were removed in T02 of the `remove-session-models-direct-claude-model-id` plan; `diff-trace` now uses only direct payload `model_id` and `tool_version` values. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. - No user-invocable `sce sync` command is wired in the current runtime; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by setup, while checkout/global DB health/repair flow through the doctor surface and checkout DB discovery flows through the `trace` group (`sce trace db list`). - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses `AgentTraceDbLifecycle::setup()` to establish checkout identity and initialize the per-checkout DB, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce trace db list` discovers checkouts via filesystem scan of `agent-trace-*.db` files on disk. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode and Claude hook callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, checkout identity, bash_policy, version, completion, help, patch, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/checkout/` owns checkout ID file infrastructure and per-checkout Agent Trace DB lazy resolution for hook runtime; setup uses `AgentTraceDbLifecycle::setup()` to establish checkout identity and initialize the per-checkout DB, while `sce doctor` surfaces checkout identity and per-checkout Agent Trace DB status when a checkout ID exists and `sce trace db list` discovers checkouts via filesystem scan of `agent-trace-*.db` files on disk. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, and policy. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. -- `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.95.0.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with shared host Cargo metadata plus a filtered repo-root source that preserves the Cargo tree, `cli/assets/hooks`, and only the generated config/schema inputs required by CLI embedding, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. +- `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.95.0.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with shared host Cargo metadata plus a filtered repo-root source that preserves the Cargo tree, `cli/assets/hooks`, and only the generated config/schema inputs required by CLI embedding, then injects generated OpenCode/Claude/Pi config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets. The same `cli/build.rs` now scans `cli/migrations/*/*.sql` and writes `cli/src/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. - The root flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed paths for the CLI crate; `cli-fmt` intentionally uses the CLI-only Cargo source set rather than the package source with generated config/assets so formatting is not invalidated by release/config asset churn. The flake also exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and `pkl-parity` uses a narrowed Pkl/generated-output source set instead of copying the entire repository. - Flatpak flake tooling is Linux-only and reduced to a minimal app surface: `apps.sce-flatpak` is the umbrella entrypoint (`nix run .#sce-flatpak -- ` for `validate`, `prepare-local-manifest --repo-root `, etc.) that delegates to `packaging/flatpak/sce-flatpak.sh`; `apps.release-flatpak-package` emits deterministic Flatpak GitHub Release source-manifest assets; `apps.release-flatpak-bundle` builds a source-built `.flatpak` bundle; helper apps `apps.regenerate-flatpak-manifest` and `apps.regenerate-cargo-sources` rewrite the checked-in generated artifacts from their Nix sources. The standalone `flatpak-validate`, `flatpak-local-manifest`, and `flatpak-build` wrapper apps are removed in favor of `sce-flatpak `. `checks..flatpak-static-validation` runs the Nix-built static validator script during default `nix flake check`, and `checks..flatpak-manifest-parity` plus `checks..cargo-sources-parity` enforce the generated-artifact parity contracts; default `nix flake check` does not run `flatpak-builder`. The Linux dev shell includes `appstreamcli`, `flatpak`, and `flatpak-builder`. - The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index f3b83479..fd34bb2d 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -53,17 +53,17 @@ Operator onboarding currently comes from `sce --help`, command-local `--help` ou - `auth` and `hooks` stay parser-valid and directly invocable, but are hidden from those top-level help surfaces Deferred or gated command surfaces currently avoid claiming unimplemented behavior. -`hooks` routes through implemented subcommand parsing/dispatch for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`; `session-model` is no longer a supported hooks route. Current behavior remains attribution-only and enabled by default for commit attribution unless explicitly opted out (via `SCE_ATTRIBUTION_HOOKS_DISABLED`, `SCE_DISABLED`, or `policies.attribution_hooks.enabled = false`), gated by the staged-diff AI-overlap preflight so the trailer is appended only when AI/editor evidence is found. `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains the active intersection + Agent Trace DB path. `diff-trace` is active STDIN intake with required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, required `u64` `time` validation, direct nullable `model_id` / `tool_version` attribution (no session fallback), non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion including tool-prefixed stored `session_id` values (`oc_` for OpenCode, `cc_` for Claude) plus nullable `model_id` and `tool_version`; Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. +`hooks` routes through implemented subcommand parsing/dispatch for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`; `session-model` is no longer a supported hooks route. Current behavior remains attribution-only and enabled by default for commit attribution unless explicitly opted out (via `SCE_ATTRIBUTION_HOOKS_DISABLED`, `SCE_DISABLED`, or `policies.attribution_hooks.enabled = false`), gated by the staged-diff AI-overlap preflight so the trailer is appended only when AI/editor evidence is found. `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains the active intersection + Agent Trace DB path. `diff-trace` is active STDIN intake with required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, required `u64` `time` validation, direct nullable `model_id` / `tool_version` attribution (no session fallback), non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion including tool-prefixed stored `session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi) plus nullable `model_id` and `tool_version`; Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. `config` exposes deterministic inspect/validate entrypoints (`sce config show`, `sce config validate`) with explicit precedence (`flags > env > config file > defaults`), a shared auth-runtime resolver for supported keys that declare env/config/optional baked-default inputs starting with `workos_client_id`, first-class `policies.bash` reporting for preset/custom blocked-command rules, and deterministic text/JSON output modes where `show` reports resolved values with provenance while `validate` reports pass/fail plus validation issues and warnings only. `version` exposes deterministic runtime identification output in text mode by default and JSON mode via `--format json`. `completion` exposes deterministic shell completion generation via `sce completion --shell `. -`setup` defaults to an `inquire` interactive target selection (OpenCode, Claude, Both) and accepts mutually-exclusive non-interactive target flags (`--opencode`, `--claude`, `--both`); the interactive prompt title and target labels now reuse shared prompt styling helpers when stdout color is enabled. +`setup` defaults to an `inquire` interactive target selection (OpenCode, Claude, Pi, All) and accepts mutually-exclusive non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--all`); the former `--both` flag was removed in favor of `--all` (opencode+claude+pi); the interactive prompt title and target labels reuse shared prompt styling helpers when stdout color is enabled. `auth` now emits auth-local guidance for bare `sce auth` and `sce auth --help`, listing `login`, `logout`, and `status` plus copy-ready next steps. `setup`, `doctor`, `hooks`, `version`, and `completion` all support command-local `--help`/`-h` usage output via top-level parser routing in `cli/src/app.rs`. -`setup` now also exposes compile-time embedded config assets for OpenCode/Claude targets, sourced from the generated `config/.opencode/**` and `config/.claude/**` trees via `cli/build.rs` with normalized forward-slash relative paths and target-scoped iteration APIs; the embedded asset set includes the OpenCode bash-policy plugin wrapper plus Claude settings `PreToolUse` Bash policy hook, both delegating to the Rust `sce policy bash` path. -`setup` additionally includes a repository-root install engine (`install_embedded_setup_assets`) that stages embedded files and uses a unified remove-and-replace policy for `.opencode/`/`.claude/` (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure) while treating bash-policy enforcement files as first-class SCE-managed assets. +`setup` now also exposes compile-time embedded config assets for OpenCode/Claude/Pi targets, sourced from the generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees via `cli/build.rs` with normalized forward-slash relative paths and target-scoped iteration APIs; the embedded asset set includes the OpenCode bash-policy plugin wrapper plus Claude settings `PreToolUse` Bash policy hook, both delegating to the Rust `sce policy bash` path. +`setup` additionally includes a repository-root install engine (`install_embedded_setup_assets`) that stages embedded files and uses a unified remove-and-replace policy for `.opencode/`/`.claude/`/`.pi/` (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure) while treating bash-policy enforcement files as first-class SCE-managed assets. `setup` now executes end-to-end and prints deterministic completion details including selected target(s) and per-target install count. -`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only and `sce doctor --fix` selects repair-intent mode. Agent Trace DB checkout discovery has moved out of `doctor` into the `trace` group (`sce trace db list`, `sce trace status`, `sce trace status --all`); see [trace-command.md](trace-command.md). The current `doctor` runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode and Claude integration presence/content health for their embedded setup assets. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. +`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only and `sce doctor --fix` selects repair-intent mode. Agent Trace DB checkout discovery has moved out of `doctor` into the `trace` group (`sce trace db list`, `sce trace status`, `sce trace status --all`); see [trace-command.md](trace-command.md). The current `doctor` runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode, Claude, and Pi integration presence/content health for their embedded setup assets. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. A user-invocable `sync` command is not wired in the current CLI surface; local DB and Agent Trace DB bootstrap currently happen through `setup`, and DB health/repair currently happens through `doctor`. Command wiring for `sce sync` is deferred to `0.4.0`. ## Command loop and error model @@ -88,10 +88,10 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/setup/mod.rs` defines setup parsing/selection contracts plus runtime install orchestration (`run_setup_for_mode`) over the embedded asset install engine; `cli/src/services/setup/command.rs` owns the setup runtime command handler. Setup now aggregates `ServiceLifecycle::setup` calls across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) in order, using a `ContextWithRepoRoot`-scoped context with resolved repository root. - `cli/src/services/setup/mod.rs` now keeps its larger internal responsibilities behind focused inline support modules: `install` owns repository canonicalization, staging/swap install flows, required-hook installation, and repo/writeability guards, while `prompt` owns interactive target selection and styled prompt labels. - `cli/src/services/config/mod.rs` defines config parser/runtime contracts (`show`, `validate`, `--help`), strict config-file key/type validation, deterministic text/JSON rendering, repo-configured bash-policy preset/custom validation and reporting under `policies.bash`, and shared auth-key metadata that declares env key, config-file key, and optional baked-default eligibility for supported auth runtime values starting with `workos_client_id` (`WORKOS_CLIENT_ID` vs `workos_client_id`); auth-key provenance/preference metadata stays on `show`, while `validate` stays trimmed to validation status plus issues/warnings. `cli/src/services/config/lifecycle.rs` implements `ServiceLifecycle` for config health checks and setup (global/local config validation and repo-local config bootstrap). -- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorAction`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, checkout-database discovery, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode and Claude integration inventory derived from embedded setup asset catalogs, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. Claude grouping is path-based: `settings.json`/`hooks/**` as `ClaudeCode plugins` (including `.claude/hooks/run-sce-or-show-install-guidance.sh`), plus `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`. +- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorAction`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, checkout-database discovery, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode, Claude, and Pi integration inventory derived from embedded setup asset catalogs, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. Claude grouping is path-based: `settings.json`/`hooks/**` as `ClaudeCode plugins` (including `.claude/hooks/run-sce-or-show-install-guidance.sh`), plus `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`; Pi grouping is path-based: `prompts/**` as `Pi prompts` and `skills/**` as `Pi skills`. - `cli/src/services/version/mod.rs` defines the version parser/output contract (`parse_version_request`, `render_version`) with deterministic text/JSON output modes; `cli/src/services/version/command.rs` owns the version runtime command handler. - `cli/src/services/completion/mod.rs` defines the completion output contract (`render_completion`) using clap_complete to generate deterministic shell scripts for Bash, Zsh, and Fish; `cli/src/services/completion/command.rs` owns the completion runtime command handler. -- `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path; `diff-trace` performs STDIN JSON intake, required-field validation, and best-effort AgentTraceDb insertion with tool-prefixed stored `session_id` values plus direct nullable `model_id` / `tool_version` attribution. Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. `session-model` is no longer a supported hooks route. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). +- `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path with best-effort git-note JSON persistence after DB insert; `diff-trace` performs STDIN JSON intake, required-field validation, and best-effort AgentTraceDb insertion with tool-prefixed stored `session_id` values plus direct nullable `model_id` / `tool_version` attribution. Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. `session-model` is no longer a supported hooks route. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). - `cli/src/services/resilience.rs` defines shared bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) with deterministic failure messaging and retry observability hooks. - No `cli/src/services/sync.rs` module exists in the current codebase; `sce sync` command wiring is deferred, while local DB initialization and health ownership are split between setup and doctor. - `cli/src/services/default_paths.rs` defines the canonical per-user persisted-location seam for config/state/cache roots plus named default file paths for current persisted artifacts (`global config`, `auth tokens`, `local DB`, `agent trace DB`) used by config discovery, token storage, database adapters, and doctor diagnostics; its internal `roots` seam now owns the platform-aware root-directory resolution so non-test production modules consume shared path accessors instead of resolving owned roots directly. diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index a2689941..bcb57dfb 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -25,6 +25,8 @@ Resolved runtime values follow this deterministic order: Repo-configured bash-tool policy values are config-file only in this task slice: they load from `policies.bash` in the selected config files, merge `global -> local` alongside the rest of the config object, and currently have no flag or environment override layer. +Agent Trace hook policy currently includes `policies.agent_trace.git_notes_ref`, which resolves as config-file value over default `refs/notes/sce-agent-trace`, and `policies.agent_trace.push_notes.enabled`, which resolves as config-file value over default `true`. These keys have no flag or environment override layer in the current implementation slice. The resolved notes ref is consumed by the post-commit Agent Trace flow when writing the validated full Agent Trace JSON to git notes after Agent Trace DB persistence succeeds and when pushing that same notes ref. The resolved push-notes boolean gates the post-commit best-effort `git push ` attempt; explicit `false` skips the push. + Resolved observability values that currently have no CLI flag layer follow the same lower-precedence chain without a flag step: 1. environment values (`SCE_LOG_FORMAT`, `SCE_LOG_FILE`, `SCE_LOG_FILE_MODE`) @@ -56,7 +58,7 @@ When a default-discovered global or repo-local config file exists but fails JSON - The canonical JSON Schema artifact for both global and repo-local `sce/config.json` files is authored in `config/pkl/base/sce-config-schema.pkl` and generated to `config/schema/sce-config.schema.json`. - `cli/src/services/config/schema.rs` embeds that generated artifact at compile time as `SCE_CONFIG_SCHEMA_JSON` and uses it for runtime schema validation before mapping parsed files into typed serde DTOs. - `sce config validate` and `sce doctor` both validate config-file structure against that shared generated schema before applying Rust-owned semantic checks such as duplicate custom `argv_prefix` detection and redundancy warnings. -- After schema validation, `cli/src/services/config/schema.rs` deserializes top-level and nested config structure (`policies`, `policies.bash`, `policies.attribution_hooks`) into typed serde DTOs and applies focused Rust-owned mapping helpers for enum conversion and source attribution; policy-specific semantic checks are owned by `cli/src/services/config/policy.rs`. +- After schema validation, `cli/src/services/config/schema.rs` deserializes top-level and nested config structure (`policies`, `policies.bash`, `policies.attribution_hooks`, `policies.agent_trace`) into typed serde DTOs and applies focused Rust-owned mapping helpers for enum conversion and source attribution; policy-specific semantic checks are owned by `cli/src/services/config/policy.rs`. - The canonical top-level schema declaration `"$schema": "https://sce.crocoder.dev/config.json"` is a supported config key for both explicit and discovered `sce/config.json` files, including command-startup paths like `sce version` and other config-loading commands that parse config before normal command dispatch. - Startup/runtime config resolution now degrades gracefully only for default-discovered files: invalid discovered files are skipped and reported via collected `validation_errors`, while explicit `--config` / `SCE_CONFIG_FILE` targets still fail immediately on the same parse or validation errors. @@ -73,11 +75,13 @@ When a default-discovered global or repo-local config file exists but fails JSON - `integrations` must be an object when present and currently allows only `target`. - `integrations.target` must be an array of unique canonical target IDs when present. -- Supported target ID values: `opencode`, `claude`. +- Supported target ID values: `opencode`, `claude`, `pi`. - Unknown target IDs fail schema validation. -- `policies` must be an object when present and currently allows `attribution_hooks`, `database_retry`, and `bash`. +- `policies` must be an object when present and currently allows `attribution_hooks`, `agent_trace`, `database_retry`, and `bash`. - `policies.attribution_hooks` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` remains a valid opt-out alongside the runtime `SCE_ATTRIBUTION_HOOKS_DISABLED` environment opt-out. +- `policies.agent_trace` must be an object when present and currently allows `git_notes_ref` and `push_notes`; the generated schema documents default `refs/notes/sce-agent-trace`, and Rust mapping rejects blank/whitespace-only refs. +- `policies.agent_trace.push_notes` must be an object when present and currently allows `enabled`; the generated schema documents default `true`, and explicit `enabled: false` opts out of post-commit Agent Trace notes auto-push. - `policies.bash` must be an object when present and currently allows only `presets` and `custom`. - `policies.bash.presets` must be an array of unique built-in preset IDs: `forbid-git-all`, `forbid-git-commit`, `use-pnpm-over-npm`, `use-bun-over-npm`, `use-nix-flake-over-cargo`. - `use-pnpm-over-npm` and `use-bun-over-npm` are mutually exclusive and fail validation when both are present. @@ -98,6 +102,7 @@ When a default-discovered global or repo-local config file exists but fails JSON - `validate` text output is limited to `SCE config validation`, `Validation issues`, and `Validation warnings` lines. - `validate` JSON output is limited to `result.command`, `result.valid`, `result.issues`, and `result.warnings`. - `show` includes resolved bash-tool policies under `result.resolved.policies.bash`. +- `show` includes resolved Agent Trace hook policy under `result.resolved.policies.agent_trace`, including `git_notes_ref` and `push_notes.enabled` with source metadata. - Bash-policy output includes resolved preset IDs, expanded custom entries (`id`, `match.argv_prefix`, `message`), and config-file source metadata when present. - `show` text output renders `policies.bash` as a single deterministic line and reports `(unset)` when no policy config resolves. - `show` text output renders observability values as deterministic per-key lines, reporting `(unset)` for `log_file` when no value resolves. diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index a3f55e47..94785e58 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -25,6 +25,7 @@ - `.sce/`, `.sce/config.json`, `.sce/sce.log` - `.opencode/`, `.opencode/opencode.json` - `.claude/` +- `.pi/` - `.git/`, `.git/hooks/`, `.git/COMMIT_EDITMSG` - `context/`, `context/plans/`, `context/decisions/`, `context/handovers/`, `context/tmp/` diff --git a/context/context-map.md b/context/context-map.md index 623ba8f4..163a9586 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -16,7 +16,7 @@ Feature/domain context: - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, and Rust golden fixture coverage) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `context/cli/trace-command.md` (`sce trace` command group: discovery of per-checkout `agent-trace-*.db` files under `/sce/` with mtime-desc + checkout-id tiebreak alias assignment and six-required-table readiness probing, implemented `sce trace db shell ` wiring that resolves aliases/checkout IDs and opens the embedded in-process SQL shell without external `turso`, including `.tables` table-name listing for visible/internal tables, implemented `sce trace db list` text + JSON rendering using `services::style::heading`, implemented `sce trace status` per-checkout rendering with `StatusError::{NotInGitRepo, NoCheckoutId, DbMissing}` mapped to validation-class exits and skipped-DB pass-through, implemented `sce trace status --all` aggregation across every discovered DB, and the completed removal of `sce doctor dbs` whose discovery scan/rendering moved into `services::trace`) -- `context/cli/config-precedence-contract.md` (implemented `sce config` show/validate command contract, deterministic `flags > env > config file > defaults` resolution order, focused `config/resolver.rs` ownership for config discovery/merge/runtime precedence plus default-discovered invalid-file degradation, focused `config/render.rs` ownership for `show`/`validate` text+JSON output construction, canonical `$schema` acceptance for startup-loaded `sce/config.json` files, shared auth-key env/config/optional baked-default support starting with `workos_client_id`, shared runtime resolution for flat logging observability keys, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata, config-file selection order, `show` provenance output, and trimmed `validate` output contract) +- `context/cli/config-precedence-contract.md` (implemented `sce config` show/validate command contract, deterministic `flags > env > config file > defaults` resolution order, focused `config/resolver.rs` ownership for config discovery/merge/runtime precedence plus default-discovered invalid-file degradation, focused `config/render.rs` ownership for `show`/`validate` text+JSON output construction, canonical `$schema` acceptance for startup-loaded `sce/config.json` files, shared auth-key env/config/optional baked-default support starting with `workos_client_id`, shared runtime resolution for flat logging observability keys, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata, `policies.agent_trace.git_notes_ref` default `refs/notes/sce-agent-trace`, and `policies.agent_trace.push_notes.enabled` default-true/explicit-false metadata, config-file selection order, `show` provenance output, and trimmed `validate` output contract) - `context/cli/capability-traits.md` (current broad CLI capability seam in `cli/src/services/capabilities.rs`, including `FsOps`/`StdFsOps`, `GitOps`/`ProcessGitOps`, git root/hooks resolution behavior, compile-time-typed borrowed AppContext wiring with associated-type narrow capability accessors plus `ContextWithRepoRoot` repo-root-scoped context derivation, generic command execution bounds, and test-only unimplemented stubs; current service internals do not consume fs/git traits until later lifecycle migration tasks) - `context/cli/service-lifecycle.md` (current compile-safe lifecycle seam in `cli/src/services/lifecycle.rs`, including default no-op `ServiceLifecycle` diagnose/fix/setup methods against narrow `HasRepoRoot`, lifecycle-owned health/fix/setup result types with generic setup messages, doctor/setup adapter boundaries, the static `LifecycleProvider` enum catalog/dispatcher, hook/config/local_db/auth_db/agent_trace_db lifecycle providers including setup-time checkout identity registration plus per-checkout Agent Trace DB initialization, implemented doctor aggregation over diagnose/fix providers, and implemented setup aggregation over `setup` providers in order config → local_db → auth_db → agent_trace_db → hooks when requested) - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with env-over-config fallback, concrete logger/telemetry runtime behavior plus logger and object-safe telemetry trait boundaries, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, and the trimmed `sce config validate` status-only validation surface) @@ -34,7 +34,7 @@ Feature/domain context: - `context/sce/agent-trace-commit-msg-coauthor-policy.md` (current commit-msg canonical co-author trailer policy with enabled-by-default attribution hooks, explicit opt-out controls, `SCE_DISABLED` kill switch, caller-provided `ai_contribution_present` transformer seam wired from staged-diff AI-overlap preflight, idempotent dedupe, the `agent_trace::patches_have_overlap` pure overlap seam, the `StagedDiffAiOverlapResult` three-valued evidence gate, and `sce.hooks.commit_msg.ai_overlap_error` error logging) - `context/sce/agent-trace-post-commit-dual-write.md` (historical post-commit no-op/dual-write reference; current post-commit behavior is documented in `agent-trace-hooks-command-routing.md`) - `context/sce/agent-trace-hook-doctor.md` (approved operator-environment contract for broadening `sce doctor` into the canonical health-and-repair entrypoint, including stable problem taxonomy, `--fix` semantics, checkout-aware Agent Trace DB reporting, setup-to-doctor alignment rules, and the approved downstream human text-mode layout/status/integration contract) -- `context/sce/doctor-human-text-contract.md` (implemented `sce doctor` human text layout contract: section order, `[PASS]`/`[FAIL]`/`[MISS]` status vocabulary, simplified hook rows, target-scoped integration checks with configured/detected/empty target resolution, no-installed-integrations guidance, and OpenCode plus Claude integration group rendering rules) +- `context/sce/doctor-human-text-contract.md` (implemented `sce doctor` human text layout contract: section order, `[PASS]`/`[FAIL]`/`[MISS]` status vocabulary, simplified hook rows, target-scoped integration checks with configured/detected/empty target resolution, no-installed-integrations guidance, and OpenCode, Claude, plus Pi integration group rendering rules including the `Pi extensions` group) - `context/sce/setup-githooks-install-contract.md` (T01 canonical `sce setup --hooks` install contract for target-path resolution, idempotent outcomes, remove-and-replace behavior, and doctor-readiness alignment) - `context/sce/setup-no-backup-policy-seam.md` (implemented unified remove-and-replace install policy for both config-install and required-hook install flows, with no backup creation and deterministic recovery guidance on swap failure) - `context/sce/setup-githooks-hook-asset-packaging.md` (T02 compile-time `sce setup --hooks` required-hook template packaging contract, including current post-commit missing-`sce` install guidance, origin remote lookup, remote-URL forwarding/fallback behavior, and setup-service accessor surface) @@ -52,10 +52,11 @@ Feature/domain context: - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) - `context/sce/agent-trace-minimal-generator.md` (implemented a library minimal Agent Trace generator seam at `cli/src/services/agent_trace.rs`, used by the active post-commit hook flow to produce strict `0.1.0` JSON payloads with top-level `version`, UUIDv7 `id` derived from commit-time metadata, caller-provided commit-time `timestamp`, optional top-level `vcs` metadata emitted when present (`type` from enum `git|jj|hg|svn`, `revision` from metadata input; current post-commit flow provides `git`), optional top-level `tool` metadata (`name`/`version`) sourced from builder metadata inputs when overlapping AI content exists, and always-emitted `metadata.sce.version` sourced from the compiled `sce` CLI package version, plus per-file trace data from patch inputs via `intersect_patches(constructed_patch, post_commit_patch)` then `post_commit_patch`-anchored hunk classification into `ai`/`mixed`/`unknown` contributor categories, serialized per conversation with a required lookup `url` derived from top-level `AgentTrace.id`, nested `contributor.type` with optional `contributor.model_id` omitted when provenance is missing, one derived `ranges[{start_line,end_line,content_hash}]` entry per post-commit or embedded-patch hunk, and range `content_hash` values that hash touched-line kind/content independent of positions and metadata) -- `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection and Agent Trace DB persistence, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude), direct nullable `model_id`/`tool_version` persistence without session fallback, Claude direct model metadata extraction from top-level or nested `model` fields with `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) +- `context/sce/agent-trace-hooks-command-routing.md` (implemented `sce hooks` command routing plus current runtime behavior: enabled-by-default commit-msg attribution with explicit opt-out controls, no-op `pre-commit`/`post-rewrite` entrypoints, active `post-commit` intersection plus Agent Trace DB persistence, best-effort git-note JSON persistence, and default-enabled silent fail-open git-notes push controlled by `policies.agent_trace.push_notes.enabled`, DB-only `diff-trace` STDIN intake with OpenCode normalized payloads and Claude structured `PostToolUse` payload classification, tool-prefixed stored `diff_traces.session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), direct nullable `model_id`/`tool_version` persistence without session fallback, Claude direct model metadata extraction from top-level or nested `model` fields with `claude/` prefix normalization, no parsed-payload artifact persistence under `context/tmp`, `session-model` removed from the supported hook surface, and `conversation-trace` STDIN intake for normalized batches plus supported raw Claude events; this document also owns the current `diff-trace` and `conversation-trace` fail-open intake contracts.) - `context/sce/automated-profile-contract.md` (deterministic gate policy for automated OpenCode profile, including 10 gate categories, permission mappings, automated `/commit` single-commit execution behavior, and automated profile constraints) - `context/sce/bash-tool-policy-enforcement-contract.md` (approved bash-tool blocking contract plus current Rust evaluator seam and OpenCode/Claude delegation references, including config schema, argv-prefix matching, shell/nix unwrapping, fixed preset catalog/messages, and precedence rules) - `context/sce/generated-opencode-plugin-registration.md` (current generated OpenCode plugin-registration contract, canonical Pkl ownership, generated manifest/plugin paths including `sce-bash-policy` + `sce-agent-trace`, TypeScript source ownership, and Claude generated settings boundary including Agent Trace hooks plus `PreToolUse` Bash policy hook registration through the missing-CLI install-guidance helper) +- `context/sce/pi-extension-runtime.md` (project-local Pi extension runtime: `config/lib/pi-plugin/sce-pi-extension.ts` emitted verbatim to `config/.pi/extensions/sce/index.ts`, Pi auto-discovery registration model with no manifest, implemented bash policy adapter delegating to `sce policy bash` with block-by-return `{ block, reason }` and fail-open behavior, implemented `message_end` conversation text capture piping mixed `message`/`message.part` batches (text + reasoning parts, `responseId`-or-random message IDs) to `sce hooks conversation-trace` fail-open, and implemented edit/write diff capture producing `git diff --no-index` unified diffs emitted as synthetic-message `patch` conversation parts plus normalized `sce hooks diff-trace` payloads with `tool_name: "pi"`, nullable `model_id`/`tool_version`, Rust-side `pi_` stored session-ID prefixing, and asset-pipeline shipping — whole-tree sync into `cli/assets/generated/config/pi/`, embedded install via `sce setup --pi`, and `sce doctor` `Pi extensions` health group) - `context/sce/opencode-agent-trace-plugin-runtime.md` (current OpenCode agent-trace plugin runtime behavior, including captured `message.updated` handoff with `summary.diffs` branching: when diffs exist sends one `-patch` mixed batch containing a synthetic parent message plus per-diff `message.part` patch items, when no diffs sends the original `message.updated` payload; in-memory dedup `Set` keyed by `"${sessionID}:${messageID}"`; captured `message.part.updated` handoff to `sce hooks conversation-trace` for `text`/`reasoning` parts with non-empty text plus completed `question` tool parts emitted as `part_type: "question"` with JSON-stringified `{ question, answer }[]`; existing user-message diff extraction for `{ sessionID, diff, time, model_id }`; session-scoped OpenCode client version capture from `session.created`/`session.updated`; and CLI handoff to `sce hooks diff-trace` over STDIN JSON with required `tool_name="opencode"` plus required nullable `tool_version`; Rust hook parsing and AgentTraceDb insertion persist `oc_`-prefixed session IDs plus required payload fields including `model_id`) - `context/sce/cli-first-install-channels-contract.md` (current `sce` install/distribution contract covering supported Nix/Cargo/npm plus source-built Flatpak channel scope, implemented GitHub Release Flatpak source-manifest and source-built `.flatpak` bundle asset packaging, canonical `.version` release authority, manual GitHub Release `prerelease` checkbox behavior, Nix-owned build/release policy, Nix-owned Flatpak manifest generation via `pkgs.formats.yaml.generate` plus Nix-owned cargo-sources generation from `cli/Cargo.lock` plus dedicated Nix-built Bash validator scripts for static, version-parity, and local-manifest validation, thin imperative `packaging/flatpak/sce-flatpak.sh` orchestration around `flatpak-builder` / `flatpak build-bundle`, the reduced Linux flake app surface (`sce-flatpak`, `release-flatpak-package`, `release-flatpak-bundle`, plus `regenerate-flatpak-manifest` / `regenerate-cargo-sources` helpers) with `flatpak-static-validation` / `flatpak-manifest-parity` / `cargo-sources-parity` checks, the implemented `packaging/flatpak/dev.crocoder.sce.yml` + AppStream/Cargo-source packaging surface as generated artifacts, and the `dev.crocoder.sce` host-git bridge decision) - `context/sce/cli-release-artifact-contract.md` (shared `sce` binary release artifact naming, checksum/manifest outputs, pre-archive staged-binary preparation including macOS `libiconv` install-name sanitization/ad-hoc re-signing, native portability audit app/check for forbidden `/nix/store/` runtime references, GitHub Releases as the canonical artifact publication surface, manual dispatch `prerelease` flag behavior, the current three-target Linux/macOS release workflow topology including pre-upload extracted-archive smoke/audit validation in each native lane, implemented Flatpak source-manifest and source-built `.flatpak` bundle package assets uploaded by `.github/workflows/release-sce.yml`, and Flatpak's explicit source-built non-binary exception) diff --git a/context/glossary.md b/context/glossary.md index 12d3c961..4471389b 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -6,7 +6,8 @@ - disposable plan lifecycle: Policy where `context/plans/` holds active execution artifacts only; completed plans are disposable and durable outcomes must be reflected in current-state context files and/or `context/decisions/`. - important change (context sync): A completed task change that affects cross-cutting behavior, repository-wide policy/contracts, architecture boundaries, or canonical terminology; these changes require root context edits in `context/overview.md`, `context/architecture.md`, and/or `context/glossary.md` instead of verify-only handling. - verify-only root context pass: Context-sync mode for localized tasks where root-level behavior, architecture, and terminology are unchanged; root shared files are checked against code truth but are not edited by default. -- generated-owned outputs: Files materialized by `config/pkl/generate.pkl` under `config/.opencode/**`, `config/automated/.opencode/**`, and `config/.claude/**`, including OpenCode plugin entrypoints, generated OpenCode `package.json` manifests, generated OpenCode `opencode.json` manifests, generated Claude plugin entrypoints, and Claude settings assets. +- generated-owned outputs: Files materialized by `config/pkl/generate.pkl` under `config/.opencode/**`, `config/automated/.opencode/**`, `config/.claude/**`, and `config/.pi/**`, including OpenCode plugin entrypoints, generated OpenCode `package.json` manifests, generated OpenCode `opencode.json` manifests, generated Claude plugin entrypoints, Claude settings assets, Pi prompt/skill assets, and the Pi extension entrypoint at `config/.pi/extensions/sce/index.ts`. +- `Pi agent-role prompt template`: Generated Pi prompt at `config/.pi/prompts/agent-{slug}.md` that adapts an SCE agent (shared-context-plan, shared-context-code) to Pi, which has no native sub-agent format: frontmatter carries `description`/`argument-hint`, and the body prepends an act-as-role preamble plus `$ARGUMENTS` input before the canonical agent body. Rendered by `config/pkl/renderers/pi-content.pkl` with metadata from `config/pkl/renderers/pi-metadata.pkl`. - `canonical OpenCode plugin registration source`: Shared Pkl-authored plugin-registration definition in `config/pkl/base/opencode.pkl`, re-exported from `config/pkl/renderers/common.pkl` as the canonical plugin list/path JSON consumed by OpenCode renderers before they emit generated `opencode.json` manifests; the current entries are `sce-bash-policy` and `sce-agent-trace`. - `checkout identity`: Stable UUIDv7 identifier assigned to a cloned repository or linked Git worktree, stored in `/sce/checkout-id` (never committed) and resolved via `git rev-parse --git-dir`. The identity is created or reused by `sce setup` through `AgentTraceDbLifecycle::setup()` and also auto-created by hook runtime when `sce setup` has not been run. Each checkout identity maps to a per-checkout Agent Trace DB file at `/sce/agent-trace-{checkout_id}.db`; setup initializes that DB with migrations, while hook runtime remains a lazy fallback when setup has not run or schema metadata is incomplete. See `context/cli/checkout-identity.md`. - `checkout registry` (removed): The central JSON registry at `/sce/checkout-registry.json` was removed in the `remove-checkout-registry` plan. `sce trace db list` now discovers checkouts by scanning `/sce/agent-trace-*.db` files on disk. `checkout_id`, `database_path`, and `last_seen` (from file mtime) are derived from the filesystem; `path` and `remote_url` are no longer rendered. See `context/cli/checkout-identity.md`. @@ -59,11 +60,11 @@ - `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by the setup command, while hook runtime keeps a lazy Agent Trace DB fallback for checkouts where setup has not run or schema metadata is incomplete, and DB health/repair flows through the doctor surface. - `CLI bounded resilience wrapper`: Shared policy in `cli/src/services/resilience.rs` (`RetryPolicy`, async `run_with_retry`, sync `run_with_retry_sync`) that applies deterministic retries/timeouts/capped backoff to transient operations, emits retry observability events, and returns actionable terminal failure guidance. The sync helper is currently wired into shared database constructors for local open/connect retry and into `TursoDb`/`EncryptedTursoDb` operation retry for `execute()`/`query()`/`query_map()`. - `setup service orchestration`: Setup execution logic in `cli/src/services/setup/command.rs` that resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, dispatches `setup` through the static lifecycle provider catalog (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive target selection for config asset installation, and emits deterministic success messaging per target. -- `setup target flags`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--both`) that force non-interactive mode for automation. +- `setup target flags`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--pi`, `--all`) that force non-interactive mode for automation; `--all` expands to opencode+claude+pi and replaced the removed `--both` flag. - `setup mode contract`: `cli/src/services/setup/mod.rs` model where `SetupMode::Interactive` is the default and `SetupMode::NonInteractive(SetupTarget)` is selected only when exactly one target flag is provided. -- `setup interactive target prompt`: `inquire::Select` flow in `cli/src/services/setup/mod.rs` (`InquireSetupTargetPrompter`) that presents OpenCode, Claude, and Both when `sce setup` runs without target flags. +- `setup interactive target prompt`: `inquire::Select` flow in `cli/src/services/setup/mod.rs` (`InquireSetupTargetPrompter`) that presents OpenCode, Claude, Pi, and All (OpenCode + Claude + Pi) when `sce setup` runs without target flags. - `setup dispatch outcome`: Execution model in `cli/src/services/setup/mod.rs` (`SetupDispatch`) where setup either proceeds with a selected/non-interactive target or exits as cancelled without file changes. -- `setup embedded asset manifest`: Compile-time generated file index emitted by `cli/build.rs` into `OUT_DIR/setup_embedded_assets.rs`, embedding bytes from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` as deterministic normalized relative-path entries consumed by `cli/src/services/setup/mod.rs`. +- `setup embedded asset manifest`: Compile-time generated file index emitted by `cli/build.rs` into `OUT_DIR/setup_embedded_assets.rs`, embedding bytes from `config/.opencode/**`, `config/.claude/**`, `config/.pi/**`, and `cli/assets/hooks/**` (via the synced `cli/assets/generated/config/{opencode,claude,pi}` mirrors) as deterministic normalized relative-path entries consumed by `cli/src/services/setup/mod.rs`; `OPENCODE_EMBEDDED_ASSETS`, `CLAUDE_EMBEDDED_ASSETS`, and `PI_EMBEDDED_ASSETS` all back live setup targets. - `setup required-hook embedded assets`: Setup-service accessors in `cli/src/services/setup/mod.rs` (`iter_required_hook_assets`, `get_required_hook_asset`) that expose canonical embedded templates for `pre-commit`, `commit-msg`, and `post-commit` without runtime config reads. - `setup required-hook install orchestration`: Setup-service flow in `cli/src/services/setup/mod.rs` (`install_required_git_hooks`) that resolves repository root + effective hooks directory via git truth, installs canonical required hooks with deterministic per-hook outcomes (`Installed`, `Updated`, `Skipped`), enforces executable permissions, and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failure. - `setup hooks CLI mode`: `sce setup` behavior activated by `--hooks` (with optional `--repo `), supporting both hooks-only runs and composable target+hooks runs in one invocation; implemented through `cli/src/services/setup/command.rs` + `cli/src/services/setup/mod.rs`, enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits stable setup/hook status output. @@ -111,6 +112,8 @@ - `sce config schema artifact`: Canonical JSON Schema for global and repo-local `sce/config.json` files, authored in `config/pkl/base/sce-config-schema.pkl`, generated to `config/schema/sce-config.schema.json`, and embedded by `cli/src/services/config/schema.rs` for shared `sce config validate` and doctor config validation. The current schema accepts the canonical `$schema` declaration, flat logging keys (`log_level`, `log_format`, `log_file`, `log_file_mode`), existing auth/config keys, and enforces the schema-level dependency that `log_file_mode` requires `log_file`. - `bash tool policy config surface`: Nested repo config namespace under `.sce/config.json` at `policies.bash`, currently supporting unique built-in `presets` plus repo-owned `custom` argv-prefix rules with deterministic validation, merged global/local resolution, and first-class `sce config show|validate` reporting. - `attribution hooks gate`: Enabled-by-default local hook runtime gate resolved through shared config precedence in `cli/src/services/config/mod.rs` (with parsing in `schema.rs`): opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides repo/global config key `policies.attribution_hooks.enabled` with inverted semantics, and the current enabled path activates commit-msg-only attribution gated by the staged-diff AI-overlap preflight. +- `Agent Trace git-notes ref`: Configurable Agent Trace hook policy value at `policies.agent_trace.git_notes_ref`; defaults to `refs/notes/sce-agent-trace`, rejects blank/whitespace-only refs during Rust config mapping, and is exposed through hook runtime config for post-commit git-note persistence wiring. +- `Agent Trace notes auto-push gate`: Configurable Agent Trace hook policy value at `policies.agent_trace.push_notes.enabled`; defaults to `true`, accepts explicit `false` as an opt-out, and gates the post-commit best-effort `git push ` attempt after local Agent Trace git-note persistence succeeds. - `StagedDiffAiOverlapResult`: Three-valued enum in `cli/src/services/hooks/mod.rs` returned by the staged-diff AI-overlap evidence check: `Overlap` (staged diff overlaps with at least one recent AI/editor diff trace), `NoOverlap` (no overlap found; staged diff and recent traces were both available but share no touched lines, or staged patch has no touched lines), `Error` (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). Both `NoOverlap` and `Error` map to `ai_contribution_present = false` at the commit-msg policy seam; `Error` additionally triggers `sce.hooks.commit_msg.ai_overlap_error` logging. - `sce.hooks.commit_msg.ai_overlap_error`: Logger event ID emitted by `staged_diff_has_ai_overlap` when the staged-diff AI-overlap preflight encounters an error (DB open failure, schema not ready, query error, staged diff read failure, or clock failure). - `bash policy preset catalog`: Canonical authored preset source at `config/pkl/base/bash-policy-presets.pkl`, rendered to JSON by `config/pkl/generate.pkl` and embedded by the CLI from `config/.opencode/lib/bash-policy-presets.json` so CLI validation and OpenCode enforcement share the same preset IDs, argv-prefix matchers, fixed messages, and conflict metadata. @@ -140,8 +143,8 @@ - `agent trace historical reference docs`: Retained `context/sce/agent-trace-*.md` artifacts that describe the removed pre-v0.3 Agent Trace design and task slices; they are reference-only and do not describe the active local-hook runtime. - `agent trace commit-msg co-author policy`: Current contract in `cli/src/services/hooks/mod.rs` (`apply_commit_msg_coauthor_policy`) that applies exactly one canonical trailer (`Co-authored-by: SCE `) only when attribution hooks are enabled, SCE is not disabled, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); `NoOverlap` and `Error` both suppress the trailer, with `Error` logged via `sce.hooks.commit_msg.ai_overlap_error`; duplicate canonical trailers are deduped idempotently. - `local DB migration contract`: `cli/src/services/local_db/mod.rs` delegates migration execution to `TursoDb` through the `DbSpec::migrations()` contract. The current `LocalDbSpec` migration list is empty, so `LocalDb::new()` opens/creates the canonical local DB without creating local tables. -- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path, `post-commit` persists intersections and built Agent Trace payloads without post-commit file artifacts, `diff-trace` validates STDIN payloads, uses only direct payload `model_id` and `tool_version` (session-model fallback removed), applies stored `session_id` prefixes (`oc_`/`cc_`), and inserts DB-only AgentTraceDb rows, and `conversation-trace` is the active message/part intake path. `session-model` is no longer a supported hook route. -- `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, checkout DB discovery lives under `sce trace`, and output exposes deterministic doctor mode, readiness, stable problem taxonomy/fixability fields, checkout/database records, and fix-result records. The runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode plus Claude integration content health. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity + Agent Trace checkout DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows where missing files surface as `[MISS]`, mismatches/read failures as `[FAIL]`, and affected parent groups as `[FAIL]`. Current integration groups are `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, `OpenCode skills`, `ClaudeCode plugins`, `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`; Claude `settings.json` plus `hooks/**` belong to `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`. Fix mode reuses canonical setup hook installation for missing/stale/non-executable required hooks and missing hooks directories and can bootstrap canonical missing SCE-owned DB parent directories. +- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path, `post-commit` persists intersections and built Agent Trace payloads without post-commit file artifacts, `diff-trace` validates STDIN payloads, uses only direct payload `model_id` and `tool_version` (session-model fallback removed), applies stored `session_id` prefixes (`oc_`/`cc_`/`pi_`), and inserts DB-only AgentTraceDb rows, and `conversation-trace` is the active message/part intake path. `session-model` is no longer a supported hook route. +- `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, checkout DB discovery lives under `sce trace`, and output exposes deterministic doctor mode, readiness, stable problem taxonomy/fixability fields, checkout/database records, and fix-result records. The runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode, Claude, plus Pi integration content health. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity + Agent Trace checkout DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows where missing files surface as `[MISS]`, mismatches/read failures as `[FAIL]`, and affected parent groups as `[FAIL]`. Current integration groups include `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, `OpenCode skills`, `ClaudeCode plugins`, `ClaudeCode agents`, `ClaudeCode commands`, `ClaudeCode skills`, `Pi prompts`, and `Pi skills`; Claude `settings.json` plus `hooks/**` belong to `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, while Pi `prompts/**` and `skills/**` map to the Pi groups. Fix mode reuses canonical setup hook installation for missing/stale/non-executable required hooks and missing hooks directories and can bootstrap canonical missing SCE-owned DB parent directories. - `cli warnings-denied lint policy`: `cli/Cargo.toml` sets `warnings = "deny"`, so plain `cargo clippy --manifest-path cli/Cargo.toml` already fails on warnings without needing an extra `-- -D warnings` tail. - `agent trace local DB schema migration contract`: Retired `apply_core_schema_migrations` behavior removed from the current runtime during `agent-trace-removal-and-hook-noop-reset` T01; the local DB baseline is now file open/create only. - `agent trace removed local-hook paths`: Current-state shorthand for the removed local-hook runtime behaviors that are no longer active: staged-checkpoint persistence, post-commit dual-write, post-rewrite remap ingestion, rewrite trace transformation, and retry replay. diff --git a/context/overview.md b/context/overview.md index d8546643..30205066 100644 --- a/context/overview.md +++ b/context/overview.md @@ -1,6 +1,6 @@ # Overview -This repository maintains shared assistant configuration for OpenCode and Claude from a single canonical Pkl authoring source. It validates that generated outputs stay deterministic and in sync via `nix run .#pkl-check-generated` and `nix flake check`. It supports both manual and automated profile variants; the automated profile applies deterministic non-interactive behavior for CI/automation workflows. +This repository maintains shared assistant configuration for OpenCode, Claude, and Pi from a single canonical Pkl authoring source. It validates that generated outputs stay deterministic and in sync via `nix run .#pkl-check-generated` and `nix flake check`. It supports both manual and automated profile variants; the automated profile applies deterministic non-interactive behavior for CI/automation workflows. It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, agent-trace hooks, bash-policy evaluation, and trace database inspection. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. @@ -23,19 +23,19 @@ The app command dispatcher now enforces a centralized stdout/stderr stream contr The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for checkout identity setup, per-checkout Agent Trace DB path health/parent readiness when an ID exists, and legacy global fallback outside checkout context. Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. Agent Trace lifecycle setup now also initializes the per-checkout Agent Trace DB via `AgentTraceDb::open_at(path)`; hook runtime lazy initialization remains a fallback when setup has not prepared the DB or schema metadata is incomplete. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors`, automatic TTY detection, and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports color-detection, conditional styling, help/diagnostic/label/prompt styling, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. -The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/both with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--both`), deterministic mutually-exclusive validation, and non-destructive cancellation exits. -The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` via `cli/build.rs`; `cli/src/services/setup/mod.rs` exposes deterministic normalized relative paths plus file bytes and target-scoped iteration without runtime reads from `config/`. The same build script also discovers `cli/migrations//*.sql` at compile time and writes `cli/src/generated_migrations.rs` constants sorted by numeric filename prefix for database migration consumers. +The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/Pi/All with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--all`), deterministic mutually-exclusive validation, and non-destructive cancellation exits; the former `--both` flag was removed in favor of `--all` (opencode+claude+pi). +The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, `config/.pi/**`, and `cli/assets/hooks/**` via `cli/build.rs`; `cli/src/services/setup/mod.rs` exposes deterministic normalized relative paths plus file bytes and target-scoped iteration without runtime reads from `config/`. The same build script also discovers `cli/migrations//*.sql` at compile time and writes `cli/src/generated_migrations.rs` constants sorted by numeric filename prefix for database migration consumers. The setup service also provides repository-root install orchestration: it resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup uses a unified remove-and-replace policy for all write flows — it removes existing targets before swapping staged content and returns deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs` now separates install-flow logic from interactive prompt logic through focused support seams. The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction, `sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. -The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. +The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`, `policies.agent_trace.push_notes.enabled` with default `true`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. Invalid default-discovered config files now also degrade gracefully at startup: `sce` keeps running with degraded observability defaults, logs `sce.config.invalid_config` warnings, and reserves hard failures for explicit `--config` / `SCE_CONFIG_FILE` targets or other truly invalid runtime observability inputs. `cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*` and `pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show` / `sce config validate` text+JSON output construction to `cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config` unchanged. The CLI now has a generic borrowed `AppContext` dependency view in `cli/src/app.rs`; `AppRuntime` owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional `repo_root: Option`. `AppContext::with_repo_root(...)` / `ContextWithRepoRoot` derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in `cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root/hooks-directory resolution. The shared default path service in `cli/src/services/default_paths.rs` is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal `roots` seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, embedded-asset, install, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. The Rust CLI also centralizes SCE-owned web URI construction in `cli/src/services/agent_trace.rs`, with `SCE_WEB_BASE_URL` as the single Rust owner for `https://sce.crocoder.dev` and helpers consumed by Agent Trace conversation URLs, Agent Trace persisted trace URLs, Agent Trace session URLs, and setup-created repo-local config schema URLs. The same config resolver now also owns the attribution-hooks gate used by local hook runtime: opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides `policies.attribution_hooks.enabled` with inverted semantics, and the gate defaults to enabled unless explicitly disabled. The config service split now includes `cli/src/services/config/resolver.rs` as the focused owner for config-file discovery, file-layer merging, env/flag/default precedence, auth-key resolution, observability resolution, attribution-hooks resolution, and default-discovered invalid-file degradation; `cli/src/services/config/mod.rs` remains the facade/rendering orchestration surface while preserving existing `services::config` imports. -Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and persists tool-prefixed `session_id` values (`oc_` for OpenCode, `cc_` for Claude), `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... -Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. +Generated config now includes repo-local plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and persists tool-prefixed `session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` (and `config/automated/.opencode/plugins/sce-bash-policy.ts`) is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... +Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call` handler blocks denied bash commands via `sce policy bash` and fails open when the policy check cannot run (see `context/sce/pi-extension-runtime.md`). Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID and initializes the per-checkout `agent-trace-{checkout_id}.db` with embedded migrations; hook runtime still lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. Doctor validates checkout/global DB paths/health and can bootstrap missing parent directories; checkout DB discovery now lives in the `trace` group (`sce trace db list`). Wiring a user-invocable `sce sync` command is deferred to `0.4.0`. The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.95.0` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. @@ -59,24 +59,24 @@ Context sync now uses an important-change gate: cross-cutting/policy/architectur The `/change-to-plan` command body is also intentionally thin orchestration: it delegates clarification and plan-shape contracts to `sce-plan-authoring` (including one-task/one-atomic-commit task slicing) while keeping wrapper-level plan output and handoff obligations explicit. The generated OpenCode command doc now also emits `entry-skill: sce-plan-authoring` plus an ordered `skills` list. The targeted support commands (`handover`, `commit`, `validate`) keep their thin-wrapper behavior and now also emit machine-readable OpenCode command frontmatter describing their entry skill and ordered skill chain. `/commit` is now split by profile: manual generated commands remain proposal-only and allow split guidance when staged changes mix unrelated goals, while the automated OpenCode `/commit` command generates exactly one commit message and runs `git commit` against the staged diff. The shared `sce-atomic-commit` contract also requires commit bodies to cite affected plan slug(s) and updated task ID(s) when staged changes include `context/plans/*.md`, and to stop for clarification instead of inventing those references when the staged plan diff is ambiguous. The prior no-git-wrapper Agent Trace design artifacts under `context/sce/agent-trace-*.md` are retained only as historical reference; the current CLI runtime no longer wires the removed Agent Trace schema adaptation, payload building, retry replay, or rewrite handling paths into local hook execution. -The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and continues with `None` for missing attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. +The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces`, then writes the same full JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`), and after a successful local note write attempts a silent fail-open `git push ` unless `policies.agent_trace.push_notes.enabled` is `false`, without creating a post-commit Agent Trace file artifact; `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, uses only direct payload `model_id` and `tool_version` (no longer resolves from `session_models`), and continues with `None` for missing attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake extracts direct model metadata from top-level or nested `model` fields and normalizes it with the `claude/` prefix when present. The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses `AgentTraceDb = TursoDb` with legacy global plus active per-checkout DB paths, fresh-start migrations for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and retired `015_create_session_models` metadata handling; active hook runtime writes direct nullable diff-trace attribution without a `session_models` API/table dependency. -The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, and remains the active bounded recent-diff-trace intersection path, and `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct `model_id` and `tool_version` values (no session-model fallback), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. +The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, and remains the active bounded recent-diff-trace intersection path, and `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct `model_id` and `tool_version` values (no session-model fallback), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`), and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. -The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--both --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. +The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. ## Repository model - Author once in canonical Pkl content organized by concern: `config/pkl/base/shared-content-{common,plan,code,commit}.pkl` for manual profile and `config/pkl/base/shared-content-automated-{common,plan,code,commit}.pkl` for automated profile; aggregation surfaces `config/pkl/base/shared-content.pkl` and `config/pkl/base/shared-content-automated.pkl` import from these grouped modules for downstream renderers. - Apply target-specific metadata/rendering in `config/pkl/renderers/`. -- Generate derived artifacts into `config/.opencode/**` (manual profile), `config/automated/.opencode/**` (automated profile), and `config/.claude/**` via `config/pkl/generate.pkl`. +- Generate derived artifacts into `config/.opencode/**` (manual profile), `config/automated/.opencode/**` (automated profile), `config/.claude/**`, and `config/.pi/**` (Pi prompts/skills plus the Pi extension at `config/.pi/extensions/sce/index.ts`, manual profile only) via `config/pkl/generate.pkl`. - Treat generated outputs as build artifacts, not primary editing surfaces. ## Ownership boundaries -- Generation-owned paths are authored config artifacts under `config/.opencode/**`, `config/automated/.opencode/**`, and `config/.claude/**` (agents, commands, skills, shared runtime libraries, OpenCode plugin files, generated OpenCode package manifests, generated OpenCode `opencode.json` manifests including SCE plugin registration, and Claude hook/settings assets). +- Generation-owned paths are authored config artifacts under `config/.opencode/**`, `config/automated/.opencode/**`, `config/.claude/**`, and `config/.pi/**` (agents, commands, skills, shared runtime libraries, OpenCode plugin files, generated OpenCode package manifests, generated OpenCode `opencode.json` manifests including SCE plugin registration, Claude hook/settings assets, Pi prompt/skill assets, and the Pi extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts` to `config/.pi/extensions/sce/index.ts`). - Runtime/install artifacts are not generation-owned (for example `node_modules`, lockfiles, install outputs). - Code and behavior changes must be made in canonical sources and renderer metadata, then regenerated. @@ -101,7 +101,7 @@ Lightweight post-task verification baseline (required after each completed task) ## Cross-target parity -- OpenCode and Claude are generated from the same canonical content with per-target capability mapping. +- OpenCode, Claude, and Pi are generated from the same canonical content with per-target capability mapping (Pi renders SCE agents as agent-role prompt templates because it has no native sub-agent format). - When capabilities differ, parity is implemented by supported target-specific behavior rather than forcing unsupported fields. ## Context navigation diff --git a/context/patterns.md b/context/patterns.md index 987ae776..7dbb7783 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -79,7 +79,7 @@ - Keep generated-output parity anchored to `nix run .#pkl-check-generated` and the root `nix flake check` `pkl-parity` derivation; no dedicated generated-parity workflow is currently checked in. - Treat `nix run .#pkl-check-generated` and `nix flake check` as the lightweight post-task verification baseline and run both after each completed task. - For non-destructive verification during development, run `nix develop -c pkl eval -m context/tmp/t04-generated config/pkl/generate.pkl` and inspect emitted paths under `context/tmp/`. -- Keep `output.files` limited to generated-owned paths only (`config/{opencode_root}/{agent,command,skills,lib,plugins}`, generated `config/{opencode_root}/package.json`, and `config/{claude_root}/{agents,commands,skills,hooks,settings.json}`, where roots map to `.opencode` and `.claude`). +- Keep `output.files` limited to generated-owned paths only (`config/{opencode_root}/{agent,command,skills,lib,plugins}`, generated `config/{opencode_root}/package.json`, `config/{claude_root}/{agents,commands,skills,hooks,settings.json}`, and `config/{pi_root}/{prompts,skills,extensions}`, where roots map to `.opencode`, `.claude`, and `.pi`). - For OpenCode pre-execution bash-policy hooks, keep the generated plugin entrypoint thin (`plugins/sce-bash-policy.ts`) and delegate policy evaluation to the Rust `sce policy bash --input normalized --output json` command so OpenCode and Claude share one evaluator. ## Internal subagent parity mapping @@ -113,7 +113,7 @@ - For interactive setup flows, isolate prompt handling behind a service-layer prompter seam so selection mapping and cancellation behavior can be tested without a live TTY. - When setup or path-catalog modules grow dense, extract focused internal support seams (for example install-flow, prompt-flow, or root-resolution helpers) before adding new behavior so orchestration files stay navigable without changing command contracts. - Treat setup prompt cancellation/interrupt as a non-destructive exit path with explicit user messaging (no file mutations and no partial side effects). -- For setup install prep, generate compile-time embedded asset manifests from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` in `cli/build.rs`, keep relative paths normalized to forward-slash form, and expose target-scoped iterators/lookups from the setup service layer for installer wiring. +- For setup install prep, generate compile-time embedded asset manifests from `config/.opencode/**`, `config/.claude/**`, `config/.pi/**`, and `cli/assets/hooks/**` in `cli/build.rs`, keep relative paths normalized to forward-slash form, and expose target-scoped iterators/lookups from the setup service layer for installer wiring. - For CLI database migration prep, keep SQL files under immediate `cli/migrations//` directories named `NNN_description.sql`; `cli/build.rs` discovers those files at compile time, sorts by the numeric prefix before `_`, and writes deterministic `cli/src/generated_migrations.rs` constants with `include_str!` references for service `DbSpec` consumers. - For setup install execution, write selected embedded assets into a per-target staging directory first, then remove the existing target and swap staged content into place; on swap failure, clean temporary staging paths and return deterministic recovery guidance (recover from version control). No backup artifacts are created. - For required-hook setup execution, resolve repository root and effective hooks directory from git (`rev-parse --show-toplevel`, `rev-parse --git-path hooks`), then apply deterministic per-hook outcomes (`Installed`, `Updated`, `Skipped`) with staged writes, executable-bit enforcement, and remove-and-replace behavior that removes existing hooks before swapping staged content. @@ -133,7 +133,7 @@ - For cross-service CLI dependencies exposed through the borrowed `AppContext` view, prefer shared capability/accessor traits over one-off per-service abstractions; keep production wrappers thin over `std::fs` and `git` process execution until call-site migration tasks approve deeper service refactors, and keep command execution generic over the narrow accessors each command needs where practical. - For future CLI domains, define trait-first service contracts with request/plan models in `cli/src/services/*` and keep placeholder implementations explicitly non-runnable until production behavior is approved. - Model deferred integration boundaries with concrete event/capability data structures (for example hook-runtime attribution snapshots/policies and cloud-sync checkpoints) so later tasks can implement behavior without reshaping public seams. -- For the current local-hook baseline, keep `pre-commit` and `post-rewrite` as deterministic no-op entrypoints; keep `post-commit` as the active bounded recent-diff-trace intersection entrypoint with validated `--remote-url` plumbed through Agent Trace flow and any direct diagnostics printed to stderr; keep `diff-trace` as an explicit STDIN intake path with deterministic required-field validation for `sessionID`, `diff`, `time`, `tool_name`, optional `model_id` (absent/`null` → `None`), and `tool_version` (present and either `null` or non-empty string), same-tool-idempotent stored `diff_traces.session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude), non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion whose failure is logged and reflected in deterministic success text without creating a `context/tmp` artifact fallback; keep `conversation-trace` as the active message/part intake path. `session-model` is no longer a supported hook intake path. +- For the current local-hook baseline, keep `pre-commit` and `post-rewrite` as deterministic no-op entrypoints; keep `post-commit` as the active bounded recent-diff-trace intersection entrypoint with validated `--remote-url` plumbed through Agent Trace flow, best-effort git-note persistence, default-enabled silent fail-open notes push, and any direct diagnostics printed to stderr; keep `diff-trace` as an explicit STDIN intake path with deterministic required-field validation for `sessionID`, `diff`, `time`, `tool_name`, optional `model_id` (absent/`null` → `None`), and `tool_version` (present and either `null` or non-empty string), same-tool-idempotent stored `diff_traces.session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and AgentTraceDb insertion whose failure is logged and reflected in deterministic success text without creating a `context/tmp` artifact fallback; keep `conversation-trace` as the active message/part intake path. `session-model` is no longer a supported hook intake path. - For `diff-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, parse/validation, and setup/persistence failures are logged with `sce.hooks.diff_trace.error` and converted into command success; preserve the existing valid-payload success text and the AgentTraceDb write-warning success path. - For `conversation-trace` hook intake, keep producer-facing failure behavior fail-open: STDIN read, top-level parse/validation, unsupported raw Claude hook events, and AgentTraceDb setup/persistence failures are logged with `sce.hooks.conversation_trace.error` and converted into command success; preserve valid-payload mixed-batch accounting, skipped-item logging, and batch-insert warning behavior. - For diff-trace attribution persistence, persist direct payload `model_id` and `tool_version` values as-is; missing attribution fields are stored as `NULL` in `diff_traces`. The former `session_models` fallback lookup was removed. diff --git a/context/plans/agent-trace-git-notes-auto-push.md b/context/plans/agent-trace-git-notes-auto-push.md new file mode 100644 index 00000000..0428da18 --- /dev/null +++ b/context/plans/agent-trace-git-notes-auto-push.md @@ -0,0 +1,123 @@ +# Plan: Auto-push Agent Trace git notes after SCE post-commit + +## Change summary + +Extend the existing `sce hooks post-commit` Agent Trace git-notes flow so SCE also attempts to push the configured Agent Trace notes ref to the commit remote after successfully writing the local git note. The default behavior is automatic push to the existing post-commit remote (`origin` as passed by the setup-installed hook template), with an optional config switch to disable the push. + +The push is best-effort and silent on failure: if the push cannot complete, the hook must not fail and SCE should simply try again on a later post-commit hook invocation. + +## Success criteria + +- After a successful git `sce hooks post-commit --vcs git --remote-url ` Agent Trace run, SCE writes the local git note as today and then attempts to push the configured notes ref to the target remote. +- The default notes ref remains `refs/notes/sce-agent-trace` unless overridden by `policies.agent_trace.git_notes_ref`. +- Notes auto-push is enabled by default and can be disabled through SCE config. +- Push failures are fail-open and silent from the user's perspective: they do not fail the hook, do not block commits, and do not require immediate action. +- A later successful post-commit hook invocation can attempt the push again without requiring a retry queue or persisted failed-push state. +- Tests cover default enabled behavior, config-disabled behavior, configured notes ref behavior, command construction, and silent fail-open push errors. +- Current-state context documents the new default auto-push behavior, disable switch, and fail-open/no-retry-queue posture. + +## Constraints and non-goals + +- Constraints: + - Preserve the existing post-commit local git-note write behavior and its best-effort posture. + - Push only after Agent Trace JSON validation, Agent Trace DB persistence, and local git-note write have completed successfully enough to have a local note to publish. + - Reuse existing hook config resolution and git command execution patterns; avoid shell interpolation. + - Keep stdout/stderr output stable unless existing logging conventions require a debug-level internal event. + - Honor the configured Agent Trace notes ref consistently for local note write and remote push. + - Use the post-commit remote context already available to the hook flow rather than inventing a separate remote-discovery path in this plan. +- Non-goals: + - Fetching notes from remotes. + - Backfilling or pushing historical notes outside normal post-commit hook flow. + - Adding a retry queue, background daemon, scheduled sync, or persisted failed-push state. + - Introducing a user-facing `sce sync` command. + - Changing Agent Trace JSON schema or Agent Trace DB schema. + - Blocking commits or surfacing push failures as hook failures. + +## Assumptions + +- The disable switch should live under the existing config namespace, e.g. `policies.agent_trace.push_notes.enabled`, with default `true`; exact naming may follow existing config style during implementation. +- The remote push target should be the remote used by the setup-installed hook template (`origin` in current behavior) or an equivalent validated remote derived from the existing post-commit handoff; this plan should not add arbitrary remote selection UX. +- Silent fail means no user-facing failure and no new stdout/stderr warning. Internal debug/error logging may still be acceptable if it follows existing observability conventions and does not disturb normal hook UX. + +## Task stack + +- [x] T01: `Add config switch for Agent Trace notes auto-push` (status:done) + - Task ID: T01 + - Completed: 2026-07-15 + - Files changed: `config/pkl/base/sce-config-schema.pkl`, `config/schema/sce-config.schema.json`, `cli/assets/generated/config/schema/sce-config.schema.json`, `cli/src/services/config/{types,schema,resolver,render}.rs`, `context/architecture.md`, `context/overview.md`, `context/glossary.md`, `context/context-map.md`, `context/cli/config-precedence-contract.md` + - Evidence: `nix develop -c pkl eval -m . config/pkl/generate.pkl`; `nix run .#pkl-check-generated`; `nix flake check --print-build-logs` passed (144 Rust tests, clippy/fmt/parity checks clean). + - Goal: Add a typed config value that controls whether post-commit Agent Trace git notes are auto-pushed, defaulting to enabled. + - Boundaries (in/out of scope): In - Pkl schema/config schema updates, Rust config DTO/resolver mapping, default `true`, explicit `false` override, `sce config show|validate` visibility if required by existing policy rendering. Out - git push execution, hook runtime wiring, remote selection behavior. + - Done when: runtime config exposes a resolved auto-push boolean; default resolution is enabled; explicit config disable is honored; generated schema/parity covers the new field; invalid config shapes fail validation consistently with existing config policy fields. + - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; targeted config tests if appropriate; `nix run .#pkl-check-generated`; `nix flake check`. + +- [x] T02: `Introduce git-notes push helper` (status:done) + - Task ID: T02 + - Completed: 2026-07-15 + - Files changed: `cli/src/services/hooks/mod.rs` + - Evidence: Direct targeted `cargo test` was blocked by repository bash policy; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check --print-build-logs` passed (148 Rust tests, clippy/fmt/parity checks clean). + - Goal: Add a small, injectable helper that attempts to push the configured Agent Trace notes ref to the chosen git remote without shell interpolation. + - Boundaries (in/out of scope): In - helper function/type near existing git-note writer logic, command construction for pushing one notes ref, validation of non-blank remote/ref inputs, tests for command args and failure outcome. Out - deciding when to call the helper, config resolution, backfill/fetch/retry behavior. + - Done when: helper constructs a deterministic `git push `-equivalent invocation for the configured notes ref, returns a structured success/failure outcome, does not emit user-facing output directly, and focused tests cover success and git-command failure. + - Verification notes (commands or checks): targeted hook/helper tests if appropriate; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check`. + +- [x] T03: `Wire silent auto-push into post-commit Agent Trace flow` (status:done) + - Task ID: T03 + - Completed: 2026-07-15 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/overview.md`, `context/glossary.md`, `context/context-map.md`, `context/cli/config-precedence-contract.md`, `context/sce/agent-trace-hooks-command-routing.md`, `context/sce/setup-githooks-hook-asset-packaging.md` + - Evidence: Direct targeted `cargo test post_commit_agent_trace_flow` was blocked by repository bash policy; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check --print-build-logs` passed (150 Rust tests, clippy/fmt/parity checks clean); `git diff --check`; `nix run .#pkl-check-generated` passed. + - Goal: After successful local Agent Trace git-note persistence, conditionally attempt a best-effort notes push when auto-push config is enabled. + - Boundaries (in/out of scope): In - post-commit flow ordering, config gate, git-only behavior, existing remote context reuse, fail-open/silent handling, tests proving enabled/default attempt, disabled skip, configured ref use, and push failure does not change hook success. Out - retry queue, user-facing command output changes, fetch/backfill, non-git VCS note pushing. + - Done when: default git post-commit flow attempts the push after local note write; explicit config disable skips the push; configured notes ref is used; push failure is swallowed from hook success/output and can be retried by a later hook invocation. + - Verification notes (commands or checks): targeted post-commit hook tests if appropriate; manual local dry-run/review of command construction; `nix flake check`. + +- [x] T04: `Document Agent Trace notes auto-push behavior` (status:done) + - Task ID: T04 + - Completed: 2026-07-15 + - Files changed: `context/sce/agent-trace-hooks-command-routing.md`, `context/architecture.md`, `context/patterns.md`, `context/plans/agent-trace-git-notes-auto-push.md` + - Evidence: `rg "git-notes|git notes|push_notes|push notes|No git-notes" context/` reviewed current/historical matches; `git diff --check` passed; context sync verified root overview/architecture/glossary/patterns/context-map alignment. + - Goal: Sync current-state context to describe default auto-push, disable config, and silent fail-open retry-on-next-commit behavior. + - Boundaries (in/out of scope): In - focused updates to `context/sce/agent-trace-hooks-command-routing.md`, `context/cli/config-precedence-contract.md`, `context/sce/setup-githooks-hook-asset-packaging.md` if hook behavior text needs adjustment, `context/context-map.md`, and glossary entry if a new term is introduced. Out - broad narrative docs rewrites, completed-work summaries in durable context, implementation code. + - Done when: context no longer states “No git-notes push/fetch/backfill behavior” as current behavior without qualification; documents that push is default-enabled, config-disableable, silent fail-open, and retried only by future post-commit invocations. + - Verification notes (commands or checks): `rg "git-notes|git notes|push_notes|push notes|No git-notes" context/`; manual diff review; `git diff --check`. + +- [x] T05: `Validate notes auto-push and cleanup` (status:done) + - Task ID: T05 + - Completed: 2026-07-15 + - Files changed: `context/plans/agent-trace-git-notes-auto-push.md` + - Evidence: `git diff --check` passed; `nix run .#pkl-check-generated` passed (generated outputs up to date); `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/` reviewed current and historical matches; `nix flake check --print-build-logs` passed (150 Rust tests plus clippy/fmt/parity and other flake checks clean). + - Goal: Run final validation for the complete plan and clean up temporary scaffolding. + - Boundaries (in/out of scope): In - full repo validation, generated-output parity, formatting/lint/test checks, stale-string review, cleanup of temporary repos/remotes/notes refs used during testing, plan status/evidence updates. Out - new behavior beyond completed task stack. + - Done when: `nix flake check` passes or any failure is documented as pre-existing/unrelated; `nix run .#pkl-check-generated` passes; context sync is verified; no temporary scaffolding remains. + - Verification notes (commands or checks): `nix flake check`; `nix run .#pkl-check-generated`; `git diff --check`; `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/`. + +## Validation Report + +### Commands run + +- `git diff --check` -> exit 0 (no whitespace errors). +- `nix run .#pkl-check-generated` -> exit 0 (`Generated outputs are up to date.`). +- `rg "refs/notes/sce-agent-trace|push_notes|git notes.*push|No git-notes" cli/ config/ context/` -> exit 0; reviewed current implementation/config/context references plus historical plan/reference matches. +- `nix flake check --print-build-logs` -> exit 0 (`all checks passed`; 150 Rust tests passed; clippy/fmt/parity and other flake checks clean). +- `find context/tmp -mindepth 1 -maxdepth 1 -type f -newermt '2026-07-15 00:00:00' -print` -> exit 0 with no output; no task-created temporary scaffolding remained. + +### Success-criteria verification + +- [x] Successful post-commit Agent Trace flow writes the local git note then attempts a push by default -> covered by hook tests in `nix flake check` (`post_commit_agent_trace_flow_writes_git_note_after_db_insert`, push helper tests, and default enabled config resolver test). +- [x] Default notes ref remains `refs/notes/sce-agent-trace` unless overridden -> covered by config resolver and hook tests plus stale-string review. +- [x] Notes auto-push is default-enabled and config-disableable -> covered by `agent_trace_push_notes_enabled_uses_default`, `agent_trace_push_notes_enabled_uses_explicit_config_false`, and `post_commit_agent_trace_flow_skips_push_when_config_disabled` in flake check. +- [x] Push failures are silent/fail-open and retried only by later hook invocations -> covered by `post_commit_agent_trace_flow_swallows_git_notes_push_failure` and current-state context in `context/sce/agent-trace-hooks-command-routing.md`. +- [x] Tests cover command construction and configured ref behavior -> covered by `git_notes_push_helper_builds_push_command`, `git_notes_push_helper_honors_configured_ref`, and `post_commit_agent_trace_flow_honors_configured_git_notes_ref`. +- [x] Current-state context documents the default auto-push behavior, disable switch, and fail-open/no-retry-queue posture -> verified in `context/overview.md`, `context/architecture.md`, `context/patterns.md`, `context/context-map.md`, `context/glossary.md`, `context/cli/config-precedence-contract.md`, and `context/sce/agent-trace-hooks-command-routing.md`. + +### Failed checks and follow-ups + +None. + +### Residual risks + +None identified. + +## Open questions + +None. Plan task stack is complete. diff --git a/context/plans/agent-trace-git-notes.md b/context/plans/agent-trace-git-notes.md new file mode 100644 index 00000000..7d90de0a --- /dev/null +++ b/context/plans/agent-trace-git-notes.md @@ -0,0 +1,148 @@ +# Plan: Persist Agent Trace JSON to git notes on post-commit + +## Change summary + +Extend the existing `sce hooks post-commit` Agent Trace flow so every successfully built and schema-validated Agent Trace payload is also written to a git note on the just-created commit. The note content is the full Agent Trace JSON already persisted in `agent_traces.trace_json`. Git-note persistence is best-effort: failures are logged for diagnostics but must not block the git commit or make the post-commit hook command fail when Agent Trace DB persistence succeeded. + +The default notes ref is dedicated to SCE Agent Trace data and is configurable. + +## Decisions + +- Default git-notes ref: `refs/notes/sce-agent-trace`. +- Config surface: add a repo config field for the notes ref (for example `policies.agent_trace.git_notes_ref`, final naming to follow existing config style during implementation) with default `refs/notes/sce-agent-trace`. +- Note content: full Agent Trace JSON string after schema validation, matching the payload persisted to `agent_traces.trace_json`. +- Write posture: best-effort/non-blocking. Git-note write failures are logged and surfaced only as diagnostics, not as post-commit hook failures. +- Write mode: use replace/upsert semantics for the commit note so rerunning the hook for the same commit updates the SCE Agent Trace note instead of failing on an existing note. + +## Success criteria + +- On a successful `sce hooks post-commit --vcs git --remote-url ` run that builds and validates an Agent Trace payload, the current commit has a git note under `refs/notes/sce-agent-trace` by default. +- The note content is the full Agent Trace JSON and can be read back with `git notes --ref refs/notes/sce-agent-trace show `. +- The git-notes ref is configurable through SCE config and generated schema/docs reflect the default. +- If writing the git note fails, the post-commit hook remains successful when existing Agent Trace DB persistence succeeded; the failure is logged with a stable event name. +- Existing Agent Trace DB insertion remains unchanged and continues to be the source of persisted trace rows. +- Tests cover successful note write orchestration, configured ref use, existing-note replacement/upsert behavior, and non-blocking failure handling. +- Context documents describe the new post-commit git-notes behavior and the no-blocking-error posture. + +## Constraints and non-goals + +- Constraints: + - Keep `post-commit` as the integration point because the commit SHA and Agent Trace JSON are available there. + - Do not write a git note unless Agent Trace JSON validation has passed. + - Preserve stdout/stderr contracts as much as possible; diagnostics belong in logging/stderr, not new stdout payloads. + - Reuse existing hook config resolution and git command helper patterns instead of introducing a new dependency. + - Keep note writes scoped to git; no behavior is required for non-git VCS values. + - Keep failures non-blocking only for the git-note write step. Existing validation/DB insertion failures keep their current behavior. +- Non-goals: + - Backfilling git notes for historical commits. + - Pushing/fetching notes to/from remotes. + - Changing Agent Trace JSON schema or DB schema. + - Replacing Agent Trace DB persistence with git notes. + - Adding a retry queue for failed note writes. + +## Assumptions + +- The dedicated default ref should be exactly `refs/notes/sce-agent-trace`. +- The implementation may write notes by invoking the local `git` binary through existing command helpers. +- Configurability means changing the notes ref, not disabling the feature. Disabling can be added later if a separate product decision requests it. + +## Task stack + +- [x] T01: `Add config surface for Agent Trace git-notes ref` (status:done) + - Task ID: T01 + - Goal: Add a typed SCE config value for the Agent Trace git-notes ref with default `refs/notes/sce-agent-trace`. + - Boundaries (in/out of scope): In - config type/resolver updates, env/config precedence only if this config area already has an established pattern, unit tests for default and explicit configured ref. Out - git-note writing, hook runtime wiring, generated schema/Pkl output. + - Done when: runtime config exposes the resolved notes ref; default resolution returns `refs/notes/sce-agent-trace`; explicit config overrides the default; invalid empty/blank refs are rejected or normalized consistently with existing config validation patterns. + - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo fmt'`; targeted config tests if permitted by policy; otherwise `nix flake check`. + - Status: done + - Completed: 2026-07-14 + - Files changed: `cli/src/services/config/types.rs`, `cli/src/services/config/schema.rs`, `cli/src/services/config/resolver.rs`, `config/pkl/base/sce-config-schema.pkl`, `config/schema/sce-config.schema.json` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test` was blocked by SCE bash policy preferring `nix flake check`; `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). + - Notes: User approved Option A scope expansion to include the minimal Pkl/generated schema update required for explicit config-file override validation. Git-note writing and post-commit hook wiring remain out of scope for T01. + +- [x] T02: `Sync Pkl schema and generated config docs for git-notes ref` (status:done) + - Task ID: T02 + - Goal: Update canonical Pkl config schema and regenerate generated JSON/config artifacts so the Agent Trace git-notes ref is documented and parity checks pass. + - Boundaries (in/out of scope): In - Pkl source, generated JSON schema/config outputs, default/description text for the new ref. Out - Rust resolver logic from T01, hook runtime behavior from later tasks. + - Done when: generated outputs include the new config field/default; `nix run .#pkl-check-generated` passes; no unrelated generated drift is present. + - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; `nix run .#pkl-check-generated`. + - Status: done + - Completed: 2026-07-14 + - Files changed: `context/plans/agent-trace-git-notes.md` + - Evidence: `nix develop -c pkl eval -m . config/pkl/generate.pkl` completed and rewrote no files; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."); `git status --short` after regeneration showed no generated drift. + - Notes: Canonical Pkl and generated schema already included `policies.agent_trace.git_notes_ref` with default `refs/notes/sce-agent-trace`; T02 was a verification/regeneration task only. + +- [x] T03: `Introduce git-notes writer helper for Agent Trace JSON` (status:done) + - Task ID: T03 + - Goal: Add a small, testable helper that writes full Agent Trace JSON to a git note for a commit/ref using replace/upsert semantics. + - Boundaries (in/out of scope): In - helper surface in the hooks or git utility layer, command construction for `git notes --ref add -f -F `, validation that commit/ref/content inputs are non-empty, unit tests with injected command runner covering success, configured ref, existing-note replacement flag, and command failure. Out - calling the helper from post-commit runtime, changing Agent Trace build/validation, adding a DB migration. + - Done when: helper is deterministic, avoids shell interpolation, handles multiline JSON safely, returns structured success/error for caller-side logging, and has focused tests. + - Verification notes (commands or checks): targeted hooks/git-helper tests if permitted; `nix develop -c sh -c 'cd cli && cargo fmt'`; `nix flake check` as fallback. + - Status: done + - Completed: 2026-07-14 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/plans/agent-trace-git-notes.md` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test git_note_writer` was blocked by SCE bash policy preferring `nix flake check`; first `nix flake check` caught a clippy issue and then a git-note writer test failure, both fixed; final `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). + - Notes: Added an injectable helper that validates non-blank ref/commit/content, invokes `git notes --ref add -f -F - ` without shell interpolation, pipes Agent Trace JSON through stdin preserving content bytes, returns `GitNoteWriteOutcome`, and includes focused unit coverage for command construction, configured refs, blank input rejection, and command failure context. The helper is intentionally not wired into post-commit runtime until T04. + +- [x] T04: `Wire git-note persistence into post-commit Agent Trace flow` (status:done) + - Task ID: T04 + - Goal: After Agent Trace JSON validation and DB insertion succeed, write the same full JSON to the configured git-notes ref for the committed SHA, while keeping note-write failures non-blocking. + - Boundaries (in/out of scope): In - post-commit flow wiring, resolved config read, stable log event for note-write failure (for example `sce.hooks.post_commit.agent_trace_git_note_write_failed`), tests proving successful write is attempted after DB insert and failures do not change hook success. Out - backfill, notes push/fetch, non-git VCS note behavior, changing existing DB failure semantics. + - Done when: default post-commit writes a note under `refs/notes/sce-agent-trace`; configured ref is honored; note write is skipped or treated as no-op for unsupported/non-git contexts if necessary; note write failure logs diagnostics but does not fail the hook after DB persistence succeeds. + - Verification notes (commands or checks): targeted post-commit hook tests if permitted; manual local check with `git notes --ref refs/notes/sce-agent-trace show HEAD`; `nix flake check`. + - Status: done + - Completed: 2026-07-15 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/plans/agent-trace-git-notes.md` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'` passed; targeted `cargo test post_commit_agent_trace_flow -- --nocapture` was blocked by SCE bash policy preferring `nix flake check`; first `nix flake check` caught a clippy `too_many_arguments` issue, fixed by grouping git-note persistence inputs; final `nix flake check` passed; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."). + - Notes: Post-commit now resolves `policies.agent_trace.git_notes_ref`, writes the validated serialized Agent Trace JSON to git notes after Agent Trace DB insertion succeeds, skips note writes for explicit non-git VCS values, and logs non-blocking note-write failures with `sce.hooks.post_commit.agent_trace_git_note_write_failed`. + +- [x] T05: `Update Agent Trace context for git-notes persistence` (status:done) + - Task ID: T05 + - Goal: Document the new git-notes persistence contract in current-state context. + - Boundaries (in/out of scope): In - update `context/sce/agent-trace-hooks-command-routing.md`, `context/sce/agent-trace-db.md`, `context/sce/setup-githooks-hook-asset-packaging.md` if hook behavior text needs adjustment, and `context/context-map.md` entries. Out - implementation code, broad docs rewrites unrelated to post-commit Agent Trace persistence. + - Done when: context states the default notes ref, config override, full-JSON note content, and non-blocking failure behavior; stale `No git-notes persistence` text is removed or qualified. + - Verification notes (commands or checks): `rg "git-notes|git notes|No git-notes" context/`; manual diff review. + - Status: done + - Completed: 2026-07-15 + - Files changed: `context/cli/config-precedence-contract.md`, `context/sce/setup-githooks-hook-asset-packaging.md`, `context/plans/agent-trace-git-notes.md` + - Evidence: `rg "git-notes|git notes|No git-notes" context/` reviewed remaining current/historical matches; `git diff --check` passed. + - Notes: Removed stale config-contract wording that described git-note runtime wiring as future-only and clarified that setup-installed post-commit hooks hand off to Rust, where Agent Trace DB persistence remains required and git-note writes are best-effort under the configured notes ref. + +- [x] T06: `Validate git-notes Agent Trace behavior and cleanup` (status:done) + - Task ID: T06 + - Goal: Run final validation for the complete plan and clean up any planning or test scaffolding. + - Boundaries (in/out of scope): In - full repo validation, generated-output parity, focused grep for stale docs/config strings, cleanup of temporary test repositories or notes refs created during manual checks. Out - new behavior beyond the completed task stack. + - Done when: `nix flake check` passes or any failure is documented as pre-existing/unrelated; `nix run .#pkl-check-generated` passes; context sync is verified; no temporary scaffolding remains. + - Verification notes (commands or checks): `nix flake check`; `nix run .#pkl-check-generated`; `git diff --check`; `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/`. + - Status: done + - Completed: 2026-07-15 + - Files changed: `context/plans/agent-trace-git-notes.md` + - Evidence: `git diff --check` passed; `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/` reviewed current and historical matches; `nix run .#pkl-check-generated` passed ("Generated outputs are up to date."); `nix flake check` passed ("all checks passed"). + - Notes: No temporary test repositories or notes refs were created during T06, so no cleanup was required. + +## Validation Report + +### Commands run +- `git diff --check` -> exit 0 (no whitespace errors). +- `rg "refs/notes/sce-agent-trace|agent_trace.*git.*note|No git-notes" cli/ config/ context/` -> exit 0; reviewed current Agent Trace git-notes references plus historical `No git-notes` references. +- `nix run .#pkl-check-generated` -> exit 0 (`Generated outputs are up to date.`). +- `nix flake check` -> exit 0 (`all checks passed!`). +- `git status --short --untracked-files=all` -> reviewed staged/unstaged plan/context changes; no T06-created temporary scaffolding found. + +### Success-criteria verification +- [x] Successful post-commit Agent Trace writes a default git note under `refs/notes/sce-agent-trace` after validation/DB persistence -> covered by completed T04 implementation evidence and final `nix flake check`. +- [x] Note content is the same full Agent Trace JSON persisted to `agent_traces.trace_json` -> covered by completed T04 implementation/context evidence and final grep review. +- [x] Configurable notes ref documented in generated schema/context -> confirmed by final grep review and `nix run .#pkl-check-generated`. +- [x] Git-note write failures remain non-blocking and use stable logging -> confirmed by final grep review and `nix flake check`. +- [x] Existing Agent Trace DB persistence remains the required source of trace rows -> confirmed by context review and final validation. +- [x] Context describes current git-notes behavior and non-blocking posture -> confirmed by context sync review. + +### Failed checks and follow-ups +- None. + +### Residual risks +- No residual risks identified for the completed plan. + +## Open questions + +None. Plan is ready for T01 execution. diff --git a/context/plans/pi-extension-sce-integration.md b/context/plans/pi-extension-sce-integration.md new file mode 100644 index 00000000..77a8ed4b --- /dev/null +++ b/context/plans/pi-extension-sce-integration.md @@ -0,0 +1,120 @@ +# Plan: pi-extension-sce-integration + +## Change summary + +Extend the completed Pi harness integration (`pi-harness-integration.md`) with a project-local Pi extension that wires Pi into SCE's two runtime systems: bash policy enforcement and Agent Trace capture. A single combined extension source lives at `config/lib/pi-plugin/sce-pi-extension.ts`, is emitted verbatim by Pkl into `config/.pi/extensions/sce/index.ts` (mirroring how `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` is emitted into `config/.opencode/plugins/sce-agent-trace.ts` via `config/pkl/generate.pkl`), synced into `cli/assets/generated/config/pi/`, embedded, and installed by the existing `sce setup --pi` whole-tree copy. + +The extension implements three adapters, all fail-open and all delegating persistence/validation to the Rust CLI: + +1. **Bash policy**: Pi `tool_call` event narrowed to `bash`, evaluated via `sce policy bash --input normalized --output json`, returning `{ block: true, reason }` on `decision === "deny"` with policy ID and message. +2. **Conversation trace**: Pi `message_end` events mapped to the mixed `message` / `message.part` envelope (`text`, `reasoning` parts) piped to `sce hooks conversation-trace`. +3. **Edit/write diff trace**: pre/post file snapshots around Pi `edit`/`write` tool calls; unified diffs produced by spawning `git diff --no-index --no-ext-diff` on temp files (no npm dependencies); emitted as a conversation `patch` part and as a normalized `sce hooks diff-trace` payload with `tool_name: "pi"`. + +On the Rust side, `prefixed_diff_trace_session_id()` (`cli/src/services/hooks/mod.rs:44`) gains a `"pi" => "pi_"` arm so Pi sessions carry distinct provenance instead of raw passthrough. + +Decisions resolved with the user (2026-07-13): +- Unified diffs via `git diff --no-index` on temp files — no npm dependencies, single-file extension. +- User-shell `!`/`!!` policy enforcement and bash-mutation diff tracing (repo snapshots around bash calls) are excluded; recorded as non-goals for a possible follow-up plan. +- Rust prefix change adds `pi_` only; unknown `tool_name` passthrough behavior is unchanged (no generic `tool_` fallback). + +## Success criteria + +- `nix develop -c pkl eval -m . config/pkl/generate.pkl` deterministically emits `config/.pi/extensions/sce/index.ts`; a clean re-run produces no diff; `nix develop -c ./config/pkl/check-generated.sh` covers the new path and passes. +- `bash scripts/prepare-cli-generated-assets.sh` syncs the extension into `cli/assets/generated/config/pi/extensions/sce/index.ts`, and `sce setup --pi --non-interactive` in a scratch repo installs it to `.pi/extensions/sce/index.ts` without disturbing other `.pi/` content. +- With a deny rule configured (e.g. `forbid-git-commit`), an agent bash tool call to `git commit` in a Pi session is blocked before subprocess execution, and the block reason contains the policy ID and policy message; allowed commands run unchanged; missing `sce`, timeout, non-zero exit, and invalid JSON all fail open. +- Pi user and assistant messages produce `message` and `message.part` rows via `sce hooks conversation-trace` (text and reasoning preserved as separate parts), keyed by the Pi session ID. +- Pi `edit`/`write` tool calls produce unified diffs recorded both as conversation `patch` parts and as `diff_traces` rows with `tool_name: "pi"`, `model_id` in `provider/model` form, nullable `tool_version`, and session IDs prefixed `pi_`. +- Rust unit tests cover `pi_` prefixing (fresh and already-prefixed IDs) and Pi normalized payload ingestion; post-commit intersection preserves Pi provenance for overlapping diffs. +- `sce doctor` reports the Pi extension asset as part of Pi integration health (present/missing/drifted). +- `nix flake check` passes (canonical replacement for direct `cargo test` per repo bash policy). + +## Constraints and non-goals + +- No Pi core changes; integration is entirely a project-local extension plus SCE-side support. +- No policy enforcement for Pi user-shell `!`/`!!` commands (`user_bash` event) — deferred. +- No bash-mutation diff tracing (git snapshots around bash tool calls) — deferred; attribution for bash-based edits is knowingly incomplete in this release, matching Claude's structured-patch scope. +- No npm dependencies or `node_modules` inside `.pi/extensions/`; the extension is one self-contained TypeScript file (type-only imports from `@earendil-works/pi-coding-agent` are allowed). +- No direct Agent Trace database writes from the extension; all persistence goes through `sce hooks ...` / `sce policy bash`. +- No change to unknown-`tool_name` session-ID passthrough; no `tool_` generic prefix. +- Do not hand-edit generated artifacts (`config/.pi/**`, `cli/assets/generated/**`); all content changes go through `config/lib/pi-plugin/` and Pkl. +- Do not change existing OpenCode/Claude plugin behavior or generated outputs. + +## Assumptions + +- Pi auto-discovers `.pi/extensions/*/index.ts` in a trusted project (per the source write-up) with no registration step. +- Pi's `tool_call` handler can block by returning `{ block: true, reason }`, and `ctx.sessionManager.getSessionId()` is stable across a session. +- `sce hooks conversation-trace` and `sce hooks diff-trace` accept the normalized (non-Claude) payload shapes exactly as the OpenCode plugin emits them; Pi reuses those shapes with `tool_name: "pi"`. +- Pi tool version is sourced by resolving `@earendil-works/pi-coding-agent/package.json` at runtime, falling back to `null` (normalized diff traces permit nullable `tool_version`). + +## Task stack + +- [x] T01: `Add Pi extension with bash policy enforcement, emitted via Pkl` (status:done, completed 2026-07-14) + - Task ID: T01 + - Files changed: `config/lib/pi-plugin/sce-pi-extension.ts` (new), `config/lib/package.json` + `config/lib/bun.lock` (added `@earendil-works/pi-coding-agent@0.80.6`, type-only), `config/lib/tsconfig.json` (include `pi-plugin/**/*.ts`), `config/pkl/generate.pkl` (verbatim emission), `config/pkl/check-generated.sh` (`config/.pi/extensions` path), `config/.pi/extensions/sce/index.ts` (generated). + - Evidence: `pkl eval` emits `config/.pi/extensions/sce/index.ts` identical to source (`diff` clean, re-run diff-clean); `nix develop -c ./config/pkl/check-generated.sh` passes; `tsc --noEmit` reports zero pi-plugin errors (43 pre-existing bash-policy test errors unchanged); biome check/format clean; `bun test` 12/12 pass. + - Notes: Pi 0.80.6 API reconciled — default-export `ExtensionFactory`, `pi.on("tool_call")` with `ToolCallEventResult { block, reason }`, `isToolCallEventType("bash", ...)` all match plan assumptions. Deny reason passes through Rust `format_policy_block_message` (`Blocked by SCE bash-tool policy '{id}': {message}`), so policy ID + message are included. All failure paths (ENOENT/timeout/non-zero exit/empty/invalid JSON) return null → fail-open. + - Goal: Create `config/lib/pi-plugin/sce-pi-extension.ts` implementing the bash policy adapter, and wire Pkl to emit it verbatim as `config/.pi/extensions/sce/index.ts`. + - Boundaries (in/out of scope): In — extension skeleton (default export receiving `ExtensionAPI`), `tool_call` handler narrowed to `bash` via `isToolCallEventType`, synchronous `sce policy bash --input normalized --output json` invocation with `{ "command": ... }` on stdin, `{ block: true, reason }` on deny including policy ID and policy message, fail-open on missing CLI / timeout (10s) / non-zero exit / empty or invalid JSON; Pkl emission wiring in `config/pkl/generate.pkl` (and the pi renderer/base files as needed, following the `config/lib` → `config/.opencode/plugins` pattern in `config/pkl/base/opencode.pkl`); extend `config/pkl/check-generated.sh` paths to cover `config/.pi/extensions`. Out — conversation/diff tracing (T02/T03), asset sync (`cli/assets/generated`), doctor, docs. + - Done when: `nix develop -c pkl eval -m . config/pkl/generate.pkl` writes `config/.pi/extensions/sce/index.ts` identical to the `config/lib/pi-plugin` source; re-run is diff-clean; `nix develop -c ./config/pkl/check-generated.sh` passes; the extension type-checks under `config/lib` tooling (`bun`/`tsc` per `config/lib/tsconfig.json`). + - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl && git status --short config/.pi`; `diff config/lib/pi-plugin/sce-pi-extension.ts config/.pi/extensions/sce/index.ts`; `nix develop -c ./config/pkl/check-generated.sh`; type-check in `config/lib`; manual read confirming deny path surfaces `policy_id` + `reason` and all failure paths return without blocking. + +- [x] T02: `Add conversation text capture to the Pi extension` (status:done, completed 2026-07-14) + - Task ID: T02 + - Files changed: `config/lib/pi-plugin/sce-pi-extension.ts` (message_end handler + conversation-trace spawn helper), `config/.pi/extensions/sce/index.ts` (regenerated). + - Evidence: `pkl eval` regeneration diff-clean; `nix develop -c ./config/pkl/check-generated.sh` passes; `tsc --noEmit` zero pi-plugin errors (43 pre-existing baseline unchanged); biome clean; `bun test` 12/12 pass. Live Pi scratch smoke not run this session (deferred to T07 end-to-end smoke). + - Notes: Pi 0.80.6 exposes no per-message IDs on `message_end` (`AgentMessage` union lacks `id`); handler uses `AssistantMessage.responseId` when present, else `randomUUID()` — message + parts ship in one mixed batch so no cross-event mapping is needed. Roles narrowed to `user`/`assistant` (skips `toolResult`/custom messages). `text` parts from `TextContent.text`, `reasoning` parts from `ThinkingContent.thinking`, empty text skipped. Spawn helper mirrors OpenCode adapter fail-open shape (stdio pipe/ignore/ignore, ENOENT warn, resolve-only promise, fire-and-forget), cwd from `ctx.cwd`. + - Goal: Capture completed Pi user/assistant messages and pipe them to `sce hooks conversation-trace` as mixed `message` + `message.part` batches. + - Boundaries (in/out of scope): In — `message_end` handler in `config/lib/pi-plugin/sce-pi-extension.ts`; session ID from `ctx.sessionManager.getSessionId()`; preserve Pi message IDs when exposed, otherwise generate one and keep an event-local mapping so parts share it; emit `text` and `reasoning` parts separately; async fire-and-forget `spawn("sce", ["hooks", "conversation-trace"])` helper writing JSON to stdin with stderr ignored and no unhandled rejections (mirror the OpenCode adapter's fail-open behavior); Pkl regeneration of the emitted copy. Out — patch parts (T03), diff traces (T03), Rust changes. + - Done when: In a Pi session against a scratch repo, a user prompt and assistant reply produce `message` and `message.part` rows in the Agent Trace DB (verifiable via `sce` trace/db tooling); reasoning content lands as `part_type: "reasoning"`; missing `sce` binary causes no extension error; regeneration is diff-clean. + - Verification notes (commands or checks): regenerate + `check-generated.sh`; scratch-repo smoke: run Pi with the extension installed, send a prompt, then inspect the trace DB (e.g. `sce` db shell/tables) for the new message rows keyed by the Pi session ID; temporarily rename `sce` off PATH and confirm the session continues cleanly. + +- [x] T03: `Add edit/write diff capture to the Pi extension` (status:done, completed 2026-07-14) + - Task ID: T03 + - Files changed: `config/lib/pi-plugin/sce-pi-extension.ts` (edit/write tool_call capture, tool_result diff emission, diff-trace spawn helper, tool-version resolution), `config/.pi/extensions/sce/index.ts` (regenerated). + - Evidence: `pkl eval` regeneration identical to source and diff-clean on re-run; `nix develop -c ./config/pkl/check-generated.sh` passes; `tsc --noEmit` zero pi-plugin errors (43 pre-existing bash-policy test baseline unchanged); biome check clean; `bun test` 12/12 pass; `git diff --no-index` exit-1/header-shape behavior verified in a temp-dir smoke. Live Pi scratch smoke deferred to T07. + - Notes: Pending-call map keyed by `toolCallId`, entry deleted first on every `tool_result` (covers errors/duplicates). `before` read pre-execution (undefined when absent → `--- /dev/null` label); no-op when post-read fails or contents unchanged; `git diff --no-index --no-ext-diff` on `mkdtemp` files, only exit status 1 accepted, temp dir removed in `finally`. Header labels rewritten only before the first `@@` to avoid touching content lines. Patch part shipped as synthetic assistant message + `patch` part (`${toolCallId}-patch`), mirroring OpenCode. `model_id` = `${ctx.model.provider}/${ctx.model.id}` else null (Rust accepts null). `tool_version` resolved by walking up from the resolved package entry (package.json not in Pi's `exports` map), nullable. + - Goal: Record unified diffs for Pi `edit` and `write` tool calls as conversation `patch` parts and normalized `diff-trace` payloads with `tool_name: "pi"`. + - Boundaries (in/out of scope): In — `tool_call` handler for `edit`/`write` recording tool-call ID, resolved target path, and prior file contents (undefined when absent); `tool_result` handler that skips errored results, reads post-contents, and produces a unified diff by writing before/after contents to temp files and spawning `git diff --no-index --no-ext-diff` (empty diff → no-op; temp files always cleaned up; path labels rewritten to the repo-relative target path); emit `message.part` with `part_type: "patch"` to `conversation-trace` and the normalized `{ sessionID, diff, time, model_id, tool_name: "pi", tool_version }` payload to `diff-trace`; `model_id` as `${ctx.model.provider}/${ctx.model.id}` when available; `tool_version` resolved from the Pi package's `package.json`, nullable; pending-call map cleanup on every result including duplicates/failures. Out — bash-produced modifications, Rust prefixing (T04), multi-file tools. + - Done when: In a scratch-repo Pi session, a `write` of a new file and an `edit` of an existing file each produce a `diff_traces` row with a valid unified diff, `tool_name: "pi"`, and correct `model_id`, plus a conversation `patch` part; failed edits and no-op writes produce no rows; regeneration is diff-clean. + - Verification notes (commands or checks): regenerate + `check-generated.sh`; scratch smoke covering: new-file write, existing-file edit, edit emptying a file, failed edit, write producing identical content; inspect `diff_traces` rows for diff validity, `tool_name`, `model_id`, `tool_version`; confirm no temp files leak (`ls /tmp` pattern used by the helper). + +- [x] T04: `Add pi_ session prefix and Pi ingestion coverage in Rust` (status:done, completed 2026-07-14) + - Task ID: T04 + - Files changed: `cli/src/services/hooks/mod.rs` (`PI_TOOL_NAME` + `DIFF_TRACE_PI_SESSION_ID_PREFIX` constants, `"pi"` match arm in `prefixed_diff_trace_session_id()`, 4 tests). + - Evidence: `nix flake check` passes ("all checks passed!"). Tests cover fresh `pi_` prefixing, already-prefixed idempotency, normalized `tool_name: "pi"` payload parse + persistence (`pi_` session ID, `model_id`, null `tool_version`, `patch` payload type), and post-commit intersection flow preserving `tool_name: Some("pi")` provenance. + - Notes: Unknown-`tool_name` passthrough unchanged; `oc_`/`cc_` arms untouched. An extra unknown-tool passthrough test was added then dropped per user request — coverage limited to plan-required cases. + - Goal: Give Pi diff traces distinct provenance by prefixing normalized Pi session IDs with `pi_`. + - Boundaries (in/out of scope): In — add `PI_TOOL_NAME` / `DIFF_TRACE_PI_SESSION_ID_PREFIX` ("pi_") and a `"pi"` match arm to `prefixed_diff_trace_session_id()` (`cli/src/services/hooks/mod.rs:38-56`); unit tests for fresh and already-prefixed Pi IDs; an ingestion test asserting a normalized payload with `tool_name: "pi"` persists a `diff_traces` row with the `pi_` session ID, `model_id`, and nullable `tool_version`; a post-commit intersection test (or extension of an existing one) confirming Pi provenance survives into the final Agent Trace. Out — unknown-`tool_name` fallback changes, conversation-trace session prefixing changes, extension code. + - Done when: New tests pass; existing `oc_`/`cc_` behavior and unknown-tool passthrough are unchanged; `nix flake check` passes. + - Verification notes (commands or checks): `nix flake check` (repo policy blocks direct `cargo test`); review test assertions cover both prefix idempotency cases and end-to-end ingestion with `tool_name: "pi"`. + +- [x] T05: `Sync, embed, and doctor-check the Pi extension asset` (status:done, completed 2026-07-14) + - Task ID: T05 + - Files changed: `cli/assets/generated/config/pi/extensions/sce/index.ts` (new, synced), `cli/src/services/default_paths.rs` (`pi_asset::EXTENSIONS_DIR`), `cli/src/services/doctor/types.rs` (`PI_EXTENSIONS_LABEL`), `cli/src/services/doctor/inspect.rs` (`extensions/` bucket + `Pi extensions` group in `collect_pi_integration_groups()`). + - Evidence: `prepare-cli-generated-assets.sh` + `diff -r config/.pi cli/assets/generated/config/pi` clean; `nix flake check` passes; scratch smoke (isolated XDG roots): `sce setup --pi --non-interactive` installs `.pi/extensions/sce/index.ts`, doctor shows `[PASS] Pi extensions`, deleted file → `[FAIL]` group with `[MISS]` child, tampered file → `[FAIL]` content mismatch. + - Notes: No script or setup enumeration changes needed — the sync script already copies the whole `config/.pi` tree and `cli/build.rs` embeds `assets/generated/config/pi` wholesale into `PI_EMBEDDED_ASSETS`. Health problems flow through existing `push_pi_integration_*` functions unchanged. Unit tests for group bucketing were added then removed per user request. + - Goal: Ship the extension through the asset pipeline and surface its health in `sce doctor`. + - Boundaries (in/out of scope): In — run `bash scripts/prepare-cli-generated-assets.sh` and commit `cli/assets/generated/config/pi/extensions/sce/index.ts` (the script already copies the whole `config/.pi` tree; extend it only if the copy misses the new subdirectory); verify `sce setup --pi` installs the extension via the existing embedded whole-tree deploy, adjusting setup asset enumeration only if needed; add a `Pi extensions` group (or extend existing Pi groups) in `collect_pi_integration_groups()` (`cli/src/services/doctor/inspect.rs`) so doctor reports the extension present/missing/drifted; update doctor tests. Out — new doctor problem categories, extension content changes, docs (T06). + - Done when: `bash scripts/prepare-cli-generated-assets.sh && diff -r config/.pi cli/assets/generated/config/pi` is clean; scratch `sce setup --pi --non-interactive` installs `.pi/extensions/sce/index.ts`; `sce doctor` passes after setup and flags a deleted or modified extension file; `nix flake check` passes. + - Verification notes (commands or checks): asset diff command above; scratch smoke: setup, `sce doctor` (PASS), delete `.pi/extensions/sce/index.ts`, `sce doctor` (reports the problem with `sce setup --pi` remediation); `nix flake check`. + +- [x] T06: `Document the Pi extension integration` (status:done, completed 2026-07-14) + - Task ID: T06 + - Files changed: `README.md` (supported integrations table + Pi extension coverage/gaps), `context/architecture.md` (generation classes + Pi bash-policy caller ownership), `config/pkl/README.md` (Pi extension generation ownership and regeneration paths). + - Evidence: targeted stale-claim search across `README.md`, `context/architecture.md`, and `config/pkl/README.md` returned 0 matches for `No hooks for Pi`, `extensions are out of scope`, and `only OpenCode`; proofread touched sections confirm Pi bash policy + Agent Trace coverage, `pi_` session prefix, and deferred `!`/`!!` policy plus bash-mutation tracing gaps are documented. + - Notes: Documentation-only task; no code or generated artifacts changed. + - Goal: Update docs so Pi is described as covered by bash policy and Agent Trace, including scope limits. + - Boundaries (in/out of scope): In — README and `context/architecture.md` sections describing integrations/policy/trace coverage; `config/pkl/README.md` ownership notes for `config/lib/pi-plugin` → `config/.pi/extensions`; note the deferred gaps (no `!`/`!!` policy, no bash-mutation tracing) where integration coverage is described. Out — code changes, Pi user tutorials. + - Done when: Touched docs accurately state Pi extension coverage, the `pi_` session prefix, and the deferred gaps; no stale claims that Pi has no hooks/extensions remain in touched docs. + - Verification notes (commands or checks): `grep -rn "No hooks for Pi\|extensions are out of scope\|only OpenCode" context/ README.md config/pkl/README.md` returns no stale claims; proofread rendered sections. + +- [ ] T07: `Validation and cleanup` (status:todo) + - Task ID: T07 + - Goal: Run the full verification suite, confirm the end-to-end Pi extension flow, remove scaffolding, and sync context. + - Boundaries (in/out of scope): In — full checks, end-to-end scratch smoke (setup → policy deny → conversation/edit tracing → commit → post-commit trace with Pi provenance), removal of any temporary scaffolding, context sync verification, plan status updates and validation report. Out — new features. + - Done when: `nix flake check` passes; `nix develop -c ./config/pkl/check-generated.sh` and `nix run .#pkl-check-generated` pass; clean regeneration + asset-prep re-run yields no git diff; scratch smoke succeeds end to end including a denied `git commit` bash call and a post-commit Agent Trace containing `tool_name: "pi"` with a `pi_` session ID; plan checkboxes and touched context docs reflect final state. + - Verification notes (commands or checks): `nix flake check`; `nix develop -c sh -c 'pkl eval -m . config/pkl/generate.pkl && bash scripts/prepare-cli-generated-assets.sh' && git status --short`; `nix run .#pkl-check-generated`; scratch-repo smoke with isolated XDG roots; review `context/plans/pi-extension-sce-integration.md` statuses. + +## Open questions + +- None blocking. If Pi's actual extension API diverges from the write-up (event names, blocking contract, `ctx` shape), reconcile against the installed Pi package's types in T01 before finalizing the handler signatures. diff --git a/context/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md new file mode 100644 index 00000000..359383ee --- /dev/null +++ b/context/plans/pi-harness-integration.md @@ -0,0 +1,152 @@ +# Plan: pi-harness-integration + +## Change summary + +Add the Pi coding-agent harness as a first-class SCE integration target alongside OpenCode and Claude. Pi consumes project-local configuration from `.pi/`: slash commands are Markdown prompt templates in `.pi/prompts/`, and skills are Agent Skills-format `SKILL.md` packages in `.pi/skills/`. Pi has no native sub-agent format, so SCE agents (shared-context-plan, shared-context-code) are rendered as agent-role prompt templates (`.pi/prompts/agent-*.md`) that instruct Pi to act in that role within the current session. + +The change follows the existing target pattern end to end: a new Pkl renderer produces a canonical `config/.pi/` tree, the asset-prep script syncs it into `cli/assets/generated/config/pi`, `build.rs` embeds it, `sce setup --pi` installs it and persists `"pi"` into `integrations.target`, and `sce doctor` detects and health-checks the Pi installation. + +Decisions resolved with the user (2026-07-13): +- Generate a dedicated `.pi/skills/` tree from Pkl (no `.pi/settings.json` redirection to `.claude/skills`). +- Render SCE agents as agent-role prompt templates in `.pi/prompts/`. +- CLI: add `--pi` and a new `--all` flag. +- Updated during T04 (2026-07-13, user decision): remove `--both` entirely (flag, `SetupTarget::Both`, and interactive option); `--all` (opencode+claude+pi) replaces it. Interactive prompt offers OpenCode, Claude, Pi, All. +- Do not generate or append `AGENTS.md`; skills and prompts carry the workflow. + +## Success criteria + +- `nix develop -c pkl eval -m . config/pkl/generate.pkl` deterministically emits `config/.pi/prompts/*.md` (commands + agent-role prompts) and `config/.pi/skills/*/SKILL.md`, and a clean re-run produces no diff. +- `nix develop -c ./config/pkl/check-generated.sh` covers the new Pi outputs and passes. +- `sce setup --pi` installs the Pi assets into the target repo's `.pi/` directory and persists `"pi"` in `.sce/config.json` `integrations.target`. +- `sce setup --all` installs opencode, claude, and pi targets; `--both` is removed and rejected as an unknown flag. +- `sce doctor` reports Pi integration health (prompts, skills) when `"pi"` is configured or a `.pi/` directory is detected, and its remediation text mentions `sce setup --pi`. +- `.sce/config.json` schema validation accepts `"pi"` and still rejects unknown target IDs. +- Full workspace checks pass (`cargo test`, pkl parity via `nix run .#pkl-check-generated` or `nix flake check`). + +## Constraints and non-goals + +- Manual profile only: no Pi equivalent of the OpenCode automated profile in this change. +- No hooks for Pi (Pi extensions are out of scope per the source write-up; the Claude hook mechanism has no Pi analogue here). +- No `.pi/SYSTEM.md`, `.pi/APPEND_SYSTEM.md`, `.pi/settings.json`, or root `AGENTS.md` generation. +- No Pi packaging (`pi install` npm/Git package) support. +- Do not change existing OpenCode/Claude generated outputs. (`--both` removal was explicitly approved during T04.) +- Do not hand-edit generated artifacts; all content changes go through Pkl sources. + +## Assumptions + +- Pi auto-discovers `.pi/prompts/` and `.pi/skills/` in a trusted repo without extra registration (per the provided write-up). +- Pi prompt-template frontmatter uses `description` and optional `argument-hint`; arguments use `$ARGUMENTS`/`$1` syntax, which maps cleanly from existing Claude command templates. + +## Task stack + +- [x] T01: `Add Pi Pkl renderer and generate config/.pi tree` (status:done) + - Task ID: T01 + - Completed: 2026-07-13 + - Files changed: config/pkl/renderers/pi-content.pkl (new), config/pkl/renderers/pi-metadata.pkl (new), config/pkl/generate.pkl, config/pkl/check-generated.sh, config/pkl/renderers/metadata-coverage-check.pkl, config/.pi/** (generated: 5 command prompts, 2 agent-role prompts, 8 SKILL.md) + - Evidence: `pkl eval -m . config/pkl/generate.pkl` emitted config/.pi tree; re-run diff-clean; `nix develop -c ./config/pkl/check-generated.sh` passed covering config/.pi/prompts + config/.pi/skills; metadata-coverage-check.pkl evaluates with pi coverage blocks; spot-checked frontmatter (`description` + `argument-hint`) and `$ARGUMENTS` usage. + - Goal: Render the canonical Pi target tree from the shared Pkl sources: `config/.pi/skills/{slug}/SKILL.md` for each SCE skill, `config/.pi/prompts/{slug}.md` for each SCE command, and `config/.pi/prompts/agent-{slug}.md` agent-role prompt templates for shared-context-plan and shared-context-code. + - Boundaries (in/out of scope): In — new `config/pkl/renderers/pi-content.pkl` and `pi-metadata.pkl` (mirroring the claude renderer pair), wiring into `config/pkl/generate.pkl`, extending the `paths` array in `config/pkl/check-generated.sh`, and updating `metadata-coverage-check.pkl` if it enumerates targets. Agent-role prompts adapt agent bodies to Pi semantics: act-as-role instructions, `$ARGUMENTS` argument passing, reference to loading the matching skill. Out — CLI/Rust changes, asset sync, automated profile. + - Done when: `nix develop -c pkl eval -m . config/pkl/generate.pkl` writes the `config/.pi/` tree; re-running produces no diff; `nix develop -c ./config/pkl/check-generated.sh` passes and covers Pi paths; generated prompt frontmatter contains valid `description` (and `argument-hint` where the source command takes arguments). + - Verification notes (commands or checks): `nix develop -c pkl eval -m . config/pkl/generate.pkl`; `git status --short config/.pi`; `nix develop -c ./config/pkl/check-generated.sh`; manual read of one command prompt, one agent-role prompt, and one SKILL.md for correct Pi frontmatter and argument syntax. + +- [x] T02: `Sync and embed Pi assets into the CLI binary` (status:done) + - Task ID: T02 + - Completed: 2026-07-13 + - Files changed: scripts/prepare-cli-generated-assets.sh, cli/build.rs, flake.nix (Nix-build asset staging for config/.pi), cli/assets/generated/config/pi/** (synced: 7 prompts, 8 SKILL.md), cli/assets/generated/config/schema/sce-config.schema.json (pre-existing mirror drift from 641c4e7 picked up by re-sync) + - Evidence: `bash scripts/prepare-cli-generated-assets.sh && diff -r config/.pi cli/assets/generated/config/pi` clean; `cargo build` succeeded (3m21s) after adding `#[allow(dead_code)]` emission for the unused-until-T04 `PI_EMBEDDED_ASSETS`; `nix flake check` passed (exit 0) + - Goal: Make the generated Pi tree available as embedded CLI assets. + - Boundaries (in/out of scope): In — extend `scripts/prepare-cli-generated-assets.sh` to copy `config/.pi/` into `cli/assets/generated/config/pi/`, add a `PI_EMBEDDED_ASSETS` target entry in `cli/build.rs` (alongside the opencode/claude entries at `cli/build.rs:11-24`), and commit the synced `cli/assets/generated/config/pi/` tree. Out — setup command wiring, doctor. + - Done when: `./scripts/prepare-cli-generated-assets.sh` produces `cli/assets/generated/config/pi/` matching `config/.pi/`, and `cargo build -p` the CLI crate succeeds with the new embedded asset set compiled in. + - Verification notes (commands or checks): `./scripts/prepare-cli-generated-assets.sh && diff -r config/.pi cli/assets/generated/config/pi`; `cargo build` in `cli/`; `cargo test` for any asset-embedding tests. + +- [x] T03: `Add "pi" integration target ID to config types and schema` (status:done) + - Task ID: T03 + - Completed: 2026-07-13 + - Files changed: cli/src/services/config/types.rs (Pi variant + parse + unit tests), cli/src/services/doctor/inspect.rs (compile-only no-op Pi match arm; full doctor behavior in T05), config/pkl/base/sce-config-schema.pkl (target enum + "pi"), config/schema/sce-config.schema.json (regenerated), cli/assets/generated/config/schema/sce-config.schema.json (re-synced mirror) + - Evidence: `nix flake check` passed (tests, clippy, fmt, pkl-parity); Pkl regen + asset-prep re-run diff-clean beyond intended schema enum change; mirror `diff` clean; new unit tests cover `"pi"` parse and unknown-ID rejection message listing `opencode, claude, pi` + - Notes: Approved scope additions during review — Pkl-owned JSON schema enum update (runtime validates against the embedded schema) and the minimal doctor Pi arm required for exhaustive-match compilation + - Goal: Teach the config layer about the `pi` target so `.sce/config.json` can persist and validate it. + - Boundaries (in/out of scope): In — add `Pi` variant to `IntegrationTargetId` (`cli/src/services/config/types.rs:269-289`) with `parse()`/string round-trip as `"pi"`, and update `integrations.target` schema validation (`cli/src/services/config/schema.rs:586-624`) including its error message listing valid IDs. Out — setup flags, doctor detection. + - Done when: `"pi"` parses and serializes; unknown IDs still rejected with the updated valid-values message; unit tests cover the new variant. + - Verification notes (commands or checks): `cargo test` scoped to config services (e.g. `cargo test -p config`); check schema error text lists `opencode`, `claude`, `pi`. + +- [x] T04: `Add sce setup --pi and --all flags with Pi asset installation` (status:done) + - Task ID: T04 + - Completed: 2026-07-13 + - Files changed: cli/src/cli_schema.rs (flags: --pi/--all added, --both removed), cli/src/services/parse/command_runtime.rs, cli/src/services/setup/mod.rs (SetupTarget::{Pi,All}, Both removed, interactive prompt gains Pi + All, generalized EmbeddedAssetSelectionIter, new unit tests), cli/src/services/setup/command.rs (untouched; request flow unchanged), cli/src/services/default_paths.rs (repo_dir::PI + pi_target_dir), cli/src/command_surface.rs (setup usage line), cli/src/services/doctor/inspect.rs (remediation text --both → --all), cli/build.rs (PI_EMBEDDED_ASSETS no longer dead_code) + - Evidence: `nix flake check` passed (exit 0; tests, clippy, fmt, pkl parity); scratch-repo smoke via nix-built binary: `setup --pi --non-interactive` installed 15 files to `.pi/` and persisted `"pi"`; `setup --all --non-interactive` installed .opencode (19) + .claude (17) + .pi (15) and persisted all three IDs; `--pi --all` rejected as conflicting; `--both` rejected as unknown option + - Notes: Scope change approved mid-task by user — `--both` removed entirely (flag, enum variant, interactive option); `--all` is the only multi-target selector; interactive menu now OpenCode/Claude/Pi/All + - Goal: `sce setup --pi` installs embedded Pi assets to the repo's `.pi/` directory and persists `"pi"` into `integrations.target`; `sce setup --all` expands to opencode + claude + pi. + - Boundaries (in/out of scope): In — `SetupTarget::Pi` and `SetupTarget::All` variants (`cli/src/services/setup/mod.rs:24-29`), `--pi`/`--all` clap flags with mutual-exclusion validation (`cli/src/services/setup/command.rs`), `integration_target_id_str()` mapping (`setup/mod.rs:412-420`), `install_embedded_setup_assets()` deploy path for the pi asset root, and `persist_integration_targets()` handling. `--both` was removed and replaced by `--all` (opencode+claude+pi). Out — hooks for Pi (no Pi hook assets), doctor. + - Done when: In a scratch repo, `sce setup --pi --non-interactive` creates `.pi/prompts/` and `.pi/skills/` matching embedded assets and `.sce/config.json` contains `"pi"`; `sce setup --all --non-interactive` installs all three trees; flag-conflict validation rejects combining `--pi` with `--both`/`--all` etc.; setup unit/integration tests updated. + - Verification notes (commands or checks): `cargo test` for setup services; manual smoke: `cargo run -- setup --pi --non-interactive --repo ` then inspect `.pi/` and `.sce/config.json`. + +- [x] T05: `Add Pi integration health checks to sce doctor` (status:done) + - Task ID: T05 + - Completed: 2026-07-13 + - Files changed: cli/src/services/doctor/inspect.rs (Pi detection/group inspection/problems), cli/src/services/doctor/types.rs (Pi labels/problem kinds), cli/src/services/doctor/mod.rs and cli/src/services/lifecycle.rs (problem-kind plumbing), cli/src/services/default_paths.rs (Pi repo path/asset constants) + - Evidence: targeted `cargo test doctor::inspect::tests` was blocked by the repo bash policy preferring `nix flake check`; `nix flake check` passed (cli-tests, clippy, fmt, pkl parity, JS checks, workflow lint, Flatpak parity/static checks); `nix run .#pkl-check-generated` passed with generated outputs up to date. + - Notes: Pi doctor coverage uses the existing integration group-health model with `Pi prompts` and `Pi skills` groups; `.pi/` directory fallback detection works when no integration target is configured; no Pi hooks or new problem categories were added. A temporary `cli/tests/doctor_pi.rs` integration test file was removed after user feedback. + - Goal: `sce doctor` detects and inspects the Pi integration. + - Boundaries (in/out of scope): In — include `Pi` in `resolve_doctor_integration_targets()` with `.pi/` directory fallback detection (`cli/src/services/doctor/inspect.rs:419-451`), add `collect_pi_integration_groups()` covering prompts and skills groups, iterate it in `inspect_repository_integrations()` (`inspect.rs:453-502`), and extend `NoIntegrationsInstalled` remediation to mention `sce setup --pi`. Out — new problem categories beyond the existing group-health model. + - Done when: Doctor reports healthy Pi groups after `sce setup --pi`, reports missing/drifted files when `.pi/` content is deleted or altered, and falls back to `.pi/` directory detection when `.sce/config.json` lacks targets; doctor tests updated. + - Verification notes (commands or checks): `cargo test` for doctor services; manual smoke: run `sce doctor` in the scratch repo from T04 before and after deleting a `.pi/skills/*/SKILL.md`. + +- [x] T06: `Document the Pi target in architecture and setup docs` (status:done) + - Task ID: T06 + - Completed: 2026-07-13 + - Files changed: README.md, config/pkl/README.md, context/architecture.md, context/plans/pi-harness-integration.md + - Evidence: targeted proofread of touched sections; exact stale-claim scan passed for pre-T06 README/Pkl README wording (`generated configs that make OpenCode and Claude Code`, `OpenCode and Claude Code are first-class`, `OpenCode and/or Claude config`, `This regenerates both manual and automated OpenCode profiles plus Claude outputs`); no code/generated-output changes. + - Notes: Important-change context sync applies because README and architecture/Pkl generation docs now present Pi as a first-class target; repaired the stale T04 plan note that still described `--both` as unchanged. + - Goal: Update repository documentation to describe the third target tree and new flags. + - Boundaries (in/out of scope): In — `context/architecture.md` (target-tree overview at lines 3-9), `config/pkl/README.md` ownership/profile/command sections to include the Pi profile and `config/.pi/**` outputs, and README/setup docs mentioning `--pi`/`--all`. Out — code changes, Pi user tutorials. + - Done when: Docs accurately describe Pi generation ownership, setup flags, and doctor coverage; no stale references implying only two targets remain in touched docs. + - Verification notes (commands or checks): `grep -rn "opencode and claude\|two.*target" context/ config/pkl/README.md` returns no stale claims; proofread rendered sections. + +- [x] T07: `Validation and cleanup` (status:done) + - Task ID: T07 + - Completed: 2026-07-13 + - Files changed: context/plans/pi-harness-integration.md + - Evidence: direct `cargo test` was blocked by the repo bash policy (`use-nix-flake-check-over-cargo-test`); `nix flake check` passed, including `cli-tests`, `cli-clippy`, `cli-fmt`, `pkl-parity`, JS checks, workflow lint, native portability audit, and Flatpak parity/static checks; `nix develop -c ./config/pkl/check-generated.sh` passed; `nix run .#pkl-check-generated` passed; clean regeneration plus `bash scripts/prepare-cli-generated-assets.sh` left no additional git diff beyond pre-existing staged T06 docs/plan updates; scratch smoke with isolated XDG roots ran `sce setup --pi --non-interactive`, persisted `"pi"` in `.sce/config.json`, installed Pi prompts/skills, and `sce doctor` reported Pi prompts/skills PASS (full doctor PASS after installing required hooks in the scratch repo). + - Notes: No code or generated-output changes were needed. The plan is now fully complete; context sync should verify current-state docs remain aligned. + - Goal: Run the full verification suite, confirm end-to-end Pi flow, and sync context. + - Boundaries (in/out of scope): In — full checks and context sync; fixing regressions surfaced by checks. Out — new features. + - Done when: `cargo test` (workspace) passes; `nix develop -c ./config/pkl/check-generated.sh` and `nix run .#pkl-check-generated` pass; a clean regeneration + asset-prep re-run yields no git diff; end-to-end smoke (`setup --pi` → `doctor`) succeeds in a scratch repo; `context/` plan checkboxes and any touched context docs reflect final state. + - Verification notes (commands or checks): `cargo test`; `nix develop -c pkl eval -m . config/pkl/generate.pkl && ./scripts/prepare-cli-generated-assets.sh && git status --short`; `nix run .#pkl-check-generated`; scratch-repo smoke test; review `context/plans/pi-harness-integration.md` statuses. + +## Open questions + +- None blocking. If Pi's actual installed version diverges from the write-up (e.g. prompt frontmatter keys), adjust the renderer in T01 against `pi --help` / real Pi docs before finalizing frontmatter. + +## Validation Report + +### Commands run + +- `nix develop -c sh -c 'cd cli && cargo test'` -> blocked by repo bash policy `use-nix-flake-check-over-cargo-test`; replaced with canonical repo validation below. +- `nix develop -c ./config/pkl/check-generated.sh` -> exit 0; generated outputs up to date. +- `nix run .#pkl-check-generated` -> exit 0; generated outputs up to date. +- `nix flake check` -> exit 0; all checks passed, including CLI tests, clippy, fmt, Pkl parity, JS checks, workflow lint, native portability audit, and Flatpak parity/static checks. +- `nix develop -c sh -c 'pkl eval -m . config/pkl/generate.pkl && bash scripts/prepare-cli-generated-assets.sh' && git status --short` -> exit 0; no additional diff beyond pre-existing staged T06 docs/plan updates. +- Scratch smoke with isolated `XDG_STATE_HOME`/`XDG_CONFIG_HOME`: `sce setup --pi --non-interactive` -> exit 0; installed 15 Pi files and persisted `"pi"` in `.sce/config.json`. +- Scratch smoke follow-up: `sce doctor` after installing required hooks in the same scratch repo -> exit 0; Pi prompts and Pi skills reported `[PASS]`; summary reported 0 blocking problems and 0 warnings. +- Removed temporary scratch repo/state/config directories under `/tmp/opencode/`. + +### Success-criteria verification + +- [x] `config/.pi/prompts/*.md` and `config/.pi/skills/*/SKILL.md` are generated deterministically: verified by `check-generated.sh`, `pkl-check-generated`, and regeneration + asset-prep diff check. +- [x] Pkl stale-output detection covers Pi outputs: `nix develop -c ./config/pkl/check-generated.sh` passed. +- [x] `sce setup --pi` installs Pi assets and persists `"pi"`: verified in scratch repo with isolated SCE state/config roots. +- [x] `sce setup --all` and `--both` behavior were covered earlier in T04 evidence; final `nix flake check` kept that test surface passing. +- [x] `sce doctor` reports Pi prompts/skills health: verified in scratch repo; Pi groups reported `[PASS]`. +- [x] `.sce/config.json` schema accepts `"pi"` and rejects unknown target IDs: covered by prior T03 tests and final `nix flake check`. +- [x] Workspace checks pass: `nix flake check` passed; direct `cargo test` was policy-blocked in favor of that canonical repo check. + +### Failed checks and follow-ups + +- Direct `cargo test` was intentionally blocked by repo bash policy; no implementation failure. Canonical replacement `nix flake check` passed. +- Initial scratch `sce setup --pi` attempt used the developer's default auth DB with a mismatched temporary encryption key and failed to open the encrypted auth DB. Re-run with isolated XDG state/config roots succeeded; no code change required. +- A first regeneration command attempted to execute `./scripts/prepare-cli-generated-assets.sh` directly and hit `Permission denied`; re-run through `bash scripts/prepare-cli-generated-assets.sh` succeeded. + +### Residual risks + +- None identified for the implemented Pi harness integration. Pi frontmatter assumptions remain as stated in Open questions and should be revisited only if real Pi behavior diverges from the source write-up. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index e43c7162..2f703d81 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -174,13 +174,13 @@ Both triggers compare `OLD.*` vs `NEW.*` for all mutable columns (excluding `upd `sce hooks diff-trace` is the current runtime writer for `diff_traces`. - The hook path validates required STDIN `{ sessionID, diff, time, tool_name, tool_version }` before persistence, with `model_id` accepted as optional (absent or `null`) and `tool_version` accepted as nullable. Missing attribution remains `None`; `diff_traces.model_id` is the only active model-attribution storage for diff traces and there is no session-level fallback lookup. -- Direct payload `model_id` and `tool_version` values pass into `DiffTraceInsert` as-is. The stored `session_id` is tool-prefixed before insert construction: `opencode` payloads store `oc_`, `claude` structured payloads store `cc_`, and same-tool-prefixed values are not double-prefixed. The `payload_type` field is set to `PAYLOAD_TYPE_PATCH` for `OpenCode` normalized diff-trace payloads and `PAYLOAD_TYPE_STRUCTURED` for Claude structured `PostToolUse` payloads. Claude structured intake best-effort extracts direct `model`/`model_id`/`modelId` metadata, including nested `model.id` / `model.model` / `model.name`, normalizes values with the `claude/` prefix when present, and leaves `model_id` nullable when metadata is absent. +- Direct payload `model_id` and `tool_version` values pass into `DiffTraceInsert` as-is. The stored `session_id` is tool-prefixed before insert construction: `opencode` payloads store `oc_`, `claude` structured payloads store `cc_`, `pi` normalized payloads store `pi_`, and same-tool-prefixed values are not double-prefixed. The `payload_type` field is set to `PAYLOAD_TYPE_PATCH` for `OpenCode` normalized diff-trace payloads and `PAYLOAD_TYPE_STRUCTURED` for Claude structured `PostToolUse` payloads. Claude structured intake best-effort extracts direct `model`/`model_id`/`modelId` metadata, including nested `model.id` / `model.model` / `model.name`, normalizes values with the `claude/` prefix when present, and leaves `model_id` nullable when metadata is absent. - `time` is accepted as a `u64` Unix epoch millisecond input and must fit the signed `i64` `time_ms` column before any persistence starts. - The hook inserts the parsed payload fields plus nullable direct attribution through `AgentTraceDb::insert_diff_trace()` without writing a parsed-payload artifact under `context/tmp`. - AgentTraceDb open/insert failures are logged and reflected in deterministic success text as failed DB persistence; no artifact fallback is created. - Existing artifact files are not backfilled into the database. -Post-commit intersection rows are written by the active `post-commit` hook flow through per-checkout lazy AgentTraceDb access, and the same flow now also inserts built Agent Trace payloads into `agent_traces` via `AgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version plus `content_hash` on every emitted range. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. +Post-commit intersection rows are written by the active `post-commit` hook flow through per-checkout lazy AgentTraceDb access, and the same flow now also inserts built Agent Trace payloads into `agent_traces` via `AgentTraceDb::insert_agent_trace()` (see [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md)). The persisted `trace_json` is the schema-validated `build_agent_trace(...)` output and includes top-level `metadata.sce.version` from the compiled `sce` CLI package version plus `content_hash` on every emitted range. After DB insertion succeeds in git contexts, the same full serialized JSON is also written best-effort to a git note on the committed SHA under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`); git-note write failures are logged but do not roll back or fail DB persistence. Range `content_hash` values are computed from the touched-line kind/content of the post-commit hunk that produced the persisted range, not from DB IDs, paths, line positions, or runtime metadata. `sce hooks conversation-trace` is the current runtime writer for `messages` and `parts`. diff --git a/context/sce/agent-trace-hook-doctor.md b/context/sce/agent-trace-hook-doctor.md index 4f1cc544..9ae21439 100644 --- a/context/sce/agent-trace-hook-doctor.md +++ b/context/sce/agent-trace-hook-doctor.md @@ -41,10 +41,10 @@ The runtime in `cli/src/services/doctor/mod.rs` exposes the approved doctor comm - top-level-only human text hook rows for `pre-commit`, `commit-msg`, and `post-commit`, with nested `content` / `executable` detail removed from text mode - required hook presence and executable permissions for `pre-commit`, `commit-msg`, and `post-commit` when repo-scoped checks apply (delegated to `HooksLifecycle::diagnose`) - byte-for-byte stale-content detection for required hook payloads against canonical embedded SCE-managed hook assets (delegated to `HooksLifecycle::diagnose`) -- integration target resolution that reads `integrations.target` from repo-local `.sce/config.json` when present, or falls back to detecting repo-root `.opencode/` and `.claude/` directories when config has no `integrations` or `integrations.target`; only the resolved targets are inspected -- repo-root installed OpenCode integration inventory for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`, plus Claude integration inventory for `ClaudeCode plugins`, `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`, scoped to the resolved targets +- integration target resolution that reads `integrations.target` from repo-local `.sce/config.json` when present, or falls back to detecting repo-root `.opencode/`, `.claude/`, and `.pi/` directories when config has no `integrations` or `integrations.target`; only the resolved targets are inspected +- repo-root installed OpenCode integration inventory for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`, Claude integration inventory for `ClaudeCode plugins`, `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`, plus Pi integration inventory for `Pi prompts` and `Pi skills`, scoped to the resolved targets - integration child-row reporting validates installed files against embedded SHA-256 content; missing files render as `[MISS]`, content mismatches render as `[FAIL]`, and any affected parent group renders as `[FAIL]` -- OpenCode plugin inventory includes the installed manifest file plus plugin/preset artifacts as required presence-only files; Claude groups are derived from embedded `.claude` assets (`settings.json` and `hooks/**` under `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, then `agents/**`, `commands/**`, and `skills/**`); generated `config/.opencode/**` and `config/.claude/**` trees are not inspected by doctor +- OpenCode plugin inventory includes the installed manifest file plus plugin/preset artifacts as required presence-only files; Claude groups are derived from embedded `.claude` assets (`settings.json` and `hooks/**` under `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, then `agents/**`, `commands/**`, and `skills/**`); Pi groups are derived from embedded `.pi` assets (`prompts/**` under `Pi prompts`, `skills/**` under `Pi skills`); generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees are not inspected by doctor - repair-mode delegation to `ServiceLifecycle::fix` implementations: `HooksLifecycle::fix` reuses `install_required_git_hooks` for missing hooks directories plus missing, stale, or non-executable required hooks; `LocalDbLifecycle::fix`, `AuthDbLifecycle::fix`, and `AgentTraceDbLifecycle::fix` handle bootstrap of missing canonical SCE-owned DB parent directories ## Approved human text-mode contract diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 29ede5ba..3aee6430 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -59,14 +59,17 @@ - The built Agent Trace payload is converted to JSON `Value` and validated via `agent_trace::validate_agent_trace_value(...)` before persistence. - Validation failures are returned through the same post-commit runtime failure path/class used for Agent Trace DB insertion failures (no silent fallback). - When validation passes, the payload is serialized and inserted into Agent Trace DB `agent_traces` using `commit_id` from flow-result commit metadata, `commit_time_ms` from flow-result post-commit timestamp metadata, a derived non-null `url` value formatted as `sce.crocoder.dev/trace/`, and the validated runtime `--remote-url` value persisted to nullable `agent_traces.remote_url`. - - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. + - After Agent Trace DB insertion succeeds, git post-commit contexts also write the same full serialized Agent Trace JSON to a git note on the committed SHA. The default ref is `refs/notes/sce-agent-trace`, resolved through `policies.agent_trace.git_notes_ref`; explicit non-git `--vcs` values skip the note write. + - Git-note writes use replace/upsert semantics (`git notes --ref add -f -F - `) and preserve multiline JSON by piping content through stdin. + - When the local git-note write succeeds and `policies.agent_trace.push_notes.enabled` resolves to its default `true`, the post-commit flow immediately attempts `git push ` using the validated `--remote-url` handoff value and the same configured notes ref. Explicit `enabled: false` skips this push. + - Post-commit Agent Trace success requires both schema validation and Agent Trace DB `agent_traces` persistence to succeed. Git-note write failures are best-effort: they are logged with `sce.hooks.post_commit.agent_trace_git_note_write_failed` and do not fail the hook after DB persistence succeeded. Git-notes push failures are silent fail-open: no user-facing warning or hook failure is emitted, and a later post-commit hook invocation can try the push again without a retry queue. - Current command-surface success output is: `post-commit hook processed intersection: commit=, intersection_files=`. - `post-rewrite` is a deterministic no-op entrypoint. - `diff-trace` reads STDIN JSON and classifies the payload: - **Claude structured payloads** (detected by presence of top-level `hook_event_name`): the STDIN JSON is validated through `derive_claude_structured_patch`. Supported `PostToolUse` `Write` create and `Edit` structured-patch events produce a `DiffTracePayload` with `payload_type="structured"` and the raw event JSON stored as the `diff` column without conversion to unified-diff text. Direct model metadata is extracted best-effort from top-level `model`, `model_id`, or `modelId`, or from nested `model.id`, `model.model`, or `model.name`; non-empty values are normalized with the `claude/` prefix. If Claude omits model metadata, `model_id` remains nullable and downstream Agent Trace JSON omits contributor `model_id`. Unsupported Claude events (non-`PostToolUse`, unsupported tools, invalid payloads) produce a deterministic `NoOp` success result. - **OpenCode normalized payloads** (no `hook_event_name`): existing flat `{ sessionID, diff, time, model_id?, tool_name, tool_version }` validation applies unchanged, with `payload_type="patch"`. - The `DiffTracePayload` struct carries a `payload_type: String` field consumed by `persist_diff_trace_payload_to_agent_trace_db_with` to pass the correct discriminator to `DiffTraceInsert`. - - Before `DiffTraceInsert` construction, Rust prefixes the stored `diff_traces.session_id` by source tool: OpenCode normalized payloads store `oc_`, Claude structured payloads store `cc_`, and already same-tool-prefixed values are left unchanged. Raw non-empty session-ID validation still happens before prefixing. + - Before `DiffTraceInsert` construction, Rust prefixes the stored `diff_traces.session_id` by source tool: OpenCode normalized payloads store `oc_`, Claude structured payloads store `cc_`, Pi normalized payloads (`tool_name: "pi"`) store `pi_`, and already same-tool-prefixed values are left unchanged. Unknown `tool_name` values pass the raw session ID through unprefixed. Raw non-empty session-ID validation still happens before prefixing. - Missing `model_id` or `tool_version` stays nullable; Rust does not perform session-level fallback attribution. Direct payload values are persisted as-is after payload-specific validation/normalization, making `diff_traces.model_id` the only active model-attribution storage for diff traces. - Persistence: resolves the current per-checkout AgentTraceDb lazily and inserts the parsed payload fields via `DiffTraceInsert` + `insert_diff_trace()` using tool-prefixed `session_id` plus nullable direct `model_id` and `tool_version`. No parsed-payload artifact is written under `context/tmp`. - Current producers are the OpenCode agent-trace plugin and the generated Claude `sce hooks` command hooks (no TypeScript intermediary). @@ -115,9 +118,9 @@ ## Explicit non-goals in the current baseline - No checkpoint handoff file -- No git-notes persistence +- No git-notes fetch/backfill behavior; the only remote notes operation is the default-enabled, config-disableable, silent fail-open push after a successful local post-commit note write - No backfill/import of existing `context/tmp/*-diff-trace.json` artifacts into AgentTraceDb -- No retry queue replay +- No retry queue replay; failed git-notes pushes are retried only by later successful post-commit hook invocations - No rewrite remap ingestion - No `conversation-trace` retry/backfill path or `context/tmp` artifact persistence - No runtime Claude diff-trace persistence or AgentTraceDb writes from the capture route itself, and no direct artifact/DB writes from the Claude or OpenCode TypeScript runtimes diff --git a/context/sce/doctor-human-text-contract.md b/context/sce/doctor-human-text-contract.md index bafe7792..53384c39 100644 --- a/context/sce/doctor-human-text-contract.md +++ b/context/sce/doctor-human-text-contract.md @@ -46,9 +46,9 @@ This simplification is text-mode only and does not change JSON output requiremen Integration checks are target-scoped. The doctor resolves which integration targets to inspect using the following priority: -1. **Configured targets**: If `.sce/config.json` has `integrations.target` with a non-empty array, only the listed targets (`opencode`, `claude`) are inspected. +1. **Configured targets**: If `.sce/config.json` has `integrations.target` with a non-empty array, only the listed targets (`opencode`, `claude`, `pi`) are inspected. 2. **Empty target array**: If `integrations.target` exists but is an empty array `[]`, the user has not recorded any integration targets. The doctor returns no targets and renders a guidance message instead of group rows. -3. **Directory detection fallback**: When config has no `integrations` property or `integrations.target` property is absent, the doctor falls back to detecting installed repo-root directories — `.opencode/` is detected as OpenCode, `.claude/` is detected as Claude. +3. **Directory detection fallback**: When config has no `integrations` property or `integrations.target` property is absent, the doctor falls back to detecting installed repo-root directories — `.opencode/` is detected as OpenCode, `.claude/` is detected as Claude, and `.pi/` is detected as Pi. 4. **No targets**: When directory detection identifies no installed directories either, the `Integrations` section renders `[FAIL] No integrations installed; run 'sce setup'` and a blocking `NoIntegrationsInstalled` problem is recorded, so the Summary counts it as a blocking problem. Human text output renders group rows only for the resolved targets: @@ -61,10 +61,13 @@ Human text output renders group rows only for the resolved targets: - `ClaudeCode agents` - `ClaudeCode commands` - `ClaudeCode skills` +- `Pi prompts` +- `Pi skills` +- `Pi extensions` Integration checks for this contract inspect installed repo-root artifacts only. -They validate file presence and content hashes against embedded OpenCode and Claude setup assets. -Generated `config/.opencode/**` and `config/.claude/**` trees are out of scope for doctor integration checks in this change stream. +They validate file presence and content hashes against embedded OpenCode, Claude, and Pi setup assets. +Generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees are out of scope for doctor integration checks in this change stream. Claude installed assets are grouped by repo-root `.claude/` relative path: @@ -73,7 +76,13 @@ Claude installed assets are grouped by repo-root `.claude/` relative path: - `commands/**` -> `ClaudeCode commands` - `skills/**` -> `ClaudeCode skills` -For `agents`, `commands`, and `skills`, the installed repo-root trees are required inventory. +Pi installed assets are grouped by repo-root `.pi/` relative path: + +- `prompts/**` -> `Pi prompts` +- `skills/**` -> `Pi skills` +- `extensions/**` -> `Pi extensions` + +For each resolved target, the grouped installed repo-root asset trees are required inventory. If any required file in an integration group is missing or mismatched: - missing child rows render `[MISS]` diff --git a/context/sce/opencode-agent-trace-plugin-runtime.md b/context/sce/opencode-agent-trace-plugin-runtime.md index 5c36c641..55584fbf 100644 --- a/context/sce/opencode-agent-trace-plugin-runtime.md +++ b/context/sce/opencode-agent-trace-plugin-runtime.md @@ -114,7 +114,7 @@ When extraction succeeds, `buildQuestionToolConversationTracePayload(eventPart)` - OpenCode uses a generated TypeScript event runtime as an event-shape adapter before handing normalized diff-trace payloads to the shared Rust hook intake. - Claude registration uses generated `.claude/settings.json` command hooks that call `.claude/hooks/run-sce-or-show-install-guidance.sh` before `sce hooks` (no TypeScript runtime intermediary): matched `PostToolUse Write|Edit|MultiEdit|NotebookEdit` pipes the raw hook event to `sce hooks diff-trace`, and supported conversation events pipe raw hook events to `sce hooks conversation-trace`; `SessionStart` is no longer registered. -- Rust `diff-trace` intake detects Claude payloads via `hook_event_name` and derives structured patches from the raw JSON with `payload_type="structured"` and stored `diff_traces.session_id` prefix `cc_`; OpenCode normalized payloads (no `hook_event_name`) are stored as `payload_type="patch"` and session prefix `oc_`. +- Rust `diff-trace` intake detects Claude payloads via `hook_event_name` and derives structured patches from the raw JSON with `payload_type="structured"` and stored `diff_traces.session_id` prefix `cc_`; OpenCode normalized payloads (no `hook_event_name`) are stored as `payload_type="patch"` and session prefix `oc_`; Pi normalized payloads (`tool_name: "pi"`) follow the same normalized path with session prefix `pi_`. - `sce hooks session-model` is no longer a supported shared Rust boundary. Rust remains the only writer of AgentTraceDb `diff_traces` rows; no active runtime path reads or writes `session_models` and no parsed `context/tmp/*-diff-trace.json` artifacts are written by the TypeScript runtime. - Claude attribution differs from OpenCode attribution: OpenCode reads provider/model data from the OpenCode event and includes `model_id` in the payload, while Claude `diff-trace` best-effort extracts direct model metadata from the raw `PostToolUse` payload and leaves `model_id` nullable when Claude omits it. diff --git a/context/sce/pi-extension-runtime.md b/context/sce/pi-extension-runtime.md new file mode 100644 index 00000000..d6d87560 --- /dev/null +++ b/context/sce/pi-extension-runtime.md @@ -0,0 +1,115 @@ +# Pi extension runtime (SCE) + +Project-local Pi extension that wires the Pi coding agent into SCE runtime +systems. Source of truth: `config/lib/pi-plugin/sce-pi-extension.ts`, emitted +verbatim by `config/pkl/generate.pkl` to `config/.pi/extensions/sce/index.ts` +(same source-to-generated pattern as the OpenCode plugins under +`config/lib/{bash-policy-plugin,agent-trace-plugin}/`). + +## Registration model + +- Pi auto-discovers `.pi/extensions/*/index.ts` in a trusted project; there is + no registration manifest (unlike OpenCode's `opencode.json` `plugin` field). +- The extension is a single self-contained TypeScript file with no npm + dependencies; imports from `@earendil-works/pi-coding-agent` are type-only + (plus the `isToolCallEventType` narrowing helper). +- `config/lib/package.json` declares `@earendil-works/pi-coding-agent` so the + source type-checks under `config/lib` tooling; nothing is installed under + `.pi/extensions/`. +- Entry contract: default export `ExtensionFactory` — `(pi: ExtensionAPI) => + void` — registering handlers via `pi.on(event, handler)`. + +## Implemented slice: bash policy enforcement + +- `pi.on("tool_call", ...)` narrowed to bash via + `isToolCallEventType("bash", event)`; non-bash events pass through. +- Delegates to `spawnSync("sce", ["policy", "bash", "--input", "normalized", + "--output", "json"])` with `{ "command": ... }` on stdin and a 10s timeout — + the same Rust evaluator contract used by the OpenCode plugin and the Claude + `PreToolUse` hook. +- On `decision === "deny"` with a `reason`, the handler returns + `{ block: true, reason }` (Pi's block-by-return contract; OpenCode blocks by + throwing). The reason string comes from Rust + `format_policy_block_message`, so it carries the policy ID and message. +- Fail-open: missing `sce` binary (ENOENT logs install guidance), timeout, + non-zero exit, empty stdout, or invalid JSON all return without blocking. + +## Implemented slice: conversation text capture + +- `pi.on("message_end", ...)` captures completed messages, narrowed to + `role === "user" | "assistant"` (skips `toolResult` and custom messages). +- Pi 0.80.6 exposes no per-message IDs on `message_end`; the handler uses + `AssistantMessage.responseId` when present, otherwise `randomUUID()`. The + parent `message` item and its `message.part` items ship in one mixed batch, + so no cross-event ID mapping is needed. +- Part extraction: `TextContent.text` → `part_type: "text"`, + `ThinkingContent.thinking` → `part_type: "reasoning"`; string user content + becomes a single text part; empty text is skipped. +- Batches are piped to `sce hooks conversation-trace` (same normalized mixed + `message` / `message.part` envelope as the OpenCode agent-trace plugin), + keyed by `ctx.sessionManager.getSessionId()` with `cwd` from `ctx.cwd`. +- Fail-open fire-and-forget spawn: stdio `["pipe", "ignore", "ignore"]`, + ENOENT logs install guidance, the promise resolves on every outcome and is + not awaited by the handler. + +## Implemented slice: edit/write diff capture + +- `pi.on("tool_call", ...)` narrowed to `edit`/`write` records a pending + mutation keyed by `toolCallId`: absolute target path (resolved against + `ctx.cwd`), a repo-relative diff label (absolute when outside `cwd`), and + prior file contents (`undefined` when the file does not exist yet). +- `pi.on("tool_result", ...)` looks up and deletes the pending entry first + (cleanup on every result, including errors and duplicates), skips + `isError` results, then re-reads the file; missing post-contents or + unchanged contents are no-ops. +- Unified diffs come from writing before/after contents to `mkdtemp` temp + files and spawning `git diff --no-index --no-ext-diff` — no npm + dependencies. Only exit status 1 ("files differ") is accepted; the temp dir + is always removed in `finally`. Header labels (`diff --git`, `---`, `+++`) + are rewritten to the diff label only before the first `@@` marker so + content lines are never touched; file creation rewrites to `--- /dev/null`. +- Each diff is emitted twice, both fire-and-forget fail-open spawns: + - `sce hooks conversation-trace`: a mixed batch with a synthetic assistant + `message` (`message_id` = `${toolCallId}-patch`) plus one + `part_type: "patch"` part, mirroring the OpenCode patch-batch shape. + - `sce hooks diff-trace`: normalized `{ sessionID, diff, time, model_id, + tool_name: "pi", tool_version }` where `model_id` is + `${ctx.model.provider}/${ctx.model.id}` or null, and `tool_version` is + resolved from the installed Pi package (walking up from the resolved + entry point, since `package.json` is not in Pi's `exports` map), nullable. + +## Verification + +- `nix develop -c ./config/pkl/check-generated.sh` covers + `config/.pi/extensions` in its parity paths. +- Regeneration must be diff-clean; edit the `config/lib/pi-plugin/` source, + never the generated copy. + +## Rust-side session provenance + +Rust `diff-trace` intake prefixes stored `diff_traces.session_id` values for +`tool_name: "pi"` payloads with `pi_` (idempotent for already-prefixed IDs), +via the `"pi"` arm in `prefixed_diff_trace_session_id()` +(`cli/src/services/hooks/mod.rs`). Unknown tool names still pass through +unprefixed. + +## Asset pipeline, install, and doctor coverage + +- `scripts/prepare-cli-generated-assets.sh` copies the whole `config/.pi` tree + (including `extensions/sce/index.ts`) into + `cli/assets/generated/config/pi/`; `cli/build.rs` embeds that tree wholesale + as `PI_EMBEDDED_ASSETS`, so `sce setup --pi` installs the extension to + repo-root `.pi/extensions/sce/index.ts` with no per-asset enumeration. +- `sce doctor` buckets embedded Pi assets under `extensions/` into a + `Pi extensions` integration group (`collect_pi_integration_groups()` in + `cli/src/services/doctor/inspect.rs`, `pi_asset::EXTENSIONS_DIR`), reporting + present/missing/content-mismatch through the existing Pi integration problem + kinds. + +## Deferred non-goals + +User-shell `!`/`!!` policy enforcement and bash-mutation diff tracing are +deferred (see `context/plans/pi-extension-sce-integration.md`). + +See also: [generated-opencode-plugin-registration.md](generated-opencode-plugin-registration.md), +[bash-tool-policy-enforcement-contract.md](bash-tool-policy-enforcement-contract.md) diff --git a/context/sce/setup-githooks-cli-ux.md b/context/sce/setup-githooks-cli-ux.md index d4f2fc8d..836fadd3 100644 --- a/context/sce/setup-githooks-cli-ux.md +++ b/context/sce/setup-githooks-cli-ux.md @@ -10,7 +10,8 @@ Task `sce-setup-githooks-any-repo` `T04` defines the `sce setup` command-surface - `sce setup --hooks --repo ` - `sce setup --opencode --hooks` - `sce setup --claude --hooks` -- `sce setup --both --hooks` +- `sce setup --pi --hooks` +- `sce setup --all --hooks` - `sce setup` (interactive target selection plus hook install in one run) `--hooks` runs required-hook installation (`pre-commit`, `commit-msg`, `post-commit`) through the setup service hook installer. @@ -31,18 +32,19 @@ Target-install mode contract: - `sce setup` defaults to interactive target selection - default interactive `sce setup` installs selected config assets and required hooks in one run -- `--opencode`, `--claude`, and `--both` remain mutually exclusive for non-interactive target install -- `--non-interactive` is an explicit fail-fast control that disables prompting and requires one target flag (`--opencode`, `--claude`, or `--both`) -- legacy one-purpose invocations remain valid (`sce setup --hooks` for hooks-only, and `sce setup --opencode|--claude|--both` for config-only) +- `--opencode`, `--claude`, `--pi`, and `--all` are mutually exclusive for non-interactive target install; `--both` was removed and now fails as an unknown option (use `--all` for multi-target installs) +- `--non-interactive` is an explicit fail-fast control that disables prompting and requires one target flag (`--opencode`, `--claude`, `--pi`, or `--all`) +- legacy one-purpose invocations remain valid (`sce setup --hooks` for hooks-only, and `sce setup --opencode|--claude|--pi|--all` for config-only) - interactive setup without a TTY returns actionable guidance to rerun with `--non-interactive` plus a target flag ## Integration target persistence -Non-interactive `--opencode`, `--claude`, and `--both` target installs persist the selected target(s) into `.sce/config.json` under `integrations.target` after successful config asset installation: +Non-interactive `--opencode`, `--claude`, `--pi`, and `--all` target installs persist the selected target(s) into `.sce/config.json` under `integrations.target` after successful config asset installation: - `--opencode` records `["opencode"]`. - `--claude` adds `"claude"` to an existing array (e.g. `["opencode"]` → `["opencode", "claude"]`). -- `--both` records both `["opencode", "claude"]` atomically. +- `--pi` adds `"pi"` the same way. +- `--all` records `["opencode", "claude", "pi"]` atomically. - Repeated runs are idempotent — existing targets are deduplicated; previously unrelated config keys (`$schema`, `log_level`, etc.) are preserved. - If the config file does not exist, it is bootstrapped first, then the targets are written. - `--hooks` only setup (`sce setup --hooks`) does not modify `integrations.target`. diff --git a/context/sce/setup-githooks-hook-asset-packaging.md b/context/sce/setup-githooks-hook-asset-packaging.md index ad592370..5861655f 100644 --- a/context/sce/setup-githooks-hook-asset-packaging.md +++ b/context/sce/setup-githooks-hook-asset-packaging.md @@ -19,6 +19,7 @@ Current `post-commit` template behavior is: - resolve `origin` with `git remote get-url origin`; if `sce` is not on `PATH`, print `sce CLI not found. Install it from https://sce.crocoder.dev/docs/getting-started#install-cli` to stderr and exit successfully so missing local CLI installation does not block the commit - if the remote lookup returns a non-empty URL, invoke `sce hooks post-commit --vcs git --remote-url "$remote_url" "$@"` - otherwise still invoke `sce hooks post-commit --vcs git "$@"`; Rust-side validation fails this missing-URL path without blocking git commit completion under the hook script policy. +- the Rust `post-commit` runtime handles Agent Trace persistence after this handoff: DB insertion remains the required persistence path, successful git contexts write the validated full Agent Trace JSON best-effort to git notes under the configured Agent Trace notes ref (default `refs/notes/sce-agent-trace`), and successful local note writes attempt a silent fail-open push to the same `remote_url` unless `policies.agent_trace.push_notes.enabled` is `false`. ## Setup-service accessor surface diff --git a/context/sce/setup-repo-local-config-bootstrap.md b/context/sce/setup-repo-local-config-bootstrap.md index 30f2849b..60b377aa 100644 --- a/context/sce/setup-repo-local-config-bootstrap.md +++ b/context/sce/setup-repo-local-config-bootstrap.md @@ -15,11 +15,12 @@ Task `setup-repo-gate-and-local-config-bootstrap` T02 and `turso-local-db-sync` ## Post-install integration target persistence -After config asset installation succeeds for a non-interactive target (`--opencode`, `--claude`, or `--both`), setup persists the selected target(s) into `.sce/config.json` under `integrations.target`: +After config asset installation succeeds for a non-interactive target (`--opencode`, `--claude`, `--pi`, or `--all`), setup persists the selected target(s) into `.sce/config.json` under `integrations.target`: - `--opencode` records `["opencode"]`. - `--claude` adds `"claude"` to an existing array (e.g. `["opencode"]` → `["opencode", "claude"]`). -- `--both` records both `["opencode", "claude"]` atomically. +- `--pi` adds `"pi"` the same way. +- `--all` records `["opencode", "claude", "pi"]` atomically. (`--both` was removed when `--all` was introduced.) - Repeated runs are idempotent — existing targets are deduplicated; previously unrelated config keys (`$schema`, `log_level`, etc.) are preserved. - If the config file does not exist, it is bootstrapped first, then the targets are written. - `--hooks` only setup does not modify `integrations.target`. diff --git a/flake.nix b/flake.nix index 86432fce..de87458a 100644 --- a/flake.nix +++ b/flake.nix @@ -213,6 +213,7 @@ (pkgs.lib.fileset.maybeMissing ./config/.claude/commands) (pkgs.lib.fileset.maybeMissing ./config/.claude/skills) (pkgs.lib.fileset.maybeMissing ./config/schema/sce-config.schema.json) + ./config/lib/pi-plugin/sce-pi-extension.ts ./config/lib/bash-policy-plugin/opencode-bash-policy-plugin.ts ./config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts ]; @@ -232,6 +233,9 @@ }; nativeBuildInputs = [ bunPackage ]; dontBuild = true; + # Shebang patching would embed the bash store path, breaking the + # fixed-output hash whenever bash changes. + dontFixup = true; installPhase = '' bun install --frozen-lockfile --no-progress # Remove Bun's cache symlinks that point to build directory @@ -243,8 +247,8 @@ outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = if pkgs.stdenv.isLinux - then "sha256-yDKVHH46EzzyiCwBSISEXnJJbqZ2ihvS2H0SGgITaPY=" - else "sha256-KpUXn9+gHy5whrKWXBt9KZI9RwSpa7DLNfRLL/bMT4Q="; + then "sha256-jQAnW/deCeux0/oxmH27lBXmJoK3RGNJDtIGM+Eepmo=" + else "sha256-ia8V9TQM4pHR+A7jEjsARGMX1vljmZDp30M87Wi4oWA="; }; version = pkgs.lib.strings.trim (builtins.readFile ./.version); @@ -275,6 +279,7 @@ mkdir -p "$sourceRoot/cli/assets/generated/config" cp -R ${./config/.opencode} "$sourceRoot/cli/assets/generated/config/opencode" cp -R ${./config/.claude} "$sourceRoot/cli/assets/generated/config/claude" + cp -R ${./config/.pi} "$sourceRoot/cli/assets/generated/config/pi" mkdir -p "$sourceRoot/cli/assets/generated/config/schema" cp ${./config/schema/sce-config.schema.json} "$sourceRoot/cli/assets/generated/config/schema/sce-config.schema.json" cp ${./config/schema/agent-trace.schema.json} "$sourceRoot/cli/assets/generated/config/schema/agent-trace.schema.json" diff --git a/scripts/prepare-cli-generated-assets.sh b/scripts/prepare-cli-generated-assets.sh index 0b622c1a..a9c88563 100644 --- a/scripts/prepare-cli-generated-assets.sh +++ b/scripts/prepare-cli-generated-assets.sh @@ -8,16 +8,18 @@ fi source_opencode="${repo_root}/config/.opencode" source_claude="${repo_root}/config/.claude" +source_pi="${repo_root}/config/.pi" source_schema="${repo_root}/config/schema/sce-config.schema.json" source_agent_trace_schema="${repo_root}/config/schema/agent-trace.schema.json" target_root="${repo_root}/cli/assets/generated/config" -if [ ! -d "${source_opencode}" ] || [ ! -d "${source_claude}" ] || [ ! -f "${source_schema}" ] || [ ! -f "${source_agent_trace_schema}" ]; then +if [ ! -d "${source_opencode}" ] || [ ! -d "${source_claude}" ] || [ ! -d "${source_pi}" ] || [ ! -f "${source_schema}" ] || [ ! -f "${source_agent_trace_schema}" ]; then cat >&2 <