@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.
Files changed (50) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +275 -0
  3. package/assets/kkt-readme-modern.png +0 -0
  4. package/bin/kkt-install.mjs +25 -0
  5. package/cmd/kkt/main.go +15 -0
  6. package/go.mod +3 -0
  7. package/internal/workflow/cli.go +349 -0
  8. package/internal/workflow/cli_test.go +1266 -0
  9. package/internal/workflow/guardrails.go +900 -0
  10. package/internal/workflow/init.go +164 -0
  11. package/internal/workflow/init_test.go +113 -0
  12. package/internal/workflow/operations.go +1855 -0
  13. package/internal/workflow/validation_commands.go +291 -0
  14. package/internal/workflow/workspace.go +802 -0
  15. package/internal/workflow/workspace_test.go +285 -0
  16. package/package.json +45 -0
  17. package/scripts/install-cli.sh +210 -0
  18. package/scripts/install.sh +644 -0
  19. package/skills/kkt/SKILL.md +74 -0
  20. package/skills/kkt/references/discovery-tooling.md +53 -0
  21. package/skills/kkt/references/feature-optimization-model.md +120 -0
  22. package/skills/kkt/references/kkt-kernel.md +58 -0
  23. package/skills/kkt/references/layered-modeling-methods.md +101 -0
  24. package/skills/kkt/references/plan-assimilation.md +46 -0
  25. package/skills/kkt/references/schemas.md +231 -0
  26. package/skills/kkt/references/state-contract.md +76 -0
  27. package/skills/kkt-loop/SKILL.md +99 -0
  28. package/skills/kkt-loop/references/discovery-tooling.md +53 -0
  29. package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
  30. package/skills/kkt-loop/references/kkt-kernel.md +58 -0
  31. package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
  32. package/skills/kkt-loop/references/plan-assimilation.md +46 -0
  33. package/skills/kkt-loop/references/schemas.md +231 -0
  34. package/skills/kkt-loop/references/state-contract.md +76 -0
  35. package/skills/kkt-model/SKILL.md +76 -0
  36. package/skills/kkt-model/references/discovery-tooling.md +53 -0
  37. package/skills/kkt-model/references/feature-optimization-model.md +120 -0
  38. package/skills/kkt-model/references/kkt-kernel.md +58 -0
  39. package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
  40. package/skills/kkt-model/references/plan-assimilation.md +46 -0
  41. package/skills/kkt-model/references/schemas.md +231 -0
  42. package/skills/kkt-model/references/state-contract.md +76 -0
  43. package/skills/kkt-run/SKILL.md +62 -0
  44. package/skills/kkt-run/references/discovery-tooling.md +53 -0
  45. package/skills/kkt-run/references/feature-optimization-model.md +120 -0
  46. package/skills/kkt-run/references/kkt-kernel.md +58 -0
  47. package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
  48. package/skills/kkt-run/references/plan-assimilation.md +46 -0
  49. package/skills/kkt-run/references/schemas.md +231 -0
  50. 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
+ }