41ed6414c6
Bundles three operator-reliability guardrails (Vikunja #28/#29/#30) plus the in-flight branch WIP they were built on top of (reasoning_content capture, operator/project profile editor, write-jail workspaceRoot fix) — the tree is interdependent (SessionOrchestrator references reasoningArtifactId from the WIP) and does not compile as separable subsets, so it lands as one commit. Guardrails: - #28 mid-stage steering: ClientMessage.SteerSession -> GlobalStreamHandler -> orchestrator.submitSteering, reusing SteeringNoteAddedEvent + existing context fold (advisory, non-authoritative; invariants #3/#7). Closes the gap where steering typed off an approval gate was silently dropped. - #29 shell-in-file guardrail: ShellInFileContentRule (core:toolintent) blocks a file_write whose content is a bare shell command (e.g. "mkdir -p ..."); FileWriteTool description now advertises auto-mkdir of parent dirs. Basename-allowlist so the extensionless case is caught; scripts/Makefiles/multiline exempt. - #30 pt1 capability-gap detector: deterministic CapabilityGapDetector maps stage intent -> implied ToolCapability, compares to granted tools, emits advisory CapabilityGapDetectedEvent in FreestyleDriver.lockAndRun. Recorded, never fails the gate and never auto-grants (invariants #3/#4/#5). Reflection rung is pt2. Verified: ./gradlew check green (whole tree).
58 lines
2.0 KiB
TOML
58 lines
2.0 KiB
TOML
id = "freestyle_planning"
|
|
start = "discovery"
|
|
|
|
# discovery runs before the analyst: it vets the request against the real repo and either clears
|
|
# it to plan or parks on a batched list of operator questions (grounds contradictions too, e.g. a
|
|
# request naming an endpoint the server doesn't expose). Read-only; owns clarification exclusively.
|
|
[[stages]]
|
|
id = "discovery"
|
|
prompt = "prompts/discovery.md"
|
|
produces = [{ name = "discovery", kind = "discovery" }]
|
|
allowed_tools = ["file_read", "list_dir", "shell"]
|
|
token_budget = 16384
|
|
max_retries = 2
|
|
|
|
# analyst writes no files, but it owns task framing: task_search/task_context (read-only) find
|
|
# existing work; task_create (T2, approval-gated — a task is an event-log entry, not a file write)
|
|
# opens a single task; task_decompose (T2, one approval for the whole graph) splits a goal with
|
|
# dependency seams into parent + DEPENDS_ON-linked children. Either way the analysis names the task
|
|
# id the run will work, so the architect threads it into the plan's implementation stages.
|
|
[[stages]]
|
|
id = "analyst"
|
|
prompt = "prompts/analyst_freestyle.md"
|
|
produces = [{ name = "analysis", kind = "analysis" }]
|
|
allowed_tools = ["file_read", "list_dir", "shell", "task_search", "task_context", "task_create", "task_decompose"]
|
|
token_budget = 16384
|
|
max_retries = 2
|
|
|
|
[[stages]]
|
|
id = "architect"
|
|
requires_approval = true
|
|
inject_artifact_kinds = true
|
|
prompt = "prompts/architect_freestyle.md"
|
|
needs = ["analysis"]
|
|
produces = [{ name = "execution_plan", kind = "execution_plan" }]
|
|
token_budget = 16384
|
|
max_retries = 2
|
|
|
|
[[transitions]]
|
|
id = "discovery-to-analyst"
|
|
from = "discovery"
|
|
to = "analyst"
|
|
condition_type = "artifact_validated"
|
|
condition_artifact_id = "discovery"
|
|
|
|
[[transitions]]
|
|
id = "analyst-to-architect"
|
|
from = "analyst"
|
|
to = "architect"
|
|
condition_type = "artifact_validated"
|
|
condition_artifact_id = "analysis"
|
|
|
|
[[transitions]]
|
|
id = "architect-to-done"
|
|
from = "architect"
|
|
to = "done"
|
|
condition_type = "artifact_validated"
|
|
condition_artifact_id = "execution_plan"
|