Skip to content

[AI-701] validate-plan artifact endpoint + workspace_root emission - #318

Merged
alexeyzimarev merged 4 commits into
mainfrom
alexeyzimarev/ai-701-validate-plan-artifacts
Jul 12, 2026
Merged

alexeyzimarev merged 4 commits into
mainfrom
alexeyzimarev/ai-701-validate-plan-artifacts

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

CLI half of AI-701 — pairs with kurrent-io/kcap-server#1039 (either side works without the other).

  • kcap validate-plan now makes two calls on new servers: GET /api/sessions/{id}/plan-artifacts?chain=true for plans (primary selected by is_primary, degraded/truncated/unavailable markers byte-identical to the server's PlanRowRendering) plus the existing recap call for current-session work rows and whats_done. A 404 from plan-artifacts falls back to the legacy recap-only filtering unchanged; 200-empty renders "No plan found"; an unavailable primary is reported as not validatable.
  • Session-start hooks for all supported harnesses (and the history importer, remap-aware) emit an optional workspace_root from the existing fail-open git-root discovery — feeds workspace-scoped artifact identity server-side.
  • PlanArtifactDto/PlanArtifactsResponseDto registered in the AOT CapacitorJsonContext; osx-arm64 publish clean with zero AOT warnings.

🤖 Generated with Claude Code

alexeyzimarev and others added 2 commits July 11, 2026 22:05
ValidatePlanCommand now tries GET /api/sessions/{id}/plan-artifacts?chain=true
first (new plan/spec/design/checklist discovery), rendering the primary
artifact under "## Plan" with truncated/unavailable markers; work rows and
"what's done" summaries still come from the existing recap endpoint. A 404
(older server or non-visible session) falls back to the original recap-only
rendering byte-for-byte.

Adds PlanArtifactDto/PlanArtifactsResponseDto to Capacitor.Cli.Core, mirroring
the server's Capacitor.Plans.PlanArtifact field-for-field, registered in the
AOT source-generated CapacitorJsonContext.

Session-start hook payloads across every harness (Claude, Codex, Gemini,
Copilot, Kiro, Pi, OpenCode, Antigravity) and the historical importer now
carry a best-effort workspace_root (via the existing GitRepository.FindRoot
git-root discovery), fail-open — omitted when cwd is absent or no repo is
found.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_root

Fix three review findings: the degraded (is_complete==false) plan state
was never rendered in `kcap validate-plan`'s plan section; the historical
importer derived workspace_root from the raw, unremapped transcript cwd
instead of the already resolved cwd; add a multi-artifact rendering-order
regression test.
@linear-code

linear-code Bot commented Jul 12, 2026

Copy link
Copy Markdown

AI-701

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Validate-plan via plan-artifacts endpoint and emit workspace_root in session hooks

✨ Enhancement 🧪 Tests 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Add validate-plan support for /plan-artifacts with legacy recap-only fallback on 404.
• Emit best-effort workspace_root in all session-start hooks and historical imports.
• Register plan-artifact DTOs for AOT JSON and add regression-focused unit tests.
Diagram

graph TD
  V["ValidatePlanCommand"] --> PA["GET /api/sessions/{id}/plan-artifacts?chain=true"] --> R["GET /api/sessions/{id}/recap?chain=true"]
  V --> J["CapacitorJsonContext (AOT)"]
  H["Session-start hooks"] --> GR["GitRepository.FindRoot"] --> H
  I["ImportCommand (history)"] --> GR --> I
  H --> SS["POST /hooks/session-start"]
  I --> SS
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Single server endpoint for validate-plan rendering inputs
  • ➕ Avoids two HTTP round-trips and reduces partial-failure states
  • ➕ Centralizes plan/work/summary selection rules server-side for consistency
  • ➖ Requires larger server-side change and coordinated rollout
  • ➖ Less reusable for other clients that want artifacts separately
2. Shared contract package for PlanRowRendering marker strings
  • ➕ Eliminates risk of marker text drifting between CLI and server
  • ➕ Reduces duplicated “byte-for-byte” constants across repos
  • ➖ Introduces cross-repo dependency/versioning concerns
  • ➖ May complicate AOT/linker trimming if not carefully structured
3. Model server enums as CLI enums (kind/source/content_state/confidence)
  • ➕ Compile-time safety for comparisons and rendering rules
  • ➕ Better IDE discoverability and documentation
  • ➖ Higher maintenance when server expands vocabulary
  • ➖ CLI still mostly displays values; string passthrough is simpler

Recommendation: The chosen approach (try /plan-artifacts first, fall back to legacy recap-only on 404) is the best fit for a staggered CLI/server rollout and preserves byte-for-byte legacy output when the new route is unavailable. The only strategic improvement worth considering later is a consolidated server endpoint if latency/consistency becomes a problem; for now, the two-call design keeps responsibilities clear (artifacts vs. recap) and minimizes compatibility risk.

Files changed (14) +856 / -39

Enhancement (10) +310 / -33
Models.csAdd plan-artifact DTOs and register them in AOT JSON context +48/-0

Add plan-artifact DTOs and register them in AOT JSON context

• Introduces PlanArtifactDto and PlanArtifactsResponseDto to mirror the server contract for /plan-artifacts. Registers both types in CapacitorJsonContext for source-generated serialization/AOT-friendly publishing.

src/Capacitor.Cli.Core/Models.cs

AntigravityHookCommand.csEmit workspace_root on session-start when cwd is inside a git repo +6/-1

Emit workspace_root on session-start when cwd is inside a git repo

• Extends the forwarded session-start payload to include workspace_root derived via GitRepository.FindRoot(cwd). Keeps behavior fail-open by omitting the field when cwd is null or no repo root is found.

src/Capacitor.Cli/Commands/AntigravityHookCommand.cs

ClaudeHookCommand.csInject workspace_root into Claude session-start JSON payload +18/-0

Inject workspace_root into Claude session-start JSON payload

• Adds best-effort git-root discovery for the session cwd and injects workspace_root into the outgoing JSON body. Uses a try/catch around JsonNode parsing to keep the hook fail-open and tolerant of unexpected payload shapes.

src/Capacitor.Cli/Commands/ClaudeHookCommand.cs

CodexHookCommand.csAdd workspace_root to Codex session-start payload before enrichment +6/-0

Add workspace_root to Codex session-start payload before enrichment

• Derives workspace_root from the posted cwd (if present) and attaches it to the JsonNode. Keeps the field omitted when cwd is missing or no git root exists.

src/Capacitor.Cli/Commands/CodexHookCommand.cs

CopilotHookCommand.csEmit workspace_root in Copilot hook session-start forwarding +6/-1

Emit workspace_root in Copilot hook session-start forwarding

• Adds best-effort git-root discovery when cwd is available and forwards workspace_root alongside cwd. Preserves fail-open behavior by omitting the field when no repo is found.

src/Capacitor.Cli/Commands/CopilotHookCommand.cs

GeminiHookCommand.csEmit workspace_root in Gemini hook session-start forwarding +6/-1

Emit workspace_root in Gemini hook session-start forwarding

• Adds workspace_root emission based on git-root discovery from cwd. Keeps behavior fail-open by only sending the field when a repo root is found.

src/Capacitor.Cli/Commands/GeminiHookCommand.cs

KiroHookCommand.csEmit workspace_root on Kiro agent-spawn/session-start forwarding +6/-1

Emit workspace_root on Kiro agent-spawn/session-start forwarding

• Adds workspace_root derived from git-root discovery when cwd is present. Keeps the payload unchanged when no repository root is found.

src/Capacitor.Cli/Commands/KiroHookCommand.cs

OpenCodeHookCommand.csEmit workspace_root in OpenCode hook session-start forwarding +6/-1

Emit workspace_root in OpenCode hook session-start forwarding

• Extends forwarded payload to include workspace_root from GitRepository.FindRoot(cwd) when possible. Maintains fail-open omission when no repo exists.

src/Capacitor.Cli/Commands/OpenCodeHookCommand.cs

PiHookCommand.csEmit workspace_root in Pi hook session-start forwarding +6/-1

Emit workspace_root in Pi hook session-start forwarding

• Adds best-effort git-root discovery for cwd and forwards workspace_root when available. Leaves existing started_at/agent_host_id handling unchanged.

src/Capacitor.Cli/Commands/PiHookCommand.cs

ValidatePlanCommand.csTwo-call validate-plan flow with plan-artifacts rendering and legacy fallback +202/-27

Two-call validate-plan flow with plan-artifacts rendering and legacy fallback

• Implements AI-701 behavior: fetch /plan-artifacts?chain=true first and fall back to legacy recap-only rendering on 404 to preserve output byte-for-byte. Renders primary artifact first with degraded/truncated/unavailable markers (including a single-sourced degraded marker string), then uses recap for current-session work rows and “what’s done” summaries.

src/Capacitor.Cli/Commands/ValidatePlanCommand.cs

Bug fix (1) +24 / -6
ImportCommand.csPropagate resolved session cwds to derive workspace_root during imports +24/-6

Propagate resolved session cwds to derive workspace_root during imports

• Threads an optional sessionCwds map through ImportChainsAsync/ImportSingleSessionAsync to prefer already remap/worktree-resolved cwd when building session-start events. Adds workspace_root to the emitted startHook using GitRepository.FindRoot(resolved cwd), fixing a regression where raw transcript cwd prevented discovery.

src/Capacitor.Cli/Commands/ImportCommand.cs

Tests (3) +522 / -0
ClaudeHookCommandTests.csAdd tests for workspace_root emission in Claude session-start hook +32/-0

Add tests for workspace_root emission in Claude session-start hook

• Adds coverage ensuring workspace_root is included when cwd is inside a temp git repo and omitted entirely when no repo exists. Validates fail-open behavior (no null fields sent).

test/Capacitor.Cli.Tests.Unit/ClaudeHookCommandTests.cs

ImportChainsTests.csRegression test for importer workspace_root using remapped cwd +61/-0

Regression test for importer workspace_root using remapped cwd

• Adds a test ensuring ImportChainsAsync derives workspace_root from the remapped/resolved cwd rather than raw transcript metadata. Uses a synthetic transcript and sessionCwds map to confirm the emitted session-start payload carries the resolved cwd and workspace_root.

test/Capacitor.Cli.Tests.Unit/Import/ImportChainsTests.cs

ValidatePlanCommandTests.csNew unit tests for plan-artifacts validate-plan behavior and rendering +429/-0

New unit tests for plan-artifacts validate-plan behavior and rendering

• Adds comprehensive tests for the new two-call flow: primary rendering, empty responses skipping recap, 404 fallback to legacy recap behavior, truncated/unavailable markers, degraded marker ordering, and primary ordering/deduplication when also present in artifacts array.

test/Capacitor.Cli.Tests.Unit/ValidatePlanCommandTests.cs

@qodo-code-review

qodo-code-review Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials

Grey Divider


Remediation recommended

1. README missing validate-plan update ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
kcap validate-plan now uses a new two-call flow (/plan-artifacts then /recap) and emits new
user-visible output markers (e.g., degraded/truncated/unavailable plan rendering), but the README’s
Plan validation section does not reflect these behavior/output changes. This can confuse users and
violates the requirement to update README in the same PR for user-facing CLI surface changes.
Code

src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[R15-46]

+    /// <summary>
+    /// Test-friendly core: caller owns the <see cref="HttpClient"/> (mirrors
+    /// <see cref="ClaudeHookCommand.HandleCore"/>'s seam). Two-call flow (AI-701):
+    /// <c>GET /api/sessions/{id}/plan-artifacts?chain=true</c> for the discovered plan
+    /// artifact set, then the existing <c>GET /api/sessions/{id}/recap?chain=true</c> for
+    /// current-session work rows and AI-generated "what's done" summaries. A 404 on the
+    /// artifacts route (old server without the route, or a non-visible session) falls back
+    /// to <see cref="RenderLegacyAsync"/> — the original recap-only behavior, unchanged.
+    /// </summary>
+    internal static async Task<int> HandleCore(HttpClient httpClient, string baseUrl, string sessionId) {
+        HttpResponseMessage artifactsResp;

        try {
-            resp = await httpClient.GetWithRetryAsync($"{baseUrl}/api/sessions/{sessionId}/recap?chain=true");
+            artifactsResp = await httpClient.GetWithRetryAsync($"{baseUrl}/api/sessions/{sessionId}/plan-artifacts?chain=true");
        } catch (HttpRequestException ex) {
            HttpClientExtensions.WriteUnreachableError(baseUrl, ex);

            return 1;
        }

-        if (await HttpClientExtensions.HandleUnauthorizedAsync(resp)) {
+        if (await HttpClientExtensions.HandleUnauthorizedAsync(artifactsResp)) {
            return 1;
        }

-        if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) {
-            await Console.Error.WriteLineAsync($"Session not found: {sessionId}");
-
-            return 1;
+        if (artifactsResp.StatusCode == HttpStatusCode.NotFound) {
+            // Older server without the route, or the session/candidate isn't visible —
+            // preserve the original recap-only behavior byte-for-byte.
+            return await RenderLegacyAsync(httpClient, baseUrl, sessionId);
        }

-        if (!resp.IsSuccessStatusCode) {
-            await Console.Error.WriteLineAsync($"HTTP {(int)resp.StatusCode}");
+        if (!artifactsResp.IsSuccessStatusCode) {
+            await Console.Error.WriteLineAsync($"HTTP {(int)artifactsResp.StatusCode}");
Evidence
The new ValidatePlanCommand implementation adds a new server call to
/api/sessions/{id}/plan-artifacts?chain=true with fallback behavior and new output rendering for
degraded/truncated/unavailable plan artifacts. The README’s Plan validation documentation remains
generic and does not mention these new behaviors/output details, so documentation is not
synchronized with the CLI surface change as required.

CLAUDE.md: Update README.md in the same PR for any user-facing CLI surface change
src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[15-46]
README.md[274-286]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR changes `kcap validate-plan` behavior/output (new `/plan-artifacts` call + new rendering markers), but `README.md` is not updated to match.

## Issue Context
The compliance checklist requires updating `README.md` in the same PR for user-facing CLI surface changes.

## Fix Focus Areas
- README.md[274-286]
- README.md[188-200]
- src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[15-46]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unavailable plan exits success ✓ Resolved 🐞 Bug ≡ Correctness
Description
When the primary artifact is unavailable, the command prints "Validation is not possible" but still
returns exit code 0 from HandleCore. This makes scripts/automation treat the run as successful even
though the tool states it could not perform validation.
Code

src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[R98-101]

+        await RenderPlanArtifacts(primary, artifacts);
+        await RenderWhatsDoneAndInstructions(summaries, work);
+
+        return 0;
Evidence
The code prints a message explicitly stating validation is impossible for an unavailable primary,
but the main flow still returns 0 after rendering.

src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[94-101]
src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[152-158]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ValidatePlanCommand` emits an explicit message that validation cannot be performed when the *primary* plan artifact is `content_state == "unavailable"`, but `HandleCore` always returns `0` after rendering. This contradicts the command semantics and can mislead callers that rely on the process exit code.

### Issue Context
The output already distinguishes the "unavailable primary" case with a strong statement:
"Validation is not possible: the plan content could not be retrieved...".
However, `HandleCore` unconditionally returns success after printing.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[94-101]
- src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[152-158]

### Suggested fix
- Decide on a non-zero exit code for "validation not possible" (e.g., `2` to distinguish from network/auth errors).
- Implement a check in `HandleCore` (or have `RenderPlanArtifacts` return a `bool canValidate`) such that when the primary artifact is unavailable (and thus plan text cannot be retrieved), `HandleCore` returns that non-zero code.
- Add/extend a unit test asserting the returned exit code for the unavailable-primary case (the existing test currently asserts only stdout content).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Malformed truncated marker ✓ Resolved 🐞 Bug ≡ Correctness
Description
RenderPlanArtifacts formats the truncated marker using OriginalBytes even though it is nullable,
yielding output like "of  bytes" when the server omits original_bytes. It also reports "first 0"
bytes when content is null, which is misleading for a truncated artifact.
Code

src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[R142-145]

+                case "truncated": {
+                    var n = artifact.Content is null ? 0 : Encoding.UTF8.GetByteCount(artifact.Content);
+                    await Console.Out.WriteLineAsync($"[plan truncated: first {n} of {artifact.OriginalBytes} bytes]");
+
Evidence
The rendering logic interpolates a nullable OriginalBytes directly into the output string, and
computes n=0 when Content is null, causing malformed/misleading markers.

src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[141-148]
src/Capacitor.Cli.Core/Models.cs[818-823]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ValidatePlanCommand.RenderPlanArtifacts` prints the truncated marker as:
`[plan truncated: first {n} of {artifact.OriginalBytes} bytes]`.
Because `PlanArtifactDto.OriginalBytes` is nullable (`long?`), this can produce malformed output (`of  bytes`) if the server omits the field. Additionally, when `content` is null but `content_state == "truncated"`, the code prints `first 0 ...` which is misleading.

### Issue Context
This affects the user-visible output of `kcap validate-plan` in edge cases / partial server payloads.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/ValidatePlanCommand.cs[141-149]
- src/Capacitor.Cli.Core/Models.cs[818-823]

### Suggested fix
- In the `"truncated"` branch:
 - Treat `OriginalBytes == null` as unknown (e.g., print `?`), or fall back to `n` while keeping the marker well-formed.
 - If `artifact.Content` is null, print a clear placeholder (or treat it as unavailable) rather than implying a 0-byte prefix.
- Consider adding/adjusting a unit test covering `content_state=truncated` with missing `original_bytes` and/or null `content` so the output stays stable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli/Commands/ValidatePlanCommand.cs
Comment thread src/Capacitor.Cli/Commands/ValidatePlanCommand.cs Outdated
Comment thread src/Capacitor.Cli/Commands/ValidatePlanCommand.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9806bb81d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

importResult = r!;
} else {
importResult = await ImportChainsAsync(httpClient, baseUrl, chains, events, CancellationToken.None);
importResult = await ImportChainsAsync(httpClient, baseUrl, chains, events, CancellationToken.None, sessionCwds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Thread workspace roots through routed imports

Passing sessionCwds only into ImportChainsAsync fixes the file-based chain path, but routed imports skip this call entirely and enter the ImportSessionAsync phase just below; I checked the routed source payload builders (for example OpenCode/Pi/Copilot/Kiro/Antigravity/Cursor) and they still post cwd/workspace_roots without the new workspace_root. As a result kcap import for those vendors still cannot provide workspace-scoped artifact identity even when the discovered cwd is inside a git repo, so the new plan-artifact matching remains unavailable for those historical sessions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c903411 — routed imports (OpenCode/Pi/Copilot/Kiro/Antigravity) now derive workspace_root via the same fail-open GitRepository.FindRoot in their session-start payload builders. Cursor intentionally untouched (its workspace_roots is a different, pre-existing IDE concept). Tests added on the OpenCode source.

alexeyzimarev and others added 2 commits July 12, 2026 12:09
… 2 on unavailable, routed-import workspace_root

Four findings from the Qodo/Codex PR review of the plan-artifacts change:
- README: document the new plan-artifacts fetch + recap-fallback + degraded/truncated/unavailable markers under "Plan validation".
- RenderPlanArtifacts: a null OriginalBytes now renders "of ? bytes" instead of "of  bytes", and a "truncated" artifact with null Content renders like "unavailable" instead of "first 0 of ... bytes".
- HandleCore now returns exit code 2 when the PRIMARY artifact's content is unavailable (distinct from 0 success/no-plan and 1 generic error).
- Routed imports (OpenCode/Pi/Copilot/Kiro/Antigravity) now derive workspace_root from cwd via GitRepository.FindRoot, matching the file-based ImportChainsAsync path. Cursor is untouched (separate workspace_roots concept).
…ndows AV race)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit e5d87a2 into main Jul 12, 2026
5 checks passed
@alexeyzimarev
alexeyzimarev deleted the alexeyzimarev/ai-701-validate-plan-artifacts branch July 12, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant