Skip to content

Commit 8db0673

Browse files
authored
Merge pull request #123 from morluto/feature/repeatable-validation-telemetry
feat(validation): add repeatable runs with process telemetry
2 parents 12400cc + 24526f6 commit 8db0673

37 files changed

Lines changed: 2339 additions & 258 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,18 @@ gitcontribute validation define --kind=test --command="go test ./..." \
202202
--working-dir=/path/to/workspace <investigation-id>
203203
gitcontribute validation run <validation-id> --kind=base --execute
204204
gitcontribute validation run <validation-id> --kind=candidate --execute
205+
gitcontribute validation repeat <validation-id> --kind=both --runs=5 \
206+
--concurrency=2 --execute
205207
gitcontribute validation compare <base-run-id> <candidate-run-id>
206208
```
207209

210+
Repeat validation stores bounded per-attempt results plus stable/flaky/
211+
inconclusive aggregates. CPU time, peak RSS, child count, process identity, and
212+
post-shutdown cleanup are sampled through `gopsutil`; unsupported metrics stay
213+
explicitly unavailable. To measure MCP readiness without parsing stdout, define
214+
the validation with `--protocol=mcp_stdio --readiness-timeout=30s`. The official
215+
MCP Go SDK then records initialize and tools/list milestones.
216+
208217
### 4. Check readiness
209218

210219
Run a deterministic local readiness gate before turning a candidate into a

docs/architecture.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,21 @@ application and domain packages expose product-owned values and interfaces.
5959
| DeepWiki external read | public repository structure, contents, questions | yes | no | no | no |
6060
| Git acquisition | acquire, workspace create | remote-dependent | yes | `git` only | no |
6161
| Local merge check | compare already-fetched revisions | no | no | `git` only | no |
62-
| Validation | validation run with explicit execution | no by default | yes | yes | no |
62+
| Validation | validation run/repeat with explicit execution | no by default | yes | yes | no |
6363

6464
Version 1 has no GitHub mutation path. Adding one requires a separate
6565
application capability and protocol annotation; it must not be hidden behind a
6666
read operation.
6767

68+
Validation definitions remain shell-free and execution requires an explicit
69+
authorization flag. Repeat groups bound attempts, concurrency, per-run and
70+
overall timeouts, captured output, and sampler frequency. The generic process
71+
runner records spawn/execution/shutdown boundaries and uses `gopsutil` for
72+
PID-plus-start-time process-tree metrics. Declared `mcp_stdio` definitions use
73+
the official MCP SDK for initialize and tools/list milestones; protocol state
74+
is never inferred from arbitrary stdout text. Metric availability and cleanup
75+
status are persisted separately from semantic pass/fail classification.
76+
6877
## Corpus model
6978

7079
The corpus separates source history from convenient current state:

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/modelcontextprotocol/go-sdk v1.6.1
1818
github.com/pelletier/go-toml/v2 v2.4.3
1919
github.com/pressly/goose/v3 v3.24.0
20+
github.com/shirou/gopsutil/v4 v4.26.6
2021
github.com/sourcegraph/go-diff v0.8.0
2122
github.com/zalando/go-keyring v0.2.8
2223
golang.org/x/mod v0.37.0
@@ -45,10 +46,13 @@ require (
4546
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
4647
github.com/danieljoos/wincred v1.2.3 // indirect
4748
github.com/dustin/go-humanize v1.0.1 // indirect
49+
github.com/ebitengine/purego v0.10.0 // indirect
4850
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
51+
github.com/go-ole/go-ole v1.2.6 // indirect
4952
github.com/godbus/dbus/v5 v5.2.2 // indirect
5053
github.com/google/go-querystring v1.2.0 // indirect
5154
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
55+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
5256
github.com/mattn/go-isatty v0.0.20 // indirect
5357
github.com/mattn/go-localereader v0.0.1 // indirect
5458
github.com/mattn/go-runewidth v0.0.20 // indirect
@@ -58,13 +62,17 @@ require (
5862
github.com/muesli/cancelreader v0.2.2 // indirect
5963
github.com/muesli/termenv v0.16.0 // indirect
6064
github.com/ncruces/go-strftime v1.0.0 // indirect
65+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
6166
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
6267
github.com/rivo/uniseg v0.4.7 // indirect
6368
github.com/segmentio/asm v1.2.0 // indirect
6469
github.com/segmentio/encoding v0.5.4 // indirect
6570
github.com/sethvargo/go-retry v0.3.0 // indirect
71+
github.com/tklauser/go-sysconf v0.3.16 // indirect
72+
github.com/tklauser/numcpus v0.11.0 // indirect
6673
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
6774
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
75+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
6876
go.uber.org/multierr v1.11.0 // indirect
6977
golang.org/x/oauth2 v0.35.0 // indirect
7078
golang.org/x/sync v0.21.0 // indirect

go.sum

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
6666
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6767
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
6868
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
69+
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
70+
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
6971
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
7072
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
73+
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
74+
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
7175
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
7276
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
7377
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
7478
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
7579
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
7680
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
81+
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7782
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7883
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
7984
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -95,6 +100,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
95100
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
96101
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
97102
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
103+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
104+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
98105
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
99106
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
100107
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -119,6 +126,8 @@ github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdD
119126
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
120127
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
121128
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
129+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
130+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
122131
github.com/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72VU=
123132
github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
124133
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -131,16 +140,24 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv
131140
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
132141
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
133142
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
143+
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
144+
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
134145
github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY=
135146
github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
136147
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
137148
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
138149
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
139150
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
151+
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
152+
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
153+
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
154+
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
140155
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
141156
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
142157
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
143158
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
159+
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
160+
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
144161
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
145162
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
146163
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -153,6 +170,8 @@ golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
153170
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
154171
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
155172
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
173+
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
174+
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
156175
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
157176
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158177
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
@@ -163,6 +182,7 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
163182
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
164183
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
165184
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
185+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
166186
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
167187
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
168188
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=

internal/app/evidence.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ func (s *Service) DefineValidation(ctx context.Context, investigationID string,
6060
Timeout: opts.Timeout,
6161
MaxOutputBytes: opts.MaxOutputBytes,
6262
Observation: observationContractToEvidence(opts.Observation),
63+
Protocol: evidence.ValidationProtocol(opts.Protocol),
64+
ReadinessTimeout: opts.ReadinessTimeout,
6365
}
6466

6567
evSvc := evidence.NewService(c, evidence.NewExecRunner())
@@ -223,6 +225,29 @@ func bindValidationWorkspace(ctx context.Context, service *Service, c *corpus.Co
223225
return nil
224226
}
225227

228+
// RunValidationGroup executes a bounded repeat/stress validation group.
229+
func (s *Service) RunValidationGroup(ctx context.Context, id string, opts cli.RepeatValidationOptions) (*cli.ValidationRunGroupResult, error) {
230+
if !opts.Execute {
231+
return nil, evidence.ErrExecutionNotAuthorized
232+
}
233+
kinds := make([]evidence.RunKind, len(opts.Kinds))
234+
for index, kind := range opts.Kinds {
235+
kinds[index] = evidence.RunKind(kind)
236+
}
237+
c, err := s.openCorpus(ctx)
238+
if err != nil {
239+
return nil, err
240+
}
241+
group, err := evidence.NewService(c, evidence.NewExecRunner()).RunValidationGroup(ctx, id, evidence.RepeatValidationOptions{
242+
Kinds: kinds, RunCount: opts.RunCount, Concurrency: opts.Concurrency,
243+
PerRunTimeout: opts.PerRunTimeout, OverallTimeout: opts.OverallTimeout, SampleInterval: opts.SampleInterval,
244+
})
245+
if err != nil {
246+
return nil, mapEvidenceError(err)
247+
}
248+
return validationRunGroupResult(group), nil
249+
}
250+
226251
// CompareValidation compares a base validation run with a candidate validation run.
227252
func (s *Service) CompareValidation(ctx context.Context, baseRunID, candidateRunID string) (*cli.ValidationComparisonResult, error) {
228253
c, err := s.openReadOnlyCorpus(ctx)
@@ -363,6 +388,10 @@ func validationResult(def *evidence.ValidationDefinition) *cli.ValidationResult
363388
if def.Timeout > 0 {
364389
timeout = def.Timeout.String()
365390
}
391+
readinessTimeout := ""
392+
if def.ReadinessTimeout > 0 {
393+
readinessTimeout = def.ReadinessTimeout.String()
394+
}
366395
return &cli.ValidationResult{
367396
ID: def.ID,
368397
InvestigationID: def.InvestigationID,
@@ -378,6 +407,8 @@ func validationResult(def *evidence.ValidationDefinition) *cli.ValidationResult
378407
Timeout: timeout,
379408
MaxOutputBytes: def.MaxOutputBytes,
380409
Observation: observationContractToCLI(def.Observation),
410+
Protocol: string(def.Protocol),
411+
ReadinessTimeout: readinessTimeout,
381412
CreatedAt: formatTime(def.CreatedAt),
382413
}
383414
}
@@ -402,7 +433,77 @@ func validationRunResult(run *evidence.ValidationRun) *cli.ValidationRunResult {
402433
WorkspaceSnapshotAfter: run.WorkspaceSnapshotAfter,
403434
WorkspaceBindingStatus: run.WorkspaceBindingStatus,
404435
WorkspaceBindingReason: run.WorkspaceBindingReason,
436+
Process: validationProcessIdentity(run.Process),
437+
Phases: validationPhases(run.Phases),
438+
TimeoutPhase: run.TimeoutPhase,
439+
FailurePhase: run.FailurePhase,
440+
Resources: validationResources(run.Resources),
441+
Cleanup: validationCleanup(run.Cleanup),
442+
}
443+
}
444+
445+
func validationRunGroupResult(group *evidence.ValidationRunGroup) *cli.ValidationRunGroupResult {
446+
result := &cli.ValidationRunGroupResult{
447+
ID: group.ID, DefinitionID: group.DefinitionID, InvestigationID: group.InvestigationID,
448+
ConfigurationSHA256: group.ConfigurationSHA256, RequestedRuns: group.RequestedRuns, CompletedRuns: group.CompletedRuns,
449+
Concurrency: group.Concurrency, PerRunTimeout: group.PerRunTimeout.String(), OverallTimeout: group.OverallTimeout.String(),
450+
SampleInterval: group.SampleInterval.String(), Classification: string(group.Classification),
451+
StartedAt: formatTime(group.StartedAt), CompletedAt: formatTime(group.CompletedAt),
452+
}
453+
for _, attempt := range group.Attempts {
454+
result.Attempts = append(result.Attempts, cli.ValidationAttemptResult{
455+
Index: attempt.Index, Kind: string(attempt.Kind), RunID: attempt.RunID,
456+
StartedAt: formatTime(attempt.StartedAt), CompletedAt: formatTime(attempt.CompletedAt), ExitCode: attempt.ExitCode,
457+
Classification: string(attempt.Classification), ObservationStatus: string(attempt.ObservationStatus),
458+
TimeoutPhase: attempt.TimeoutPhase, FailurePhase: attempt.FailurePhase,
459+
Error: attempt.Error, Process: validationProcessIdentity(attempt.Process),
460+
Phases: validationPhases(attempt.Phases),
461+
Resources: validationResources(attempt.Resources), Cleanup: validationCleanup(attempt.Cleanup),
462+
})
463+
}
464+
for _, aggregate := range group.Aggregates {
465+
result.Aggregates = append(result.Aggregates, cli.ValidationAggregateResult{
466+
Kind: string(aggregate.Kind), Requested: aggregate.Requested, Completed: aggregate.Completed,
467+
Passing: aggregate.Passing, Failing: aggregate.Failing, Inconclusive: aggregate.Inconclusive,
468+
Cancelled: aggregate.Cancelled, Classification: string(aggregate.Classification),
469+
ResourceClassification: aggregate.ResourceClassification,
470+
})
471+
}
472+
if group.Comparison != nil {
473+
result.Comparison = &cli.ValidationGroupComparisonResult{Classification: string(group.Comparison.Classification), Explanation: group.Comparison.Explanation}
474+
}
475+
return result
476+
}
477+
478+
func validationPhases(value evidence.RunPhases) cli.ValidationRunPhases {
479+
return cli.ValidationRunPhases{
480+
SpawnStartedAt: formatTime(value.SpawnStartedAt), ProcessStartedAt: formatTime(value.ProcessStartedAt),
481+
InitializedAt: formatTime(value.InitializedAt), ToolsListedAt: formatTime(value.ToolsListedAt),
482+
FirstResponseAt: formatTime(value.FirstResponseAt), ExecutionEndedAt: formatTime(value.ExecutionEndedAt),
483+
ShutdownStartedAt: formatTime(value.ShutdownStartedAt), ShutdownCheckedAt: formatTime(value.ShutdownCheckedAt),
484+
}
485+
}
486+
487+
func validationProcessIdentity(value evidence.ProcessIdentity) cli.ValidationProcessIdentity {
488+
return cli.ValidationProcessIdentity{PID: value.PID, CreateTimeUnixMilli: value.CreateTimeUnixMilli}
489+
}
490+
491+
func validationResources(value evidence.ResourceTelemetry) cli.ValidationResourceTelemetry {
492+
return cli.ValidationResourceTelemetry{
493+
Provider: value.Provider, Platform: value.Platform, SampleInterval: value.SampleInterval.String(), SampleCount: value.SampleCount,
494+
CPUTimeMillis: cli.ValidationInt64Metric{Value: value.CPUTimeMillis.Value, UnavailableReason: value.CPUTimeMillis.UnavailableReason},
495+
PeakRSSBytes: cli.ValidationUint64Metric{Value: value.PeakRSSBytes.Value, UnavailableReason: value.PeakRSSBytes.UnavailableReason},
496+
PeakChildCount: cli.ValidationInt64Metric{Value: value.PeakChildCount.Value, UnavailableReason: value.PeakChildCount.UnavailableReason},
497+
SamplerOverheadNanoseconds: value.SamplerOverheadNanoseconds,
498+
}
499+
}
500+
501+
func validationCleanup(value evidence.CleanupResult) cli.ValidationCleanupResult {
502+
result := cli.ValidationCleanupResult{Status: value.Status, Reason: value.Reason, CheckedAt: formatTime(value.CheckedAt)}
503+
for _, survivor := range value.Survivors {
504+
result.Survivors = append(result.Survivors, validationProcessIdentity(survivor))
405505
}
506+
return result
406507
}
407508

408509
func observationContractToEvidence(contract *cli.ValidationObservationContract) *evidence.ObservationContract {

internal/app/mcp_v1.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,14 @@ func (r *MCPReader) DefineValidation(ctx context.Context, in mcpserver.DefineVal
598598
}
599599
timeout = d
600600
}
601+
var readinessTimeout time.Duration
602+
if in.ReadinessTimeout != "" {
603+
d, err := time.ParseDuration(in.ReadinessTimeout)
604+
if err != nil {
605+
return mcpserver.ValidationOutput{}, fmt.Errorf("invalid readiness timeout: %w", err)
606+
}
607+
readinessTimeout = d
608+
}
601609
opts := cli.DefineValidationOptions{
602610
Kind: in.Kind,
603611
Command: in.Command,
@@ -608,6 +616,8 @@ func (r *MCPReader) DefineValidation(ctx context.Context, in mcpserver.DefineVal
608616
Timeout: timeout,
609617
MaxOutputBytes: in.MaxOutputBytes,
610618
Observation: observationContractMCPToCLI(in.Observation),
619+
Protocol: in.Protocol,
620+
ReadinessTimeout: readinessTimeout,
611621
}
612622
res, err := r.Service.DefineValidation(ctx, in.InvestigationID, opts)
613623
if err != nil {
@@ -632,6 +642,8 @@ func validationResultToMCP(res *cli.ValidationResult) mcpserver.ValidationOutput
632642
Timeout: res.Timeout,
633643
MaxOutputBytes: res.MaxOutputBytes,
634644
Observation: observationContractCLIToMCP(res.Observation),
645+
Protocol: res.Protocol,
646+
ReadinessTimeout: res.ReadinessTimeout,
635647
CreatedAt: res.CreatedAt,
636648
}
637649
}

0 commit comments

Comments
 (0)