@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,802 @@
1
+ package workflow
2
+
3
+ import (
4
+ "bufio"
5
+ "errors"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "regexp"
10
+ "sort"
11
+ "strings"
12
+ "time"
13
+ )
14
+
15
+ type Workspace struct {
16
+ Path string
17
+ Profile string
18
+ }
19
+
20
+ type State struct {
21
+ SchemaVersion string
22
+ WorkspaceType string
23
+ Profile string
24
+ Status string
25
+ ActiveLayer string
26
+ ApprovalStatus string
27
+ }
28
+
29
+ type ValidationResult struct {
30
+ OK bool
31
+ Issues []string
32
+ }
33
+
34
+ type StatusReport struct {
35
+ SchemaVersion int `json:"schema_version"`
36
+ Workspace string `json:"workspace"`
37
+ Status string `json:"status"`
38
+ ActiveLayer string `json:"active_layer"`
39
+ Profile string `json:"profile"`
40
+ Approval string `json:"approval"`
41
+ CurrentTask string `json:"current_task,omitempty"`
42
+ Validation ValidationResult `json:"validation"`
43
+ StaleComplete bool `json:"stale_complete"`
44
+ Next NextAction `json:"next"`
45
+ }
46
+
47
+ func StartWorkflow(root, request, profile string) (Workspace, error) {
48
+ if profile != "plan" && profile != "loop" && profile != "model" && profile != "run" {
49
+ return Workspace{}, fmt.Errorf("unsupported profile %q", profile)
50
+ }
51
+ projectRootDir, err := projectRoot(root)
52
+ if err != nil {
53
+ return Workspace{}, err
54
+ }
55
+ now := time.Now().UTC()
56
+ slug := fmt.Sprintf("%s-%s", now.Format("20060102-150405"), slugify(request))
57
+ base := filepath.Join(projectRootDir, ".kkt")
58
+ workspace := workspacePath(base, profile, slug)
59
+ if err := os.MkdirAll(workspace, 0o755); err != nil {
60
+ return Workspace{}, err
61
+ }
62
+
63
+ sourceWorkspace := normalizeRepoPath(filepath.Join(".kkt", currentPointer(profile, slug)))
64
+ files := workspaceFiles(request, profile, now, sourceWorkspace)
65
+ for name, content := range files {
66
+ if err := os.WriteFile(filepath.Join(workspace, name), []byte(content), 0o644); err != nil {
67
+ return Workspace{}, err
68
+ }
69
+ }
70
+ if err := os.WriteFile(filepath.Join(base, "current"), []byte(currentPointer(profile, slug)+"\n"), 0o644); err != nil {
71
+ return Workspace{}, err
72
+ }
73
+ return Workspace{Path: workspace, Profile: profile}, nil
74
+ }
75
+
76
+ func ResolveWorkspace(root, candidate string) (string, error) {
77
+ if candidate != "" {
78
+ info, err := os.Stat(candidate)
79
+ if err != nil {
80
+ return "", err
81
+ }
82
+ if !info.IsDir() {
83
+ return "", fmt.Errorf("workspace path is not a directory: %s", candidate)
84
+ }
85
+ return candidate, nil
86
+ }
87
+
88
+ projectRootDir, err := projectRoot(root)
89
+ if err != nil {
90
+ return "", err
91
+ }
92
+ base := filepath.Join(projectRootDir, ".kkt")
93
+ current, err := os.ReadFile(filepath.Join(base, "current"))
94
+ if err == nil {
95
+ path := filepath.Clean(filepath.Join(base, strings.TrimSpace(string(current))))
96
+ if info, statErr := os.Stat(path); statErr == nil && info.IsDir() {
97
+ return path, nil
98
+ }
99
+ }
100
+
101
+ entries, err := os.ReadDir(base)
102
+ if err != nil {
103
+ if errors.Is(err, os.ErrNotExist) {
104
+ return "", errors.New("no .kkt workspace found; run kkt start plan|model|run|loop first")
105
+ }
106
+ return "", err
107
+ }
108
+ type workspaceCandidate struct {
109
+ sortKey string
110
+ path string
111
+ }
112
+ var dirs []workspaceCandidate
113
+ for _, entry := range entries {
114
+ if !entry.IsDir() {
115
+ continue
116
+ }
117
+ if entry.Name() == "model" || entry.Name() == "run" || entry.Name() == "loop" {
118
+ nested, readErr := os.ReadDir(filepath.Join(base, entry.Name()))
119
+ if readErr != nil {
120
+ return "", readErr
121
+ }
122
+ for _, nestedEntry := range nested {
123
+ if nestedEntry.IsDir() {
124
+ dirs = append(dirs, workspaceCandidate{
125
+ sortKey: nestedEntry.Name(),
126
+ path: filepath.Join(base, entry.Name(), nestedEntry.Name()),
127
+ })
128
+ }
129
+ }
130
+ continue
131
+ }
132
+ if entry.Name() != "." {
133
+ dirs = append(dirs, workspaceCandidate{
134
+ sortKey: entry.Name(),
135
+ path: filepath.Join(base, entry.Name()),
136
+ })
137
+ }
138
+ }
139
+ if len(dirs) == 0 {
140
+ return "", errors.New("no .kkt workspace found; run kkt start plan|model|run|loop first")
141
+ }
142
+ sort.Slice(dirs, func(i, j int) bool {
143
+ return dirs[i].sortKey < dirs[j].sortKey
144
+ })
145
+ return dirs[len(dirs)-1].path, nil
146
+ }
147
+
148
+ func projectRoot(start string) (string, error) {
149
+ root, err := filepath.Abs(start)
150
+ if err != nil {
151
+ return "", err
152
+ }
153
+ info, err := os.Stat(root)
154
+ if err != nil {
155
+ return "", err
156
+ }
157
+ if !info.IsDir() {
158
+ root = filepath.Dir(root)
159
+ }
160
+ fallback := root
161
+ for {
162
+ if _, err := os.Stat(filepath.Join(root, ".git")); err == nil {
163
+ return root, nil
164
+ } else if !errors.Is(err, os.ErrNotExist) {
165
+ return "", err
166
+ }
167
+ parent := filepath.Dir(root)
168
+ if parent == root {
169
+ return fallback, nil
170
+ }
171
+ root = parent
172
+ }
173
+ }
174
+
175
+ func ReadState(workspace string) (State, error) {
176
+ file, err := os.Open(filepath.Join(workspace, "kkt.yaml"))
177
+ if err != nil {
178
+ return State{}, err
179
+ }
180
+ defer file.Close()
181
+
182
+ state := State{}
183
+ inApproval := false
184
+ scanner := bufio.NewScanner(file)
185
+ for scanner.Scan() {
186
+ rawLine := scanner.Text()
187
+ line := strings.TrimSpace(rawLine)
188
+ if line == "approval:" {
189
+ inApproval = true
190
+ continue
191
+ }
192
+ if rawLine != "" && !strings.HasPrefix(rawLine, " ") && !strings.HasPrefix(rawLine, "-") && line != "approval:" {
193
+ inApproval = false
194
+ }
195
+ key, value, ok := strings.Cut(line, ":")
196
+ if !ok {
197
+ continue
198
+ }
199
+ value = strings.Trim(strings.TrimSpace(value), `"`)
200
+ switch key {
201
+ case "schema_version":
202
+ state.SchemaVersion = value
203
+ case "workspace_type":
204
+ state.WorkspaceType = value
205
+ case "profile":
206
+ state.Profile = value
207
+ case "status":
208
+ if inApproval {
209
+ state.ApprovalStatus = value
210
+ } else if state.Status == "" {
211
+ state.Status = value
212
+ }
213
+ case "active_layer":
214
+ state.ActiveLayer = value
215
+ }
216
+ }
217
+ if err := scanner.Err(); err != nil {
218
+ return State{}, err
219
+ }
220
+ if state.ApprovalStatus == "" {
221
+ state.ApprovalStatus = "pending"
222
+ }
223
+ return state, nil
224
+ }
225
+
226
+ func NextInstruction(state State) string {
227
+ switch state.ActiveLayer {
228
+ case "intent":
229
+ return "next: record adaptive intent with kkt intent --method <method>, then inspect the repo and record discovery with kkt discovery --method <method>"
230
+ case "discovery":
231
+ if state.WorkspaceType == "plan" {
232
+ return "next: inspect the repo, then record objective_function, files_to_modify, constraint_functions, decision_variables, and validation_proof with kkt model before edits"
233
+ }
234
+ return "next: record discovery with repo facts, constraints, validation paths, and unknowns using kkt discovery --method <method>"
235
+ case "modeling":
236
+ if state.WorkspaceType == "plan" {
237
+ return "next: record objective_function, files_to_modify, constraint_functions, decision_variables, and validation_proof with kkt model; then get explicit approval before edits"
238
+ }
239
+ return "next: record the selected model with kkt model --method <method>, show it, and get explicit approval before edits"
240
+ case "execution":
241
+ if (state.WorkspaceType == "loop" || state.WorkspaceType == "run") && state.ApprovalStatus != "approved" {
242
+ return "next: show the selected model and record approval with kkt approve before execution"
243
+ }
244
+ return "next: execute only the approved plan and record progress with kkt progress"
245
+ case "validation":
246
+ if state.WorkspaceType == "model" {
247
+ return "next: run kkt validate, then finish the decision brief with kkt done"
248
+ }
249
+ return "next: run validation, record evidence with kkt evidence, then finish with kkt done"
250
+ default:
251
+ return "next: inspect kkt.yaml and continue from the active layer"
252
+ }
253
+ }
254
+
255
+ func ValidateWorkspace(workspace string) (ValidationResult, error) {
256
+ state, err := ReadState(workspace)
257
+ if err != nil {
258
+ return ValidationResult{}, err
259
+ }
260
+ required := requiredFiles(state.WorkspaceType)
261
+ result := ValidationResult{OK: true}
262
+ for _, name := range required {
263
+ path := filepath.Join(workspace, name)
264
+ info, err := os.Stat(path)
265
+ if err != nil {
266
+ result.OK = false
267
+ result.Issues = append(result.Issues, fmt.Sprintf("missing %s", name))
268
+ continue
269
+ }
270
+ if info.Size() == 0 && name != "events.jsonl" {
271
+ result.OK = false
272
+ result.Issues = append(result.Issues, fmt.Sprintf("empty %s", name))
273
+ }
274
+ }
275
+ if state.WorkspaceType == "plan" {
276
+ for _, issue := range planContractIssues(workspace) {
277
+ result.OK = false
278
+ result.Issues = append(result.Issues, issue)
279
+ }
280
+ }
281
+ if state.WorkspaceType == "run" || state.WorkspaceType == "loop" || (state.WorkspaceType == "model" && state.ActiveLayer == "validation") {
282
+ for _, issue := range validateGuardrails(workspace) {
283
+ result.OK = false
284
+ result.Issues = append(result.Issues, issue)
285
+ }
286
+ if contract, guardrailErr := readGuardrails(workspace); guardrailErr == nil {
287
+ for _, issue := range guardrailExecutionReadinessIssues(contract) {
288
+ result.OK = false
289
+ result.Issues = append(result.Issues, issue)
290
+ }
291
+ }
292
+ }
293
+ if state.WorkspaceType == "run" || state.WorkspaceType == "loop" {
294
+ for _, issue := range validationCommandProofIssues(workspace) {
295
+ result.OK = false
296
+ result.Issues = append(result.Issues, issue)
297
+ }
298
+ }
299
+ evidence, err := os.ReadFile(filepath.Join(workspace, "evidence.md"))
300
+ if state.WorkspaceType == "run" && err == nil && strings.Contains(string(evidence), "Status: pending") {
301
+ result.OK = false
302
+ result.Issues = append(result.Issues, "evidence.md is still pending")
303
+ }
304
+ if state.WorkspaceType == "loop" && err == nil && strings.Contains(string(evidence), "Status: pending") {
305
+ result.OK = false
306
+ result.Issues = append(result.Issues, "evidence.md is still pending")
307
+ }
308
+ if state.WorkspaceType == "run" && state.ApprovalStatus != "approved" {
309
+ result.OK = false
310
+ result.Issues = append(result.Issues, "approval is not approved")
311
+ }
312
+ if state.WorkspaceType == "loop" {
313
+ if state.ApprovalStatus != "approved" {
314
+ result.OK = false
315
+ result.Issues = append(result.Issues, "approval is not approved")
316
+ }
317
+ loop, loopErr := readLoopState(workspace)
318
+ if loopErr != nil {
319
+ result.OK = false
320
+ result.Issues = append(result.Issues, loopErr.Error())
321
+ } else {
322
+ for _, task := range loop.Tasks {
323
+ if task.Status != "done" && task.Status != "skipped" {
324
+ result.OK = false
325
+ result.Issues = append(result.Issues, fmt.Sprintf("task %s is %s", task.ID, task.Status))
326
+ }
327
+ }
328
+ for _, criterion := range loop.AcceptanceCriteria {
329
+ if criterion.Status != "satisfied" {
330
+ result.OK = false
331
+ result.Issues = append(result.Issues, fmt.Sprintf("criterion %s is %s", criterion.ID, criterion.Status))
332
+ }
333
+ }
334
+ for _, stop := range loop.StopConditions {
335
+ if stop.Status == "active" {
336
+ result.OK = false
337
+ result.Issues = append(result.Issues, fmt.Sprintf("stop condition active: %s", stop.Text))
338
+ }
339
+ }
340
+ if len(loop.Evidence) == 0 {
341
+ result.OK = false
342
+ result.Issues = append(result.Issues, "no loop evidence recorded")
343
+ }
344
+ for _, issue := range evidenceMappingIssues(loop) {
345
+ result.OK = false
346
+ result.Issues = append(result.Issues, issue)
347
+ }
348
+ }
349
+ }
350
+ if state.Status == "complete" {
351
+ for _, issue := range completeLayerIssues(workspace, state.WorkspaceType) {
352
+ result.OK = false
353
+ result.Issues = append(result.Issues, issue)
354
+ }
355
+ }
356
+ return result, nil
357
+ }
358
+
359
+ func planContractIssues(workspace string) []string {
360
+ content, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
361
+ if err != nil {
362
+ return []string{err.Error()}
363
+ }
364
+ text := string(content)
365
+ var issues []string
366
+ for _, field := range requiredPlanContractFields() {
367
+ if !strings.Contains(text, field+":") {
368
+ issues = append(issues, "missing plan contract field "+field)
369
+ continue
370
+ }
371
+ if planContractFieldStatus(text, field) == "pending" {
372
+ issues = append(issues, "plan contract field "+field+" is pending")
373
+ }
374
+ }
375
+ return issues
376
+ }
377
+
378
+ func planContractFieldStatus(text, field string) string {
379
+ lines := strings.Split(text, "\n")
380
+ start := -1
381
+ for i, line := range lines {
382
+ if strings.TrimSpace(line) == field+":" {
383
+ start = i
384
+ break
385
+ }
386
+ }
387
+ if start < 0 {
388
+ return ""
389
+ }
390
+ for i := start + 1; i < len(lines); i++ {
391
+ line := lines[i]
392
+ if line != "" && !strings.HasPrefix(line, " ") {
393
+ return ""
394
+ }
395
+ if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") && strings.HasSuffix(strings.TrimSpace(line), ":") {
396
+ return ""
397
+ }
398
+ trimmed := strings.TrimSpace(line)
399
+ if strings.HasPrefix(trimmed, "status:") {
400
+ return strings.Trim(strings.TrimSpace(strings.TrimPrefix(trimmed, "status:")), `"`)
401
+ }
402
+ }
403
+ return ""
404
+ }
405
+
406
+ func completeLayerIssues(workspace, workspaceType string) []string {
407
+ statuses, err := layerStatuses(workspace)
408
+ if err != nil {
409
+ return []string{err.Error()}
410
+ }
411
+ var required []string
412
+ switch workspaceType {
413
+ case "plan":
414
+ required = []string{"intent", "discovery", "modeling"}
415
+ case "model":
416
+ required = []string{"intent", "discovery", "modeling"}
417
+ case "run", "loop":
418
+ required = []string{"intent", "discovery", "modeling", "execution", "validation"}
419
+ }
420
+ var issues []string
421
+ for _, layer := range required {
422
+ status := statuses[layer]
423
+ if status != "complete" {
424
+ if status == "" {
425
+ status = "missing"
426
+ }
427
+ issues = append(issues, fmt.Sprintf("complete workspace has %s layer %s", layer, status))
428
+ }
429
+ }
430
+ return issues
431
+ }
432
+
433
+ func layerStatuses(workspace string) (map[string]string, error) {
434
+ content, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
435
+ if err != nil {
436
+ return nil, err
437
+ }
438
+ statuses := map[string]string{}
439
+ inLayers := false
440
+ currentLayer := ""
441
+ for _, line := range strings.Split(string(content), "\n") {
442
+ trimmed := strings.TrimSpace(line)
443
+ if trimmed == "layers:" {
444
+ inLayers = true
445
+ currentLayer = ""
446
+ continue
447
+ }
448
+ if inLayers && line != "" && !strings.HasPrefix(line, " ") {
449
+ break
450
+ }
451
+ if !inLayers {
452
+ continue
453
+ }
454
+ if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") && strings.HasSuffix(trimmed, ":") {
455
+ currentLayer = strings.TrimSuffix(trimmed, ":")
456
+ continue
457
+ }
458
+ if currentLayer != "" && strings.HasPrefix(trimmed, "status:") {
459
+ statuses[currentLayer] = strings.Trim(strings.TrimSpace(strings.TrimPrefix(trimmed, "status:")), `"`)
460
+ }
461
+ }
462
+ return statuses, nil
463
+ }
464
+
465
+ func requiredPlanContractFields() []string {
466
+ return []string{
467
+ "planning_contract",
468
+ "objective_function",
469
+ "files_to_modify",
470
+ "constraint_functions",
471
+ "decision_variables",
472
+ "validation_proof",
473
+ }
474
+ }
475
+
476
+ func stateYAML(request, profile string, now time.Time) string {
477
+ escapedRequest := strings.ReplaceAll(request, `"`, `\"`)
478
+ if profile == "plan" {
479
+ return fmt.Sprintf(`schema_version: 1
480
+ workflow_type: kkt
481
+ workspace_type: plan
482
+ profile: plan
483
+ status: modeling
484
+ active_layer: modeling
485
+ created_at: %s
486
+ request: "%s"
487
+ layers:
488
+ intent:
489
+ status: complete
490
+ method: goal_anti_goal
491
+ summary: "Initial user request captured by kkt start plan."
492
+ discovery:
493
+ status: pending
494
+ method: traceability_matrix
495
+ summary: ""
496
+ modeling:
497
+ status: pending
498
+ method: lexicographic
499
+ summary: ""
500
+ decision_log: []
501
+ planning_contract:
502
+ objective_function:
503
+ status: pending
504
+ summary: ""
505
+ files_to_modify:
506
+ status: pending
507
+ items: []
508
+ constraint_functions:
509
+ status: pending
510
+ hard: []
511
+ soft: []
512
+ decision_variables:
513
+ status: pending
514
+ items: []
515
+ validation_proof:
516
+ status: pending
517
+ commands: []
518
+ evidence: []
519
+ artifact_refs:
520
+ state: kkt.yaml
521
+ approval:
522
+ required: true
523
+ status: pending
524
+ approved_scope: ""
525
+ stop_conditions:
526
+ - "No feasible plan satisfies hard constraints."
527
+ - "User does not approve the selected model."
528
+ - "Destructive action, credentials, paid service, or external access is required."
529
+ `, now.Format(time.RFC3339), escapedRequest)
530
+ }
531
+ if profile == "model" {
532
+ return fmt.Sprintf(`schema_version: 1
533
+ workflow_type: kkt
534
+ workspace_type: model
535
+ profile: model
536
+ status: modeling
537
+ active_layer: intent
538
+ created_at: %s
539
+ request: "%s"
540
+ layers:
541
+ intent:
542
+ status: pending
543
+ method: pending
544
+ summary: ""
545
+ artifact: intent.md
546
+ discovery:
547
+ status: pending
548
+ method: pending
549
+ summary: ""
550
+ artifact: discovery.md
551
+ modeling:
552
+ status: pending
553
+ method: pending
554
+ summary: ""
555
+ artifact: model.md
556
+ method_invocations: []
557
+ decision_log: []
558
+ artifact_refs:
559
+ intent: intent.md
560
+ discovery: discovery.md
561
+ model: model.md
562
+ guardrails: guardrails.json
563
+ approval:
564
+ required: false
565
+ status: not_required
566
+ approved_scope: ""
567
+ stop_conditions:
568
+ - "No feasible model satisfies hard constraints."
569
+ - "A product, risk, scope, or implementation tradeoff cannot be resolved from repository evidence."
570
+ `, now.Format(time.RFC3339), escapedRequest)
571
+ }
572
+ if profile == "run" {
573
+ return fmt.Sprintf(`schema_version: 1
574
+ workflow_type: kkt
575
+ workspace_type: run
576
+ profile: run
577
+ status: modeling
578
+ active_layer: intent
579
+ created_at: %s
580
+ request: "%s"
581
+ layers:
582
+ intent:
583
+ status: pending
584
+ method: pending
585
+ summary: ""
586
+ artifact: intent.md
587
+ discovery:
588
+ status: pending
589
+ method: pending
590
+ summary: ""
591
+ artifact: discovery.md
592
+ modeling:
593
+ status: pending
594
+ method: pending
595
+ summary: ""
596
+ artifact: model.md
597
+ execution:
598
+ status: pending
599
+ method: pending
600
+ summary: ""
601
+ artifact: plan.md
602
+ validation:
603
+ status: pending
604
+ method: pending
605
+ summary: ""
606
+ artifact: evidence.md
607
+ method_invocations: []
608
+ decision_log: []
609
+ artifact_refs:
610
+ intent: intent.md
611
+ discovery: discovery.md
612
+ model: model.md
613
+ guardrails: guardrails.json
614
+ plan: plan.md
615
+ progress: progress.md
616
+ evidence: evidence.md
617
+ notes: notes.md
618
+ approval:
619
+ required: true
620
+ status: pending
621
+ approved_scope: ""
622
+ stop_conditions:
623
+ - "No feasible plan satisfies hard constraints."
624
+ - "User does not approve the selected model."
625
+ - "Model-ready judge blocks execution."
626
+ - "Destructive action, credentials, paid service, or external access is required."
627
+ `, now.Format(time.RFC3339), escapedRequest)
628
+ }
629
+ return fmt.Sprintf(`schema_version: 1
630
+ workflow_type: kkt
631
+ workspace_type: loop
632
+ profile: loop
633
+ status: modeling
634
+ active_layer: intent
635
+ created_at: %s
636
+ request: "%s"
637
+ layers:
638
+ intent:
639
+ status: pending
640
+ method: pending
641
+ summary: ""
642
+ artifact: intent.md
643
+ discovery:
644
+ status: pending
645
+ method: pending
646
+ summary: ""
647
+ artifact: discovery.md
648
+ modeling:
649
+ status: pending
650
+ method: pending
651
+ summary: ""
652
+ artifact: model.md
653
+ execution:
654
+ status: pending
655
+ method: pending
656
+ summary: ""
657
+ artifact: plan.md
658
+ validation:
659
+ status: pending
660
+ method: pending
661
+ summary: ""
662
+ artifact: evidence.md
663
+ method_invocations: []
664
+ decision_log: []
665
+ artifact_refs:
666
+ intent: intent.md
667
+ discovery: discovery.md
668
+ model: model.md
669
+ guardrails: guardrails.json
670
+ plan: plan.md
671
+ progress: progress.md
672
+ evidence: evidence.md
673
+ notes: notes.md
674
+ events: events.jsonl
675
+ approval:
676
+ required: true
677
+ status: pending
678
+ approved_scope: ""
679
+ stop_conditions:
680
+ - "No feasible plan satisfies hard constraints."
681
+ - "User does not approve the selected model."
682
+ - "Destructive action, credentials, paid service, or external access is required."
683
+ loop_state:
684
+ current_task: ""
685
+ tasks:
686
+ acceptance_criteria:
687
+ evidence:
688
+ stop_conditions:
689
+ - id: "no-feasible-plan"
690
+ text: "No feasible plan satisfies hard constraints."
691
+ status: "clear"
692
+ - id: "missing-approval"
693
+ text: "User does not approve the selected model."
694
+ status: "clear"
695
+ - id: "unsafe-action"
696
+ text: "Destructive action, credentials, paid service, or external access is required."
697
+ status: "clear"
698
+ `, now.Format(time.RFC3339), escapedRequest)
699
+ }
700
+
701
+ func workspacePath(base, profile, slug string) string {
702
+ switch profile {
703
+ case "plan":
704
+ return base
705
+ case "model":
706
+ return filepath.Join(base, "model", slug)
707
+ case "run":
708
+ return filepath.Join(base, "run", slug)
709
+ default:
710
+ return filepath.Join(base, "loop", slug)
711
+ }
712
+ }
713
+
714
+ func currentPointer(profile, slug string) string {
715
+ switch profile {
716
+ case "plan":
717
+ return "."
718
+ case "model":
719
+ return filepath.Join("model", slug)
720
+ case "run":
721
+ return filepath.Join("run", slug)
722
+ default:
723
+ return filepath.Join("loop", slug)
724
+ }
725
+ }
726
+
727
+ func workspaceFiles(request, profile string, now time.Time, sourceWorkspace string) map[string]string {
728
+ files := map[string]string{
729
+ "kkt.yaml": stateYAML(request, profile, now),
730
+ }
731
+ if profile == "plan" {
732
+ return files
733
+ }
734
+ files["intent.md"] = intentMarkdown(request)
735
+ files["discovery.md"] = "# Discovery\n\nStatus: pending\n\nRecord repo facts, discovered constraints, validation paths, and remaining unknowns here.\n"
736
+ files["model.md"] = "# Model\n\nStatus: pending\n\nRecord method selection, objective function, known constraints, files to modify, constraint functions, decision variables, candidate feasibility, selected plan, binding constraints, validation proof, execution implications, and residual risk here.\n"
737
+ files["guardrails.json"] = defaultGuardrailsJSON(request, profile, sourceWorkspace)
738
+ if profile == "model" {
739
+ return files
740
+ }
741
+ files["plan.md"] = "# Plan\n\nStatus: pending\n\nRecord acceptance criteria, validation plan, evidence required, stop conditions, and continuation policy here.\n"
742
+ files["progress.md"] = "# Progress\n\nStatus: pending\n\n- [ ] Complete discovery\n- [ ] Complete model\n- [ ] Get approval before implementation\n- [ ] Execute approved plan\n- [ ] Validate with evidence\n"
743
+ files["evidence.md"] = "# Evidence\n\nStatus: pending\n\nRecord validation commands, outputs, artifacts, and final constraint audit here.\n"
744
+ files["notes.md"] = "# Notes\n\n"
745
+ if profile == "loop" {
746
+ files["events.jsonl"] = ""
747
+ }
748
+ return files
749
+ }
750
+
751
+ func requiredFiles(workspaceType string) []string {
752
+ switch workspaceType {
753
+ case "plan":
754
+ return []string{"kkt.yaml"}
755
+ case "model":
756
+ return []string{"kkt.yaml", "intent.md", "discovery.md", "model.md"}
757
+ case "run":
758
+ return []string{"kkt.yaml", "intent.md", "discovery.md", "model.md", "guardrails.json", "plan.md", "progress.md", "evidence.md", "notes.md"}
759
+ default:
760
+ return []string{"kkt.yaml", "intent.md", "discovery.md", "model.md", "guardrails.json", "plan.md", "progress.md", "evidence.md", "notes.md", "events.jsonl"}
761
+ }
762
+ }
763
+
764
+ func intentMarkdown(request string) string {
765
+ return fmt.Sprintf(`# Intent
766
+
767
+ Status: pending
768
+
769
+ ## User Goal
770
+
771
+ %s
772
+
773
+ ## Desired Behavior
774
+
775
+ To be refined by the agent after discovery if the request has hidden product choices.
776
+
777
+ ## User-Visible Success
778
+
779
+ The selected implementation satisfies the request while preserving discovered constraints.
780
+
781
+ ## Scope Boundary
782
+
783
+ One existing coding agent uses KKT as a workflow tool. KKT does not own the session, spawn subagents, or route models.
784
+ `, request)
785
+ }
786
+
787
+ var nonSlug = regexp.MustCompile(`[^a-z0-9]+`)
788
+
789
+ func slugify(value string) string {
790
+ value = strings.ToLower(value)
791
+ value = nonSlug.ReplaceAllString(value, "-")
792
+ value = strings.Trim(value, "-")
793
+ parts := strings.Split(value, "-")
794
+ if len(parts) > 8 {
795
+ parts = parts[:8]
796
+ }
797
+ value = strings.Join(parts, "-")
798
+ if value == "" {
799
+ return "request"
800
+ }
801
+ return value
802
+ }