@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,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
|
+
}
|