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
2 changes: 1 addition & 1 deletion docs/agent-kernel-adapter-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ 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, and cache prune dry-runs, but sprint and Jira transition plans may still need command-specific normalization. | Extend the shared `approval` object to the remaining non-ticket dry-run mutation reports. |
| 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. |
| 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. |

Expand Down
16 changes: 16 additions & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -7456,6 +7456,10 @@ func runSprint(args []string, stdout io.Writer, stderr io.Writer) int {
fmt.Fprintf(stderr, "%v\n", err)
return 1
}
gira.EnsureSprintPlanReportSchema(&report)
if report.Mode == "dry-run" {
report.Approval = gira.SprintPlanApprovalEvidence(report)
}
Comment on lines +7459 to +7462
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

This logic is redundant. The gira.PlanSprint function (called at line 7454) already populates the SchemaVersion and Approval fields when in dry-run mode. Re-assigning them here in the CLI layer is unnecessary and can be removed to simplify the code.

output, _ := json.MarshalIndent(report, "", " ")
fmt.Fprintf(stdout, "%s\n", output)
return 0
Expand Down Expand Up @@ -7487,6 +7491,10 @@ func runSprint(args []string, stdout io.Writer, stderr io.Writer) int {
fmt.Fprintf(stderr, "%v\n", err)
return 1
}
gira.EnsureSprintStartReportSchema(&report)
if report.Mode == "dry-run" {
report.Approval = gira.SprintStartApprovalEvidence(report)
}
Comment on lines +7494 to +7497
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

Redundant logic. gira.StartSprint already ensures the schema version and populates the approval evidence for dry-runs. These lines should be removed to keep the CLI layer clean and avoid duplicate processing.

