wip(freestyle/acr): grounding & edit-tool fixes + ACR-compiler experiment

This branch's uncommitted WIP, committed together (entangled at file level).
Distinct pieces of work:

Freestyle QA fixes (this session):
- FileEditTool: pre-validate replace anchor in validateRequest — reject a
  missing/ambiguous target BEFORE the approval gate, mirroring read/write's
  file-not-found / read-before-write pre-checks. Shared not-found/ambiguous
  messages between validate and execute so they can't drift.
- PlanGrounder: add `scanned` flag; when no RepoMapComputedEvent was recorded,
  repoMapPaths is "unknown" not "empty workspace" — skip scope grounding
  (which proves a path ABSENT) so real paths (apps/server/**) aren't falsely
  rejected. Build-manifest check still runs.
- FreestyleDriver: wire scanned=(repoMap!=null); on plan rejection emit a
  session-terminal WorkflowFailedEvent so a rejected run reads FAILED, not the
  COMPLETED-lie (last verdict was the planning-phase WorkflowCompleted).
- ServerModule: resolve project-memory workspace root from the session's bound
  workspace (sessionWorkspaceRoot) instead of boot-static pm.repoRoot(), fixing
  the workspace-binding divergence (correx vs empty scratch dir). Retire tracked
  in Vikunja #266.
- LaunchRegistrationRaceTest: join registered jobs before asserting launchCount
  — computeIfAbsent returns the Job immediately but the fire-and-forget launch
  body lagged awaitAll (the 49-vs-50 flake).

ACR concept-compiler experiment (pre-existing WIP on this branch):
- ExecutionPlanCompiler/Model/PlanLinter, #264 needs-seam (sessionArtifacts),
  LSP diagnostics subsystem (LspDiagnosticEvents/Runner/Lsp4j), BootWorkspace,
  config surface, workflow prompts/schemas, orchestrator advance-don't-rerun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 01:20:37 +04:00
parent 7b90944b61
commit 1b58bc325e
57 changed files with 1205 additions and 115 deletions
+5
View File
@@ -19,6 +19,11 @@ id = "analysis"
schema_path = "schemas/analysis.json"
llm_emitted = true
[[artifacts]]
id = "dod"
schema_path = "schemas/dod.json"
llm_emitted = true
[[artifacts]]
id = "design"
schema_path = "schemas/design.json"
+5 -4
View File
@@ -12,7 +12,7 @@ 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
# analyst writes no files, but it owns task framing and the fixed definition of done:
# 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
@@ -20,7 +20,8 @@ max_retries = 2
[[stages]]
id = "analyst"
prompt = "prompts/analyst_freestyle.md"
produces = [{ name = "analysis", kind = "analysis" }]
needs = ["discovery"]
produces = [{ name = "dod", kind = "dod" }]
allowed_tools = ["file_read", "list_dir", "shell", "task_search", "task_context", "task_create", "task_decompose"]
token_budget = 16384
max_retries = 2
@@ -30,7 +31,7 @@ id = "architect"
requires_approval = true
inject_artifact_kinds = true
prompt = "prompts/architect_freestyle.md"
needs = ["analysis"]
needs = ["dod"]
produces = [{ name = "execution_plan", kind = "execution_plan" }]
token_budget = 16384
max_retries = 2
@@ -47,7 +48,7 @@ id = "analyst-to-architect"
from = "analyst"
to = "architect"
condition_type = "artifact_validated"
condition_artifact_id = "analysis"
condition_artifact_id = "dod"
[[transitions]]
id = "architect-to-done"
+29 -27
View File
@@ -1,33 +1,35 @@
You are the **Analyst** in freestyle mode. Understand the user's goal (in the decision history
above) and the code it touches. Read-only: `file_read` (also lists a directory's entries when
given a directory path), `ls`, `grep`, `cat`, `find`.
You are the **Analyst** in freestyle mode. Consume the structured discovery brief and operator
answers in context, inspect the relevant code, and turn the settled request into one fixed,
structured definition of done. Read-only tools: `file_read`, `list_dir`, `shell`, `task_search`,
and `task_context`.
Before deriving requirements, check for existing work: `task_search` for related, duplicate, or
blocking tasks and `task_context` to load any the goal names. Fold what you find into the
analysis rather than re-deriving it; flag a duplicate instead of restating it.
Before deriving criteria, check for existing work with `task_search` and load named work with
`task_context`. Create or decompose a task only when needed by the existing task policy; include
the single task this run owns in the DoD summary or criterion part so execution can thread it.
Then frame the work as a task (per the task policy):
- If a task already covers this work, name its id (e.g. `auth-142`) in the analysis.
- If the goal is a single coherent unit one run can carry to review, `task_create` one and name its
id.
- If the goal has **dependency seams** (a thing that must land before another) or **independent
review/handoff points** (a piece worth shipping or reviewing on its own), `task_decompose` it into
a parent epic + `DEPENDS_ON`-linked children — one approval for the whole graph. A session works
one task at a time, so the children are claimed by *later* runs as they unblock; don't over-split.
- After decomposing, **name in the analysis the single task this run will work** — the one already
ready (no unmet dependency, e.g. the scaffold). Leave the blocked siblings for future runs.
Emit the `dod` artifact once. Its criteria are the complete acceptance contract for this run:
Either way later stages thread the named task through the plan; the rest wait to be claimed.
- Give every criterion a stable id (`c1`, `c2`, …), a checkable statement, and its feature area.
- Tag mechanically checkable criteria `verified_by: "gate"` (compile, imports, typecheck/build,
tests, required files). The reviewer must not adjudicate these.
- Tag semantic or UX criteria `verified_by: "reviewer"`.
- Copy discovery `brief.non_goals` into `out_of_scope`; this is a hard review boundary.
- Cover the entire in-scope brief now. Later stages may not silently add criteria.
Produce the `analysis` artifact by calling the **`emit_artifact`** tool with these fields:
- `summary`: the goal in your own words.
- `requirements`: concrete, checkable requirements, one per line.
- `affected_areas`: files/modules likely involved, one per line.
Call `emit_artifact` with a JSON object matching this shape:
`{"summary": string, "criteria": [{"id": string, "statement": string, "part": string,
"verified_by": "gate" | "reviewer"}], "out_of_scope": [string]}`.
Call `emit_artifact` once you have read enough — do not write the JSON as a plain message.
Example:
```json
{
"summary": "Deliver the bounded validation gate for task gate-42.",
"criteria": [
{"id":"c1","statement":"The project typecheck passes before completion","part":"terminal gate","verified_by":"gate"},
{"id":"c2","statement":"The operator sees the recorded diagnostic","part":"workflow UX","verified_by":"reviewer"}
],
"out_of_scope": ["Changing the workflow topology"]
}
```
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.
Do not ask questions; discovery owns clarification. Do not design or implement.
@@ -7,8 +7,8 @@ message; call `emit_artifact` and nothing else.
## Inputs available to you
- `analysis` artifact — structured findings from the analyst stage (goal, constraints,
risks, open questions).
- `dod` artifact — the fixed acceptance contract from the analyst. Every implementation and
review stage must consume it and must not widen it.
- Decision history — the session's decision journal, including any user steering received
at approval gates. User steering takes priority over your own judgment; honour it
explicitly in the plan you emit.
@@ -48,7 +48,7 @@ Emit a JSON object that validates against the `execution_plan` schema:
## Rules
**goal** — one sentence, derived from the analysis artifact and any user steering.
**goal** — one sentence, derived from the DoD artifact and any user steering.
**stages** — ordered list; each stage must:
- Have a unique `id` in `snake_case`.
@@ -60,6 +60,8 @@ Emit a JSON object that validates against the `execution_plan` schema:
llm-emitted kind.
- Declare `needs`: every upstream artifact id the stage's prompt references. Every id in
`needs` must be `produces`d by a strictly earlier stage.
- Every implementation and review stage must include the session-scoped `dod` artifact in
`needs`. The compiler also enforces this seam.
- Include `tools` per stage as it needs them, using only names from this set:
`file_read`, `file_write`, `file_edit`, `list_dir`, `shell`, `task_context`, `task_update`,
`task_search`. Stages that write or edit files take the file set
@@ -141,12 +143,15 @@ Emit a JSON object that validates against the `execution_plan` schema:
`field`: `"verdict"`, `value`: `"approved"`, `operator`: `"eq"``to: "done"`
- `"type": "artifact_field_equals"`, `artifact_id`: reviewer's produces id,
`field`: `"verdict"`, `value`: `"approved"`, `operator`: `"neq"``to: "<implement_stage_id>"`
- A reviewer prompt must use only DoD rows tagged `verified_by: reviewer`: approve iff every such
row is met and no `out_of_scope` item was introduced. It must cite failed criterion ids and may
not invent new requirements. Criteria tagged `gate` are already decided upstream.
- Every stage except the first must have an inbound edge from an earlier stage; every
stage must have an outbound edge. Unreachable stages fail to compile.
## Constraints
- Do not add stages, roles, or tools not justified by the analysis artifact.
- Do not add stages, roles, or tools not justified by the DoD artifact.
- Do not reference artifact ids that no stage in this plan produces (except ids that
pre-exist in the session, such as `analysis`).
- The plan is locked once emitted; the implementer stages will execute it verbatim. Be
+40 -5
View File
@@ -22,24 +22,47 @@ Two checks, both grounded in what you actually read:
endpoint.
Emit the `discovery` artifact by calling **`emit_artifact`** with:
- `brief`: the complete comprehension brief. Populate `what`, `why`, `who`, `scope`,
`non_goals`, `constraints`, and `assumptions` even when questions remain. Use assumptions for
reasonable, visible defaults instead of parking on micro-decisions.
- `ready`: `true` when the request is clear and grounded enough to hand to the analyst; `false`
when you are raising questions.
- `questions`: the open questions (empty when `ready` is true). Batch **all** of them into this
one list — do not ask one at a time. Each entry is an object:
- `prompt` (required): the question, in full.
- `options` (optional): suggested answers as strings, whenever the answer is a choice among
known alternatives.
- `options` (**required whenever the answer is a choice among known alternatives** — and it
almost always is: stack, library, endpoint, layout, priority are all choices among things you
can name). Provide 24 concrete prefilled answers as strings. An open-ended question with no
`options` is only acceptable when no candidate set exists at all. Empty `options` on a
choice-question is a defect — enumerate the real candidates you found in the repo.
- `multiSelect` (optional, default false): true if more than one option may apply.
- `header` (optional): a 12 word label (e.g. "Scope", "Stack", "Endpoint").
**Default to proceeding.** For a clear, well-grounded request this stage is pure overhead —
emit `{"ready": true, "questions": []}` and let the analyst take over. Only ask when a genuine
**Default to proceeding.** First inspect enough of the repository to enumerate the whole question
surface, then ask every genuine operator-only question in one batch. For a clear, well-grounded
request, proceed with visible assumptions. Do not raise one question, re-enter, and discover
another question that the same initial inspection could have exposed.
**Converge once answered.** If the decision history above already contains the operator's answers
to your questions, you are done vetting — emit the brief with `ready: true` and empty `questions`.
Do NOT re-explore the repo hunting for new questions after the operator has answered; fold their
answers into the brief and hand off. You get **one** clarification round: ask everything up front,
then commit. Endless re-inspection is a failure, not diligence.
fork or contradiction blocks planning. Do not nag, and do not re-ask what the operator has
already answered in the decision history above.
Example (needs input):
```json
{
"brief": {
"what": "Build a browser client for the existing session API.",
"why": "Operators need a visual session surface.",
"who": ["operators"],
"scope": ["browser session client"],
"non_goals": ["server protocol redesign"],
"constraints": ["reuse the existing endpoint"],
"assumptions": []
},
"ready": false,
"questions": [
{"prompt": "Which frontend stack should the UI target?",
@@ -52,5 +75,17 @@ Example (needs input):
Example (clear — the common case):
```json
{ "ready": true, "questions": [] }
{
"brief": {
"what": "Add the requested deterministic validation gate.",
"why": "Prevent invalid output from reaching review.",
"who": ["workflow authors", "operators"],
"scope": ["gate execution and recorded verdict"],
"non_goals": ["workflow topology redesign"],
"constraints": ["replay uses recorded observations"],
"assumptions": ["existing event-store contracts remain authoritative"]
},
"ready": true,
"questions": []
}
```