Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/agent-kernel-adapter-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines 2713 to +2716
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The runJiraTransition function should follow the standard pattern used in other commands (like runSetupGlobal or runRepoRegister) to emit a JSON report even when an error occurs. This ensures that automated tools using the --json flag receive a machine-readable response for all outcomes, which is critical for the adapter contract described in the documentation.

		if *jsonOutput {
			gira.EnsureJiraTransitionPlanReportSchema(&report)
			output, _ := json.MarshalIndent(report, "", "  ")
			fmt.Fprintf(stdout, "%s\n", output)
		}
		fmt.Fprintf(stderr, "%v\n", err)
		return 2
	}
	gira.EnsureJiraTransitionPlanReportSchema(&report)

if *jsonOutput {
output, err := json.MarshalIndent(report, "", " ")
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions internal/gira/jira_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -36,6 +39,12 @@ type JiraTransitionPlanReport struct {
ReadOnly bool `json:"read_only"`
}

func EnsureJiraTransitionPlanReportSchema(report *JiraTransitionPlanReport) {
if report.SchemaVersion == "" {
report.SchemaVersion = JiraTransitionPlanReportSchemaVersion
}
}
Comment on lines +42 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure consistent JSON output for automated tools, EnsureJiraTransitionPlanReportSchema should also populate the Command, DryRun, and ReadOnly fields when initializing a report from an empty state (e.g., in error scenarios). Since this specific report type is defined as a read-only planner report, these fields should always be set to their canonical values.

func EnsureJiraTransitionPlanReportSchema(report *JiraTransitionPlanReport) {
	if report.SchemaVersion == "" {
		report.SchemaVersion = JiraTransitionPlanReportSchemaVersion
		report.Command = "jira transition"
		report.DryRun = true
		report.ReadOnly = true
	}
}


type JiraTransitionCandidate struct {
ID string `json:"id"`
Name string `json:"name"`
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions internal/gira/jira_transition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down