@dannylee1020/kkt 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +275 -0
- package/assets/kkt-readme-modern.png +0 -0
- package/bin/kkt-install.mjs +25 -0
- package/cmd/kkt/main.go +15 -0
- package/go.mod +3 -0
- package/internal/workflow/cli.go +349 -0
- package/internal/workflow/cli_test.go +1266 -0
- package/internal/workflow/guardrails.go +900 -0
- package/internal/workflow/init.go +164 -0
- package/internal/workflow/init_test.go +113 -0
- package/internal/workflow/operations.go +1855 -0
- package/internal/workflow/validation_commands.go +291 -0
- package/internal/workflow/workspace.go +802 -0
- package/internal/workflow/workspace_test.go +285 -0
- package/package.json +45 -0
- package/scripts/install-cli.sh +210 -0
- package/scripts/install.sh +644 -0
- package/skills/kkt/SKILL.md +74 -0
- package/skills/kkt/references/discovery-tooling.md +53 -0
- package/skills/kkt/references/feature-optimization-model.md +120 -0
- package/skills/kkt/references/kkt-kernel.md +58 -0
- package/skills/kkt/references/layered-modeling-methods.md +101 -0
- package/skills/kkt/references/plan-assimilation.md +46 -0
- package/skills/kkt/references/schemas.md +231 -0
- package/skills/kkt/references/state-contract.md +76 -0
- package/skills/kkt-loop/SKILL.md +99 -0
- package/skills/kkt-loop/references/discovery-tooling.md +53 -0
- package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
- package/skills/kkt-loop/references/kkt-kernel.md +58 -0
- package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-loop/references/plan-assimilation.md +46 -0
- package/skills/kkt-loop/references/schemas.md +231 -0
- package/skills/kkt-loop/references/state-contract.md +76 -0
- package/skills/kkt-model/SKILL.md +76 -0
- package/skills/kkt-model/references/discovery-tooling.md +53 -0
- package/skills/kkt-model/references/feature-optimization-model.md +120 -0
- package/skills/kkt-model/references/kkt-kernel.md +58 -0
- package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-model/references/plan-assimilation.md +46 -0
- package/skills/kkt-model/references/schemas.md +231 -0
- package/skills/kkt-model/references/state-contract.md +76 -0
- package/skills/kkt-run/SKILL.md +62 -0
- package/skills/kkt-run/references/discovery-tooling.md +53 -0
- package/skills/kkt-run/references/feature-optimization-model.md +120 -0
- package/skills/kkt-run/references/kkt-kernel.md +58 -0
- package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-run/references/plan-assimilation.md +46 -0
- package/skills/kkt-run/references/schemas.md +231 -0
- package/skills/kkt-run/references/state-contract.md +76 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
package workflow
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"os"
|
|
11
|
+
"os/exec"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"regexp"
|
|
14
|
+
"sort"
|
|
15
|
+
"strings"
|
|
16
|
+
"time"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type GuardrailContract struct {
|
|
20
|
+
SchemaVersion int `json:"schema_version"`
|
|
21
|
+
Mode string `json:"mode,omitempty"`
|
|
22
|
+
Source GuardrailSource `json:"source"`
|
|
23
|
+
Constraints []GuardrailConstraint `json:"constraints,omitempty"`
|
|
24
|
+
Scope GuardrailScope `json:"scope,omitempty"`
|
|
25
|
+
ChangeBounds GuardrailBounds `json:"change_bounds"`
|
|
26
|
+
Workflow GuardrailWorkflow `json:"workflow"`
|
|
27
|
+
Validation GuardrailChecks `json:"validation"`
|
|
28
|
+
StopConditions []string `json:"stop_conditions,omitempty"`
|
|
29
|
+
DriftPolicy GuardrailDriftPolicy `json:"drift_policy"`
|
|
30
|
+
GeneratedAt string `json:"generated_at,omitempty"`
|
|
31
|
+
ModelReadyCheckpoint []string `json:"model_ready_checkpoint,omitempty"`
|
|
32
|
+
ContinuationCheckpoint []string `json:"continuation_checkpoint,omitempty"`
|
|
33
|
+
FinalizeCheckpoint []string `json:"finalize_checkpoint,omitempty"`
|
|
34
|
+
ImplementationNotes []string `json:"implementation_notes,omitempty"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type GuardrailSource struct {
|
|
38
|
+
WorkspaceType string `json:"workspace_type"`
|
|
39
|
+
Workspace string `json:"workspace"`
|
|
40
|
+
Request string `json:"request"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type GuardrailScope struct {
|
|
44
|
+
Allowed []string `json:"allowed,omitempty"`
|
|
45
|
+
Blocked []string `json:"blocked,omitempty"`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type GuardrailConstraint struct {
|
|
49
|
+
ID string `json:"id"`
|
|
50
|
+
Kind string `json:"kind"`
|
|
51
|
+
Severity string `json:"severity"`
|
|
52
|
+
Statement string `json:"statement"`
|
|
53
|
+
AllowedPaths []string `json:"allowed_paths,omitempty"`
|
|
54
|
+
BlockedPaths []string `json:"blocked_paths,omitempty"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type GuardrailBounds struct {
|
|
58
|
+
AllowedPaths []string `json:"allowed_paths,omitempty"`
|
|
59
|
+
BlockedPaths []string `json:"blocked_paths,omitempty"`
|
|
60
|
+
AllowedPathsOrSurfaces []string `json:"allowed_paths_or_surfaces,omitempty"`
|
|
61
|
+
BlockedPathsOrSurfaces []string `json:"blocked_paths_or_surfaces,omitempty"`
|
|
62
|
+
RequireExplicitApprovalOutsideAllowed bool `json:"require_explicit_approval_outside_allowed,omitempty"`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type GuardrailWorkflow struct {
|
|
66
|
+
ExecutionMode string `json:"execution_mode"`
|
|
67
|
+
RequiresApprovalBeforeMutation bool `json:"requires_approval_before_mutation"`
|
|
68
|
+
RequiresValidationBeforeDone bool `json:"requires_validation_before_done"`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type GuardrailChecks struct {
|
|
72
|
+
AcceptanceCriteria []string `json:"acceptance_criteria"`
|
|
73
|
+
RequiredCommands []string `json:"required_commands"`
|
|
74
|
+
EvidenceRequired []string `json:"evidence_required"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type GuardrailDriftPolicy struct {
|
|
78
|
+
BlockOn []string `json:"block_on,omitempty"`
|
|
79
|
+
WarnOn []string `json:"warn_on,omitempty"`
|
|
80
|
+
Legacy string `json:"-"`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func (policy *GuardrailDriftPolicy) UnmarshalJSON(data []byte) error {
|
|
84
|
+
var legacy string
|
|
85
|
+
if err := json.Unmarshal(data, &legacy); err == nil {
|
|
86
|
+
policy.Legacy = legacy
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
type driftPolicy GuardrailDriftPolicy
|
|
90
|
+
var next driftPolicy
|
|
91
|
+
if err := json.Unmarshal(data, &next); err != nil {
|
|
92
|
+
return err
|
|
93
|
+
}
|
|
94
|
+
*policy = GuardrailDriftPolicy(next)
|
|
95
|
+
return nil
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type JudgeResult struct {
|
|
99
|
+
SchemaVersion int `json:"schema_version"`
|
|
100
|
+
Verdict string `json:"verdict"`
|
|
101
|
+
Checkpoint string `json:"checkpoint"`
|
|
102
|
+
Mode string `json:"mode"`
|
|
103
|
+
Workspace string `json:"workspace,omitempty"`
|
|
104
|
+
WorkspaceType string `json:"workspace_type,omitempty"`
|
|
105
|
+
ActiveLayer string `json:"active_layer,omitempty"`
|
|
106
|
+
DriftType string `json:"drift_type,omitempty"`
|
|
107
|
+
Reason string `json:"reason"`
|
|
108
|
+
Repair []string `json:"repair,omitempty"`
|
|
109
|
+
Evidence []string `json:"evidence,omitempty"`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type ApprovalBaseline struct {
|
|
113
|
+
SchemaVersion int `json:"schema_version"`
|
|
114
|
+
RecordedAt string `json:"recorded_at"`
|
|
115
|
+
Paths map[string]string `json:"paths"`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func runGuardrails(args []string, stdout io.Writer) error {
|
|
119
|
+
if len(args) == 0 {
|
|
120
|
+
return errors.New("guardrails requires an action: show, set, or validate")
|
|
121
|
+
}
|
|
122
|
+
action := args[0]
|
|
123
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
124
|
+
if err != nil {
|
|
125
|
+
return err
|
|
126
|
+
}
|
|
127
|
+
path := filepath.Join(workspace, "guardrails.json")
|
|
128
|
+
switch action {
|
|
129
|
+
case "show":
|
|
130
|
+
if len(args) > 1 {
|
|
131
|
+
return errors.New("guardrails show accepts no content")
|
|
132
|
+
}
|
|
133
|
+
payload, err := os.ReadFile(path)
|
|
134
|
+
if err != nil {
|
|
135
|
+
return err
|
|
136
|
+
}
|
|
137
|
+
_, err = stdout.Write(payload)
|
|
138
|
+
return err
|
|
139
|
+
case "set":
|
|
140
|
+
content, err := commandContent(args[1:])
|
|
141
|
+
if err != nil {
|
|
142
|
+
return err
|
|
143
|
+
}
|
|
144
|
+
content = strings.TrimSpace(content)
|
|
145
|
+
if content == "" {
|
|
146
|
+
return errors.New("guardrails set requires JSON content")
|
|
147
|
+
}
|
|
148
|
+
var raw map[string]any
|
|
149
|
+
if err := json.Unmarshal([]byte(content), &raw); err != nil {
|
|
150
|
+
return err
|
|
151
|
+
}
|
|
152
|
+
if err := os.WriteFile(path, []byte(content+"\n"), 0o644); err != nil {
|
|
153
|
+
return err
|
|
154
|
+
}
|
|
155
|
+
fmt.Fprintln(stdout, "recorded: guardrails")
|
|
156
|
+
return nil
|
|
157
|
+
case "validate":
|
|
158
|
+
if len(args) > 1 {
|
|
159
|
+
return errors.New("guardrails validate accepts no content")
|
|
160
|
+
}
|
|
161
|
+
issues := validateGuardrails(workspace)
|
|
162
|
+
if len(issues) == 0 {
|
|
163
|
+
fmt.Fprintf(stdout, "valid: %s\n", path)
|
|
164
|
+
return nil
|
|
165
|
+
}
|
|
166
|
+
fmt.Fprintf(stdout, "invalid: %s\n", path)
|
|
167
|
+
for _, issue := range issues {
|
|
168
|
+
fmt.Fprintf(stdout, "- %s\n", issue)
|
|
169
|
+
}
|
|
170
|
+
return errors.New("guardrails validation failed")
|
|
171
|
+
default:
|
|
172
|
+
return fmt.Errorf("unsupported guardrails action %q", action)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func runRun(args []string, stdout io.Writer) error {
|
|
177
|
+
if len(args) == 0 {
|
|
178
|
+
return errors.New("run requires an action: from-model")
|
|
179
|
+
}
|
|
180
|
+
switch args[0] {
|
|
181
|
+
case "from-model":
|
|
182
|
+
if len(args) > 2 {
|
|
183
|
+
return errors.New("run from-model accepts at most one model workspace")
|
|
184
|
+
}
|
|
185
|
+
workspace, err := createRunFromModel(".", firstArg(args[1:]))
|
|
186
|
+
if err != nil {
|
|
187
|
+
return err
|
|
188
|
+
}
|
|
189
|
+
fmt.Fprintf(stdout, "created: %s\n", workspace.Path)
|
|
190
|
+
fmt.Fprintln(stdout, "profile: run")
|
|
191
|
+
fmt.Fprintln(stdout, "next: run kkt judge --checkpoint model-ready --json, then get approval before mutation")
|
|
192
|
+
return nil
|
|
193
|
+
default:
|
|
194
|
+
return fmt.Errorf("unsupported run action %q", args[0])
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func runJudge(args []string, stdout io.Writer) error {
|
|
199
|
+
checkpoint, jsonOutput, path, err := parseJudgeArgs(args)
|
|
200
|
+
if err != nil {
|
|
201
|
+
return err
|
|
202
|
+
}
|
|
203
|
+
result, err := judgeWorkspace(".", path, checkpoint)
|
|
204
|
+
if err != nil {
|
|
205
|
+
return err
|
|
206
|
+
}
|
|
207
|
+
if jsonOutput {
|
|
208
|
+
if err := writeJSON(stdout, result); err != nil {
|
|
209
|
+
return err
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
fmt.Fprintf(stdout, "%s: %s\n", result.Verdict, result.Reason)
|
|
213
|
+
for _, repair := range result.Repair {
|
|
214
|
+
fmt.Fprintf(stdout, "- %s\n", repair)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if result.Verdict == "block" {
|
|
218
|
+
return errors.New(result.Reason)
|
|
219
|
+
}
|
|
220
|
+
return nil
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func parseJudgeArgs(args []string) (string, bool, string, error) {
|
|
224
|
+
checkpoint := ""
|
|
225
|
+
jsonOutput := false
|
|
226
|
+
path := ""
|
|
227
|
+
for i := 0; i < len(args); i++ {
|
|
228
|
+
switch args[i] {
|
|
229
|
+
case "--checkpoint":
|
|
230
|
+
i++
|
|
231
|
+
if i >= len(args) || strings.TrimSpace(args[i]) == "" {
|
|
232
|
+
return "", false, "", errors.New("judge --checkpoint requires a checkpoint")
|
|
233
|
+
}
|
|
234
|
+
checkpoint = strings.TrimSpace(args[i])
|
|
235
|
+
case "--json":
|
|
236
|
+
jsonOutput = true
|
|
237
|
+
default:
|
|
238
|
+
if strings.HasPrefix(args[i], "-") {
|
|
239
|
+
return "", false, "", fmt.Errorf("judge does not accept flag: %s", args[i])
|
|
240
|
+
}
|
|
241
|
+
if path != "" {
|
|
242
|
+
return "", false, "", errors.New("judge accepts at most one workspace path")
|
|
243
|
+
}
|
|
244
|
+
path = args[i]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if checkpoint == "" {
|
|
248
|
+
return "", false, "", errors.New("judge requires --checkpoint")
|
|
249
|
+
}
|
|
250
|
+
return checkpoint, jsonOutput, path, nil
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func judgeWorkspace(root, path, checkpoint string) (JudgeResult, error) {
|
|
254
|
+
workspace, err := ResolveWorkspace(root, path)
|
|
255
|
+
if err != nil {
|
|
256
|
+
if strings.Contains(err.Error(), "no .kkt workspace found") {
|
|
257
|
+
return JudgeResult{
|
|
258
|
+
SchemaVersion: 1,
|
|
259
|
+
Verdict: "allow",
|
|
260
|
+
Checkpoint: checkpoint,
|
|
261
|
+
Mode: "observe-inactive",
|
|
262
|
+
Reason: "no active .kkt workspace found",
|
|
263
|
+
}, nil
|
|
264
|
+
}
|
|
265
|
+
return JudgeResult{}, err
|
|
266
|
+
}
|
|
267
|
+
state, err := ReadState(workspace)
|
|
268
|
+
if err != nil {
|
|
269
|
+
return JudgeResult{}, err
|
|
270
|
+
}
|
|
271
|
+
result := JudgeResult{
|
|
272
|
+
SchemaVersion: 1,
|
|
273
|
+
Verdict: "allow",
|
|
274
|
+
Checkpoint: checkpoint,
|
|
275
|
+
Mode: "enforce-active",
|
|
276
|
+
Workspace: workspace,
|
|
277
|
+
WorkspaceType: state.WorkspaceType,
|
|
278
|
+
ActiveLayer: state.ActiveLayer,
|
|
279
|
+
Reason: "checkpoint allowed",
|
|
280
|
+
}
|
|
281
|
+
contract, contractErr := readGuardrails(workspace)
|
|
282
|
+
guardrailIssues := validateGuardrails(workspace)
|
|
283
|
+
hasGuardrails := len(guardrailIssues) == 0
|
|
284
|
+
if !hasGuardrails {
|
|
285
|
+
result.Verdict = "warn"
|
|
286
|
+
result.DriftType = "guardrail_contract"
|
|
287
|
+
result.Reason = "guardrail contract is incomplete"
|
|
288
|
+
result.Repair = append(result.Repair, "record a complete guardrails.json before relying on semantic drift enforcement")
|
|
289
|
+
result.Evidence = append(result.Evidence, guardrailIssues...)
|
|
290
|
+
}
|
|
291
|
+
switch checkpoint {
|
|
292
|
+
case "model-ready":
|
|
293
|
+
if state.WorkspaceType == "run" || state.WorkspaceType == "loop" {
|
|
294
|
+
if !hasGuardrails {
|
|
295
|
+
result.Verdict = "block"
|
|
296
|
+
result.Reason = "model-ready checkpoint requires valid guardrails"
|
|
297
|
+
}
|
|
298
|
+
if contractErr == nil {
|
|
299
|
+
if issues := guardrailExecutionReadinessIssues(contract); len(issues) > 0 {
|
|
300
|
+
result.Verdict = "block"
|
|
301
|
+
result.DriftType = "guardrail_contract"
|
|
302
|
+
result.Reason = "model-ready checkpoint requires constraint path bounds"
|
|
303
|
+
result.Evidence = append(result.Evidence, issues...)
|
|
304
|
+
result.Repair = append(result.Repair, "populate guardrails.json constraints and change_bounds.allowed_paths from the selected model")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if state.ActiveLayer != "execution" && state.ActiveLayer != "validation" {
|
|
308
|
+
result.Verdict = "block"
|
|
309
|
+
result.DriftType = "model_ready"
|
|
310
|
+
result.Reason = "model-ready checkpoint requires an execution contract"
|
|
311
|
+
result.Repair = append(result.Repair, "record the selected model and plan before execution")
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
case "pre-mutation":
|
|
315
|
+
if (state.WorkspaceType == "run" || state.WorkspaceType == "loop") && state.ApprovalStatus != "approved" {
|
|
316
|
+
result.Verdict = "block"
|
|
317
|
+
result.DriftType = "approval"
|
|
318
|
+
result.Reason = "mutation requires approval"
|
|
319
|
+
result.Repair = append(result.Repair, "show the selected model and record approval with kkt approve")
|
|
320
|
+
}
|
|
321
|
+
if (state.WorkspaceType == "run" || state.WorkspaceType == "loop") && contractErr == nil {
|
|
322
|
+
if issues := changedPathIssues(root, workspace, contract); len(issues) > 0 {
|
|
323
|
+
result.Verdict = "block"
|
|
324
|
+
result.DriftType = "path_scope"
|
|
325
|
+
result.Reason = "changed paths violate guardrail bounds"
|
|
326
|
+
result.Evidence = append(result.Evidence, issues...)
|
|
327
|
+
result.Repair = append(result.Repair, "revert out-of-scope files or re-model and update guardrails.json before continuing")
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
case "continuation":
|
|
331
|
+
if state.WorkspaceType == "loop" {
|
|
332
|
+
replay, replayErr := CheckReplay(workspace)
|
|
333
|
+
if replayErr != nil {
|
|
334
|
+
return JudgeResult{}, replayErr
|
|
335
|
+
}
|
|
336
|
+
if !replay.OK {
|
|
337
|
+
result.Verdict = "block"
|
|
338
|
+
result.DriftType = "replay"
|
|
339
|
+
result.Reason = "loop replay drift detected"
|
|
340
|
+
result.Evidence = append(result.Evidence, replay.Issues...)
|
|
341
|
+
result.Repair = append(result.Repair, "inspect kkt.yaml and events.jsonl before continuing")
|
|
342
|
+
}
|
|
343
|
+
loop, loopErr := readLoopState(workspace)
|
|
344
|
+
if loopErr == nil {
|
|
345
|
+
for _, stop := range loop.StopConditions {
|
|
346
|
+
if stop.Status == "active" {
|
|
347
|
+
result.Verdict = "block"
|
|
348
|
+
result.DriftType = "stop_condition"
|
|
349
|
+
result.Reason = "active stop condition blocks continuation"
|
|
350
|
+
result.Evidence = append(result.Evidence, stop.Text)
|
|
351
|
+
result.Repair = append(result.Repair, "resolve the active stop condition before continuing")
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
case "finalize":
|
|
358
|
+
validation, validationErr := ValidateWorkspace(workspace)
|
|
359
|
+
if validationErr != nil {
|
|
360
|
+
return JudgeResult{}, validationErr
|
|
361
|
+
}
|
|
362
|
+
if !validation.OK {
|
|
363
|
+
result.Verdict = "block"
|
|
364
|
+
result.DriftType = "validation"
|
|
365
|
+
result.Reason = "workspace validation failed"
|
|
366
|
+
result.Evidence = append(result.Evidence, validation.Issues...)
|
|
367
|
+
result.Repair = append(result.Repair, "record required evidence and satisfy validation before completion")
|
|
368
|
+
}
|
|
369
|
+
if (state.WorkspaceType == "run" || state.WorkspaceType == "loop") && contractErr == nil {
|
|
370
|
+
if issues := changedPathIssues(root, workspace, contract); len(issues) > 0 {
|
|
371
|
+
result.Verdict = "block"
|
|
372
|
+
result.DriftType = "path_scope"
|
|
373
|
+
result.Reason = "changed paths violate guardrail bounds"
|
|
374
|
+
result.Evidence = append(result.Evidence, issues...)
|
|
375
|
+
result.Repair = append(result.Repair, "revert out-of-scope files or re-model and update guardrails.json before completion")
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
case "pre-tool", "post-tool", "pre-compact", "post-compact":
|
|
379
|
+
if (state.WorkspaceType == "run" || state.WorkspaceType == "loop") && contractErr == nil {
|
|
380
|
+
if issues := changedPathIssues(root, workspace, contract); len(issues) > 0 {
|
|
381
|
+
result.Verdict = "block"
|
|
382
|
+
result.DriftType = "path_scope"
|
|
383
|
+
result.Reason = "changed paths violate guardrail bounds"
|
|
384
|
+
result.Evidence = append(result.Evidence, issues...)
|
|
385
|
+
result.Repair = append(result.Repair, "revert out-of-scope files or re-model and update guardrails.json before continuing")
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
default:
|
|
389
|
+
result.Verdict = "warn"
|
|
390
|
+
result.DriftType = "unknown_checkpoint"
|
|
391
|
+
result.Reason = "unknown checkpoint; only workspace-level guardrails were checked"
|
|
392
|
+
result.Repair = append(result.Repair, "use model-ready, pre-mutation, continuation, finalize, pre-tool, post-tool, pre-compact, or post-compact")
|
|
393
|
+
}
|
|
394
|
+
return result, nil
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
func defaultGuardrailsJSON(request, profile, sourceWorkspace string) string {
|
|
398
|
+
contract := GuardrailContract{
|
|
399
|
+
SchemaVersion: 1,
|
|
400
|
+
Source: GuardrailSource{
|
|
401
|
+
WorkspaceType: profile,
|
|
402
|
+
Workspace: sourceWorkspace,
|
|
403
|
+
Request: request,
|
|
404
|
+
},
|
|
405
|
+
Constraints: []GuardrailConstraint{
|
|
406
|
+
{
|
|
407
|
+
ID: "selected-model-scope",
|
|
408
|
+
Kind: "scope",
|
|
409
|
+
Severity: "block",
|
|
410
|
+
Statement: "Implement only the selected KKT model and execution contract.",
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
ChangeBounds: GuardrailBounds{
|
|
414
|
+
AllowedPaths: []string{},
|
|
415
|
+
BlockedPaths: []string{".git/**", ".env*", "dist/**"},
|
|
416
|
+
RequireExplicitApprovalOutsideAllowed: true,
|
|
417
|
+
},
|
|
418
|
+
Workflow: GuardrailWorkflow{
|
|
419
|
+
ExecutionMode: profile,
|
|
420
|
+
RequiresApprovalBeforeMutation: profile == "run" || profile == "loop",
|
|
421
|
+
RequiresValidationBeforeDone: true,
|
|
422
|
+
},
|
|
423
|
+
Validation: GuardrailChecks{
|
|
424
|
+
AcceptanceCriteria: []string{},
|
|
425
|
+
RequiredCommands: []string{},
|
|
426
|
+
EvidenceRequired: []string{"validation evidence recorded before done"},
|
|
427
|
+
},
|
|
428
|
+
DriftPolicy: GuardrailDriftPolicy{
|
|
429
|
+
BlockOn: []string{
|
|
430
|
+
"missing_approval",
|
|
431
|
+
"empty_allowed_paths",
|
|
432
|
+
"changed_blocked_path",
|
|
433
|
+
"validation_failed",
|
|
434
|
+
},
|
|
435
|
+
WarnOn: []string{},
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
payload, err := json.MarshalIndent(contract, "", " ")
|
|
439
|
+
if err != nil {
|
|
440
|
+
return "{}\n"
|
|
441
|
+
}
|
|
442
|
+
return string(payload) + "\n"
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func readGuardrails(workspace string) (GuardrailContract, error) {
|
|
446
|
+
payload, err := os.ReadFile(filepath.Join(workspace, "guardrails.json"))
|
|
447
|
+
if err != nil {
|
|
448
|
+
return GuardrailContract{}, err
|
|
449
|
+
}
|
|
450
|
+
var contract GuardrailContract
|
|
451
|
+
if err := json.Unmarshal(payload, &contract); err != nil {
|
|
452
|
+
return GuardrailContract{}, err
|
|
453
|
+
}
|
|
454
|
+
return contract, nil
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
func validateGuardrails(workspace string) []string {
|
|
458
|
+
contract, err := readGuardrails(workspace)
|
|
459
|
+
if err != nil {
|
|
460
|
+
return []string{"guardrails.json could not be read: " + err.Error()}
|
|
461
|
+
}
|
|
462
|
+
var issues []string
|
|
463
|
+
if contract.SchemaVersion != 1 {
|
|
464
|
+
issues = append(issues, "guardrails.json schema_version must be 1")
|
|
465
|
+
}
|
|
466
|
+
if strings.TrimSpace(contract.Source.WorkspaceType) == "" {
|
|
467
|
+
issues = append(issues, "guardrails.json source.workspace_type is required")
|
|
468
|
+
}
|
|
469
|
+
if strings.TrimSpace(contract.Source.Workspace) == "" {
|
|
470
|
+
issues = append(issues, "guardrails.json source.workspace is required")
|
|
471
|
+
}
|
|
472
|
+
if strings.TrimSpace(contract.Source.Request) == "" {
|
|
473
|
+
issues = append(issues, "guardrails.json source.request is required")
|
|
474
|
+
}
|
|
475
|
+
if strings.TrimSpace(contract.Workflow.ExecutionMode) == "" {
|
|
476
|
+
issues = append(issues, "guardrails.json workflow.execution_mode is required")
|
|
477
|
+
}
|
|
478
|
+
if contract.Workflow.RequiresValidationBeforeDone && len(contract.Validation.EvidenceRequired) == 0 && len(contract.Validation.RequiredCommands) == 0 {
|
|
479
|
+
issues = append(issues, "guardrails.json requires validation but lists no evidence or commands")
|
|
480
|
+
}
|
|
481
|
+
if len(contract.DriftPolicy.BlockOn) == 0 && strings.TrimSpace(contract.DriftPolicy.Legacy) == "" {
|
|
482
|
+
issues = append(issues, "guardrails.json drift_policy.block_on is required")
|
|
483
|
+
}
|
|
484
|
+
return issues
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
func guardrailExecutionReadinessIssues(contract GuardrailContract) []string {
|
|
488
|
+
var issues []string
|
|
489
|
+
if len(contract.Constraints) == 0 {
|
|
490
|
+
issues = append(issues, "guardrails.json constraints must include at least one modeled constraint")
|
|
491
|
+
}
|
|
492
|
+
if len(contract.allowedPaths()) == 0 {
|
|
493
|
+
issues = append(issues, "guardrails.json change_bounds.allowed_paths must include the selected model's expected files or surfaces")
|
|
494
|
+
}
|
|
495
|
+
return issues
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
func changedPathIssues(root, workspace string, contract GuardrailContract) []string {
|
|
499
|
+
projectRootDir, err := projectRootForWorkspace(root, workspace)
|
|
500
|
+
if err != nil {
|
|
501
|
+
return []string{"could not resolve project root for path guardrails: " + err.Error()}
|
|
502
|
+
}
|
|
503
|
+
changed, err := changedGitPaths(projectRootDir)
|
|
504
|
+
if err != nil {
|
|
505
|
+
return []string{"could not inspect changed paths: " + err.Error()}
|
|
506
|
+
}
|
|
507
|
+
if len(changed) == 0 {
|
|
508
|
+
return nil
|
|
509
|
+
}
|
|
510
|
+
allowed := contract.allowedPaths()
|
|
511
|
+
blocked := contract.blockedPaths()
|
|
512
|
+
baseline, hasBaseline, baselineErr := readApprovalBaseline(workspace)
|
|
513
|
+
var issues []string
|
|
514
|
+
if baselineErr != nil {
|
|
515
|
+
issues = append(issues, "approval baseline could not be read: "+baselineErr.Error())
|
|
516
|
+
}
|
|
517
|
+
for _, path := range changed {
|
|
518
|
+
if isKKTPath(path) {
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
if hasBaseline && unchangedFromApprovalBaseline(projectRootDir, baseline, path) {
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
if matchesAnyPathPattern(path, blocked) {
|
|
525
|
+
issues = append(issues, "changed blocked path: "+path)
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
// Paths outside the modeled allowed bounds are treated as unrelated branch
|
|
529
|
+
// work. Guardrails enforce the implementation scope and explicit blocks;
|
|
530
|
+
// they should not require an otherwise clean working tree.
|
|
531
|
+
if len(allowed) > 0 && !matchesAnyPathPattern(path, allowed) {
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return issues
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
func writeApprovalBaseline(workspace string) error {
|
|
539
|
+
projectRootDir, err := projectRootForWorkspace(".", workspace)
|
|
540
|
+
if err != nil {
|
|
541
|
+
return err
|
|
542
|
+
}
|
|
543
|
+
changed, err := changedGitPaths(projectRootDir)
|
|
544
|
+
if err != nil {
|
|
545
|
+
return err
|
|
546
|
+
}
|
|
547
|
+
baseline := ApprovalBaseline{
|
|
548
|
+
SchemaVersion: 1,
|
|
549
|
+
RecordedAt: time.Now().UTC().Format(time.RFC3339),
|
|
550
|
+
Paths: map[string]string{},
|
|
551
|
+
}
|
|
552
|
+
for _, path := range changed {
|
|
553
|
+
if isKKTPath(path) {
|
|
554
|
+
continue
|
|
555
|
+
}
|
|
556
|
+
fingerprint, err := pathFingerprint(projectRootDir, path)
|
|
557
|
+
if err != nil {
|
|
558
|
+
return err
|
|
559
|
+
}
|
|
560
|
+
baseline.Paths[path] = fingerprint
|
|
561
|
+
}
|
|
562
|
+
payload, err := json.MarshalIndent(baseline, "", " ")
|
|
563
|
+
if err != nil {
|
|
564
|
+
return err
|
|
565
|
+
}
|
|
566
|
+
return os.WriteFile(filepath.Join(workspace, "approval-baseline.json"), append(payload, '\n'), 0o644)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
func readApprovalBaseline(workspace string) (ApprovalBaseline, bool, error) {
|
|
570
|
+
payload, err := os.ReadFile(filepath.Join(workspace, "approval-baseline.json"))
|
|
571
|
+
if err != nil {
|
|
572
|
+
if os.IsNotExist(err) {
|
|
573
|
+
return ApprovalBaseline{}, false, nil
|
|
574
|
+
}
|
|
575
|
+
return ApprovalBaseline{}, false, err
|
|
576
|
+
}
|
|
577
|
+
var baseline ApprovalBaseline
|
|
578
|
+
if err := json.Unmarshal(payload, &baseline); err != nil {
|
|
579
|
+
return ApprovalBaseline{}, true, err
|
|
580
|
+
}
|
|
581
|
+
if baseline.Paths == nil {
|
|
582
|
+
baseline.Paths = map[string]string{}
|
|
583
|
+
}
|
|
584
|
+
return baseline, true, nil
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func unchangedFromApprovalBaseline(projectRootDir string, baseline ApprovalBaseline, path string) bool {
|
|
588
|
+
approvedFingerprint, ok := baseline.Paths[path]
|
|
589
|
+
if !ok {
|
|
590
|
+
return false
|
|
591
|
+
}
|
|
592
|
+
currentFingerprint, err := pathFingerprint(projectRootDir, path)
|
|
593
|
+
return err == nil && currentFingerprint == approvedFingerprint
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
func isKKTPath(path string) bool {
|
|
597
|
+
path = normalizeRepoPath(path)
|
|
598
|
+
return path == ".kkt" || strings.HasPrefix(path, ".kkt/")
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
func pathFingerprint(projectRootDir, path string) (string, error) {
|
|
602
|
+
fullPath := filepath.Join(projectRootDir, filepath.FromSlash(path))
|
|
603
|
+
payload, err := os.ReadFile(fullPath)
|
|
604
|
+
switch {
|
|
605
|
+
case err == nil:
|
|
606
|
+
sum := sha256.Sum256(payload)
|
|
607
|
+
return hex.EncodeToString(sum[:]), nil
|
|
608
|
+
case os.IsNotExist(err):
|
|
609
|
+
return "<deleted>", nil
|
|
610
|
+
default:
|
|
611
|
+
return "", err
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
func (contract GuardrailContract) allowedPaths() []string {
|
|
616
|
+
paths := append([]string{}, contract.ChangeBounds.AllowedPaths...)
|
|
617
|
+
paths = append(paths, contract.ChangeBounds.AllowedPathsOrSurfaces...)
|
|
618
|
+
for _, constraint := range contract.Constraints {
|
|
619
|
+
paths = append(paths, constraint.AllowedPaths...)
|
|
620
|
+
}
|
|
621
|
+
return uniqueNonEmpty(paths)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
func (contract GuardrailContract) blockedPaths() []string {
|
|
625
|
+
paths := append([]string{}, contract.ChangeBounds.BlockedPaths...)
|
|
626
|
+
paths = append(paths, contract.ChangeBounds.BlockedPathsOrSurfaces...)
|
|
627
|
+
for _, constraint := range contract.Constraints {
|
|
628
|
+
paths = append(paths, constraint.BlockedPaths...)
|
|
629
|
+
}
|
|
630
|
+
return uniqueNonEmpty(paths)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
func projectRootForWorkspace(root, workspace string) (string, error) {
|
|
634
|
+
clean := filepath.Clean(workspace)
|
|
635
|
+
for {
|
|
636
|
+
if filepath.Base(clean) == ".kkt" {
|
|
637
|
+
return filepath.Dir(clean), nil
|
|
638
|
+
}
|
|
639
|
+
parent := filepath.Dir(clean)
|
|
640
|
+
if parent == clean {
|
|
641
|
+
break
|
|
642
|
+
}
|
|
643
|
+
clean = parent
|
|
644
|
+
}
|
|
645
|
+
return projectRoot(root)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
func changedGitPaths(projectRootDir string) ([]string, error) {
|
|
649
|
+
if err := runGit(projectRootDir, "rev-parse", "--is-inside-work-tree"); err != nil {
|
|
650
|
+
return nil, nil
|
|
651
|
+
}
|
|
652
|
+
commands := [][]string{
|
|
653
|
+
{"diff", "--name-only"},
|
|
654
|
+
{"diff", "--name-only", "--cached"},
|
|
655
|
+
{"ls-files", "--others", "--exclude-standard"},
|
|
656
|
+
}
|
|
657
|
+
seen := map[string]bool{}
|
|
658
|
+
var paths []string
|
|
659
|
+
for _, args := range commands {
|
|
660
|
+
output, err := gitOutput(projectRootDir, args...)
|
|
661
|
+
if err != nil {
|
|
662
|
+
return nil, err
|
|
663
|
+
}
|
|
664
|
+
for _, line := range strings.Split(output, "\n") {
|
|
665
|
+
path := normalizeRepoPath(line)
|
|
666
|
+
if path == "" || seen[path] {
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
seen[path] = true
|
|
670
|
+
paths = append(paths, path)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
sort.Strings(paths)
|
|
674
|
+
return paths, nil
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
func runGit(projectRootDir string, args ...string) error {
|
|
678
|
+
_, err := gitOutput(projectRootDir, args...)
|
|
679
|
+
return err
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
func gitOutput(projectRootDir string, args ...string) (string, error) {
|
|
683
|
+
command := exec.Command("git", append([]string{"-C", projectRootDir}, args...)...)
|
|
684
|
+
output, err := command.Output()
|
|
685
|
+
if err != nil {
|
|
686
|
+
return "", err
|
|
687
|
+
}
|
|
688
|
+
return string(output), nil
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
func matchesAnyPathPattern(path string, patterns []string) bool {
|
|
692
|
+
for _, pattern := range patterns {
|
|
693
|
+
if matchPathPattern(path, pattern) {
|
|
694
|
+
return true
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return false
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
func matchPathPattern(path, pattern string) bool {
|
|
701
|
+
path = normalizeRepoPath(path)
|
|
702
|
+
pattern = normalizeRepoPath(pattern)
|
|
703
|
+
if path == "" || pattern == "" {
|
|
704
|
+
return false
|
|
705
|
+
}
|
|
706
|
+
if !strings.ContainsAny(pattern, "*?[") {
|
|
707
|
+
return path == pattern || strings.HasPrefix(path, strings.TrimSuffix(pattern, "/")+"/")
|
|
708
|
+
}
|
|
709
|
+
expression := globPatternRegexp(pattern)
|
|
710
|
+
matched, err := regexp.MatchString(expression, path)
|
|
711
|
+
return err == nil && matched
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
func globPatternRegexp(pattern string) string {
|
|
715
|
+
var builder strings.Builder
|
|
716
|
+
builder.WriteString("^")
|
|
717
|
+
for i := 0; i < len(pattern); i++ {
|
|
718
|
+
char := pattern[i]
|
|
719
|
+
if char == '*' {
|
|
720
|
+
if i+1 < len(pattern) && pattern[i+1] == '*' {
|
|
721
|
+
builder.WriteString(".*")
|
|
722
|
+
i++
|
|
723
|
+
} else {
|
|
724
|
+
builder.WriteString(`[^/]*`)
|
|
725
|
+
}
|
|
726
|
+
continue
|
|
727
|
+
}
|
|
728
|
+
if char == '?' {
|
|
729
|
+
builder.WriteString(`[^/]`)
|
|
730
|
+
continue
|
|
731
|
+
}
|
|
732
|
+
builder.WriteString(regexp.QuoteMeta(string(char)))
|
|
733
|
+
}
|
|
734
|
+
builder.WriteString("$")
|
|
735
|
+
return builder.String()
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
func normalizeRepoPath(path string) string {
|
|
739
|
+
path = filepath.ToSlash(strings.TrimSpace(path))
|
|
740
|
+
path = strings.TrimPrefix(path, "./")
|
|
741
|
+
return strings.Trim(path, "/")
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
func uniqueNonEmpty(values []string) []string {
|
|
745
|
+
seen := map[string]bool{}
|
|
746
|
+
var unique []string
|
|
747
|
+
for _, value := range values {
|
|
748
|
+
value = normalizeRepoPath(value)
|
|
749
|
+
if value == "" || seen[value] {
|
|
750
|
+
continue
|
|
751
|
+
}
|
|
752
|
+
seen[value] = true
|
|
753
|
+
unique = append(unique, value)
|
|
754
|
+
}
|
|
755
|
+
return unique
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
func createRunFromModel(root, modelWorkspace string) (Workspace, error) {
|
|
759
|
+
if modelWorkspace == "" {
|
|
760
|
+
resolved, err := ResolveWorkspace(root, "")
|
|
761
|
+
if err != nil {
|
|
762
|
+
return Workspace{}, err
|
|
763
|
+
}
|
|
764
|
+
modelWorkspace = resolved
|
|
765
|
+
}
|
|
766
|
+
modelState, err := ReadState(modelWorkspace)
|
|
767
|
+
if err != nil {
|
|
768
|
+
return Workspace{}, err
|
|
769
|
+
}
|
|
770
|
+
if modelState.WorkspaceType != "model" {
|
|
771
|
+
return Workspace{}, fmt.Errorf("from-model requires a model workspace, got %q", modelState.WorkspaceType)
|
|
772
|
+
}
|
|
773
|
+
if modelState.Status != "complete" {
|
|
774
|
+
return Workspace{}, fmt.Errorf("from-model requires a complete model workspace, got status %q", modelState.Status)
|
|
775
|
+
}
|
|
776
|
+
projectRootDir, err := projectRoot(root)
|
|
777
|
+
if err != nil {
|
|
778
|
+
return Workspace{}, err
|
|
779
|
+
}
|
|
780
|
+
request, err := stateRequest(modelWorkspace)
|
|
781
|
+
if err != nil {
|
|
782
|
+
return Workspace{}, err
|
|
783
|
+
}
|
|
784
|
+
if request == "" {
|
|
785
|
+
request = "Run selected KKT model"
|
|
786
|
+
}
|
|
787
|
+
now := time.Now().UTC()
|
|
788
|
+
slug := fmt.Sprintf("%s-run-%s", now.Format("20060102-150405"), filepath.Base(modelWorkspace))
|
|
789
|
+
base := filepath.Join(projectRootDir, ".kkt")
|
|
790
|
+
runWorkspace := filepath.Join(base, "run", slug)
|
|
791
|
+
if err := os.MkdirAll(runWorkspace, 0o755); err != nil {
|
|
792
|
+
return Workspace{}, err
|
|
793
|
+
}
|
|
794
|
+
sourceWorkspace := normalizeRepoPath(filepath.Join(".kkt", currentPointer("run", slug)))
|
|
795
|
+
files := workspaceFiles(request, "run", now, sourceWorkspace)
|
|
796
|
+
for name, content := range files {
|
|
797
|
+
if err := os.WriteFile(filepath.Join(runWorkspace, name), []byte(content), 0o644); err != nil {
|
|
798
|
+
return Workspace{}, err
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
for _, name := range []string{"intent.md", "discovery.md", "model.md", "guardrails.json"} {
|
|
802
|
+
if err := copyWorkspaceFile(modelWorkspace, runWorkspace, name); err != nil {
|
|
803
|
+
if errors.Is(err, os.ErrNotExist) && name == "guardrails.json" {
|
|
804
|
+
if writeErr := os.WriteFile(filepath.Join(runWorkspace, name), []byte(defaultGuardrailsJSON(request, "run", workspaceSourcePath(projectRootDir, modelWorkspace))), 0o644); writeErr != nil {
|
|
805
|
+
return Workspace{}, writeErr
|
|
806
|
+
}
|
|
807
|
+
continue
|
|
808
|
+
}
|
|
809
|
+
return Workspace{}, err
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if err := updateRunSource(runWorkspace, modelWorkspace); err != nil {
|
|
813
|
+
return Workspace{}, err
|
|
814
|
+
}
|
|
815
|
+
if err := markImportedModelLayers(runWorkspace, modelWorkspace); err != nil {
|
|
816
|
+
return Workspace{}, err
|
|
817
|
+
}
|
|
818
|
+
if err := os.WriteFile(filepath.Join(base, "current"), []byte(currentPointer("run", slug)+"\n"), 0o644); err != nil {
|
|
819
|
+
return Workspace{}, err
|
|
820
|
+
}
|
|
821
|
+
return Workspace{Path: runWorkspace, Profile: "run"}, nil
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
func copyWorkspaceFile(sourceWorkspace, targetWorkspace, name string) error {
|
|
825
|
+
payload, err := os.ReadFile(filepath.Join(sourceWorkspace, name))
|
|
826
|
+
if err != nil {
|
|
827
|
+
return err
|
|
828
|
+
}
|
|
829
|
+
return os.WriteFile(filepath.Join(targetWorkspace, name), payload, 0o644)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
func updateRunSource(runWorkspace, modelWorkspace string) error {
|
|
833
|
+
if err := updateTopLevelState(runWorkspace, "active_layer", "execution"); err != nil {
|
|
834
|
+
return err
|
|
835
|
+
}
|
|
836
|
+
// Running from a completed model imports the model decision; mutation still
|
|
837
|
+
// requires the user-facing approval gate before edits in the skill flow.
|
|
838
|
+
if err := updateApproval(runWorkspace, "pending", ""); err != nil {
|
|
839
|
+
return err
|
|
840
|
+
}
|
|
841
|
+
contract, err := readGuardrails(runWorkspace)
|
|
842
|
+
if err != nil {
|
|
843
|
+
return nil
|
|
844
|
+
}
|
|
845
|
+
contract.Source.WorkspaceType = "model"
|
|
846
|
+
projectRootDir, rootErr := projectRootForWorkspace(".", runWorkspace)
|
|
847
|
+
if rootErr == nil {
|
|
848
|
+
contract.Source.Workspace = workspaceSourcePath(projectRootDir, modelWorkspace)
|
|
849
|
+
} else {
|
|
850
|
+
contract.Source.Workspace = modelWorkspace
|
|
851
|
+
}
|
|
852
|
+
contract.Workflow.ExecutionMode = "run"
|
|
853
|
+
payload, err := json.MarshalIndent(contract, "", " ")
|
|
854
|
+
if err != nil {
|
|
855
|
+
return err
|
|
856
|
+
}
|
|
857
|
+
return os.WriteFile(filepath.Join(runWorkspace, "guardrails.json"), append(payload, '\n'), 0o644)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
func markImportedModelLayers(runWorkspace, modelWorkspace string) error {
|
|
861
|
+
source := modelWorkspace
|
|
862
|
+
if projectRootDir, err := projectRootForWorkspace(".", runWorkspace); err == nil {
|
|
863
|
+
source = workspaceSourcePath(projectRootDir, modelWorkspace)
|
|
864
|
+
}
|
|
865
|
+
for _, layer := range []string{"intent", "discovery", "modeling"} {
|
|
866
|
+
summary := "Imported from " + source
|
|
867
|
+
if err := updateLayerState(runWorkspace, layer, "complete", "imported", summary); err != nil {
|
|
868
|
+
return err
|
|
869
|
+
}
|
|
870
|
+
if err := appendMethodInvocation(runWorkspace, layer, "imported", summary); err != nil {
|
|
871
|
+
return err
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return nil
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
func workspaceSourcePath(projectRootDir, workspace string) string {
|
|
878
|
+
absoluteRoot, rootErr := filepath.Abs(projectRootDir)
|
|
879
|
+
absoluteWorkspace, workspaceErr := filepath.Abs(workspace)
|
|
880
|
+
if rootErr == nil && workspaceErr == nil {
|
|
881
|
+
if rel, err := filepath.Rel(absoluteRoot, absoluteWorkspace); err == nil && rel != "." && !strings.HasPrefix(rel, "..") {
|
|
882
|
+
return normalizeRepoPath(rel)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return normalizeRepoPath(workspace)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
func stateRequest(workspace string) (string, error) {
|
|
889
|
+
payload, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
|
|
890
|
+
if err != nil {
|
|
891
|
+
return "", err
|
|
892
|
+
}
|
|
893
|
+
for _, line := range strings.Split(string(payload), "\n") {
|
|
894
|
+
trimmed := strings.TrimSpace(line)
|
|
895
|
+
if strings.HasPrefix(trimmed, "request:") {
|
|
896
|
+
return unquoteYAML(strings.TrimSpace(strings.TrimPrefix(trimmed, "request:"))), nil
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return "", nil
|
|
900
|
+
}
|