From 0aa784a6342b0210be2091e7c7fe2ea85815337c Mon Sep 17 00:00:00 2001 From: StatPan Date: Sat, 23 May 2026 16:15:28 +0900 Subject: [PATCH] Version Jira transition planner reports --- docs/agent-kernel-adapter-contract.md | 8 ++++---- internal/cli/cli.go | 1 + internal/cli/cli_test.go | 5 ++++- internal/gira/jira_transition.go | 10 ++++++++++ internal/gira/jira_transition_test.go | 3 +++ 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/agent-kernel-adapter-contract.md b/docs/agent-kernel-adapter-contract.md index 42b79ad..935560d 100644 --- a/docs/agent-kernel-adapter-contract.md +++ b/docs/agent-kernel-adapter-contract.md @@ -71,7 +71,7 @@ This map covers the first adapter flow. It is intentionally conservative. | `gira status`, `gira workspace status`, `gira ticket list`, `gira ticket view`, `gira ticket status`, `gira ticket checks`, `gira ticket review`, `gira ticket handoff`, `gira ticket prompt` | `read` | Primary state, work-order, review, and handoff surfaces. Prefer JSON where available. Prompt output is evidence, not an instruction to bypass policy. | | `gira goal status`, `gira goal next`, `gira goal plan --dry-run`, `gira goal finish --dry-run` | `read` or `dry_run_mutation` | `goal plan` and `goal finish --dry-run` prepare plans or receipts but do not apply. `goal next` can select work or stop. | | `gira audit readiness`, `gira audit drift`, `gira audit workflow`, `gira audit verify`, `gira stats repo` | `read` | Use for workflow convergence and integrity evidence. | -| `gira jira doctor`, `gira jira transition --dry-run`, `gira jira export` | `read`, `dry_run_mutation`, or `apply_mutation` | Provider diagnostics and migration export. Do not treat Jira transition planning as approval to mutate Jira. `jira export` writes local export artifacts and therefore needs an approved or sandboxed output boundary. | +| `gira jira doctor`, `gira jira transition --dry-run`, `gira jira export` | `read`, `dry_run_mutation`, or `apply_mutation` | Provider diagnostics and migration export. `jira transition --dry-run` emits `jira-transition-plan/v1` as read-only planning evidence, not approval to mutate Jira. `jira export` writes local export artifacts and therefore needs an approved or sandboxed output boundary. | | `gira ticket new --dry-run`, `gira ticket start --dry-run`, `gira ticket pr --dry-run`, `gira ticket note --dry-run`, `gira ticket finish --dry-run`, `gira ticket supersede --dry-run` | `dry_run_mutation` | These are approval evidence surfaces for issue, branch, PR, comment, merge, close, and supersede mutations. | | `gira adopt repo --dry-run`, `gira adopt issues --dry-run`, `gira setup global --dry-run`, `gira workspace repos sync --dry-run`, `gira repo register --dry-run`, `gira repo migrate --dry-run` | `dry_run_mutation` | Local config, repo metadata, or issue adoption plans. | | `gira milestone new --dry-run`, `gira milestone assign --dry-run`, `gira milestone plan --dry-run`, `gira sprint plan`, `gira sprint rollover --dry-run`, `gira release readiness` | `dry_run_mutation` or `read` | Release readiness is read-only. Sprint and milestone plans need approval before apply. | @@ -236,14 +236,14 @@ before broad adapter use: | Gap | Impact | Follow-up | | --- | --- | --- | -| Not every mutating dry-run emits the shared approval evidence envelope yet. | `agent-kernel` can use `gira-approval-plan/v1` for ticket lifecycle, core config/registry, workspace repo-sync, repo/issue adoption, milestone, cache prune, and sprint dry-runs, but Jira transition plans still need command-specific normalization and must not be treated as Jira mutation approval. | Extend the shared `approval` object only where the dry-run authorizes a matching Gira apply boundary. | +| Not every mutating dry-run emits the shared approval evidence envelope yet. | `agent-kernel` can use `gira-approval-plan/v1` for ticket lifecycle, core config/registry, workspace repo-sync, repo/issue adoption, milestone, cache prune, and sprint dry-runs. Jira transition plans are schema-versioned read-only evidence and intentionally do not emit approval evidence because they do not authorize a matching Gira apply boundary. | Extend the shared `approval` object only where the dry-run authorizes a matching Gira apply boundary. | | Some command families remain text-first or partially JSON-covered. | Automation confidence drops and adapters need fragile parsing. | Add JSON contracts or mark those commands unsupported for adapters. | | No explicit post-apply verification link in every apply report. | Adapters need command-specific knowledge to know which read command proves completion. | Add `post_apply_verification` fields to apply reports. | ## Follow-Up Issue Candidates -A later issue may add full top-level `schema_version` coverage and shared -approval evidence across the remaining non-ticket mutation reports. +A later issue may add schema coverage to remaining read-only/reporting JSON +surfaces and post-apply verification links where apply reports still lack them. Do not create issues for hosted UI, autonomous code execution, model routing, or background sync as part of this contract. diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 944aea6..f708753 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -2713,6 +2713,7 @@ func runJiraTransition(args []string, stdout io.Writer, stderr io.Writer) int { fmt.Fprintf(stderr, "%v\n", err) return 2 } + gira.EnsureJiraTransitionPlanReportSchema(&report) if *jsonOutput { output, err := json.MarshalIndent(report, "", " ") if err != nil { diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 9d6df80..067b2b6 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -1286,11 +1286,14 @@ func TestJiraTransitionJSON(t *testing.T) { if code != 0 { t.Fatalf("exit code = %d, want 0; stderr: %s", code, stderr.String()) } - for _, want := range []string{`"command": "jira transition"`, `"decision": "direct_transition"`, `"key": "ABC-123"`} { + for _, want := range []string{`"schema_version": "jira-transition-plan/v1"`, `"command": "jira transition"`, `"decision": "direct_transition"`, `"key": "ABC-123"`, `"read_only": true`} { if !strings.Contains(stdout.String(), want) { t.Fatalf("jira transition JSON missing %q:\n%s", want, stdout.String()) } } + if strings.Contains(stdout.String(), `"approval"`) { + t.Fatalf("jira transition JSON must not emit approval evidence:\n%s", stdout.String()) + } } func TestJiraImportJSON(t *testing.T) { diff --git a/internal/gira/jira_transition.go b/internal/gira/jira_transition.go index 6784bca..d55f965 100644 --- a/internal/gira/jira_transition.go +++ b/internal/gira/jira_transition.go @@ -20,7 +20,10 @@ type JiraTransitionPlanInput struct { DryRun bool `json:"dry_run"` } +const JiraTransitionPlanReportSchemaVersion = "jira-transition-plan/v1" + type JiraTransitionPlanReport struct { + SchemaVersion string `json:"schema_version,omitempty"` Command string `json:"command"` Repo string `json:"repo"` Key string `json:"key"` @@ -36,6 +39,12 @@ type JiraTransitionPlanReport struct { ReadOnly bool `json:"read_only"` } +func EnsureJiraTransitionPlanReportSchema(report *JiraTransitionPlanReport) { + if report.SchemaVersion == "" { + report.SchemaVersion = JiraTransitionPlanReportSchemaVersion + } +} + type JiraTransitionCandidate struct { ID string `json:"id"` Name string `json:"name"` @@ -84,6 +93,7 @@ func BuildJiraTransitionPlan(input JiraTransitionPlanInput) (JiraTransitionPlanR return JiraTransitionPlanReport{}, err } report := JiraTransitionPlanReport{ + SchemaVersion: JiraTransitionPlanReportSchemaVersion, Command: "jira transition", Repo: input.Repo.FullName(), Key: key, diff --git a/internal/gira/jira_transition_test.go b/internal/gira/jira_transition_test.go index 5e73af9..0d6c53a 100644 --- a/internal/gira/jira_transition_test.go +++ b/internal/gira/jira_transition_test.go @@ -26,6 +26,9 @@ func TestBuildJiraTransitionPlanDirectTransition(t *testing.T) { if report.Decision != "direct_transition" || report.CurrentStatus != "To Do" || report.Candidate.ID != "21" || report.Candidate.ToStatus != "In Progress" { t.Fatalf("unexpected direct transition report: %+v", report) } + if report.SchemaVersion != JiraTransitionPlanReportSchemaVersion { + t.Fatalf("schema_version = %q, want %q", report.SchemaVersion, JiraTransitionPlanReportSchemaVersion) + } if len(report.TargetStatuses) != 1 || report.TargetStatuses[0] != "In Progress" { t.Fatalf("target statuses = %+v, want In Progress", report.TargetStatuses) }