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
@@ -21,4 +21,13 @@ data class JsonSchemaProperty(
// array is a misread brief, not a valid artifact).
val minItems: Int = 0,
val items: JsonSchemaProperty? = null,
// For `type == "object"` (either a nested property or an array's `items`): the object's
// own shape. Lets a schema describe structured collections — e.g. the discovery stage's
// `questions: array of { prompt, options, header, ... }`. Without this the parser rejects
// any nested object schema ("unknown key 'properties'"), silently dropping the kind.
// JSON-schema convention: nested objects allow extra keys by default (additionalProperties=true),
// unlike the top-level [JsonSchema] which defaults to strict.
val properties: Map<String, JsonSchemaProperty> = emptyMap(),
val required: List<String> = emptyList(),
val additionalProperties: Boolean = true,
)