From fde848a922cdba7c9d978acd3fef130371a5721f Mon Sep 17 00:00:00 2001 From: David Abram Date: Mon, 13 Jul 2026 12:41:43 +0200 Subject: [PATCH 01/13] config: Add Pi target renderer and generate config/.pi tree Pi consumes project-local prompts (.pi/prompts) and Agent Skills (.pi/skills) but has no native sub-agent format, so SCE agents are rendered as agent-role prompt templates (agent-{slug}.md) with an act-as-role preamble and $ARGUMENTS input. Adds pi-content.pkl and pi-metadata.pkl mirroring the Claude renderer pair, wires them into generate.pkl, extends check-generated.sh path coverage, and enforces Pi metadata keys in metadata-coverage-check.pkl. Co-authored-by: SCE --- .../.pi/prompts/agent-shared-context-code.md | 62 +++++++++++ .../.pi/prompts/agent-shared-context-plan.md | 68 ++++++++++++ config/.pi/prompts/change-to-plan.md | 17 +++ config/.pi/prompts/commit.md | 42 ++++++++ config/.pi/prompts/handover.md | 15 +++ config/.pi/prompts/next-task.md | 24 +++++ config/.pi/prompts/validate.md | 15 +++ config/.pi/skills/sce-atomic-commit/SKILL.md | 102 ++++++++++++++++++ .../.pi/skills/sce-bootstrap-context/SKILL.md | 55 ++++++++++ config/.pi/skills/sce-context-sync/SKILL.md | 91 ++++++++++++++++ .../.pi/skills/sce-handover-writer/SKILL.md | 46 ++++++++ config/.pi/skills/sce-plan-authoring/SKILL.md | 87 +++++++++++++++ config/.pi/skills/sce-plan-review/SKILL.md | 89 +++++++++++++++ config/.pi/skills/sce-task-execution/SKILL.md | 56 ++++++++++ config/.pi/skills/sce-validation/SKILL.md | 45 ++++++++ config/pkl/check-generated.sh | 2 + config/pkl/generate.pkl | 16 +++ .../pkl/renderers/metadata-coverage-check.pkl | 28 +++++ config/pkl/renderers/pi-content.pkl | 82 ++++++++++++++ config/pkl/renderers/pi-metadata.pkl | 22 ++++ context/architecture.md | 12 ++- context/glossary.md | 3 +- context/overview.md | 8 +- context/plans/pi-harness-integration.md | 95 ++++++++++++++++ 24 files changed, 1073 insertions(+), 9 deletions(-) create mode 100644 config/.pi/prompts/agent-shared-context-code.md create mode 100644 config/.pi/prompts/agent-shared-context-plan.md create mode 100644 config/.pi/prompts/change-to-plan.md create mode 100644 config/.pi/prompts/commit.md create mode 100644 config/.pi/prompts/handover.md create mode 100644 config/.pi/prompts/next-task.md create mode 100644 config/.pi/prompts/validate.md create mode 100644 config/.pi/skills/sce-atomic-commit/SKILL.md create mode 100644 config/.pi/skills/sce-bootstrap-context/SKILL.md create mode 100644 config/.pi/skills/sce-context-sync/SKILL.md create mode 100644 config/.pi/skills/sce-handover-writer/SKILL.md create mode 100644 config/.pi/skills/sce-plan-authoring/SKILL.md create mode 100644 config/.pi/skills/sce-plan-review/SKILL.md create mode 100644 config/.pi/skills/sce-task-execution/SKILL.md create mode 100644 config/.pi/skills/sce-validation/SKILL.md create mode 100644 config/pkl/renderers/pi-content.pkl create mode 100644 config/pkl/renderers/pi-metadata.pkl create mode 100644 context/plans/pi-harness-integration.md 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/pkl/check-generated.sh b/config/pkl/check-generated.sh index 5dae03ff..2851838b 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -46,6 +46,8 @@ paths=( "config/.claude/agents" "config/.claude/commands" "config/.claude/skills" + "config/.pi/prompts" + "config/.pi/skills" "config/schema/sce-config.schema.json" ) diff --git a/config/pkl/generate.pkl b/config/pkl/generate.pkl index d79cdfe7..0a0204c9 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 @@ -63,6 +64,21 @@ 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/.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/context/architecture.md b/context/architecture.md index 33353f9a..3ef40620 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,10 +30,12 @@ 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) @@ -45,12 +48,13 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - 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, no hooks, and no settings/plugin manifest in the current slice. - 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 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. diff --git a/context/glossary.md b/context/glossary.md index 12d3c961..a39262db 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, and Pi prompt/skill assets. +- `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`. diff --git a/context/overview.md b/context/overview.md index d8546643..b82037cd 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. @@ -71,12 +71,12 @@ The setup command parser/dispatch now also supports composable setup+hooks runs - 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, 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, and Pi prompt/skill assets). - 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/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md new file mode 100644 index 00000000..f97eb45d --- /dev/null +++ b/context/plans/pi-harness-integration.md @@ -0,0 +1,95 @@ +# 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; `--both` keeps its current opencode+claude meaning for backwards compatibility. +- 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` behavior is unchanged. +- `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 or `--both` semantics. +- 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. + +- [ ] T02: `Sync and embed Pi assets into the CLI binary` (status:todo) + - Task ID: T02 + - 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. + +- [ ] T03: `Add "pi" integration target ID to config types and schema` (status:todo) + - Task ID: T03 + - 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`. + +- [ ] T04: `Add sce setup --pi and --all flags with Pi asset installation` (status:todo) + - Task ID: T04 + - 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` semantics unchanged (opencode+claude). 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`. + +- [ ] T05: `Add Pi integration health checks to sce doctor` (status:todo) + - Task ID: T05 + - 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`. + +- [ ] T06: `Document the Pi target in architecture and setup docs` (status:todo) + - Task ID: T06 + - 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. + +- [ ] T07: `Validation and cleanup` (status:todo) + - Task ID: T07 + - 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. From ab44d76d883e31327027d75a34f7e55a8e2de217 Mon Sep 17 00:00:00 2001 From: David Abram Date: Mon, 13 Jul 2026 14:09:31 +0200 Subject: [PATCH 02/13] cli: Sync and embed Pi assets into the CLI binary Extend prepare-cli-generated-assets.sh and the Nix build asset staging to copy config/.pi into cli/assets/generated/config/pi, and add a PI_EMBEDDED_ASSETS target in build.rs (emitted with allow(dead_code) until setup --pi consumes it). The re-sync also picks up the previously unsynced integrations block in the sce-config schema mirror. Co-authored-by: SCE --- .../pi/prompts/agent-shared-context-code.md | 62 +++++++++++ .../pi/prompts/agent-shared-context-plan.md | 68 ++++++++++++ .../config/pi/prompts/change-to-plan.md | 17 +++ .../generated/config/pi/prompts/commit.md | 42 ++++++++ .../generated/config/pi/prompts/handover.md | 15 +++ .../generated/config/pi/prompts/next-task.md | 24 +++++ .../generated/config/pi/prompts/validate.md | 15 +++ .../pi/skills/sce-atomic-commit/SKILL.md | 102 ++++++++++++++++++ .../pi/skills/sce-bootstrap-context/SKILL.md | 55 ++++++++++ .../pi/skills/sce-context-sync/SKILL.md | 91 ++++++++++++++++ .../pi/skills/sce-handover-writer/SKILL.md | 46 ++++++++ .../pi/skills/sce-plan-authoring/SKILL.md | 87 +++++++++++++++ .../config/pi/skills/sce-plan-review/SKILL.md | 89 +++++++++++++++ .../pi/skills/sce-task-execution/SKILL.md | 56 ++++++++++ .../config/pi/skills/sce-validation/SKILL.md | 45 ++++++++ .../config/schema/sce-config.schema.json | 17 +++ cli/build.rs | 13 +++ context/glossary.md | 2 +- context/overview.md | 2 +- context/plans/pi-harness-integration.md | 5 +- flake.nix | 1 + scripts/prepare-cli-generated-assets.sh | 5 +- 22 files changed, 855 insertions(+), 4 deletions(-) create mode 100644 cli/assets/generated/config/pi/prompts/agent-shared-context-code.md create mode 100644 cli/assets/generated/config/pi/prompts/agent-shared-context-plan.md create mode 100644 cli/assets/generated/config/pi/prompts/change-to-plan.md create mode 100644 cli/assets/generated/config/pi/prompts/commit.md create mode 100644 cli/assets/generated/config/pi/prompts/handover.md create mode 100644 cli/assets/generated/config/pi/prompts/next-task.md create mode 100644 cli/assets/generated/config/pi/prompts/validate.md create mode 100644 cli/assets/generated/config/pi/skills/sce-atomic-commit/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-bootstrap-context/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-context-sync/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-handover-writer/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-plan-authoring/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-plan-review/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-task-execution/SKILL.md create mode 100644 cli/assets/generated/config/pi/skills/sce-validation/SKILL.md 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..23966f09 100644 --- a/cli/assets/generated/config/schema/sce-config.schema.json +++ b/cli/assets/generated/config/schema/sce-config.schema.json @@ -338,6 +338,23 @@ } }, "additionalProperties": false + }, + "integrations": { + "type": "object", + "properties": { + "target": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "opencode", + "claude" + ] + }, + "uniqueItems": true + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/cli/build.rs b/cli/build.rs index 917d7f57..83049d32 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -12,14 +12,23 @@ 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", + // Unused until setup --pi wiring lands; drop this once consumed. + allow_dead_code: true, }, TargetSpec { const_name: "HOOK_EMBEDDED_ASSETS", relative_root: "assets/hooks", + allow_dead_code: false, }, ]; @@ -29,6 +38,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 +188,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/context/glossary.md b/context/glossary.md index a39262db..448c1d7d 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -64,7 +64,7 @@ - `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 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`; the `PI_EMBEDDED_ASSETS` constant is emitted with `#[allow(dead_code)]` until setup `--pi` wiring consumes it. - `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. diff --git a/context/overview.md b/context/overview.md index b82037cd..1a4331b7 100644 --- a/context/overview.md +++ b/context/overview.md @@ -24,7 +24,7 @@ The CLI now also enforces a shared output-format parser contract in `cli/src/ser 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 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`. diff --git a/context/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md index f97eb45d..e3b786b6 100644 --- a/context/plans/pi-harness-integration.md +++ b/context/plans/pi-harness-integration.md @@ -48,8 +48,11 @@ Decisions resolved with the user (2026-07-13): - 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. -- [ ] T02: `Sync and embed Pi assets into the CLI binary` (status:todo) +- [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. diff --git a/flake.nix b/flake.nix index 86432fce..5b8d939a 100644 --- a/flake.nix +++ b/flake.nix @@ -275,6 +275,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 < Date: Mon, 13 Jul 2026 14:31:59 +0200 Subject: [PATCH 03/13] config: Add "pi" integration target ID to types and schema Teach the config layer about the pi target so .sce/config.json can persist and validate it. Adds the Pi variant to IntegrationTargetId with parse/round-trip support and unit tests, extends the integrations.target enum in the Pkl schema (JSON schemas regenerated and the embedded CLI mirror re-synced), and adds a compile-only no-op doctor match arm until full Pi doctor coverage lands in a later task. Co-authored-by: SCE --- .../config/schema/sce-config.schema.json | 3 +- cli/src/services/config/types.rs | 36 ++++++++++++++++++- cli/src/services/doctor/inspect.rs | 2 ++ config/pkl/base/sce-config-schema.pkl | 2 +- config/schema/sce-config.schema.json | 3 +- context/cli/config-precedence-contract.md | 2 +- context/plans/pi-harness-integration.md | 6 +++- 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/cli/assets/generated/config/schema/sce-config.schema.json b/cli/assets/generated/config/schema/sce-config.schema.json index 23966f09..dfd6eff1 100644 --- a/cli/assets/generated/config/schema/sce-config.schema.json +++ b/cli/assets/generated/config/schema/sce-config.schema.json @@ -348,7 +348,8 @@ "type": "string", "enum": [ "opencode", - "claude" + "claude", + "pi" ] }, "uniqueItems": true diff --git a/cli/src/services/config/types.rs b/cli/src/services/config/types.rs index 7a95c1b0..be3bdc55 100644 --- a/cli/src/services/config/types.rs +++ b/cli/src/services/config/types.rs @@ -269,6 +269,7 @@ pub(crate) struct PerDbRetryConfig { pub(crate) enum IntegrationTargetId { Opencode, Claude, + Pi, } impl IntegrationTargetId { @@ -276,8 +277,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 +289,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/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index a974122f..cc9ca7d7 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -495,6 +495,8 @@ fn inspect_repository_integrations( inspect_claude_integration_health(&claude_groups, problems); integration_groups.extend(claude_groups); } + // Pi doctor coverage lands in a later task; compile-only arm for now. + IntegrationTargetId::Pi => {} } } diff --git a/config/pkl/base/sce-config-schema.pkl b/config/pkl/base/sce-config-schema.pkl index beb20335..e8ec8440 100644 --- a/config/pkl/base/sce-config-schema.pkl +++ b/config/pkl/base/sce-config-schema.pkl @@ -172,7 +172,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/schema/sce-config.schema.json b/config/schema/sce-config.schema.json index 23966f09..dfd6eff1 100644 --- a/config/schema/sce-config.schema.json +++ b/config/schema/sce-config.schema.json @@ -348,7 +348,8 @@ "type": "string", "enum": [ "opencode", - "claude" + "claude", + "pi" ] }, "uniqueItems": true diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index a2689941..495797d5 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -73,7 +73,7 @@ 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`. diff --git a/context/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md index e3b786b6..18b80ee8 100644 --- a/context/plans/pi-harness-integration.md +++ b/context/plans/pi-harness-integration.md @@ -58,8 +58,12 @@ Decisions resolved with the user (2026-07-13): - 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. -- [ ] T03: `Add "pi" integration target ID to config types and schema` (status:todo) +- [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. From 4569dd1a76fefaa16972b5fcff744c5c3d4931f9 Mon Sep 17 00:00:00 2001 From: David Abram Date: Mon, 13 Jul 2026 15:18:32 +0200 Subject: [PATCH 04/13] setup: Add --pi and --all target flags, remove --both `sce setup` now installs the embedded Pi asset tree into .pi/ via --pi and expands --all to opencode+claude+pi, replacing the removed --both flag (now rejected as an unknown option). The interactive prompt offers OpenCode, Claude, Pi, and All, and installed targets persist into .sce/config.json integrations.target as before. EmbeddedAssetSelectionIter is generalized from a two-variant enum to a slice-chaining iterator so any concrete-target expansion works, and PI_EMBEDDED_ASSETS drops its dead_code allowance now that it is consumed. Co-authored-by: SCE --- cli/build.rs | 3 +- cli/src/cli_schema.rs | 11 +- cli/src/command_surface.rs | 3 +- cli/src/services/default_paths.rs | 5 + cli/src/services/doctor/inspect.rs | 2 +- cli/src/services/parse/command_runtime.rs | 11 +- cli/src/services/setup/mod.rs | 196 ++++++++++++++---- context/cli/cli-command-surface.md | 6 +- context/cli/default-path-catalog.md | 1 + context/glossary.md | 6 +- context/overview.md | 4 +- context/plans/pi-harness-integration.md | 13 +- context/sce/setup-githooks-cli-ux.md | 14 +- .../sce/setup-repo-local-config-bootstrap.md | 5 +- 14 files changed, 208 insertions(+), 72 deletions(-) diff --git a/cli/build.rs b/cli/build.rs index 83049d32..c6069bc9 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -22,8 +22,7 @@ const TARGETS: &[TargetSpec] = &[ TargetSpec { const_name: "PI_EMBEDDED_ASSETS", relative_root: "assets/generated/config/pi", - // Unused until setup --pi wiring lands; drop this once consumed. - allow_dead_code: true, + allow_dead_code: false, }, TargetSpec { const_name: "HOOK_EMBEDDED_ASSETS", 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/default_paths.rs b/cli/src/services/default_paths.rs index 932a41dc..c59f423d 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"; } @@ -595,6 +596,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 cc9ca7d7..5ec991c5 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -475,7 +475,7 @@ fn inspect_repository_integrations( "No integrations are installed. Run 'sce setup' to install OpenCode and/or Claude integration assets.", ), remediation: String::from( - "Run 'sce setup --opencode', 'sce setup --claude', or 'sce setup --both' to install integration assets.", + "Run 'sce setup --opencode', 'sce setup --claude', or 'sce setup --all' to install integration assets.", ), next_action: "manual_steps", }); 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/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index f3b83479..4a870dd5 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -57,11 +57,11 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `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. 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`. 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/glossary.md b/context/glossary.md index 448c1d7d..142965f8 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -60,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/**`, `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`; the `PI_EMBEDDED_ASSETS` constant is emitted with `#[allow(dead_code)]` until setup `--pi` wiring consumes it. +- `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. diff --git a/context/overview.md b/context/overview.md index 1a4331b7..e2c967b6 100644 --- a/context/overview.md +++ b/context/overview.md @@ -23,7 +23,7 @@ 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 `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. @@ -65,7 +65,7 @@ The local DB service now provides `LocalDb` as a thin `TursoDb` ali 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 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 diff --git a/context/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md index 18b80ee8..fd23900a 100644 --- a/context/plans/pi-harness-integration.md +++ b/context/plans/pi-harness-integration.md @@ -9,7 +9,8 @@ The change follows the existing target pattern end to end: a new Pkl renderer pr 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; `--both` keeps its current opencode+claude meaning for backwards compatibility. +- 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 @@ -17,7 +18,7 @@ Decisions resolved with the user (2026-07-13): - `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` behavior is unchanged. +- `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`). @@ -28,7 +29,7 @@ Decisions resolved with the user (2026-07-13): - 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 or `--both` semantics. +- 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 @@ -69,8 +70,12 @@ Decisions resolved with the user (2026-07-13): - 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`. -- [ ] T04: `Add sce setup --pi and --all flags with Pi asset installation` (status:todo) +- [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` semantics unchanged (opencode+claude). 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. 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-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`. From e074823d6e249a99e56b1c04f79dd8cd36fd50dc Mon Sep 17 00:00:00 2001 From: David Abram Date: Mon, 13 Jul 2026 16:25:05 +0200 Subject: [PATCH 05/13] doctor: Add Pi integration health checks Add Pi target resolution, prompt/skill integration grouping, and problem-kind plumbing so `sce doctor` validates installed `.pi` assets for missing or mismatched embedded content. Co-authored-by: SCE --- cli/src/services/default_paths.rs | 9 ++ cli/src/services/doctor/inspect.rs | 172 +++++++++++++++++++++- cli/src/services/doctor/mod.rs | 10 ++ cli/src/services/doctor/types.rs | 5 + cli/src/services/lifecycle.rs | 3 + context/architecture.md | 2 +- context/cli/cli-command-surface.md | 4 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/patterns.md | 4 +- context/plans/pi-harness-integration.md | 6 +- context/sce/agent-trace-hook-doctor.md | 6 +- context/sce/doctor-human-text-contract.md | 17 ++- 13 files changed, 219 insertions(+), 23 deletions(-) diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index c59f423d..23f417ec 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -385,6 +385,11 @@ 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"; +} + #[allow(dead_code)] pub(crate) mod context_dir { pub const CONTEXT_ROOT: &str = "context"; @@ -445,6 +450,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) } diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index 5ec991c5..af98762d 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,7 @@ 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_PROMPTS_LABEL, PI_SKILLS_LABEL, }; use super::{is_executable, DoctorDependencies, DoctorMode, REQUIRED_HOOKS}; @@ -401,7 +402,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 +448,9 @@ fn resolve_doctor_integration_targets(repository_root: &Path) -> Vec {} + IntegrationTargetId::Pi => { + let pi_groups = collect_pi_integration_groups(resolved_root); + inspect_pi_integration_health(&pi_groups, problems); + integration_groups.extend(pi_groups); + } } } @@ -729,6 +736,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, @@ -935,6 +951,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, @@ -1163,6 +1282,45 @@ 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(); + + 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); + } + } + + sort_integration_children(&mut prompt_children); + sort_integration_children(&mut skill_children); + + vec![ + IntegrationGroupHealth { + label: PI_PROMPTS_LABEL, + children: prompt_children, + }, + IntegrationGroupHealth { + label: PI_SKILLS_LABEL, + children: skill_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..917a473b 100644 --- a/cli/src/services/doctor/types.rs +++ b/cli/src/services/doctor/types.rs @@ -8,6 +8,8 @@ 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"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum Readiness { @@ -138,11 +140,14 @@ pub(crate) enum ProblemKind { OpenCodeIntegrationContentMismatch, ClaudeIntegrationFilesMissing, ClaudeIntegrationContentMismatch, + PiIntegrationFilesMissing, + PiIntegrationContentMismatch, OpenCodePluginRegistryInvalid, OpenCodeAssetMissingOrInvalid, HookReadFailed, OpenCodeAssetReadFailed, ClaudeAssetReadFailed, + PiAssetReadFailed, AgentTraceDbConnectionFailed, AgentTraceDbSchemaNotReady, } 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/context/architecture.md b/context/architecture.md index 3ef40620..5c87c264 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -121,7 +121,7 @@ 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. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 4a870dd5..29c75cf2 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -63,7 +63,7 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `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,7 +88,7 @@ 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). diff --git a/context/context-map.md b/context/context-map.md index 623ba8f4..f0987531 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -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) - `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) diff --git a/context/glossary.md b/context/glossary.md index 142965f8..623787e2 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -142,7 +142,7 @@ - `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. +- `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/patterns.md b/context/patterns.md index 987ae776..66d3fffe 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}`, 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. diff --git a/context/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md index fd23900a..8ad9cf59 100644 --- a/context/plans/pi-harness-integration.md +++ b/context/plans/pi-harness-integration.md @@ -81,8 +81,12 @@ Decisions resolved with the user (2026-07-13): - 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`. -- [ ] T05: `Add Pi integration health checks to sce doctor` (status:todo) +- [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. 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/doctor-human-text-contract.md b/context/sce/doctor-human-text-contract.md index bafe7792..e5862e30 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,12 @@ Human text output renders group rows only for the resolved targets: - `ClaudeCode agents` - `ClaudeCode commands` - `ClaudeCode skills` +- `Pi prompts` +- `Pi skills` 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 +75,12 @@ 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` + +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]` From 0016473c71d2cd1aad9d538cf69fe3985d615b5b Mon Sep 17 00:00:00 2001 From: David Abram Date: Mon, 13 Jul 2026 16:47:41 +0200 Subject: [PATCH 06/13] docs: Document Pi target setup and validation Update README, Pkl generation docs, and architecture context to describe Pi as a first-class generated config target with setup flags and generated output ownership. Co-authored-by: SCE --- README.md | 20 +++++------ config/pkl/README.md | 12 ++++--- context/architecture.md | 3 +- context/plans/pi-harness-integration.md | 47 +++++++++++++++++++++++-- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index fd4618e8..766b989a 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 | — | +| 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 plus `.pi/skills/` packages; hooks and Bash policy enforcement remain OpenCode/Claude-only. ## Why this exists diff --git a/config/pkl/README.md b/config/pkl/README.md index c72e8cdc..e30a7c7b 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, and `config/.pi/skills/*/SKILL.md` Agent Skills-format skill packages. Pi is manual-profile only and has no generated hooks or settings/plugin manifest in the current slice. + 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`, and `config/.pi/{prompts,skills}/**`. -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 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/context/architecture.md b/context/architecture.md index 5c87c264..9d83b61c 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -63,6 +63,7 @@ Generated authored classes: - agent definitions - command definitions - skill definitions +- Pi prompt templates and Agent Skills packages - shared runtime library files - Claude project settings - OpenCode plugin entrypoints @@ -135,7 +136,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `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/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/plans/pi-harness-integration.md b/context/plans/pi-harness-integration.md index 8ad9cf59..359383ee 100644 --- a/context/plans/pi-harness-integration.md +++ b/context/plans/pi-harness-integration.md @@ -77,7 +77,7 @@ Decisions resolved with the user (2026-07-13): - 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` semantics unchanged (opencode+claude). Out — hooks for Pi (no Pi hook assets), doctor. + - 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`. @@ -92,15 +92,23 @@ Decisions resolved with the user (2026-07-13): - 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`. -- [ ] T06: `Document the Pi target in architecture and setup docs` (status:todo) +- [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. -- [ ] T07: `Validation and cleanup` (status:todo) +- [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. @@ -109,3 +117,36 @@ Decisions resolved with the user (2026-07-13): ## 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. From 51668ae803266eae3c11ba8fbff627d9558eb322 Mon Sep 17 00:00:00 2001 From: David Abram Date: Mon, 13 Jul 2026 17:01:47 +0200 Subject: [PATCH 07/13] pi: Add generated prompt and skill assets, enable pi target Materialize the .pi/ prompts and skills produced by the Pi asset sync so the repo carries the same SCE workflows (plan authoring, task execution, validation, commit, handover) for the Pi harness that already exist for Claude and opencode. Add "pi" to the integration targets in .sce/config.json so setup/doctor treat it as active. --- .pi/prompts/agent-shared-context-code.md | 62 +++++++++++++ .pi/prompts/agent-shared-context-plan.md | 68 +++++++++++++++ .pi/prompts/change-to-plan.md | 17 ++++ .pi/prompts/commit.md | 42 +++++++++ .pi/prompts/handover.md | 15 ++++ .pi/prompts/next-task.md | 24 +++++ .pi/prompts/validate.md | 15 ++++ .pi/skills/sce-atomic-commit/SKILL.md | 102 ++++++++++++++++++++++ .pi/skills/sce-bootstrap-context/SKILL.md | 55 ++++++++++++ .pi/skills/sce-context-sync/SKILL.md | 91 +++++++++++++++++++ .pi/skills/sce-handover-writer/SKILL.md | 46 ++++++++++ .pi/skills/sce-plan-authoring/SKILL.md | 87 ++++++++++++++++++ .pi/skills/sce-plan-review/SKILL.md | 89 +++++++++++++++++++ .pi/skills/sce-task-execution/SKILL.md | 56 ++++++++++++ .pi/skills/sce-validation/SKILL.md | 45 ++++++++++ .sce/config.json | 3 +- 16 files changed, 816 insertions(+), 1 deletion(-) create mode 100644 .pi/prompts/agent-shared-context-code.md create mode 100644 .pi/prompts/agent-shared-context-plan.md create mode 100644 .pi/prompts/change-to-plan.md create mode 100644 .pi/prompts/commit.md create mode 100644 .pi/prompts/handover.md create mode 100644 .pi/prompts/next-task.md create mode 100644 .pi/prompts/validate.md create mode 100644 .pi/skills/sce-atomic-commit/SKILL.md create mode 100644 .pi/skills/sce-bootstrap-context/SKILL.md create mode 100644 .pi/skills/sce-context-sync/SKILL.md create mode 100644 .pi/skills/sce-handover-writer/SKILL.md create mode 100644 .pi/skills/sce-plan-authoring/SKILL.md create mode 100644 .pi/skills/sce-plan-review/SKILL.md create mode 100644 .pi/skills/sce-task-execution/SKILL.md create mode 100644 .pi/skills/sce-validation/SKILL.md 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", From c5a4ca34b261cc6c788ad8ab573fcd2bfb98c47f Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 09:54:38 +0200 Subject: [PATCH 08/13] pi: Add SCE bash-policy extension emitted via Pkl Pi previously had no runtime hook into SCE bash-tool policy. Add a project-local Pi extension (config/lib/pi-plugin/sce-pi-extension.ts, emitted verbatim by config/pkl/generate.pkl to config/.pi/extensions/sce/index.ts) whose tool_call handler delegates bash-command evaluation to `sce policy bash --input normalized --output json` and blocks denied commands with the policy ID and reason. All failure paths (missing CLI, timeout, non-zero exit, invalid JSON) fail open. Adds the type-only @earendil-works/pi-coding-agent dependency, extends config/pkl/check-generated.sh drift coverage to config/.pi/extensions, and includes the plan and context updates for this slice. Co-authored-by: SCE --- config/.pi/extensions/sce/index.ts | 82 ++++++ config/lib/bun.lock | 253 ++++++++++++++++++ config/lib/package.json | 1 + config/lib/pi-plugin/sce-pi-extension.ts | 82 ++++++ config/lib/tsconfig.json | 6 +- config/pkl/check-generated.sh | 1 + config/pkl/generate.pkl | 4 + context/architecture.md | 4 +- context/context-map.md | 1 + context/glossary.md | 2 +- context/overview.md | 6 +- context/patterns.md | 2 +- context/plans/pi-extension-sce-integration.md | 105 ++++++++ context/sce/pi-extension-runtime.md | 54 ++++ flake.nix | 5 +- 15 files changed, 599 insertions(+), 9 deletions(-) create mode 100644 config/.pi/extensions/sce/index.ts create mode 100644 config/lib/pi-plugin/sce-pi-extension.ts create mode 100644 context/plans/pi-extension-sce-integration.md create mode 100644 context/sce/pi-extension-runtime.md diff --git a/config/.pi/extensions/sce/index.ts b/config/.pi/extensions/sce/index.ts new file mode 100644 index 00000000..c9272c11 --- /dev/null +++ b/config/.pi/extensions/sce/index.ts @@ -0,0 +1,82 @@ +import { spawnSync } from "node:child_process"; +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"; + +/** + * 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; + } +} + +export default function sceExtension(pi: ExtensionAPI): void { + 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; + }); +} 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..c9272c11 --- /dev/null +++ b/config/lib/pi-plugin/sce-pi-extension.ts @@ -0,0 +1,82 @@ +import { spawnSync } from "node:child_process"; +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"; + +/** + * 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; + } +} + +export default function sceExtension(pi: ExtensionAPI): void { + 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; + }); +} 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/check-generated.sh b/config/pkl/check-generated.sh index 2851838b..39bbab85 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -48,6 +48,7 @@ paths=( "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 0a0204c9..d80f3244 100644 --- a/config/pkl/generate.pkl +++ b/config/pkl/generate.pkl @@ -9,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) { @@ -79,6 +80,9 @@ output { text = document.rendered } } + ["config/.pi/extensions/sce/index.ts"] { + text = piExtensionSource + } ["config/.opencode/lib/bash-policy-presets.json"] { text = bashPolicyPresetCatalogSource } diff --git a/context/architecture.md b/context/architecture.md index 9d83b61c..f03a1a62 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -48,13 +48,13 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - 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, no hooks, and no settings/plugin manifest in the current slice. +- 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`, `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, 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 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. diff --git a/context/context-map.md b/context/context-map.md index f0987531..4707e213 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -56,6 +56,7 @@ Feature/domain context: - `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, and the planned conversation/diff-trace slices) - `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 623787e2..573e377b 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -6,7 +6,7 @@ - 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/**`, `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, and Pi prompt/skill 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`. diff --git a/context/overview.md b/context/overview.md index e2c967b6..2e6c872c 100644 --- a/context/overview.md +++ b/context/overview.md @@ -35,7 +35,7 @@ The Rust CLI also centralizes SCE-owned web URI construction in `cli/src/service 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. +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. @@ -71,12 +71,12 @@ The setup command parser/dispatch now also supports composable setup+hooks runs - 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), `config/.claude/**`, and `config/.pi/**` (Pi prompts/skills, manual profile only) 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/**`, `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, and Pi prompt/skill 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. diff --git a/context/patterns.md b/context/patterns.md index 66d3fffe..20f5049e 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`, `config/{claude_root}/{agents,commands,skills,hooks,settings.json}`, and `config/{pi_root}/{prompts,skills}`, where roots map to `.opencode`, `.claude`, and `.pi`). +- 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 diff --git a/context/plans/pi-extension-sce-integration.md b/context/plans/pi-extension-sce-integration.md new file mode 100644 index 00000000..628051d2 --- /dev/null +++ b/context/plans/pi-extension-sce-integration.md @@ -0,0 +1,105 @@ +# 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. + +- [ ] T02: `Add conversation text capture to the Pi extension` (status:todo) + - Task ID: T02 + - 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. + +- [ ] T03: `Add edit/write diff capture to the Pi extension` (status:todo) + - Task ID: T03 + - 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). + +- [ ] T04: `Add pi_ session prefix and Pi ingestion coverage in Rust` (status:todo) + - Task ID: T04 + - 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"`. + +- [ ] T05: `Sync, embed, and doctor-check the Pi extension asset` (status:todo) + - Task ID: T05 + - 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`. + +- [ ] T06: `Document the Pi extension integration` (status:todo) + - Task ID: T06 + - 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/sce/pi-extension-runtime.md b/context/sce/pi-extension-runtime.md new file mode 100644 index 00000000..9ce96368 --- /dev/null +++ b/context/sce/pi-extension-runtime.md @@ -0,0 +1,54 @@ +# 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. + +## 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. + +## Planned extensions (not yet implemented) + +Conversation trace capture (`message_end` → `sce hooks conversation-trace`), +edit/write diff capture (`sce hooks diff-trace`, `tool_name: "pi"`), `pi_` +session-ID prefixing in Rust, asset sync/embedding, and doctor coverage are +tracked in `context/plans/pi-extension-sce-integration.md` (T02–T07). +Deferred non-goals: user-shell `!`/`!!` policy enforcement and bash-mutation +diff tracing. + +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/flake.nix b/flake.nix index 5b8d939a..e70ff46d 100644 --- a/flake.nix +++ b/flake.nix @@ -232,6 +232,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,7 +246,7 @@ outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = if pkgs.stdenv.isLinux - then "sha256-yDKVHH46EzzyiCwBSISEXnJJbqZ2ihvS2H0SGgITaPY=" + then "sha256-jQAnW/deCeux0/oxmH27lBXmJoK3RGNJDtIGM+Eepmo=" else "sha256-KpUXn9+gHy5whrKWXBt9KZI9RwSpa7DLNfRLL/bMT4Q="; }; From 80a12f634d883417da79bc648f1a4ba9432e7bb3 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 11:27:55 +0200 Subject: [PATCH 09/13] pi: Add conversation trace capture to SCE extension Capture completed Pi user and assistant messages on message_end, extract text and reasoning parts, and send mixed message/message.part batches to sce hooks conversation-trace with fail-open fire-and-forget behavior. Co-authored-by: SCE --- config/.pi/extensions/sce/index.ts | 137 +++++++++++++++++- config/lib/pi-plugin/sce-pi-extension.ts | 137 +++++++++++++++++- context/context-map.md | 2 +- context/plans/pi-extension-sce-integration.md | 5 +- context/sce/pi-extension-runtime.md | 23 ++- flake.nix | 2 +- 6 files changed, 298 insertions(+), 8 deletions(-) diff --git a/config/.pi/extensions/sce/index.ts b/config/.pi/extensions/sce/index.ts index c9272c11..44c3b6a7 100644 --- a/config/.pi/extensions/sce/index.ts +++ b/config/.pi/extensions/sce/index.ts @@ -1,4 +1,5 @@ -import { spawnSync } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { type ExtensionAPI, isToolCallEventType, @@ -16,6 +17,31 @@ interface JsonPolicyResult { 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[]; +}; + /** * 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 @@ -56,6 +82,100 @@ function evaluateBashCommandPolicy(command: string): JsonPolicyResult | 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 }; +} + export default function sceExtension(pi: ExtensionAPI): void { pi.on("tool_call", (event) => { if (!isToolCallEventType("bash", event)) { @@ -79,4 +199,19 @@ export default function sceExtension(pi: ExtensionAPI): void { return undefined; }); + + 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/pi-plugin/sce-pi-extension.ts b/config/lib/pi-plugin/sce-pi-extension.ts index c9272c11..44c3b6a7 100644 --- a/config/lib/pi-plugin/sce-pi-extension.ts +++ b/config/lib/pi-plugin/sce-pi-extension.ts @@ -1,4 +1,5 @@ -import { spawnSync } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { type ExtensionAPI, isToolCallEventType, @@ -16,6 +17,31 @@ interface JsonPolicyResult { 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[]; +}; + /** * 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 @@ -56,6 +82,100 @@ function evaluateBashCommandPolicy(command: string): JsonPolicyResult | 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 }; +} + export default function sceExtension(pi: ExtensionAPI): void { pi.on("tool_call", (event) => { if (!isToolCallEventType("bash", event)) { @@ -79,4 +199,19 @@ export default function sceExtension(pi: ExtensionAPI): void { return undefined; }); + + 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/context/context-map.md b/context/context-map.md index 4707e213..9e9c087e 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -56,7 +56,7 @@ Feature/domain context: - `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, and the planned conversation/diff-trace slices) +- `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 the planned diff-trace slice) - `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/plans/pi-extension-sce-integration.md b/context/plans/pi-extension-sce-integration.md index 628051d2..395f2c4f 100644 --- a/context/plans/pi-extension-sce-integration.md +++ b/context/plans/pi-extension-sce-integration.md @@ -58,8 +58,11 @@ Decisions resolved with the user (2026-07-13): - 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. -- [ ] T02: `Add conversation text capture to the Pi extension` (status:todo) +- [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. diff --git a/context/sce/pi-extension-runtime.md b/context/sce/pi-extension-runtime.md index 9ce96368..2ff02185 100644 --- a/context/sce/pi-extension-runtime.md +++ b/context/sce/pi-extension-runtime.md @@ -34,6 +34,24 @@ verbatim by `config/pkl/generate.pkl` to `config/.pi/extensions/sce/index.ts` - 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. + ## Verification - `nix develop -c ./config/pkl/check-generated.sh` covers @@ -43,10 +61,9 @@ verbatim by `config/pkl/generate.pkl` to `config/.pi/extensions/sce/index.ts` ## Planned extensions (not yet implemented) -Conversation trace capture (`message_end` → `sce hooks conversation-trace`), -edit/write diff capture (`sce hooks diff-trace`, `tool_name: "pi"`), `pi_` +Edit/write diff capture (`sce hooks diff-trace`, `tool_name: "pi"`), `pi_` session-ID prefixing in Rust, asset sync/embedding, and doctor coverage are -tracked in `context/plans/pi-extension-sce-integration.md` (T02–T07). +tracked in `context/plans/pi-extension-sce-integration.md` (T03–T07). Deferred non-goals: user-shell `!`/`!!` policy enforcement and bash-mutation diff tracing. diff --git a/flake.nix b/flake.nix index e70ff46d..2ca388a7 100644 --- a/flake.nix +++ b/flake.nix @@ -247,7 +247,7 @@ outputHashAlgo = "sha256"; outputHash = if pkgs.stdenv.isLinux then "sha256-jQAnW/deCeux0/oxmH27lBXmJoK3RGNJDtIGM+Eepmo=" - else "sha256-KpUXn9+gHy5whrKWXBt9KZI9RwSpa7DLNfRLL/bMT4Q="; + else "sha256-ia8V9TQM4pHR+A7jEjsARGMX1vljmZDp30M87Wi4oWA="; }; version = pkgs.lib.strings.trim (builtins.readFile ./.version); From 2d015c0ee572d96dfaf3279c9e6a15bd819700f7 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 12:33:57 +0200 Subject: [PATCH 10/13] pi: Add edit/write diff capture to SCE extension Record unified diffs for Pi edit/write tool calls: a tool_call handler captures pending mutations (target path, prior contents) keyed by toolCallId, and the tool_result handler produces a diff via git diff --no-index on temp files, rewriting header labels to the repo-relative path. Each diff is emitted fire-and-forget as a synthetic patch conversation part and a normalized diff-trace payload with tool_name "pi", nullable model_id/tool_version. Failed edits and no-op writes produce no rows; missing sce binary fails open. Co-authored-by: SCE --- config/.pi/extensions/sce/index.ts | 238 ++++++++++++++++++ config/lib/pi-plugin/sce-pi-extension.ts | 238 ++++++++++++++++++ context/context-map.md | 2 +- context/plans/pi-extension-sce-integration.md | 5 +- context/sce/pi-extension-runtime.md | 31 ++- 5 files changed, 509 insertions(+), 5 deletions(-) diff --git a/config/.pi/extensions/sce/index.ts b/config/.pi/extensions/sce/index.ts index 44c3b6a7..25770ff8 100644 --- a/config/.pi/extensions/sce/index.ts +++ b/config/.pi/extensions/sce/index.ts @@ -1,5 +1,15 @@ 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, @@ -42,6 +52,21 @@ 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 @@ -176,7 +201,139 @@ function buildMessageEndConversationTracePayload( 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; @@ -200,6 +357,87 @@ export default function sceExtension(pi: ExtensionAPI): void { 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") { diff --git a/config/lib/pi-plugin/sce-pi-extension.ts b/config/lib/pi-plugin/sce-pi-extension.ts index 44c3b6a7..25770ff8 100644 --- a/config/lib/pi-plugin/sce-pi-extension.ts +++ b/config/lib/pi-plugin/sce-pi-extension.ts @@ -1,5 +1,15 @@ 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, @@ -42,6 +52,21 @@ 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 @@ -176,7 +201,139 @@ function buildMessageEndConversationTracePayload( 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; @@ -200,6 +357,87 @@ export default function sceExtension(pi: ExtensionAPI): void { 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") { diff --git a/context/context-map.md b/context/context-map.md index 9e9c087e..2f29241c 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -56,7 +56,7 @@ Feature/domain context: - `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 the planned diff-trace slice) +- `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`) - `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/plans/pi-extension-sce-integration.md b/context/plans/pi-extension-sce-integration.md index 395f2c4f..6b550ce3 100644 --- a/context/plans/pi-extension-sce-integration.md +++ b/context/plans/pi-extension-sce-integration.md @@ -68,8 +68,11 @@ Decisions resolved with the user (2026-07-13): - 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. -- [ ] T03: `Add edit/write diff capture to the Pi extension` (status:todo) +- [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. diff --git a/context/sce/pi-extension-runtime.md b/context/sce/pi-extension-runtime.md index 2ff02185..c2120a69 100644 --- a/context/sce/pi-extension-runtime.md +++ b/context/sce/pi-extension-runtime.md @@ -52,6 +52,32 @@ verbatim by `config/pkl/generate.pkl` to `config/.pi/extensions/sce/index.ts` 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 @@ -61,9 +87,8 @@ verbatim by `config/pkl/generate.pkl` to `config/.pi/extensions/sce/index.ts` ## Planned extensions (not yet implemented) -Edit/write diff capture (`sce hooks diff-trace`, `tool_name: "pi"`), `pi_` -session-ID prefixing in Rust, asset sync/embedding, and doctor coverage are -tracked in `context/plans/pi-extension-sce-integration.md` (T03–T07). +`pi_` session-ID prefixing in Rust, asset sync/embedding, and doctor coverage +are tracked in `context/plans/pi-extension-sce-integration.md` (T04–T07). Deferred non-goals: user-shell `!`/`!!` policy enforcement and bash-mutation diff tracing. From dbd5847cf2cf31a0d06b0ce5f380797fa4f3c038 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 12:44:26 +0200 Subject: [PATCH 11/13] hooks: Add pi_ session ID prefix for Pi diff-trace ingestion Pi diff-trace payloads previously stored raw session IDs, making Pi provenance indistinguishable from unprefixed sources. Add PI_TOOL_NAME and DIFF_TRACE_PI_SESSION_ID_PREFIX with a "pi" match arm in prefixed_diff_trace_session_id(), keeping oc_/cc_ behavior and unknown-tool passthrough unchanged. Tests cover fresh prefixing, idempotency, normalized payload persistence, and post-commit intersection provenance. Co-authored-by: SCE --- cli/src/services/hooks/mod.rs | 100 ++++++++++++++++++ context/architecture.md | 2 +- context/cli/cli-command-surface.md | 2 +- context/context-map.md | 4 +- context/glossary.md | 2 +- context/overview.md | 6 +- context/patterns.md | 2 +- context/plans/pi-extension-sce-integration.md | 5 +- context/sce/agent-trace-db.md | 2 +- .../sce/agent-trace-hooks-command-routing.md | 2 +- .../opencode-agent-trace-plugin-runtime.md | 2 +- context/sce/pi-extension-runtime.md | 12 ++- 12 files changed, 126 insertions(+), 15 deletions(-) diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 3dfca559..3e9700eb 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -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(), }; @@ -2284,6 +2287,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); diff --git a/context/architecture.md b/context/architecture.md index f03a1a62..ae5225a5 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -128,7 +128,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `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 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`). diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 29c75cf2..c4aa2e9b 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -53,7 +53,7 @@ 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 `. diff --git a/context/context-map.md b/context/context-map.md index 2f29241c..9d9e0b7f 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -52,11 +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 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, `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`) +- `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`, and Rust-side `pi_` stored session-ID prefixing) - `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 573e377b..18b60e17 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -141,7 +141,7 @@ - `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. +- `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. diff --git a/context/overview.md b/context/overview.md index 2e6c872c..2329cce5 100644 --- a/context/overview.md +++ b/context/overview.md @@ -34,7 +34,7 @@ The shared default path service in `cli/src/services/default_paths.rs` is now th 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... +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. @@ -59,10 +59,10 @@ 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`, 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, `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|--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`. diff --git a/context/patterns.md b/context/patterns.md index 20f5049e..085538ec 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -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 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/pi-extension-sce-integration.md b/context/plans/pi-extension-sce-integration.md index 6b550ce3..9f14ee31 100644 --- a/context/plans/pi-extension-sce-integration.md +++ b/context/plans/pi-extension-sce-integration.md @@ -78,8 +78,11 @@ Decisions resolved with the user (2026-07-13): - 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). -- [ ] T04: `Add pi_ session prefix and Pi ingestion coverage in Rust` (status:todo) +- [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. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index e43c7162..157ab772 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -174,7 +174,7 @@ 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. diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 29ede5ba..9d4a6a27 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -66,7 +66,7 @@ - **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). 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 index c2120a69..c2fa5fa7 100644 --- a/context/sce/pi-extension-runtime.md +++ b/context/sce/pi-extension-runtime.md @@ -85,10 +85,18 @@ verbatim by `config/pkl/generate.pkl` to `config/.pi/extensions/sce/index.ts` - 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. + ## Planned extensions (not yet implemented) -`pi_` session-ID prefixing in Rust, asset sync/embedding, and doctor coverage -are tracked in `context/plans/pi-extension-sce-integration.md` (T04–T07). +Asset sync/embedding and doctor coverage +are tracked in `context/plans/pi-extension-sce-integration.md` (T05–T07). Deferred non-goals: user-shell `!`/`!!` policy enforcement and bash-mutation diff tracing. From b50ed83677e07a57487a54103d4ba90325fdbefc Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 12:56:21 +0200 Subject: [PATCH 12/13] pi: Embed SCE extension asset and add doctor Pi extensions group The Pi SCE extension existed only as a project-local source file and was not shipped through the asset pipeline, so `sce setup --pi` could not install it and `sce doctor` had no visibility into its health. Sync `config/.pi/extensions/sce/index.ts` into the generated CLI assets (embedded wholesale via the existing whole-tree copy), and bucket embedded `extensions/**` Pi assets into a new `Pi extensions` doctor integration group so missing or drifted extension files are flagged through the existing Pi integration problem kinds. Co-authored-by: SCE --- README.md | 8 +- .../config/pi/extensions/sce/index.ts | 455 ++++++++++++++++++ cli/src/services/default_paths.rs | 1 + cli/src/services/doctor/inspect.rs | 14 +- cli/src/services/doctor/types.rs | 1 + config/pkl/README.md | 6 +- context/architecture.md | 5 +- context/context-map.md | 4 +- context/plans/pi-extension-sce-integration.md | 10 +- context/sce/doctor-human-text-contract.md | 2 + context/sce/pi-extension-runtime.md | 23 +- 11 files changed, 508 insertions(+), 21 deletions(-) create mode 100644 cli/assets/generated/config/pi/extensions/sce/index.ts diff --git a/README.md b/README.md index 766b989a..73731433 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,11 @@ SCE writes a local, per-repo audit trail conforming to the [Agent Trace](https:/ | Feature | OpenCode | Claude Code | Pi | |---|---|---|---| | Generated config | ✓ | ✓ | ✓ | -| Hooks + Bash policy | ✓ | ✓ | — | -| Conversation + diff trace | ✓ | ✓ | — | -| Model / session attribution | full | full | — | +| Hooks + Bash policy | ✓ | ✓ | ✓ | +| Conversation + diff trace | ✓ | ✓ | ✓ | +| Model / session attribution | full | full | full | | Shared `context/` | ✓ | ✓ | ✓ | -OpenCode, Claude Code, and Pi are first-class generated config targets. Pi receives `.pi/prompts/` command and agent-role prompt templates plus `.pi/skills/` packages; hooks and Bash policy enforcement remain OpenCode/Claude-only. - ## Why this exists AI sped up code generation, not team alignment. Without shared, durable context, every agent session starts cold and every reviewer re-derives the *why* from scratch, what we call **cognitive debt**. SCE is the infrastructure to pay it down. 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/src/services/default_paths.rs b/cli/src/services/default_paths.rs index 23f417ec..d7c03ac3 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -388,6 +388,7 @@ pub(crate) mod claude_asset { 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)] diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index af98762d..0341785e 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -21,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, PI_PROMPTS_LABEL, PI_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}; @@ -1289,6 +1290,7 @@ fn collect_pi_integration_groups(repository_root: &Path) -> Vec>(); 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); @@ -1303,11 +1305,17 @@ fn collect_pi_integration_groups(repository_root: &Path) -> Vec Vec.{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: @@ -64,6 +64,7 @@ Generated authored classes: - command definitions - skill definitions - Pi prompt templates and Agent Skills packages +- Pi extension entrypoint - shared runtime library files - Claude project settings - OpenCode plugin entrypoints @@ -134,7 +135,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - 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/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. diff --git a/context/context-map.md b/context/context-map.md index 9d9e0b7f..d81a35c5 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -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, Claude, plus Pi 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) @@ -56,7 +56,7 @@ Feature/domain context: - `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`, and Rust-side `pi_` stored session-ID prefixing) +- `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/plans/pi-extension-sce-integration.md b/context/plans/pi-extension-sce-integration.md index 9f14ee31..77a8ed4b 100644 --- a/context/plans/pi-extension-sce-integration.md +++ b/context/plans/pi-extension-sce-integration.md @@ -88,15 +88,21 @@ Decisions resolved with the user (2026-07-13): - 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"`. -- [ ] T05: `Sync, embed, and doctor-check the Pi extension asset` (status:todo) +- [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`. -- [ ] T06: `Document the Pi extension integration` (status:todo) +- [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. diff --git a/context/sce/doctor-human-text-contract.md b/context/sce/doctor-human-text-contract.md index e5862e30..53384c39 100644 --- a/context/sce/doctor-human-text-contract.md +++ b/context/sce/doctor-human-text-contract.md @@ -63,6 +63,7 @@ Human text output renders group rows only for the resolved targets: - `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, Claude, and Pi setup assets. @@ -79,6 +80,7 @@ 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: diff --git a/context/sce/pi-extension-runtime.md b/context/sce/pi-extension-runtime.md index c2fa5fa7..d6d87560 100644 --- a/context/sce/pi-extension-runtime.md +++ b/context/sce/pi-extension-runtime.md @@ -93,12 +93,23 @@ via the `"pi"` arm in `prefixed_diff_trace_session_id()` (`cli/src/services/hooks/mod.rs`). Unknown tool names still pass through unprefixed. -## Planned extensions (not yet implemented) - -Asset sync/embedding and doctor coverage -are tracked in `context/plans/pi-extension-sce-integration.md` (T05–T07). -Deferred non-goals: user-shell `!`/`!!` policy enforcement and bash-mutation -diff tracing. +## 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) From 5c1bc19128e5f23a3b8011dad9bf51b97416feaf Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 14 Jul 2026 13:17:59 +0200 Subject: [PATCH 13/13] pi: Add SCE extension runtime hooks Implement the Pi extension entrypoint with bash policy enforcement, conversation trace capture, and edit/write diff trace forwarding through the SCE CLI hooks. Co-authored-by: SCE --- .pi/extensions/sce/index.ts | 455 ++++++++++++++++++++++++++++++++++++ flake.nix | 1 + 2 files changed, 456 insertions(+) create mode 100644 .pi/extensions/sce/index.ts 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/flake.nix b/flake.nix index 2ca388a7..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 ];