feat(guardrails): steering channel + shell-in-file rule + capability-gap detector

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).
This commit is contained in:
2026-07-07 13:27:59 +04:00
parent 879672a47d
commit 41ed6414c6
43 changed files with 1592 additions and 94 deletions
@@ -26,32 +26,8 @@ Produce the `analysis` artifact by calling the **`emit_artifact`** tool with the
Call `emit_artifact` once you have read enough — do not write the JSON as a plain message.
If — and only if — something genuinely blocks a plan (an ambiguous goal, a missing decision, a
fork only the user can resolve), add a `questions` array. Each entry is an object:
- `prompt` (required): the question, in full.
- `options` (optional): an array of suggested answers as strings. Offer these whenever the
answer is a choice among known alternatives.
- `multiSelect` (optional, default false): true if more than one option may apply.
- `header` (optional): a 12 word label for the question (e.g. "Scope", "Stack").
Greenfield or new-surface work (a new UI, app, or module) almost always hides such a fork even
when the high-level goal is clear: the build tool, styling approach, state/routing libraries, or
component library are choices only the user can pin, and a placeholder instruction does not
resolve them. Ask about stack/tooling in that case rather than guessing.
Example:
```json
{
"summary": "...",
"requirements": ["..."],
"affected_areas": ["..."],
"questions": [
{"prompt": "Which frontend stack should the UI target?",
"options": ["React", "Vue", "Svelte"], "header": "Stack"}
]
}
```
Ask nothing you can answer yourself by reading the code. Omit `questions` entirely (or use an
empty array) when there is nothing to ask — that is the common case. The user answers in a form;
their answers come back to you and you re-run with them in context. Do not design or plan yet.
Always produce the analysis — this is your single exit. Open questions and operator forks are the
**Discovery** stage's job, and it has already run before you: any ambiguity or contradiction the
user needed to resolve was raised and answered upstream, and those answers are in the decision
history above. Treat the request as settled, ground your requirements in what you actually read,
and do not ask the user anything. Do not design or plan yet.