output, _ := json.MarshalIndent(report, "", " ")
if *jsonOutput {
fmt.Fprintf(stdout, "%s\n", output)
Expand Down Expand Up @@ -7530,6 +7538,10 @@ func runSprint(args []string, stdout io.Writer, stderr io.Writer) int {
fmt.Fprintf(stderr, "%v\n", err)
return 1
}
gira.EnsureSprintCloseReportSchema(&report)
if report.Mode == "dry-run" {
report.Approval = gira.SprintCloseApprovalEvidence(report)
}
Comment on lines +7541 to +7544
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

Redundant logic. gira.CloseSprint already handles schema versioning and approval evidence population for dry-runs. This duplication in the CLI layer is unnecessary.

output, _ := json.MarshalIndent(report, "", " ")
if *jsonOutput {
fmt.Fprintf(stdout, "%s\n", output)
Expand Down Expand Up @@ -7565,6 +7577,10 @@ func runSprint(args []string, stdout io.Writer, stderr io.Writer) int {
fmt.Fprintf(stderr, "%v\n", err)
return 1
}
gira.EnsureSprintRolloverReportSchema(&report)
if report.Mode == "dry-run" {
report.Approval = gira.SprintRolloverApprovalEvidence(report)
}
Comment on lines +7580 to +7583
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

Redundant logic. The domain functions called by newSprintRolloverReport already populate these fields. This assignment in the CLI layer is unnecessary and should be removed.

output, _ := json.MarshalIndent(report, "", " ")
if *jsonOutput {
fmt.Fprintf(stdout, "%s\n", output)
Expand Down
107 changes: 106 additions & 1 deletion internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4439,16 +4439,57 @@ func TestSprintPlanStartCloseJSONLifecycle(t *testing.T) {
t.Cleanup(func() { _ = os.Chdir(cwd) })

var stdout, stderr bytes.Buffer
code := Run([]string{"sprint", "plan", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--capacity", "2", "--issues", "3,1,2", "--apply", "--json"}, &stdout, &stderr)
code := Run([]string{"sprint", "plan", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--capacity", "2", "--issues", "3,1,2", "--dry-run", "--json"}, &stdout, &stderr)
if code != 0 {
t.Fatalf("plan dry-run exit code=%d stderr=%s", code, stderr.String())
}
var dryPlan gira.SprintPlanReport
if err := json.Unmarshal(stdout.Bytes(), &dryPlan); err != nil {
t.Fatalf("decode sprint plan dry-run JSON: %v\n%s", err, stdout.String())
}
if dryPlan.SchemaVersion != gira.SprintPlanReportSchemaVersion || dryPlan.Approval == nil {
t.Fatalf("sprint plan dry-run JSON missing schema or approval:\n%s", stdout.String())
}
if dryPlan.Approval.ApplyCommand != "gira sprint plan --repo StatPan/gira --iteration 2026-W18 --capacity 2 --issues 1,2,3 --apply" || dryPlan.Approval.OutputSchema != gira.SprintPlanReportSchemaVersion {
t.Fatalf("unexpected sprint plan approval evidence: %+v", dryPlan.Approval)
}
if dryPlan.Approval.Blockers == nil || dryPlan.Approval.Warnings == nil {
t.Fatalf("approval blockers and warnings must be stable arrays: %+v", dryPlan.Approval)
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"sprint", "plan", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--capacity", "2", "--issues", "3,1,2", "--apply", "--json"}, &stdout, &stderr)
if code != 0 {
t.Fatalf("plan exit code=%d stderr=%s", code, stderr.String())
}
var applyPlan gira.SprintPlanReport
if err := json.Unmarshal(stdout.Bytes(), &applyPlan); err != nil {
t.Fatalf("decode sprint plan apply JSON: %v\n%s", err, stdout.String())
}
if applyPlan.SchemaVersion != gira.SprintPlanReportSchemaVersion || applyPlan.Approval != nil {
t.Fatalf("sprint plan apply JSON should have schema and omit approval: %+v", applyPlan)
}
for _, want := range []string{`"mode": "apply"`, `"capacity_target": 2`, `"commit_count": 3`, `"capacity_breach": true`} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("sprint plan JSON missing %q:\n%s", want, stdout.String())
}
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"sprint", "start", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--dry-run", "--json"}, &stdout, &stderr)
if code != 0 {
t.Fatalf("start dry-run exit code=%d stderr=%s", code, stderr.String())
}
var dryStart gira.SprintStartReport
if err := json.Unmarshal(stdout.Bytes(), &dryStart); err != nil {
t.Fatalf("decode sprint start dry-run JSON: %v\n%s", err, stdout.String())
}
if dryStart.SchemaVersion != gira.SprintStartReportSchemaVersion || dryStart.Approval == nil || dryStart.Approval.ApplyCommand != "gira sprint start --repo StatPan/gira --iteration 2026-W18 --apply" {
t.Fatalf("unexpected sprint start approval evidence: %+v", dryStart)
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"sprint", "start", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--apply", "--json"}, &stdout, &stderr)
Expand All @@ -4461,6 +4502,23 @@ func TestSprintPlanStartCloseJSONLifecycle(t *testing.T) {
}
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"sprint", "close", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--completed", "1,3", "--spillover-disposition", "carry", "--rollover-reason", "dependency blocked", "--dry-run", "--json"}, &stdout, &stderr)
if code != 0 {
t.Fatalf("close dry-run exit code=%d stderr=%s", code, stderr.String())
}
var dryClose gira.SprintCloseReport
if err := json.Unmarshal(stdout.Bytes(), &dryClose); err != nil {
t.Fatalf("decode sprint close dry-run JSON: %v\n%s", err, stdout.String())
}
if dryClose.SchemaVersion != gira.SprintCloseReportSchemaVersion || dryClose.Approval == nil {
t.Fatalf("sprint close dry-run JSON missing schema or approval: %+v", dryClose)
}
if dryClose.Approval.ApplyCommand != "gira sprint close --repo StatPan/gira --iteration 2026-W18 --completed 1,3 --spillover-disposition carry --rollover-reason 'dependency blocked' --apply" {
t.Fatalf("unexpected sprint close approval command: %+v", dryClose.Approval)
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"sprint", "close", "--repo", "StatPan/gira", "--iteration", "2026-W18", "--completed", "1,3", "--spillover-disposition", "carry", "--rollover-reason", "dependency blocked", "--apply", "--json"}, &stdout, &stderr)
Expand All @@ -4474,6 +4532,9 @@ func TestSprintPlanStartCloseJSONLifecycle(t *testing.T) {
if closeReport.Mode != "apply" || fmt.Sprint(closeReport.Summary.CompletedItems) != "[1 3]" || fmt.Sprint(closeReport.Summary.SpilloverItems) != "[2]" || closeReport.Summary.SpilloverDisposition != "carry" || closeReport.Summary.RolloverReason != "dependency blocked" {
t.Fatalf("unexpected sprint close report: %+v", closeReport)
}
if closeReport.SchemaVersion != gira.SprintCloseReportSchemaVersion || closeReport.Approval != nil {
t.Fatalf("sprint close apply JSON should have schema and omit approval: %+v", closeReport)
}
}

func TestSprintRolloverJSONUsesInjectedReport(t *testing.T) {
Expand Down Expand Up @@ -4503,6 +4564,50 @@ func TestSprintRolloverJSONUsesInjectedReport(t *testing.T) {
t.Fatalf("sprint rollover JSON missing %q:\n%s", want, stdout.String())
}
}
var report gira.SprintRolloverReport
if err := json.Unmarshal(stdout.Bytes(), &report); err != nil {
t.Fatalf("decode sprint rollover JSON: %v\n%s", err, stdout.String())
}
if report.SchemaVersion != gira.SprintRolloverReportSchemaVersion || report.Approval != nil {
t.Fatalf("sprint rollover apply JSON should have schema and omit approval: %+v", report)
}
}

func TestSprintRolloverDryRunJSONUsesInjectedApproval(t *testing.T) {
restore := newSprintRolloverReport
t.Cleanup(func() { newSprintRolloverReport = restore })
newSprintRolloverReport = func(repo gira.RepoRef, toMilestone string, apply bool) (gira.SprintRolloverReport, error) {
if repo.FullName() != "StatPan/gira" || toMilestone != "W18" || apply {
t.Fatalf("unexpected rollover args repo=%s to=%s apply=%t", repo.FullName(), toMilestone, apply)
}
return gira.SprintRolloverReport{
Repo: repo.FullName(),
Mode: "dry-run",
TargetMilestone: &gira.SprintRolloverTarget{Number: 2, Title: "W18"},
TargetResolution: "explicit --to",
Summary: gira.SprintRolloverSummary{Candidates: 1, Applied: 1},
Items: []gira.SprintRolloverItem{{IssueNumber: 10, IssueTitle: "Carry me", FromMilestone: "W17", CandidateReason: "source milestone due date passed", Action: "would-apply", TargetMilestone: "W18"}},
}, nil
}

var stdout, stderr bytes.Buffer
code := Run([]string{"sprint", "rollover", "--repo", "StatPan/gira", "--to", "W18", "--dry-run", "--json"}, &stdout, &stderr)
if code != 0 {
t.Fatalf("rollover exit code=%d stderr=%s", code, stderr.String())
}
var report gira.SprintRolloverReport
if err := json.Unmarshal(stdout.Bytes(), &report); err != nil {
t.Fatalf("decode sprint rollover JSON: %v\n%s", err, stdout.String())
}
if report.SchemaVersion != gira.SprintRolloverReportSchemaVersion || report.Approval == nil {
t.Fatalf("sprint rollover dry-run JSON missing schema or approval:\n%s", stdout.String())
}
if report.Approval.ApplyCommand != "gira sprint rollover --repo StatPan/gira --to W18 --apply" || report.Approval.OutputSchema != gira.SprintRolloverReportSchemaVersion {
t.Fatalf("unexpected sprint rollover approval evidence: %+v", report.Approval)
}
if report.Approval.Blockers == nil || report.Approval.Warnings == nil {
t.Fatalf("approval blockers and warnings must be stable arrays: %+v", report.Approval)
}
}

func TestTriageMissingRepoReturnsTwo(t *testing.T) {
Expand Down
Loading