@kujolang/paperclip 0.1.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 (49) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/SECURITY.md +35 -0
  5. package/VERSION +1 -0
  6. package/bundled/components/changebucket/LICENSE +9 -0
  7. package/bundled/components/changebucket/changebucket.kujo +12 -0
  8. package/bundled/components/changebucket/src/analyze.kujo +240 -0
  9. package/bundled/components/changebucket/src/budget.kujo +92 -0
  10. package/bundled/components/changebucket/src/classify.kujo +221 -0
  11. package/bundled/components/changebucket/src/cli.kujo +324 -0
  12. package/bundled/components/changebucket/src/diffsrc.kujo +252 -0
  13. package/bundled/components/changebucket/src/render.kujo +346 -0
  14. package/bundled/components/changebucket/src/util.kujo +62 -0
  15. package/bundled/components/context/LICENSE +9 -0
  16. package/bundled/components/context/scent.kujo +3250 -0
  17. package/bundled/components/failure-evidence/LICENSE +9 -0
  18. package/bundled/components/failure-evidence/casefile.kujo +2061 -0
  19. package/bundled/components/patchbrief/LICENSE +9 -0
  20. package/bundled/components/patchbrief/patchbrief.kujo +153 -0
  21. package/bundled/components/patchbrief/schemas/patchbrief-handoff.schema.json +38 -0
  22. package/bundled/components/patchbrief/schemas/patchbrief-summary.schema.json +46 -0
  23. package/bundled/components/patchbrief/src/common.kujo +212 -0
  24. package/bundled/components/patchbrief/src/git.kujo +250 -0
  25. package/bundled/components/patchbrief/src/handoff.kujo +145 -0
  26. package/bundled/components/patchbrief/src/suggest_tests.kujo +137 -0
  27. package/bundled/components/patchbrief/src/summarize.kujo +309 -0
  28. package/bundled/kujo-components.lock.json +134 -0
  29. package/dist/manifest.js +14696 -0
  30. package/dist/ui/index.js +179 -0
  31. package/dist/worker.js +30536 -0
  32. package/docs/ARCHITECTURE.md +28 -0
  33. package/docs/CATALOG_SUBMISSION.md +25 -0
  34. package/docs/COMPATIBILITY.md +21 -0
  35. package/docs/CONFIGURATION.md +33 -0
  36. package/docs/INSTALLATION.md +53 -0
  37. package/docs/OPERATIONS.md +60 -0
  38. package/docs/README.md +18 -0
  39. package/docs/RELEASE_READINESS.md +46 -0
  40. package/docs/THREAT_MODEL.md +26 -0
  41. package/docs/TROUBLESHOOTING.md +12 -0
  42. package/docs/USAGE.md +95 -0
  43. package/examples/agent-workflow.md +20 -0
  44. package/package.json +71 -0
  45. package/schemas/changebucket-analysis.schema.json +25 -0
  46. package/schemas/context-pack.schema.json +23 -0
  47. package/schemas/failure-evidence.schema.json +18 -0
  48. package/schemas/review-pack.schema.json +19 -0
  49. package/skills/scoped-repository-context/SKILL.md +9 -0
@@ -0,0 +1,3250 @@
1
+ #!/usr/bin/env kujo
2
+
3
+ VERSION := "1.0.0"
4
+
5
+ func usage_text() {
6
+ return "Usage:\n scent pack --task <text> [--out <path>] [--budget <n>] [--target codex|claude|deepseek|generic] [--include <path>] [--exclude <path>] [--changed] [--staged] [--unstaged] [--max-files <n>] [--max-file-bytes <n>] [--format md|json|both] [--verbose] [--json] [--dry-run]"
7
+ }
8
+
9
+ func version_text() {
10
+ return "Scent " + VERSION
11
+ }
12
+
13
+ func is_help_arg(value) {
14
+ return value == "help" || value == "--help" || value == "-h"
15
+ }
16
+
17
+ func is_version_arg(value) {
18
+ return value == "version" || value == "--version"
19
+ }
20
+
21
+ func defaults() {
22
+ return {
23
+ "task": "",
24
+ "out": null,
25
+ "budget": 12000,
26
+ "target": "codex",
27
+ "include": [],
28
+ "exclude": [],
29
+ "changed": false,
30
+ "staged": false,
31
+ "unstaged": false,
32
+ "max_files": 40,
33
+ "max_file_bytes": 50000,
34
+ "format": "both",
35
+ "verbose": false,
36
+ "json_stdout": false,
37
+ "dry_run": false
38
+ }
39
+ }
40
+
41
+ func ok(value) {
42
+ return {"ok": true, "value": value}
43
+ }
44
+
45
+ func err(message, code) {
46
+ return {"ok": false, "error": message, "exit_code": code}
47
+ }
48
+
49
+ func to_int_safe(value, fallback) {
50
+ try {
51
+ return to_int(value)
52
+ } except e {
53
+ return fallback
54
+ }
55
+ }
56
+
57
+ func collect_repeated_flag_values(argv, flag) {
58
+ values := []
59
+ i := 0
60
+ while i < len(argv) {
61
+ arg := to_string(argv[i])
62
+ if arg == flag {
63
+ if i + 1 < len(argv) {
64
+ next := to_string(argv[i + 1])
65
+ if !starts_with(next, "-") {
66
+ values = push(values, next)
67
+ i = i + 2
68
+ continue
69
+ }
70
+ }
71
+ } else if starts_with(arg, flag + "=") {
72
+ values = push(values, substring(arg, len(flag) + 1, len(arg)))
73
+ }
74
+ i = i + 1
75
+ }
76
+ return values
77
+ }
78
+
79
+ func shell_quote(arg) {
80
+ s := to_string(arg)
81
+ return "'" + replace(s, "'", "'\"'\"'") + "'"
82
+ }
83
+
84
+ func parse_cli(argv) {
85
+ if len(argv) == 0 {
86
+ return err(usage_text(), 2)
87
+ }
88
+
89
+ parser := arg_parser()
90
+ parser := parser.add_argument("--task", "type", "string", "required", true)
91
+ parser := parser.add_argument("--out", "type", "string", "default", "")
92
+ parser := parser.add_argument("--budget", "type", "int", "default", "12000")
93
+ parser := parser.add_argument("--target", "type", "string", "default", "codex")
94
+ parser := parser.add_argument("--include", "type", "string", "default", "")
95
+ parser := parser.add_argument("--exclude", "type", "string", "default", "")
96
+ parser := parser.add_argument("--changed", "type", "bool")
97
+ parser := parser.add_argument("--staged", "type", "bool")
98
+ parser := parser.add_argument("--unstaged", "type", "bool")
99
+ parser := parser.add_argument("--max-files", "type", "int", "default", "40")
100
+ parser := parser.add_argument("--max-file-bytes", "type", "int", "default", "50000")
101
+ parser := parser.add_argument("--format", "type", "string", "default", "both")
102
+ parser := parser.add_argument("--verbose", "type", "bool")
103
+ parser := parser.add_argument("--json", "type", "bool")
104
+ parser := parser.add_argument("--dry-run", "type", "bool")
105
+
106
+ parsed := null
107
+ try {
108
+ parsed = parser.parse()
109
+ } except parse_error {
110
+ return err(usage_text() + "\n\n" + to_string(parse_error), 2)
111
+ }
112
+
113
+ positionals := []
114
+ if has_key(parsed, "_positional") && parsed["_positional"] != null {
115
+ positionals = parsed["_positional"]
116
+ }
117
+
118
+ if len(positionals) > 1 {
119
+ return err("expected a single subcommand: pack", 2)
120
+ }
121
+ if len(positionals) == 0 {
122
+ return err("expected subcommand: pack\n\n" + usage_text(), 2)
123
+ }
124
+ if len(positionals) == 1 {
125
+ cmd := positionals[0]
126
+ if is_help_arg(cmd) {
127
+ return err(usage_text(), 2)
128
+ }
129
+ if cmd != "pack" {
130
+ return err("unknown command: " + cmd + "\n\n" + usage_text(), 2)
131
+ }
132
+ }
133
+
134
+ opts := defaults()
135
+ opts["task"] = to_string(parsed["task"])
136
+ out_raw := to_string(parsed["out"])
137
+ if trim(out_raw) == "" {
138
+ opts["out"] = null
139
+ } else {
140
+ opts["out"] = out_raw
141
+ }
142
+ opts["budget"] = to_int(parsed["budget"])
143
+ opts["target"] = to_string(parsed["target"])
144
+ opts["changed"] = parsed["changed"]
145
+ opts["staged"] = parsed["staged"]
146
+ opts["unstaged"] = parsed["unstaged"]
147
+ opts["max_files"] = to_int(parsed["max-files"])
148
+ opts["max_file_bytes"] = to_int(parsed["max-file-bytes"])
149
+ opts["format"] = to_string(parsed["format"])
150
+ opts["verbose"] = parsed["verbose"]
151
+ opts["json_stdout"] = parsed["json"]
152
+ opts["dry_run"] = parsed["dry-run"]
153
+
154
+ repeated_includes := collect_repeated_flag_values(argv, "--include")
155
+ if len(repeated_includes) > 0 {
156
+ opts["include"] = repeated_includes
157
+ } else {
158
+ include_raw := to_string(parsed["include"])
159
+ if trim(include_raw) != "" {
160
+ opts["include"] = push(opts["include"], include_raw)
161
+ }
162
+ }
163
+
164
+ repeated_excludes := collect_repeated_flag_values(argv, "--exclude")
165
+ if len(repeated_excludes) > 0 {
166
+ opts["exclude"] = repeated_excludes
167
+ } else {
168
+ exclude_raw := to_string(parsed["exclude"])
169
+ if trim(exclude_raw) != "" {
170
+ opts["exclude"] = push(opts["exclude"], exclude_raw)
171
+ }
172
+ }
173
+
174
+ if trim(to_string(opts["task"])) == "" {
175
+ return err("--task is required", 2)
176
+ }
177
+ budget_value := to_int(opts["budget"])
178
+ max_files_value := to_int(opts["max_files"])
179
+ max_file_bytes_value := to_int(opts["max_file_bytes"])
180
+ opts["budget"] = budget_value
181
+ opts["max_files"] = max_files_value
182
+ opts["max_file_bytes"] = max_file_bytes_value
183
+
184
+ if budget_value <= 0 {
185
+ return err("--budget must be greater than 0", 2)
186
+ }
187
+ if max_files_value <= 0 {
188
+ return err("--max-files must be greater than 0", 2)
189
+ }
190
+ if max_file_bytes_value <= 0 {
191
+ return err("--max-file-bytes must be greater than 0", 2)
192
+ }
193
+
194
+ allowed_targets := {"codex": 1, "claude": 1, "deepseek": 1, "generic": 1}
195
+ if has_key(allowed_targets, opts["target"]) == 0 {
196
+ return err("--target must be one of: codex, claude, deepseek, generic", 2)
197
+ }
198
+
199
+ allowed_formats := {"md": 1, "json": 1, "both": 1}
200
+ if has_key(allowed_formats, opts["format"]) == 0 {
201
+ return err("--format must be one of: md, json, both", 2)
202
+ }
203
+
204
+ return ok(opts)
205
+ }
206
+
207
+ func is_absolute(path) {
208
+ return starts_with(path, "/")
209
+ }
210
+
211
+ func resolve_path(base, path) {
212
+ if is_absolute(path) {
213
+ return path
214
+ }
215
+ if path == "." {
216
+ return base
217
+ }
218
+ return path_join(base, path)
219
+ }
220
+
221
+ func path_rel(root, full) {
222
+ prefix := root + "/"
223
+ if starts_with(full, prefix) {
224
+ return substring(full, len(prefix), len(full))
225
+ }
226
+ if full == root {
227
+ return "."
228
+ }
229
+ return full
230
+ }
231
+
232
+ func has_parent_path_segment(path) {
233
+ parts := split(to_string(path), "/")
234
+ i := 0
235
+ while i < len(parts) {
236
+ if parts[i] == ".." {
237
+ return true
238
+ }
239
+ i = i + 1
240
+ }
241
+ return false
242
+ }
243
+
244
+ func strip_trailing_slashes(path) {
245
+ out := to_string(path)
246
+ while len(out) > 1 && ends_with(out, "/") {
247
+ out = substring(out, 0, len(out) - 1)
248
+ }
249
+ return out
250
+ }
251
+
252
+ func normalize_selector(repo_root, path) {
253
+ raw := strip_trailing_slashes(trim(to_string(path)))
254
+ if is_absolute(raw) {
255
+ return path_rel(repo_root, raw)
256
+ }
257
+ while starts_with(raw, "./") {
258
+ raw = substring(raw, 2, len(raw))
259
+ }
260
+ raw = strip_trailing_slashes(raw)
261
+ if raw == "" || raw == "." {
262
+ return "."
263
+ }
264
+ return raw
265
+ }
266
+
267
+ func path_has_symlink_segment(path) {
268
+ full := to_string(path)
269
+ parts := split(full, "/")
270
+ current := "/"
271
+ i := 0
272
+ while i < len(parts) {
273
+ part := parts[i]
274
+ i = i + 1
275
+ if part == "" {
276
+ continue
277
+ }
278
+ current = path_join(current, part)
279
+ if path_exists(current) && path_is_symlink(current) {
280
+ return true
281
+ }
282
+ }
283
+ return false
284
+ }
285
+
286
+ func validate_scoped_paths(repo_root, paths, flag_name) {
287
+ normalized := []
288
+ i := 0
289
+ while i < len(paths) {
290
+ raw := trim(to_string(paths[i]))
291
+ if raw == "" {
292
+ i = i + 1
293
+ continue
294
+ }
295
+ if has_parent_path_segment(raw) {
296
+ return err(flag_name + " path cannot contain '..': " + raw, 2)
297
+ }
298
+ full := resolve_path(repo_root, raw)
299
+ root_prefix := repo_root + "/"
300
+ if full != repo_root && !starts_with(full, root_prefix) {
301
+ return err(flag_name + " path must stay inside the repository: " + raw, 2)
302
+ }
303
+ if path_has_symlink_segment(full) {
304
+ return err(flag_name + " path cannot contain a symlink: " + raw, 2)
305
+ }
306
+ normalized = push(normalized, normalize_selector(repo_root, raw))
307
+ i = i + 1
308
+ }
309
+ return ok(normalized)
310
+ }
311
+
312
+ func detect_repo_root(start) {
313
+ current := start
314
+ previous := ""
315
+
316
+ while current != previous {
317
+ git_dir := path_join(current, ".git")
318
+ if path_exists(git_dir) {
319
+ return current
320
+ }
321
+ previous = current
322
+ current = dirname(current)
323
+ }
324
+
325
+ return start
326
+ }
327
+
328
+ func estimate_tokens(text) {
329
+ chars := len(text)
330
+ return (chars + 3) / 4
331
+ }
332
+
333
+ func tokenize_task(task) {
334
+ tokens := []
335
+ seen := {}
336
+
337
+ parts := regex_split(lower(task), "[^a-z0-9_-]+")
338
+ i := 0
339
+ while i < len(parts) {
340
+ part := parts[i]
341
+ p := trim(part)
342
+ if len(p) > 2 && has_key(seen, p) == 0 {
343
+ seen[p] = 1
344
+ tokens = push(tokens, p)
345
+ }
346
+ i = i + 1
347
+ }
348
+
349
+ return tokens
350
+ }
351
+
352
+ func discover_instruction_files(repo_root) {
353
+ candidates := [
354
+ "AGENTS.md",
355
+ "AGENTS.override.md",
356
+ "CLAUDE.md",
357
+ "README.md",
358
+ "CONTRIBUTING.md",
359
+ "docs/README.md"
360
+ ]
361
+ found := []
362
+
363
+ i := 0
364
+ while i < len(candidates) {
365
+ rel := candidates[i]
366
+ full := path_join(repo_root, rel)
367
+ if path_exists(full) && path_is_file(full) {
368
+ found = push(found, full)
369
+ }
370
+ i = i + 1
371
+ }
372
+
373
+ return found
374
+ }
375
+
376
+ func run_process(args) {
377
+ parts := []
378
+ i := 0
379
+ while i < len(args) {
380
+ parts = push(parts, shell_quote(args[i]))
381
+ i = i + 1
382
+ }
383
+ cmd := join(parts, " ")
384
+
385
+ try {
386
+ return execute_status(cmd)
387
+ } except e {
388
+ return {
389
+ "success": false,
390
+ "exitcode": 127,
391
+ "stdout": "",
392
+ "stderr": to_string(e),
393
+ "timed_out": false,
394
+ "stdout_truncated": false,
395
+ "stderr_truncated": false
396
+ }
397
+ }
398
+ }
399
+
400
+ func canonical_path_for_scope(path) {
401
+ raw := strip_trailing_slashes(to_string(path))
402
+ if path_exists(raw) {
403
+ resolved := run_process(["realpath", raw])
404
+ if resolved.success {
405
+ return strip_trailing_slashes(trim(to_string(resolved.stdout)))
406
+ }
407
+ }
408
+
409
+ parent := dirname(raw)
410
+ if path_exists(parent) {
411
+ resolved_parent := run_process(["realpath", parent])
412
+ if resolved_parent.success {
413
+ return path_join(strip_trailing_slashes(trim(to_string(resolved_parent.stdout))), basename(raw))
414
+ }
415
+ }
416
+ return raw
417
+ }
418
+
419
+ func split_lines(text) {
420
+ lines := split(text, "\n")
421
+ out := []
422
+ i := 0
423
+ while i < len(lines) {
424
+ line := lines[i]
425
+ t := trim(line)
426
+ if len(t) > 0 {
427
+ out = push(out, t)
428
+ }
429
+ i = i + 1
430
+ }
431
+ return out
432
+ }
433
+
434
+ func split_nul_records(text) {
435
+ nul := parse_json("\"\\u0000\"")
436
+ records := split(to_string(text), nul)
437
+ out := []
438
+ i := 0
439
+ while i < len(records) {
440
+ record := records[i]
441
+ if record != "" {
442
+ out = push(out, record)
443
+ }
444
+ i = i + 1
445
+ }
446
+ return out
447
+ }
448
+
449
+ func inspect_git(repo_root) {
450
+ result := {
451
+ "available": false,
452
+ "branch": "unknown",
453
+ "commit": "unknown",
454
+ "dirty": false,
455
+ "staged_files": [],
456
+ "unstaged_files": [],
457
+ "untracked_files": [],
458
+ "changed_files": [],
459
+ "diff_summary": []
460
+ }
461
+
462
+ staged_res := run_process(["git", "-C", repo_root, "diff", "--name-only", "--cached", "-z"])
463
+ if !staged_res.success {
464
+ return result
465
+ }
466
+
467
+ unstaged_res := run_process(["git", "-C", repo_root, "diff", "--name-only", "-z"])
468
+ if !unstaged_res.success {
469
+ return result
470
+ }
471
+
472
+ untracked_res := run_process(["git", "-C", repo_root, "ls-files", "--others", "--exclude-standard", "-z"])
473
+ if !untracked_res.success {
474
+ return result
475
+ }
476
+
477
+ result["available"] = true
478
+
479
+ branch_res := run_process(["git", "-C", repo_root, "rev-parse", "--abbrev-ref", "HEAD"])
480
+ if branch_res.success {
481
+ branch := trim(to_string(branch_res.stdout))
482
+ if branch != "" {
483
+ result["branch"] = branch
484
+ }
485
+ }
486
+
487
+ commit_res := run_process(["git", "-C", repo_root, "rev-parse", "--short", "HEAD"])
488
+ if commit_res.success {
489
+ commit := trim(to_string(commit_res.stdout))
490
+ if commit != "" {
491
+ result["commit"] = commit
492
+ }
493
+ }
494
+
495
+ staged_seen := {}
496
+ unstaged_seen := {}
497
+ untracked_seen := {}
498
+ changed_seen := {}
499
+
500
+ staged_files := split_nul_records(to_string(staged_res.stdout))
501
+ unstaged_files := split_nul_records(to_string(unstaged_res.stdout))
502
+ untracked_files := split_nul_records(to_string(untracked_res.stdout))
503
+
504
+ idx := 0
505
+ while idx < len(staged_files) {
506
+ path := staged_files[idx]
507
+ idx = idx + 1
508
+ if path == "" {
509
+ continue
510
+ }
511
+ if has_key(staged_seen, path) == 0 {
512
+ staged_seen[path] = 1
513
+ result["staged_files"] = push(result["staged_files"], path)
514
+ }
515
+ if has_key(changed_seen, path) == 0 {
516
+ changed_seen[path] = 1
517
+ result["changed_files"] = push(result["changed_files"], path)
518
+ }
519
+ result["diff_summary"] = push(result["diff_summary"], "staged: " + path)
520
+ }
521
+
522
+ idx := 0
523
+ while idx < len(unstaged_files) {
524
+ path := unstaged_files[idx]
525
+ idx = idx + 1
526
+ if path == "" {
527
+ continue
528
+ }
529
+ if has_key(unstaged_seen, path) == 0 {
530
+ unstaged_seen[path] = 1
531
+ result["unstaged_files"] = push(result["unstaged_files"], path)
532
+ }
533
+ if has_key(changed_seen, path) == 0 {
534
+ changed_seen[path] = 1
535
+ result["changed_files"] = push(result["changed_files"], path)
536
+ }
537
+ result["diff_summary"] = push(result["diff_summary"], "unstaged: " + path)
538
+ }
539
+
540
+ idx = 0
541
+ while idx < len(untracked_files) {
542
+ path := untracked_files[idx]
543
+ idx = idx + 1
544
+ if path == "" {
545
+ continue
546
+ }
547
+ if has_key(untracked_seen, path) == 0 {
548
+ untracked_seen[path] = 1
549
+ result["untracked_files"] = push(result["untracked_files"], path)
550
+ }
551
+ if has_key(changed_seen, path) == 0 {
552
+ changed_seen[path] = 1
553
+ result["changed_files"] = push(result["changed_files"], path)
554
+ }
555
+ result["diff_summary"] = push(result["diff_summary"], "untracked: " + path)
556
+ }
557
+
558
+ if len(result["staged_files"]) > 0 || len(result["unstaged_files"]) > 0 || len(result["untracked_files"]) > 0 {
559
+ result["dirty"] = true
560
+ }
561
+
562
+ return result
563
+ }
564
+
565
+ func is_ignored_dir(name) {
566
+ ignored := {
567
+ ".git": 1,
568
+ ".scent": 1,
569
+ "node_modules": 1,
570
+ "vendor": 1,
571
+ "out": 1,
572
+ "dist": 1,
573
+ "build": 1,
574
+ "target": 1
575
+ }
576
+ return has_key(ignored, name)
577
+ }
578
+
579
+ func is_binary_extension(path) {
580
+ ext := lower(path_extension(path))
581
+ binary_exts := {
582
+ "png": 1,
583
+ "jpg": 1,
584
+ "jpeg": 1,
585
+ "gif": 1,
586
+ "webp": 1,
587
+ "pdf": 1,
588
+ "zip": 1,
589
+ "gz": 1,
590
+ "tar": 1,
591
+ "bz2": 1,
592
+ "xz": 1,
593
+ "mp3": 1,
594
+ "mp4": 1,
595
+ "mov": 1,
596
+ "avi": 1,
597
+ "so": 1,
598
+ "dll": 1,
599
+ "dylib": 1,
600
+ "exe": 1,
601
+ "bin": 1,
602
+ "db": 1,
603
+ "sqlite": 1,
604
+ "class": 1
605
+ }
606
+ return has_key(binary_exts, ext)
607
+ }
608
+
609
+ func should_skip_file(path) {
610
+ name := basename(path)
611
+ if starts_with(name, ".env") {
612
+ return true
613
+ }
614
+ return is_binary_extension(path)
615
+ }
616
+
617
+ func contains_nul(text) {
618
+ nul := parse_json("\"\\u0000\"")
619
+ return contains(to_string(text), nul)
620
+ }
621
+
622
+ func is_instruction_rel(rel) {
623
+ return rel == "AGENTS.md" || rel == "AGENTS.override.md" || rel == "CLAUDE.md" || rel == "README.md" || rel == "CONTRIBUTING.md" || rel == "docs/README.md"
624
+ }
625
+
626
+ func is_doc_rel(rel) {
627
+ return ends_with(lower(rel), ".md") || starts_with(lower(rel), "docs/")
628
+ }
629
+
630
+ func is_test_rel(rel) {
631
+ lower_rel := lower(rel)
632
+ return starts_with(lower_rel, "tests/") || contains(lower_rel, "/tests/") || ends_with(lower_rel, "_test.rs") || ends_with(lower_rel, "_test.py") || ends_with(lower_rel, ".test.ts") || ends_with(lower_rel, ".spec.ts") || ends_with(lower_rel, "_test.kujo")
633
+ }
634
+
635
+ func is_config_rel(rel) {
636
+ exact := {
637
+ "Cargo.toml": 1,
638
+ "Cargo.lock": 1,
639
+ "package.json": 1,
640
+ "pnpm-lock.yaml": 1,
641
+ "yarn.lock": 1,
642
+ "package-lock.json": 1,
643
+ "pyproject.toml": 1,
644
+ "setup.cfg": 1,
645
+ "setup.py": 1,
646
+ "Makefile": 1,
647
+ "justfile": 1,
648
+ "Justfile": 1,
649
+ "tsconfig.json": 1,
650
+ "kujo.toml": 1
651
+ }
652
+ if has_key(exact, rel) {
653
+ return true
654
+ }
655
+ lower_rel := lower(rel)
656
+ return ends_with(lower_rel, ".toml") || ends_with(lower_rel, ".yaml") || ends_with(lower_rel, ".yml") || ends_with(lower_rel, ".json")
657
+ }
658
+
659
+ func path_is_excluded(full, exclude_abs) {
660
+ i := 0
661
+ while i < len(exclude_abs) {
662
+ ex := exclude_abs[i]
663
+ if full == ex || starts_with(full, ex + "/") {
664
+ return true
665
+ }
666
+ i = i + 1
667
+ }
668
+ return false
669
+ }
670
+
671
+ func read_text_file(path) {
672
+ try {
673
+ content := read_file(path)
674
+ if contains_nul(content) {
675
+ return {"ok": false, "error": "binary file"}
676
+ }
677
+ return {"ok": true, "content": content}
678
+ } except e {
679
+ return {"ok": false, "error": to_string(e)}
680
+ }
681
+ }
682
+
683
+ func walk_collect(repo_root, current, max_candidates, visited_dirs, depth) {
684
+ files := []
685
+ if max_candidates <= 0 {
686
+ return files
687
+ }
688
+ if depth > 20 {
689
+ return files
690
+ }
691
+
692
+ if has_key(visited_dirs, current) == 1 {
693
+ return files
694
+ }
695
+ visited_dirs[current] = 1
696
+
697
+ entries := []
698
+ try {
699
+ entries = list_dir(current)
700
+ } except e {
701
+ return files
702
+ }
703
+ entries = sort(entries)
704
+
705
+ idx := 0
706
+ while idx < len(entries) {
707
+ name := entries[idx]
708
+ idx = idx + 1
709
+
710
+ if len(files) >= max_candidates {
711
+ break
712
+ }
713
+
714
+ if name == "." {
715
+ continue
716
+ }
717
+ if name == ".." {
718
+ continue
719
+ }
720
+
721
+ if is_ignored_dir(name) {
722
+ continue
723
+ }
724
+
725
+ full := path_join(current, name)
726
+ if path_is_symlink(full) {
727
+ continue
728
+ }
729
+ if path_is_dir(full) {
730
+ remaining := max_candidates - len(files)
731
+ child_files := walk_collect(repo_root, full, remaining, visited_dirs, depth + 1)
732
+ child_idx := 0
733
+ while child_idx < len(child_files) {
734
+ files = push(files, child_files[child_idx])
735
+ child_idx = child_idx + 1
736
+ if len(files) >= max_candidates {
737
+ break
738
+ }
739
+ }
740
+ } else if path_is_file(full) {
741
+ files = push(files, full)
742
+ }
743
+ }
744
+
745
+ return files
746
+ }
747
+
748
+ func collect_candidates(repo_root, git_info, instruction_files, includes, excludes, task_terms) {
749
+ visited_dirs := {}
750
+ all_paths := walk_collect(repo_root, repo_root, 2000, visited_dirs, 0)
751
+
752
+ i := 0
753
+ while i < len(includes) {
754
+ inc := includes[i]
755
+ inc_abs := resolve_path(repo_root, inc)
756
+ if path_exists(inc_abs) {
757
+ if path_is_file(inc_abs) {
758
+ all_paths = push(all_paths, inc_abs)
759
+ } else if path_is_dir(inc_abs) {
760
+ include_visited := {}
761
+ include_paths := walk_collect(repo_root, inc_abs, 2000, include_visited, 0)
762
+ add_idx := 0
763
+ while add_idx < len(include_paths) {
764
+ all_paths = push(all_paths, include_paths[add_idx])
765
+ add_idx = add_idx + 1
766
+ }
767
+ }
768
+ }
769
+ i = i + 1
770
+ }
771
+
772
+ instruction_rel := {}
773
+ i = 0
774
+ while i < len(instruction_files) {
775
+ inst := instruction_files[i]
776
+ instruction_rel[path_rel(repo_root, inst)] = 1
777
+ i = i + 1
778
+ }
779
+
780
+ changed_map := {}
781
+ i = 0
782
+ while i < len(git_info["changed_files"]) {
783
+ p := git_info["changed_files"][i]
784
+ changed_map[p] = 1
785
+ i = i + 1
786
+ }
787
+ staged_map := {}
788
+ i = 0
789
+ while i < len(git_info["staged_files"]) {
790
+ p := git_info["staged_files"][i]
791
+ staged_map[p] = 1
792
+ i = i + 1
793
+ }
794
+ unstaged_map := {}
795
+ i = 0
796
+ while i < len(git_info["unstaged_files"]) {
797
+ p := git_info["unstaged_files"][i]
798
+ unstaged_map[p] = 1
799
+ i = i + 1
800
+ }
801
+
802
+ exclude_abs := []
803
+ i = 0
804
+ while i < len(excludes) {
805
+ ex := excludes[i]
806
+ exclude_abs = push(exclude_abs, resolve_path(repo_root, ex))
807
+ i = i + 1
808
+ }
809
+
810
+ seen := {}
811
+ candidates := []
812
+
813
+ i = 0
814
+ while i < len(all_paths) {
815
+ full := all_paths[i]
816
+ i = i + 1
817
+
818
+ if !path_is_file(full) {
819
+ continue
820
+ }
821
+ if should_skip_file(full) {
822
+ continue
823
+ }
824
+ if path_is_excluded(full, exclude_abs) {
825
+ continue
826
+ }
827
+
828
+ rel := path_rel(repo_root, full)
829
+ if has_key(seen, rel) {
830
+ continue
831
+ }
832
+ seen[rel] = 1
833
+
834
+ matched_terms := []
835
+ rel_lower := lower(rel)
836
+ j := 0
837
+ while j < len(task_terms) {
838
+ term := task_terms[j]
839
+ if contains(rel_lower, term) {
840
+ matched_terms = push(matched_terms, term)
841
+ }
842
+ j = j + 1
843
+ }
844
+
845
+ size := to_int_safe(file_size(full), 0)
846
+
847
+ candidates = push(candidates, {
848
+ "path": full,
849
+ "rel_path": rel,
850
+ "size_bytes": size,
851
+ "is_changed": has_key(changed_map, rel),
852
+ "is_staged": has_key(staged_map, rel),
853
+ "is_unstaged": has_key(unstaged_map, rel),
854
+ "is_instruction": has_key(instruction_rel, rel),
855
+ "is_doc": is_doc_rel(rel),
856
+ "is_test": is_test_rel(rel),
857
+ "is_config": is_config_rel(rel),
858
+ "matched_terms": matched_terms,
859
+ "selected": false,
860
+ "selection_reason": ""
861
+ })
862
+ }
863
+
864
+ return candidates
865
+ }
866
+
867
+ func explicit_include_match(rel, includes, repo_root) {
868
+ i := 0
869
+ while i < len(includes) {
870
+ inc := includes[i]
871
+ inc_rel := inc
872
+ if is_absolute(inc_rel) {
873
+ inc_rel = path_rel(repo_root, inc_rel)
874
+ }
875
+
876
+ if inc_rel == "." || rel == inc_rel || starts_with(rel, inc_rel + "/") {
877
+ return true
878
+ }
879
+ i = i + 1
880
+ }
881
+ return false
882
+ }
883
+
884
+ func score_candidate(c, opts, repo_root) {
885
+ score := 0
886
+ reasons := []
887
+
888
+ if c["is_instruction"] {
889
+ score = score + 120
890
+ reasons = push(reasons, "instruction file")
891
+ }
892
+
893
+ if explicit_include_match(c["rel_path"], opts["include"], repo_root) {
894
+ score = score + 200
895
+ reasons = push(reasons, "explicit include")
896
+ }
897
+
898
+ if opts["changed"] && c["is_changed"] {
899
+ score = score + 110
900
+ reasons = push(reasons, "changed file")
901
+ }
902
+
903
+ if opts["staged"] && c["is_staged"] {
904
+ score = score + 95
905
+ reasons = push(reasons, "staged file")
906
+ }
907
+
908
+ if opts["unstaged"] && c["is_unstaged"] {
909
+ score = score + 90
910
+ reasons = push(reasons, "unstaged file")
911
+ }
912
+
913
+ if len(c["matched_terms"]) > 0 {
914
+ score = score + (len(c["matched_terms"]) * 20)
915
+ reasons = push(reasons, "task-term match")
916
+ }
917
+
918
+ if c["is_config"] {
919
+ score = score + 12
920
+ reasons = push(reasons, "config/build file")
921
+ }
922
+
923
+ if c["is_doc"] {
924
+ score = score + 10
925
+ reasons = push(reasons, "docs/readme")
926
+ }
927
+
928
+ if c["is_test"] {
929
+ score = score + 15
930
+ reasons = push(reasons, "test file")
931
+ }
932
+
933
+ reason := ""
934
+ if len(reasons) > 0 {
935
+ reason = join(reasons, ", ")
936
+ } else {
937
+ reason = "did not meet selection threshold"
938
+ }
939
+
940
+ return {"score": score, "reason": reason}
941
+ }
942
+
943
+ func sort_scored(scored) {
944
+ n := len(scored)
945
+ i := 0
946
+ while i < n {
947
+ j := i + 1
948
+ while j < n {
949
+ left := scored[i]
950
+ right := scored[j]
951
+ swap := false
952
+ if right["score"] > left["score"] {
953
+ swap = true
954
+ } else if right["score"] == left["score"] && right["candidate"]["rel_path"] < left["candidate"]["rel_path"] {
955
+ swap = true
956
+ }
957
+
958
+ if swap {
959
+ tmp := scored[i]
960
+ scored[i] = scored[j]
961
+ scored[j] = tmp
962
+ }
963
+ j = j + 1
964
+ }
965
+ i = i + 1
966
+ }
967
+ return scored
968
+ }
969
+
970
+ func file_stem(path) {
971
+ name := basename(path)
972
+ if contains(name, ".") {
973
+ parts := split(name, ".")
974
+ if len(parts) > 1 {
975
+ return parts[0]
976
+ }
977
+ }
978
+ return name
979
+ }
980
+
981
+ func looks_like_pair(source_rel, test_rel) {
982
+ lhs := lower(file_stem(source_rel))
983
+ rhs := lower(file_stem(test_rel))
984
+ if lhs == rhs {
985
+ return true
986
+ }
987
+ if contains(rhs, lhs) || contains(lhs, rhs) {
988
+ return true
989
+ }
990
+ if contains(test_rel, "tests") {
991
+ lhs2 := replace(lhs, "_test", "")
992
+ rhs2 := replace(rhs, "_test", "")
993
+ if contains(rhs, lhs2) || contains(lhs, rhs2) {
994
+ return true
995
+ }
996
+ }
997
+ return false
998
+ }
999
+
1000
+ func select_candidates(opts, repo_root, candidates) {
1001
+ scored := []
1002
+ i := 0
1003
+ while i < len(candidates) {
1004
+ c := candidates[i]
1005
+ sr := score_candidate(c, opts, repo_root)
1006
+ if sr["score"] > 0 {
1007
+ scored = push(scored, {"score": sr["score"], "reason": sr["reason"], "candidate": c})
1008
+ }
1009
+ i = i + 1
1010
+ }
1011
+
1012
+ scored = sort_scored(scored)
1013
+
1014
+ selected := []
1015
+ seen := {}
1016
+ max_files := to_int(opts["max_files"])
1017
+
1018
+ i = 0
1019
+ while i < len(scored) {
1020
+ if to_int(len(selected)) >= to_int(max_files) {
1021
+ break
1022
+ }
1023
+ s := scored[i]
1024
+ c := s["candidate"]
1025
+ rel := c["rel_path"]
1026
+ if has_key(seen, rel) {
1027
+ i = i + 1
1028
+ continue
1029
+ }
1030
+ seen[rel] = 1
1031
+ c["selected"] = true
1032
+ c["selection_reason"] = s["reason"]
1033
+ selected = push(selected, c)
1034
+ i = i + 1
1035
+ }
1036
+
1037
+ sources := []
1038
+ i = 0
1039
+ while i < len(selected) {
1040
+ c := selected[i]
1041
+ if !c["is_test"] {
1042
+ sources = push(sources, c["rel_path"])
1043
+ }
1044
+ i = i + 1
1045
+ }
1046
+
1047
+ i = 0
1048
+ while i < len(candidates) {
1049
+ if to_int(len(selected)) >= to_int(max_files) {
1050
+ break
1051
+ }
1052
+ c := candidates[i]
1053
+ if !c["is_test"] {
1054
+ i = i + 1
1055
+ continue
1056
+ }
1057
+ if has_key(seen, c["rel_path"]) {
1058
+ i = i + 1
1059
+ continue
1060
+ }
1061
+
1062
+ matched := false
1063
+ j := 0
1064
+ while j < len(sources) {
1065
+ src := sources[j]
1066
+ if looks_like_pair(src, c["rel_path"]) {
1067
+ matched = true
1068
+ break
1069
+ }
1070
+ j = j + 1
1071
+ }
1072
+
1073
+ if matched {
1074
+ seen[c["rel_path"]] = 1
1075
+ c["selected"] = true
1076
+ c["selection_reason"] = "nearby test for selected source"
1077
+ selected = push(selected, c)
1078
+ }
1079
+ i = i + 1
1080
+ }
1081
+
1082
+ if len(selected) == 0 {
1083
+ i = 0
1084
+ while i < len(candidates) {
1085
+ if to_int(len(selected)) >= to_int(max_files) || to_int(len(selected)) >= 3 {
1086
+ break
1087
+ }
1088
+ c := candidates[i]
1089
+ rel := c["rel_path"]
1090
+ if has_key(seen, rel) {
1091
+ i = i + 1
1092
+ continue
1093
+ }
1094
+ seen[rel] = 1
1095
+ c["selected"] = true
1096
+ c["selection_reason"] = "fallback baseline selection"
1097
+ selected = push(selected, c)
1098
+ i = i + 1
1099
+ }
1100
+ }
1101
+
1102
+ return selected
1103
+ }
1104
+
1105
+ func utf8_byte_len(content) {
1106
+ encoded := encode_base64(to_string(content))
1107
+ padding := 0
1108
+ if ends_with(encoded, "==") {
1109
+ padding = 2
1110
+ } else if ends_with(encoded, "=") {
1111
+ padding = 1
1112
+ }
1113
+ return (len(encoded) / 4) * 3 - padding
1114
+ }
1115
+
1116
+ func clip_text(content, limit_bytes) {
1117
+ if utf8_byte_len(content) <= limit_bytes {
1118
+ return {"content": content, "truncated": false}
1119
+ }
1120
+
1121
+ low := 0
1122
+ high := len(content)
1123
+ while low < high {
1124
+ middle := (low + high + 1) / 2
1125
+ prefix := substring(content, 0, middle)
1126
+ if utf8_byte_len(prefix) <= limit_bytes {
1127
+ low = middle
1128
+ } else {
1129
+ high = middle - 1
1130
+ }
1131
+ }
1132
+ return {"content": substring(content, 0, low), "truncated": true}
1133
+ }
1134
+
1135
+ func normalize_credential_key(raw_key) {
1136
+ key := trim(lower(to_string(raw_key)))
1137
+ prefixes := ["export ", "let ", "const ", "var "]
1138
+ prefix_idx := 0
1139
+ while prefix_idx < len(prefixes) {
1140
+ prefix := prefixes[prefix_idx]
1141
+ if starts_with(key, prefix) {
1142
+ key = trim(substring(key, len(prefix), len(key)))
1143
+ }
1144
+ prefix_idx = prefix_idx + 1
1145
+ }
1146
+
1147
+ if len(key) > 1 && ((starts_with(key, "\"") && ends_with(key, "\"")) || (starts_with(key, "'") && ends_with(key, "'"))) {
1148
+ key = substring(key, 1, len(key) - 1)
1149
+ }
1150
+ if key == "" || contains(key, " ") || contains(key, "\t") {
1151
+ return ""
1152
+ }
1153
+
1154
+ if !regex_match(key, "^[a-z0-9_.-]+$") {
1155
+ return ""
1156
+ }
1157
+ return key
1158
+ }
1159
+
1160
+ func contains_credential_key_term(text) {
1161
+ terms := ["api_key", "apikey", "access_token", "refresh_token", "session_token", "password", "passwd", "secret", "token", "cookie", "webhook"]
1162
+ i := 0
1163
+ lower_text := lower(to_string(text))
1164
+ while i < len(terms) {
1165
+ if contains(lower_text, terms[i]) {
1166
+ return true
1167
+ }
1168
+ i = i + 1
1169
+ }
1170
+ return false
1171
+ }
1172
+
1173
+ func credential_key_matches(key, term) {
1174
+ return regex_match(key, "(^|[._-])" + term + "($|[._-])")
1175
+ }
1176
+
1177
+ func credential_assignment(line) {
1178
+ separators := ["=", ":"]
1179
+ separator_idx := 0
1180
+ while separator_idx < len(separators) {
1181
+ separator := separators[separator_idx]
1182
+ if contains(line, separator) {
1183
+ parts := split(line, separator)
1184
+ if contains_credential_key_term(parts[0]) {
1185
+ key := normalize_credential_key(parts[0])
1186
+ if key != "" {
1187
+ return {"ok": true, "key": key, "prefix": parts[0] + separator, "separator": separator}
1188
+ }
1189
+ }
1190
+ }
1191
+ separator_idx = separator_idx + 1
1192
+ }
1193
+ return {"ok": false, "key": "", "prefix": "", "separator": ""}
1194
+ }
1195
+
1196
+ func redact_text(file, content) {
1197
+ redactions := []
1198
+ text := to_string(content)
1199
+ lines := split(text, "\n")
1200
+ out := []
1201
+ in_private_key := false
1202
+
1203
+ idx := 0
1204
+ while idx < len(lines) {
1205
+ line := lines[idx]
1206
+ line_no := idx + 1
1207
+ lower_line := lower(line)
1208
+
1209
+ if contains(line, "-----BEGIN") && contains(line, "PRIVATE KEY-----") {
1210
+ in_private_key = true
1211
+ out = push(out, "[REDACTED:PRIVATE_KEY]")
1212
+ redactions = push(redactions, {"file": file, "line": line_no, "redaction_type": "private_key", "placeholder": "[REDACTED:PRIVATE_KEY]"})
1213
+ idx = idx + 1
1214
+ continue
1215
+ }
1216
+
1217
+ if in_private_key {
1218
+ if contains(line, "-----END") && contains(line, "PRIVATE KEY-----") {
1219
+ in_private_key = false
1220
+ }
1221
+ idx = idx + 1
1222
+ continue
1223
+ }
1224
+
1225
+ if contains(lower_line, "authorization:") {
1226
+ out = push(out, "authorization: [REDACTED:TOKEN]")
1227
+ redactions = push(redactions, {"file": file, "line": line_no, "redaction_type": "authorization_header", "placeholder": "[REDACTED:TOKEN]"})
1228
+ idx = idx + 1
1229
+ continue
1230
+ }
1231
+
1232
+ secret_line := false
1233
+ placeholder := ""
1234
+ redaction_type := ""
1235
+ assignment := credential_assignment(line)
1236
+
1237
+ patterns := [
1238
+ ["api_key", "[REDACTED:API_KEY]", "api_key"],
1239
+ ["apikey", "[REDACTED:API_KEY]", "api_key"],
1240
+ ["access_token", "[REDACTED:TOKEN]", "access_token"],
1241
+ ["refresh_token", "[REDACTED:TOKEN]", "refresh_token"],
1242
+ ["session_token", "[REDACTED:TOKEN]", "session_token"],
1243
+ ["password", "[REDACTED:PASSWORD]", "password"],
1244
+ ["passwd", "[REDACTED:PASSWORD]", "password"],
1245
+ ["secret", "[REDACTED:SECRET]", "secret"],
1246
+ ["token", "[REDACTED:TOKEN]", "token"],
1247
+ ["cookie", "[REDACTED:TOKEN]", "cookie"],
1248
+ ["webhook", "[REDACTED:SECRET]", "webhook"]
1249
+ ]
1250
+
1251
+ p_idx := 0
1252
+ while p_idx < len(patterns) {
1253
+ p := patterns[p_idx]
1254
+ if assignment["ok"] && credential_key_matches(assignment["key"], p[0]) {
1255
+ secret_line = true
1256
+ placeholder = p[1]
1257
+ redaction_type = p[2]
1258
+ break
1259
+ }
1260
+ p_idx = p_idx + 1
1261
+ }
1262
+
1263
+ if secret_line {
1264
+ separator_space := ""
1265
+ if assignment["separator"] == ":" {
1266
+ separator_space = " "
1267
+ }
1268
+ out = push(out, assignment["prefix"] + separator_space + placeholder)
1269
+ redactions = push(redactions, {"file": file, "line": line_no, "redaction_type": redaction_type, "placeholder": placeholder})
1270
+ idx = idx + 1
1271
+ continue
1272
+ }
1273
+
1274
+ token_patterns := [
1275
+ ["sk-[A-Za-z0-9_-]{20,}", "[REDACTED:TOKEN]", "openai_token"],
1276
+ ["ghp_[A-Za-z0-9_]{20,}", "[REDACTED:TOKEN]", "github_token"],
1277
+ ["github_pat_[A-Za-z0-9_]{20,}", "[REDACTED:TOKEN]", "github_token"],
1278
+ ["AKIA[0-9A-Z]{16}", "[REDACTED:ACCESS_KEY]", "aws_access_key"],
1279
+ ["sk_live_[A-Za-z0-9]{16,}", "[REDACTED:TOKEN]", "stripe_token"]
1280
+ ]
1281
+ token_line := line
1282
+ token_found := false
1283
+ token_idx := 0
1284
+ while token_idx < len(token_patterns) {
1285
+ tp := token_patterns[token_idx]
1286
+ if regex_match(token_line, tp[0]) {
1287
+ token_line = regex_replace(token_line, tp[0], tp[1])
1288
+ redactions = push(redactions, {"file": file, "line": line_no, "redaction_type": tp[2], "placeholder": tp[1]})
1289
+ token_found = true
1290
+ }
1291
+ token_idx = token_idx + 1
1292
+ }
1293
+ if token_found {
1294
+ out = push(out, token_line)
1295
+ idx = idx + 1
1296
+ continue
1297
+ }
1298
+
1299
+ if regex_match(line, "[A-Za-z0-9_-]{8,}[.][A-Za-z0-9_-]{8,}[.][A-Za-z0-9_-]{8,}") {
1300
+ redacted_line := regex_replace(line, "[A-Za-z0-9_-]{8,}[.][A-Za-z0-9_-]{8,}[.][A-Za-z0-9_-]{8,}", "[REDACTED:TOKEN]")
1301
+ out = push(out, redacted_line)
1302
+ redactions = push(redactions, {"file": file, "line": line_no, "redaction_type": "jwt", "placeholder": "[REDACTED:TOKEN]"})
1303
+ idx = idx + 1
1304
+ continue
1305
+ }
1306
+
1307
+ out = push(out, line)
1308
+ idx = idx + 1
1309
+ }
1310
+
1311
+ return {"content": join(out, "\n"), "redactions": redactions}
1312
+ }
1313
+
1314
+ func reserved_tokens(task) {
1315
+ return 1400 + estimate_tokens(task)
1316
+ }
1317
+
1318
+ func apply_budget(budget, reserved, selected_files) {
1319
+ budget_for_files := budget - reserved
1320
+ if budget_for_files < 0 {
1321
+ budget_for_files = 0
1322
+ }
1323
+
1324
+ manifest := []
1325
+ included := []
1326
+ used := 0
1327
+
1328
+ i := 0
1329
+ while i < len(selected_files) {
1330
+ item := selected_files[i]
1331
+ if used + item["estimated_tokens"] <= budget_for_files {
1332
+ decision := "include"
1333
+ if item["truncated"] {
1334
+ decision = "truncate"
1335
+ }
1336
+ manifest = push(manifest, {
1337
+ "path": item["path"],
1338
+ "type": "file",
1339
+ "decision": decision,
1340
+ "reason": item["reason"],
1341
+ "score": item["score"],
1342
+ "size_bytes": item["size_bytes"],
1343
+ "estimated_tokens": item["estimated_tokens"]
1344
+ })
1345
+ used = used + item["estimated_tokens"]
1346
+ included = push(included, item)
1347
+ i = i + 1
1348
+ continue
1349
+ }
1350
+
1351
+ remaining := budget_for_files - used
1352
+ if remaining > 120 {
1353
+ char_budget := remaining * 4
1354
+ if char_budget < 0 {
1355
+ char_budget = 0
1356
+ }
1357
+ item["content"] = substring(item["content"], 0, char_budget)
1358
+ item["truncated"] = true
1359
+ item["estimated_tokens"] = estimate_tokens(item["content"])
1360
+ item["content_hash"] = sha256(item["content"])
1361
+
1362
+ if item["estimated_tokens"] <= remaining {
1363
+ used = used + item["estimated_tokens"]
1364
+ manifest = push(manifest, {
1365
+ "path": item["path"],
1366
+ "type": "file",
1367
+ "decision": "truncate",
1368
+ "reason": "truncated to fit budget; " + item["reason"],
1369
+ "score": item["score"],
1370
+ "size_bytes": item["size_bytes"],
1371
+ "estimated_tokens": item["estimated_tokens"]
1372
+ })
1373
+ included = push(included, item)
1374
+ i = i + 1
1375
+ continue
1376
+ }
1377
+ }
1378
+
1379
+ manifest = push(manifest, {
1380
+ "path": item["path"],
1381
+ "type": "file",
1382
+ "decision": "exclude",
1383
+ "reason": "excluded due to budget; " + item["reason"],
1384
+ "score": item["score"],
1385
+ "size_bytes": item["size_bytes"],
1386
+ "estimated_tokens": item["estimated_tokens"]
1387
+ })
1388
+ i = i + 1
1389
+ }
1390
+
1391
+ return {"files": included, "manifest": manifest}
1392
+ }
1393
+
1394
+ func discover_commands(repo_root) {
1395
+ commands := []
1396
+
1397
+ cargo_path := path_join(repo_root, "Cargo.toml")
1398
+ if path_exists(cargo_path) {
1399
+ cargo_read := read_text_file(cargo_path)
1400
+ cargo_valid := false
1401
+ if cargo_read["ok"] {
1402
+ try {
1403
+ cargo_doc := parse_toml(cargo_read["content"])
1404
+ cargo_valid = type(cargo_doc) == "dict"
1405
+ } except cargo_parse_error {
1406
+ cargo_valid = false
1407
+ }
1408
+ }
1409
+ if cargo_valid {
1410
+ commands = push(commands, "cargo test")
1411
+ commands = push(commands, "cargo fmt -- --check")
1412
+ }
1413
+ }
1414
+
1415
+ package_path := path_join(repo_root, "package.json")
1416
+ if path_exists(package_path) {
1417
+ pkg_read := read_text_file(package_path)
1418
+ if pkg_read["ok"] {
1419
+ parsed := null
1420
+ parse_ok := false
1421
+ try {
1422
+ parsed = parse_json(pkg_read["content"])
1423
+ parse_ok = true
1424
+ } except parse_error {
1425
+ parse_ok = false
1426
+ }
1427
+
1428
+ if parse_ok && type(parsed) == "dict" && has_key(parsed, "scripts") {
1429
+ scripts := parsed["scripts"]
1430
+ if type(scripts) == "dict" {
1431
+ if has_key(scripts, "test") {
1432
+ commands = push(commands, "npm test")
1433
+ }
1434
+ if has_key(scripts, "lint") {
1435
+ commands = push(commands, "npm run lint")
1436
+ }
1437
+ if has_key(scripts, "build") {
1438
+ commands = push(commands, "npm run build")
1439
+ }
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ makefile_path := path_join(repo_root, "Makefile")
1446
+ if path_exists(makefile_path) {
1447
+ mf := read_text_file(makefile_path)
1448
+ if mf["ok"] {
1449
+ make_lines := split(mf["content"], "\n")
1450
+ i := 0
1451
+ while i < len(make_lines) {
1452
+ make_line := make_lines[i]
1453
+ if starts_with(make_line, "test:") {
1454
+ commands = push(commands, "make test")
1455
+ }
1456
+ if starts_with(make_line, "build:") {
1457
+ commands = push(commands, "make build")
1458
+ }
1459
+ if starts_with(make_line, "lint:") {
1460
+ commands = push(commands, "make lint")
1461
+ }
1462
+ i = i + 1
1463
+ }
1464
+ }
1465
+ }
1466
+
1467
+ justfile_path := path_join(repo_root, "justfile")
1468
+ if !path_exists(justfile_path) {
1469
+ justfile_path = path_join(repo_root, "Justfile")
1470
+ }
1471
+ if path_exists(justfile_path) {
1472
+ jf := read_text_file(justfile_path)
1473
+ if jf["ok"] {
1474
+ just_lines := split(jf["content"], "\n")
1475
+ j := 0
1476
+ while j < len(just_lines) {
1477
+ just_line := just_lines[j]
1478
+ if starts_with(just_line, "test:") {
1479
+ commands = push(commands, "just test")
1480
+ }
1481
+ if starts_with(just_line, "build:") {
1482
+ commands = push(commands, "just build")
1483
+ }
1484
+ if starts_with(just_line, "lint:") {
1485
+ commands = push(commands, "just lint")
1486
+ }
1487
+ j = j + 1
1488
+ }
1489
+ }
1490
+ }
1491
+
1492
+ # dedupe + sort
1493
+ seen := {}
1494
+ uniq := []
1495
+ k := 0
1496
+ while k < len(commands) {
1497
+ c := commands[k]
1498
+ if has_key(seen, c) == 0 {
1499
+ seen[c] = 1
1500
+ uniq = push(uniq, c)
1501
+ }
1502
+ k = k + 1
1503
+ }
1504
+ return sort(uniq)
1505
+ }
1506
+
1507
+ func target_notes(target) {
1508
+ if target == "codex" {
1509
+ return "Codex usage notes:\n- Follow AGENTS.md/project instructions first.\n- Use this pack as bounded context.\n- Run detected tests after changes.\n- Do not modify files outside the task scope unless necessary."
1510
+ }
1511
+ if target == "claude" {
1512
+ return "Usage notes:\n- Follow project instructions first.\n- Use this pack as bounded context.\n- Run detected tests after changes.\n- Avoid changes outside task scope unless needed."
1513
+ }
1514
+ if target == "deepseek" {
1515
+ return "Implementation notes:\n- Treat this as bounded task context.\n- Prefer minimal diff and deterministic changes.\n- Run detected tests before handoff."
1516
+ }
1517
+ return "Usage notes:\n- Use this pack as bounded task context.\n- Follow project instructions and constraints.\n- Validate with detected commands before handoff."
1518
+ }
1519
+
1520
+ func markdown_fence(content) {
1521
+ fence := "```"
1522
+ while contains(content, fence) {
1523
+ fence = fence + "`"
1524
+ }
1525
+ return fence
1526
+ }
1527
+
1528
+ func markdown_section_list(title, items, empty_line) {
1529
+ out := "## " + title + "\n\n"
1530
+ if len(items) == 0 {
1531
+ out = out + empty_line + "\n\n"
1532
+ return out
1533
+ }
1534
+ i := 0
1535
+ while i < len(items) {
1536
+ item := items[i]
1537
+ out = out + "- " + item + "\n"
1538
+ i = i + 1
1539
+ }
1540
+ out = out + "\n"
1541
+ return out
1542
+ }
1543
+
1544
+ func build_markdown(task, target, budget, generated_at, repo_root, git_info, instruction_files, selected_files, changed_files, commands, constraints, excluded_manifest, redactions, manifest, estimated_tokens) {
1545
+ out := "# Scent Context Pack\n\n"
1546
+ out = out + "Task: " + task + "\n\n"
1547
+ out = out + "Target Agent: " + target + "\n\n"
1548
+ out = out + "Budget: " + to_string(budget) + "\n\n"
1549
+ out = out + "Generated: " + generated_at + "\n\n"
1550
+ out = out + "Repository: " + repo_root + "\n\n"
1551
+ out = out + "Git Branch: " + git_info["branch"] + "\n\n"
1552
+
1553
+ status_summary := "dirty=" + to_string(git_info["dirty"]) + " staged=" + to_string(len(git_info["staged_files"])) + " unstaged=" + to_string(len(git_info["unstaged_files"])) + " untracked=" + to_string(len(git_info["untracked_files"]))
1554
+ out = out + "Git Status Summary: " + status_summary + "\n\n"
1555
+
1556
+ out = out + "## How To Use This Pack\n\n"
1557
+ out = out + "Use this context as the bounded task context.\n"
1558
+ out = out + "Do not assume omitted files are irrelevant if tests or errors point elsewhere.\n"
1559
+ out = out + "Follow project instructions first.\n"
1560
+ out = out + "Respect the constraints and guardrails.\n"
1561
+ out = out + "Run the listed validation commands when making changes.\n\n"
1562
+ out = out + target_notes(target) + "\n\n"
1563
+
1564
+ inst_rel := []
1565
+ idx := 0
1566
+ while idx < len(instruction_files) {
1567
+ inst_path := instruction_files[idx]
1568
+ inst_rel = push(inst_rel, path_rel(repo_root, inst_path))
1569
+ idx = idx + 1
1570
+ }
1571
+ out = out + markdown_section_list("Project Instructions", inst_rel, "No instruction files discovered.")
1572
+
1573
+ out = out + "## Task-Relevant Files\n\n"
1574
+ idx = 0
1575
+ while idx < len(selected_files) {
1576
+ f := selected_files[idx]
1577
+ out = out + "- " + f["path"] + " (" + f["reason"] + ", " + to_string(f["estimated_tokens"]) + " tokens)\n"
1578
+ idx = idx + 1
1579
+ }
1580
+ out = out + "\n"
1581
+
1582
+ out = out + markdown_section_list("Changed Files", changed_files, "No changed files detected.")
1583
+
1584
+ test_commands := []
1585
+ build_commands := []
1586
+ idx = 0
1587
+ while idx < len(commands) {
1588
+ c := commands[idx]
1589
+ if contains(c, "test") {
1590
+ test_commands = push(test_commands, "`" + c + "`")
1591
+ } else {
1592
+ build_commands = push(build_commands, "`" + c + "`")
1593
+ }
1594
+ idx = idx + 1
1595
+ }
1596
+ out = out + markdown_section_list("Tests And Validation Commands", test_commands, "No validation commands were confidently detected.")
1597
+ out = out + markdown_section_list("Build/Run Commands", build_commands, "No build/run commands were confidently detected.")
1598
+
1599
+ out = out + "## Constraints And Guardrails\n\n"
1600
+ idx = 0
1601
+ while idx < len(constraints) {
1602
+ c := constraints[idx]
1603
+ out = out + "- " + c + "\n"
1604
+ idx = idx + 1
1605
+ }
1606
+ out = out + "\n"
1607
+
1608
+ out = out + "## Included File Summaries\n\n"
1609
+ idx = 0
1610
+ while idx < len(selected_files) {
1611
+ f := selected_files[idx]
1612
+ out = out + "- " + f["path"] + ": reason=" + f["reason"] + ", size_bytes=" + to_string(f["size_bytes"]) + ", estimated_tokens=" + to_string(f["estimated_tokens"]) + "\n"
1613
+ idx = idx + 1
1614
+ }
1615
+ out = out + "\n"
1616
+
1617
+ out = out + "## Included File Contents\n\n"
1618
+ idx = 0
1619
+ while idx < len(selected_files) {
1620
+ f := selected_files[idx]
1621
+ out = out + "### " + f["path"] + "\n\n"
1622
+ fence := markdown_fence(f["content"])
1623
+ out = out + fence + "text\n"
1624
+ out = out + f["content"]
1625
+ if !ends_with(f["content"], "\n") {
1626
+ out = out + "\n"
1627
+ }
1628
+ out = out + fence + "\n\n"
1629
+ idx = idx + 1
1630
+ }
1631
+
1632
+ out = out + "## Excluded Or Truncated Content\n\n"
1633
+ if len(excluded_manifest) == 0 {
1634
+ out = out + "None.\n\n"
1635
+ } else {
1636
+ idx = 0
1637
+ while idx < len(excluded_manifest) {
1638
+ e := excluded_manifest[idx]
1639
+ out = out + "- " + e["path"] + " [" + e["decision"] + "] reason=" + e["reason"] + "\n"
1640
+ idx = idx + 1
1641
+ }
1642
+ out = out + "\n"
1643
+ }
1644
+
1645
+ out = out + "## Redactions\n\n"
1646
+ if len(redactions) == 0 {
1647
+ out = out + "No redactions were required.\n\n"
1648
+ } else {
1649
+ idx = 0
1650
+ while idx < len(redactions) {
1651
+ r := redactions[idx]
1652
+ line_text := "n/a"
1653
+ if has_key(r, "line") {
1654
+ line_text = to_string(r["line"])
1655
+ }
1656
+ out = out + "- file=" + r["file"] + " line=" + line_text + " type=" + r["redaction_type"] + " placeholder=" + r["placeholder"] + "\n"
1657
+ idx = idx + 1
1658
+ }
1659
+ out = out + "\n"
1660
+ }
1661
+
1662
+ out = out + "## Manifest Summary\n\n"
1663
+ out = out + "Included files: " + to_string(len(selected_files)) + "\n\n"
1664
+ out = out + "Estimated tokens: " + to_string(estimated_tokens) + " / " + to_string(budget) + "\n\n"
1665
+
1666
+ idx = 0
1667
+ while idx < len(manifest) {
1668
+ m := manifest[idx]
1669
+ out = out + "- " + m["path"] + " => " + m["decision"] + " (" + m["reason"] + ", score=" + to_string(m["score"]) + ", tokens=" + to_string(m["estimated_tokens"]) + ")\n"
1670
+ idx = idx + 1
1671
+ }
1672
+
1673
+ return out
1674
+ }
1675
+
1676
+ func ensure_dir(path) {
1677
+ if path_exists(path) {
1678
+ return true
1679
+ }
1680
+
1681
+ parent := dirname(path)
1682
+ if parent != path && !path_exists(parent) {
1683
+ ensure_dir(parent)
1684
+ }
1685
+
1686
+ result := create_dir(path)
1687
+ if !result {
1688
+ return false
1689
+ }
1690
+ return true
1691
+ }
1692
+
1693
+ func path_entry_exists(path) {
1694
+ parent := dirname(path)
1695
+ if !path_exists(parent) || !path_is_dir(parent) {
1696
+ return false
1697
+ }
1698
+ try {
1699
+ return contains(list_dir(parent), basename(path))
1700
+ } except e {
1701
+ return false
1702
+ }
1703
+ }
1704
+
1705
+ func write_text(path, content) {
1706
+ if path_entry_exists(path) && path_is_symlink(path) {
1707
+ try {
1708
+ removed := delete_file(path)
1709
+ if !removed {
1710
+ return err("failed to replace symlink artifact: " + path, 4)
1711
+ }
1712
+ } except e {
1713
+ return err("failed to replace symlink artifact " + path + ": " + to_string(e), 4)
1714
+ }
1715
+ }
1716
+
1717
+ parent := dirname(path)
1718
+ if !ensure_dir(parent) {
1719
+ return err("failed to create parent directory for " + path, 4)
1720
+ }
1721
+
1722
+ wr := write_file(path, content, true)
1723
+ if !wr {
1724
+ return err("failed to write " + path, 4)
1725
+ }
1726
+ return ok(true)
1727
+ }
1728
+
1729
+ func delete_stale_file(path) {
1730
+ if !path_entry_exists(path) {
1731
+ return ok(true)
1732
+ }
1733
+ if !path_is_file(path) && !path_is_symlink(path) {
1734
+ return err("cannot replace stale artifact because it is not a file: " + path, 4)
1735
+ }
1736
+
1737
+ try {
1738
+ removed := delete_file(path)
1739
+ if removed {
1740
+ return ok(true)
1741
+ }
1742
+ return err("failed to delete stale artifact: " + path, 4)
1743
+ } except e {
1744
+ return err("failed to delete stale artifact " + path + ": " + to_string(e), 4)
1745
+ }
1746
+ }
1747
+
1748
+ func summary_payload(result) {
1749
+ return {
1750
+ "output_dir": result["output_dir"],
1751
+ "context_md": result["context_md"],
1752
+ "context_json": result["context_json"],
1753
+ "estimated_tokens": result["estimated_tokens"],
1754
+ "budget": result["budget"],
1755
+ "included_files": result["included_files"],
1756
+ "warnings": result["warnings"]
1757
+ }
1758
+ }
1759
+
1760
+ func run_pack(opts, raw_flags) {
1761
+ started := now()
1762
+ cwd := os_getcwd()
1763
+ repo_root := detect_repo_root(cwd)
1764
+ git_info := inspect_git(repo_root)
1765
+ instruction_files := discover_instruction_files(repo_root)
1766
+ task_redacted := redact_text("[task]", to_string(opts["task"]))
1767
+ task_text := task_redacted["content"]
1768
+ budget_value := to_int(opts["budget"])
1769
+ max_file_bytes_value := to_int(opts["max_file_bytes"])
1770
+ target_value := to_string(opts["target"])
1771
+ format_value := to_string(opts["format"])
1772
+
1773
+ include_scope := validate_scoped_paths(repo_root, opts["include"], "--include")
1774
+ if !include_scope["ok"] {
1775
+ return include_scope
1776
+ }
1777
+ opts["include"] = include_scope["value"]
1778
+ exclude_scope := validate_scoped_paths(repo_root, opts["exclude"], "--exclude")
1779
+ if !exclude_scope["ok"] {
1780
+ return exclude_scope
1781
+ }
1782
+ opts["exclude"] = exclude_scope["value"]
1783
+
1784
+ stamp := "unix-" + to_string(to_int(now() * 1000))
1785
+ out_dir := opts["out"]
1786
+ if out_dir == null {
1787
+ out_dir = path_join(path_join(path_join(repo_root, ".scent"), "packs"), stamp)
1788
+ } else {
1789
+ out_dir = resolve_path(cwd, out_dir)
1790
+ }
1791
+
1792
+ candidate_excludes := opts["exclude"]
1793
+ normalized_out_dir := canonical_path_for_scope(out_dir)
1794
+ if normalized_out_dir != repo_root && starts_with(normalized_out_dir, repo_root + "/") {
1795
+ candidate_excludes = push(candidate_excludes, normalized_out_dir)
1796
+ }
1797
+
1798
+ task_terms := tokenize_task(task_text)
1799
+
1800
+ reserve := reserved_tokens(task_text)
1801
+ if budget_value <= reserve {
1802
+ return err("budget too small: " + to_string(budget_value) + ". minimum required is " + to_string(reserve + 1), 5)
1803
+ }
1804
+
1805
+ warnings := []
1806
+ if !git_info["available"] {
1807
+ warnings = push(warnings, "git unavailable; running in degraded mode")
1808
+ }
1809
+
1810
+ candidates := collect_candidates(repo_root, git_info, instruction_files, opts["include"], candidate_excludes, task_terms)
1811
+ selected_candidates := select_candidates(opts, repo_root, candidates)
1812
+
1813
+ selected_files := []
1814
+ redactions_by_path := {}
1815
+ read_failures := {}
1816
+
1817
+ loop_idx := 0
1818
+ while loop_idx < len(selected_candidates) {
1819
+ selected_candidate := selected_candidates[loop_idx]
1820
+ read_res := read_text_file(selected_candidate["path"])
1821
+ if !read_res["ok"] {
1822
+ read_failures[selected_candidate["rel_path"]] = read_res["error"]
1823
+ loop_idx = loop_idx + 1
1824
+ continue
1825
+ }
1826
+
1827
+ redacted := redact_text(selected_candidate["rel_path"], read_res["content"])
1828
+ clipped := clip_text(redacted["content"], max_file_bytes_value)
1829
+
1830
+ redactions_by_path[selected_candidate["rel_path"]] = redacted["redactions"]
1831
+
1832
+ content := clipped["content"]
1833
+ selected_files = push(selected_files, {
1834
+ "path": selected_candidate["rel_path"],
1835
+ "reason": selected_candidate["selection_reason"],
1836
+ "score": score_candidate(selected_candidate, opts, repo_root)["score"],
1837
+ "size_bytes": selected_candidate["size_bytes"],
1838
+ "estimated_tokens": estimate_tokens(content),
1839
+ "included": true,
1840
+ "truncated": clipped["truncated"],
1841
+ "content": content,
1842
+ "content_hash": sha256(content)
1843
+ })
1844
+ loop_idx = loop_idx + 1
1845
+ }
1846
+
1847
+ budget_result := apply_budget(budget_value, reserve, selected_files)
1848
+ included_files := budget_result["files"]
1849
+ manifest := budget_result["manifest"]
1850
+
1851
+ redactions := task_redacted["redactions"]
1852
+ loop_idx = 0
1853
+ while loop_idx < len(included_files) {
1854
+ final_file := included_files[loop_idx]
1855
+ final_lines := split(final_file["content"], "\n")
1856
+ if has_key(redactions_by_path, final_file["path"]) {
1857
+ file_redactions := redactions_by_path[final_file["path"]]
1858
+ red_idx := 0
1859
+ while red_idx < len(file_redactions) {
1860
+ redaction_item := file_redactions[red_idx]
1861
+ line_index := to_int(redaction_item["line"]) - 1
1862
+ if line_index >= 0 && line_index < len(final_lines) && contains(final_lines[line_index], redaction_item["placeholder"]) {
1863
+ redactions = push(redactions, redaction_item)
1864
+ }
1865
+ red_idx = red_idx + 1
1866
+ }
1867
+ }
1868
+ loop_idx = loop_idx + 1
1869
+ }
1870
+
1871
+ if len(included_files) == 0 {
1872
+ warnings = push(warnings, "no relevant files selected; output contains metadata only")
1873
+ }
1874
+
1875
+ selected_map := {}
1876
+ selected_reason_map := {}
1877
+ loop_idx = 0
1878
+ while loop_idx < len(included_files) {
1879
+ included_file := included_files[loop_idx]
1880
+ selected_map[included_file["path"]] = 1
1881
+ selected_reason_map[included_file["path"]] = included_file["reason"]
1882
+ loop_idx = loop_idx + 1
1883
+ }
1884
+
1885
+ manifested_file_map := {}
1886
+ loop_idx = 0
1887
+ while loop_idx < len(manifest) {
1888
+ existing_manifest := manifest[loop_idx]
1889
+ if existing_manifest["type"] == "file" {
1890
+ manifested_file_map[existing_manifest["path"]] = 1
1891
+ }
1892
+ loop_idx = loop_idx + 1
1893
+ }
1894
+
1895
+ loop_idx = 0
1896
+ while loop_idx < len(candidates) {
1897
+ candidate_item := candidates[loop_idx]
1898
+ if has_key(selected_map, candidate_item["rel_path"]) {
1899
+ candidate_item["selected"] = true
1900
+ candidate_item["selection_reason"] = selected_reason_map[candidate_item["rel_path"]]
1901
+ } else {
1902
+ candidate_item["selected"] = false
1903
+ if has_key(read_failures, candidate_item["rel_path"]) {
1904
+ candidate_item["selection_reason"] = "excluded because file could not be read: " + to_string(read_failures[candidate_item["rel_path"]])
1905
+ } else if candidate_item["selection_reason"] == "" {
1906
+ candidate_item["selection_reason"] = "not selected by deterministic relevance"
1907
+ }
1908
+ if has_key(manifested_file_map, candidate_item["rel_path"]) == 0 {
1909
+ manifest = push(manifest, {
1910
+ "path": candidate_item["rel_path"],
1911
+ "type": "file",
1912
+ "decision": "exclude",
1913
+ "reason": candidate_item["selection_reason"],
1914
+ "score": 0,
1915
+ "size_bytes": candidate_item["size_bytes"],
1916
+ "estimated_tokens": 0
1917
+ })
1918
+ }
1919
+ }
1920
+ candidates[loop_idx] = candidate_item
1921
+ loop_idx = loop_idx + 1
1922
+ }
1923
+
1924
+ commands := discover_commands(repo_root)
1925
+
1926
+ constraints := [
1927
+ "Keep scope tight and deterministic.",
1928
+ "Do not assume omitted files are irrelevant if errors point elsewhere.",
1929
+ "Avoid touching files outside the task scope unless necessary.",
1930
+ "Do not include secrets in output artifacts."
1931
+ ]
1932
+
1933
+ estimated_tokens := estimate_tokens(task_text) + 2
1934
+ loop_idx = 0
1935
+ while loop_idx < len(included_files) {
1936
+ estimated_tokens = estimated_tokens + included_files[loop_idx]["estimated_tokens"]
1937
+ loop_idx = loop_idx + 1
1938
+ }
1939
+
1940
+ generated_at := "unix-" + to_string(to_int(now()))
1941
+
1942
+ artifact_names := []
1943
+ if format_value == "md" || format_value == "both" {
1944
+ artifact_names = push(artifact_names, "context.md")
1945
+ }
1946
+ if format_value == "json" || format_value == "both" {
1947
+ artifact_names = push(artifact_names, "context.json")
1948
+ }
1949
+ artifact_names = push(artifact_names, "manifest.json")
1950
+ artifact_names = push(artifact_names, "files.json")
1951
+ artifact_names = push(artifact_names, "redactions.json")
1952
+ artifact_names = push(artifact_names, "metadata.json")
1953
+
1954
+ loop_idx = 0
1955
+ while loop_idx < len(artifact_names) {
1956
+ art := artifact_names[loop_idx]
1957
+ decision := "include"
1958
+ reason := "generated artifact"
1959
+ if opts["dry_run"] {
1960
+ decision = "exclude"
1961
+ reason = "dry-run; artifact not written"
1962
+ }
1963
+
1964
+ manifest = push(manifest, {
1965
+ "path": art,
1966
+ "type": "artifact",
1967
+ "decision": decision,
1968
+ "reason": reason,
1969
+ "score": 0,
1970
+ "size_bytes": 0,
1971
+ "estimated_tokens": 0
1972
+ })
1973
+ loop_idx = loop_idx + 1
1974
+ }
1975
+
1976
+ excluded_manifest := []
1977
+ loop_idx = 0
1978
+ while loop_idx < len(manifest) {
1979
+ manifest_item := manifest[loop_idx]
1980
+ if manifest_item["decision"] == "exclude" || manifest_item["decision"] == "truncate" {
1981
+ excluded_manifest = push(excluded_manifest, manifest_item)
1982
+ }
1983
+ loop_idx = loop_idx + 1
1984
+ }
1985
+
1986
+ markdown := build_markdown(
1987
+ task_text,
1988
+ target_value,
1989
+ budget_value,
1990
+ generated_at,
1991
+ repo_root,
1992
+ git_info,
1993
+ instruction_files,
1994
+ included_files,
1995
+ git_info["changed_files"],
1996
+ commands,
1997
+ constraints,
1998
+ excluded_manifest,
1999
+ redactions,
2000
+ manifest,
2001
+ estimated_tokens
2002
+ )
2003
+
2004
+ instruction_rels := []
2005
+ loop_idx = 0
2006
+ while loop_idx < len(instruction_files) {
2007
+ instruction_rels = push(instruction_rels, path_rel(repo_root, instruction_files[loop_idx]))
2008
+ loop_idx = loop_idx + 1
2009
+ }
2010
+
2011
+ context_json_obj := {
2012
+ "schema_version": "1.0.0",
2013
+ "task": task_text,
2014
+ "target": target_value,
2015
+ "budget": budget_value,
2016
+ "estimated_tokens": estimated_tokens,
2017
+ "generated_at": generated_at,
2018
+ "repo": repo_root,
2019
+ "git": git_info,
2020
+ "instructions": instruction_rels,
2021
+ "selected_files": included_files,
2022
+ "changed_files": git_info["changed_files"],
2023
+ "commands": commands,
2024
+ "constraints": constraints,
2025
+ "redactions": redactions,
2026
+ "excluded": excluded_manifest,
2027
+ "artifacts": artifact_names
2028
+ }
2029
+
2030
+ files_json := []
2031
+ loop_idx = 0
2032
+ while loop_idx < len(candidates) {
2033
+ cand := candidates[loop_idx]
2034
+ files_json = push(files_json, {
2035
+ "path": cand["rel_path"],
2036
+ "size_bytes": cand["size_bytes"],
2037
+ "is_changed": cand["is_changed"],
2038
+ "is_staged": cand["is_staged"],
2039
+ "is_unstaged": cand["is_unstaged"],
2040
+ "is_instruction": cand["is_instruction"],
2041
+ "is_doc": cand["is_doc"],
2042
+ "is_test": cand["is_test"],
2043
+ "is_config": cand["is_config"],
2044
+ "matched_terms": cand["matched_terms"],
2045
+ "selected": cand["selected"],
2046
+ "selection_reason": cand["selection_reason"]
2047
+ })
2048
+ loop_idx = loop_idx + 1
2049
+ }
2050
+
2051
+ exit_code := 0
2052
+ if len(warnings) > 0 {
2053
+ exit_code = 1
2054
+ }
2055
+
2056
+ sanitized_flags := []
2057
+ loop_idx = 0
2058
+ while loop_idx < len(raw_flags) {
2059
+ sanitized_flag := redact_text("[command]", to_string(raw_flags[loop_idx]))
2060
+ sanitized_flags = push(sanitized_flags, sanitized_flag["content"])
2061
+ loop_idx = loop_idx + 1
2062
+ }
2063
+
2064
+ metadata := {
2065
+ "tool": "scent",
2066
+ "version": VERSION,
2067
+ "generated_at": generated_at,
2068
+ "duration_ms": to_int((now() - started) * 1000),
2069
+ "repo_root": repo_root,
2070
+ "git_branch": git_info["branch"],
2071
+ "git_commit": git_info["commit"],
2072
+ "dirty": git_info["dirty"],
2073
+ "command": "scent pack",
2074
+ "flags": sanitized_flags,
2075
+ "exit_code": exit_code
2076
+ }
2077
+
2078
+ context_md_path := null
2079
+ context_json_path := null
2080
+
2081
+ if !opts["dry_run"] {
2082
+ if format_value == "md" {
2083
+ stale_json := delete_stale_file(path_join(out_dir, "context.json"))
2084
+ if !stale_json["ok"] {
2085
+ return stale_json
2086
+ }
2087
+ } else if format_value == "json" {
2088
+ stale_markdown := delete_stale_file(path_join(out_dir, "context.md"))
2089
+ if !stale_markdown["ok"] {
2090
+ return stale_markdown
2091
+ }
2092
+ }
2093
+
2094
+ if format_value == "md" || format_value == "both" {
2095
+ context_md_path = path_join(out_dir, "context.md")
2096
+ w1 := write_text(context_md_path, markdown)
2097
+ if !w1["ok"] {
2098
+ return w1
2099
+ }
2100
+ }
2101
+ if format_value == "json" || format_value == "both" {
2102
+ context_json_path = path_join(out_dir, "context.json")
2103
+ w2 := write_text(context_json_path, to_json_pretty(context_json_obj))
2104
+ if !w2["ok"] {
2105
+ return w2
2106
+ }
2107
+ }
2108
+
2109
+ w3 := write_text(path_join(out_dir, "manifest.json"), to_json_pretty(manifest))
2110
+ if !w3["ok"] {
2111
+ return w3
2112
+ }
2113
+
2114
+ w4 := write_text(path_join(out_dir, "files.json"), to_json_pretty(files_json))
2115
+ if !w4["ok"] {
2116
+ return w4
2117
+ }
2118
+
2119
+ w5 := write_text(path_join(out_dir, "redactions.json"), to_json_pretty(redactions))
2120
+ if !w5["ok"] {
2121
+ return w5
2122
+ }
2123
+
2124
+ w6 := write_text(path_join(out_dir, "metadata.json"), to_json_pretty(metadata))
2125
+ if !w6["ok"] {
2126
+ return w6
2127
+ }
2128
+ }
2129
+
2130
+ return ok({
2131
+ "output_dir": out_dir,
2132
+ "context_md": context_md_path,
2133
+ "context_json": context_json_path,
2134
+ "estimated_tokens": estimated_tokens,
2135
+ "budget": budget_value,
2136
+ "included_files": len(included_files),
2137
+ "warnings": warnings,
2138
+ "selected_files": included_files,
2139
+ "manifest": manifest,
2140
+ "redactions": redactions,
2141
+ "exit_code": exit_code
2142
+ })
2143
+ }
2144
+
2145
+ func print_lines(lines) {
2146
+ i := 0
2147
+ while i < len(lines) {
2148
+ print(lines[i])
2149
+ i = i + 1
2150
+ }
2151
+ return null
2152
+ }
2153
+
2154
+ func print_kv(label, value) {
2155
+ print(label + ": " + to_string(value))
2156
+ return null
2157
+ }
2158
+
2159
+ func print_human_result(result, opts) {
2160
+ payload := summary_payload(result)
2161
+
2162
+ if opts["json_stdout"] {
2163
+ print(to_json(payload))
2164
+ return null
2165
+ }
2166
+
2167
+ print("scent pack complete")
2168
+ print_kv("output_dir", result["output_dir"])
2169
+ print_kv("included_files", result["included_files"])
2170
+ print_kv("estimated_tokens", to_string(result["estimated_tokens"]) + " / " + to_string(result["budget"]))
2171
+ if len(result["warnings"]) > 0 {
2172
+ warning_lines := ["warnings:"]
2173
+ i := 0
2174
+ while i < len(result["warnings"]) {
2175
+ warning_lines = push(warning_lines, "- " + result["warnings"][i])
2176
+ i = i + 1
2177
+ }
2178
+ print_lines(warning_lines)
2179
+ }
2180
+
2181
+ if opts["verbose"] {
2182
+ print(to_json(payload))
2183
+ }
2184
+ return null
2185
+ }
2186
+
2187
+ func main() {
2188
+ argv := args()
2189
+
2190
+ if len(argv) > 0 {
2191
+ first := argv[0]
2192
+ if is_help_arg(first) {
2193
+ print(usage_text())
2194
+ exit(0)
2195
+ }
2196
+ if is_version_arg(first) {
2197
+ print(version_text())
2198
+ exit(0)
2199
+ }
2200
+ if first == "pack" && len(argv) > 1 {
2201
+ second := argv[1]
2202
+ if is_help_arg(second) {
2203
+ print(usage_text())
2204
+ exit(0)
2205
+ }
2206
+ if is_version_arg(second) {
2207
+ print(version_text())
2208
+ exit(0)
2209
+ }
2210
+ }
2211
+ }
2212
+
2213
+ parsed := parse_cli(argv)
2214
+
2215
+ if !parsed["ok"] {
2216
+ if parsed["exit_code"] == 2 {
2217
+ print(parsed["error"])
2218
+ } else {
2219
+ print("error: " + parsed["error"])
2220
+ }
2221
+ exit(parsed["exit_code"])
2222
+ }
2223
+
2224
+ result := run_pack(parsed["value"], argv)
2225
+ if !result["ok"] {
2226
+ print("error: " + result["error"])
2227
+ exit(result["exit_code"])
2228
+ }
2229
+
2230
+ print_human_result(result["value"], parsed["value"])
2231
+ exit(result["value"]["exit_code"])
2232
+ }
2233
+
2234
+ # =============================
2235
+ # Tests (kujo test-run scent.kujo)
2236
+ # =============================
2237
+
2238
+ test "help and version surface" {
2239
+ cwd := os_getcwd()
2240
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2241
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2242
+
2243
+ func shq(value) {
2244
+ text := to_string(value)
2245
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2246
+ }
2247
+
2248
+ func run_cmd(command) {
2249
+ res := execute_status(command, {"timeout_ms": 120000})
2250
+ if type(res) == "struct" {
2251
+ return res
2252
+ }
2253
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2254
+ }
2255
+
2256
+ func run_scent(args) {
2257
+ command := shq(kujo_bin) + " run " + shq(scent_script)
2258
+ i := 0
2259
+ while i < len(args) {
2260
+ command = command + " " + shq(args[i])
2261
+ i = i + 1
2262
+ }
2263
+ return run_cmd(command)
2264
+ }
2265
+
2266
+ help_res := run_scent(["help"])
2267
+ assert_equal(help_res.exitcode, 0)
2268
+ assert_equal(contains(help_res.stdout, "Usage:"), 1)
2269
+ assert_equal(contains(help_res.stdout, "Unknown argument"), 0)
2270
+ assert_equal(contains(help_res.stderr, "Unknown argument"), 0)
2271
+
2272
+ top_help_res := run_scent(["--help"])
2273
+ assert_equal(top_help_res.exitcode, 0)
2274
+ assert_equal(contains(top_help_res.stdout, "Usage:"), 1)
2275
+
2276
+ pack_help_res := run_scent(["pack", "--help"])
2277
+ assert_equal(pack_help_res.exitcode, 0)
2278
+ assert_equal(contains(pack_help_res.stdout, "Usage:"), 1)
2279
+ assert_equal(contains(pack_help_res.stdout, "Unknown argument"), 0)
2280
+
2281
+ pack_version_res := run_scent(["pack", "--version"])
2282
+ assert_equal(pack_version_res.exitcode, 0)
2283
+ assert_equal(contains(pack_version_res.stdout, "Scent "), 1)
2284
+
2285
+ version_res := run_scent(["version"])
2286
+ assert_equal(version_res.exitcode, 0)
2287
+ assert_equal(contains(version_res.stdout, "Scent "), 1)
2288
+
2289
+ flag_version_res := run_scent(["--version"])
2290
+ assert_equal(flag_version_res.exitcode, 0)
2291
+ assert_equal(contains(flag_version_res.stdout, "Scent "), 1)
2292
+ }
2293
+
2294
+ test "pack and dry-run smoke" {
2295
+ cwd := os_getcwd()
2296
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2297
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2298
+ smoke_root := "/tmp/scent_phase8_fix"
2299
+ smoke_repo := smoke_root + "/repo"
2300
+ smoke_out := smoke_root + "/out"
2301
+ smoke_dry_out := smoke_root + "/dry-run-out"
2302
+ smoke_human_out := smoke_root + "/dry-run-human-out"
2303
+
2304
+ func shq(value) {
2305
+ text := to_string(value)
2306
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2307
+ }
2308
+
2309
+ func run_cmd(command) {
2310
+ res := execute_status(command, {"timeout_ms": 120000})
2311
+ if type(res) == "struct" {
2312
+ return res
2313
+ }
2314
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2315
+ }
2316
+
2317
+ func run_scent_in_repo(args) {
2318
+ command := "cd " + shq(smoke_repo) + " && " + shq(kujo_bin) + " run " + shq(scent_script)
2319
+ i := 0
2320
+ while i < len(args) {
2321
+ command = command + " " + shq(args[i])
2322
+ i = i + 1
2323
+ }
2324
+ return run_cmd(command)
2325
+ }
2326
+
2327
+ func parse_json_safe(text) {
2328
+ try {
2329
+ return {"ok": true, "value": parse_json(text)}
2330
+ } except err {
2331
+ return {"ok": false, "error": to_string(err), "value": {}}
2332
+ }
2333
+ }
2334
+
2335
+ func require_truthy(value, label) {
2336
+ if value == false || value == 0 {
2337
+ print("Assertion failed: " + label)
2338
+ exit(1)
2339
+ }
2340
+ }
2341
+
2342
+ func require_files_exist(root, files) {
2343
+ i := 0
2344
+ while i < len(files) {
2345
+ file := files[i]
2346
+ require_truthy(file_exists(root + "/" + file), file + " exists")
2347
+ i = i + 1
2348
+ }
2349
+ return null
2350
+ }
2351
+
2352
+ func smoke_pack_args(task, out_dir, json_stdout, dry_run) {
2353
+ values := [
2354
+ "pack",
2355
+ "--task", task,
2356
+ "--out", out_dir,
2357
+ "--changed",
2358
+ "--staged",
2359
+ "--unstaged",
2360
+ "--include", "docs",
2361
+ "--include", "src",
2362
+ "--include", "tests",
2363
+ "--include", "logs",
2364
+ "--include", "reports",
2365
+ "--include", "notes",
2366
+ "--max-files", "10",
2367
+ "--max-file-bytes", "2000",
2368
+ "--format", "both"
2369
+ ]
2370
+
2371
+ if json_stdout {
2372
+ values = push(values, "--json")
2373
+ }
2374
+ if dry_run {
2375
+ values = push(values, "--dry-run")
2376
+ }
2377
+ return values
2378
+ }
2379
+
2380
+ _ = run_cmd("rm -rf " + shq(smoke_root))
2381
+ _ = run_cmd("mkdir -p " + shq(smoke_repo + "/docs") + " " + shq(smoke_repo + "/src") + " " + shq(smoke_repo + "/tests") + " " + shq(smoke_repo + "/logs") + " " + shq(smoke_repo + "/reports") + " " + shq(smoke_repo + "/notes"))
2382
+ _ = run_cmd("cd " + shq(smoke_repo) + " && git init -q")
2383
+ _ = run_cmd("cd " + shq(smoke_repo) + " && git config user.name " + shq("Scent Test"))
2384
+ _ = run_cmd("cd " + shq(smoke_repo) + " && git config user.email " + shq("scent@example.com"))
2385
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' '# Smoke repo' 'Initial content' > README.md")
2386
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'Initial instructions' 'Follow local cues only.' > AGENTS.md")
2387
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' '# Architecture' 'Deterministic paths only.' > docs/architecture.md")
2388
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'Potential risk' 'API_KEY=abc123' > docs/security.md")
2389
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'print(\"hello\")' > src/main.kujo")
2390
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'workflow notes' > src/workflow.kujo")
2391
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'assert true' > tests/sample_test.kujo")
2392
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'sample log' > logs/sample.log")
2393
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' '{\"score\": 7}' > reports/eval.json")
2394
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'context notes' > notes/context.md")
2395
+ _ = run_cmd("cd " + shq(smoke_repo) + " && git add . && git commit -q -m " + shq("seed"))
2396
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n' 'Unstaged task context.' >> README.md")
2397
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n' 'Staged security note.' >> docs/security.md")
2398
+ _ = run_cmd("cd " + shq(smoke_repo) + " && git add docs/security.md")
2399
+
2400
+ task := "review workflow cues and risks"
2401
+ pack_res := run_scent_in_repo(smoke_pack_args(task, smoke_out, true, false))
2402
+ assert_equal(pack_res.exitcode, 0)
2403
+
2404
+ pack_out := parse_json_safe(to_string(pack_res.stdout))
2405
+ require_truthy(pack_out["ok"], "pack output ok")
2406
+ pack_data := pack_out["value"]
2407
+ assert_equal(pack_data["estimated_tokens"], 85)
2408
+ assert_equal(pack_data["included_files"], 10)
2409
+ assert_equal(len(pack_data["warnings"]), 0)
2410
+ require_files_exist(smoke_out, ["context.json", "context.md", "files.json", "manifest.json", "metadata.json", "redactions.json"])
2411
+
2412
+ context := read_file(smoke_out + "/context.json")
2413
+ context_json := parse_json_safe(context)
2414
+ require_truthy(context_json["ok"], "context json parses")
2415
+ context_data := context_json["value"]
2416
+ assert_equal(context_data["git"]["dirty"], true)
2417
+ require_truthy(len(context_data["changed_files"]) > 0, "changed files captured")
2418
+ require_truthy(contains(to_json(context_data["changed_files"]), "README.md"), "README.md captured as changed")
2419
+ require_truthy(contains(to_json(context_data["changed_files"]), "docs/security.md"), "docs/security.md captured as changed")
2420
+ require_truthy(contains(to_json(context_data["git"]["staged_files"]), "docs/security.md"), "staged metadata captured")
2421
+ require_truthy(contains(to_json(context_data["git"]["unstaged_files"]), "README.md"), "unstaged metadata captured")
2422
+ assert_equal(contains(context, "No changed files detected."), 0)
2423
+ require_truthy(contains(context, "[REDACTED:API_KEY]"), "redaction marker present")
2424
+ require_truthy(contains(context, "\"schema_version\""), "schema version present")
2425
+ files_json := parse_json_safe(read_file(smoke_out + "/files.json"))
2426
+ require_truthy(files_json["ok"], "files json parses")
2427
+ files_data := files_json["value"]
2428
+ files_by_path := {}
2429
+ idx := 0
2430
+ while idx < len(files_data) {
2431
+ item := files_data[idx]
2432
+ files_by_path[item["path"]] = item
2433
+ idx = idx + 1
2434
+ }
2435
+ require_truthy(files_by_path["README.md"]["is_changed"], "README.md marked changed")
2436
+ require_truthy(files_by_path["README.md"]["is_unstaged"], "README.md marked unstaged")
2437
+ require_truthy(files_by_path["docs/security.md"]["is_changed"], "docs/security.md marked changed")
2438
+ require_truthy(files_by_path["docs/security.md"]["is_staged"], "docs/security.md marked staged")
2439
+
2440
+ dry_res := run_scent_in_repo(smoke_pack_args(task, smoke_dry_out, true, true))
2441
+ assert_equal(dry_res.exitcode, 0)
2442
+
2443
+ dry_out := parse_json_safe(to_string(dry_res.stdout))
2444
+ require_truthy(dry_out["ok"], "dry-run output ok")
2445
+ dry_data := dry_out["value"]
2446
+ assert_equal(type(dry_data["context_json"]), "null")
2447
+ assert_equal(type(dry_data["context_md"]), "null")
2448
+ assert_equal(dry_data["estimated_tokens"], 85)
2449
+ assert_equal(dry_data["included_files"], 10)
2450
+ assert_equal(len(dry_data["warnings"]), 0)
2451
+ assert_equal(file_exists(smoke_dry_out + "/context.json"), false)
2452
+ assert_equal(file_exists(smoke_dry_out + "/context.md"), false)
2453
+
2454
+ human_res := run_scent_in_repo(smoke_pack_args(task, smoke_human_out, false, true))
2455
+ assert_equal(human_res.exitcode, 0)
2456
+ expected_human := "scent pack complete\noutput_dir: " + smoke_human_out + "\nincluded_files: 10\nestimated_tokens: 85 / 12000\n"
2457
+ assert_equal(to_string(human_res.stdout), expected_human)
2458
+ assert_equal(file_exists(smoke_human_out + "/context.json"), false)
2459
+ assert_equal(file_exists(smoke_human_out + "/context.md"), false)
2460
+ }
2461
+
2462
+ test "redaction and scoped include hardening" {
2463
+ cwd := os_getcwd()
2464
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2465
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2466
+ smoke_root := "/tmp/scent_hardening"
2467
+ smoke_repo := smoke_root + "/repo"
2468
+ smoke_out := smoke_root + "/out"
2469
+
2470
+ func shq(value) {
2471
+ text := to_string(value)
2472
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2473
+ }
2474
+
2475
+ func run_cmd(command) {
2476
+ res := execute_status(command, {"timeout_ms": 120000})
2477
+ if type(res) == "struct" {
2478
+ return res
2479
+ }
2480
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2481
+ }
2482
+
2483
+ func run_scent_in_repo(args) {
2484
+ command := "cd " + shq(smoke_repo) + " && " + shq(kujo_bin) + " run " + shq(scent_script)
2485
+ i := 0
2486
+ while i < len(args) {
2487
+ command = command + " " + shq(args[i])
2488
+ i = i + 1
2489
+ }
2490
+ return run_cmd(command)
2491
+ }
2492
+
2493
+ func parse_json_safe(text) {
2494
+ try {
2495
+ return {"ok": true, "value": parse_json(text)}
2496
+ } except err {
2497
+ return {"ok": false, "error": to_string(err), "value": {}}
2498
+ }
2499
+ }
2500
+
2501
+ func require_truthy(value, label) {
2502
+ if value == false || value == 0 {
2503
+ print("Assertion failed: " + label)
2504
+ exit(1)
2505
+ }
2506
+ }
2507
+
2508
+ _ = run_cmd("rm -rf " + shq(smoke_root))
2509
+ _ = run_cmd("mkdir -p " + shq(smoke_repo + "/docs") + " " + shq(smoke_repo + "/src"))
2510
+ _ = run_cmd("cd " + shq(smoke_repo) + " && git init -q")
2511
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n' '# Hardening repo' > README.md")
2512
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n' 'openai leak sk-12345678901234567890' > docs/security.md")
2513
+ _ = run_cmd("cd " + shq(smoke_repo) + " && printf '%s\\n%s\\n' 'github ghp_123456789012345678901234567890123456' 'aws AKIA1234567890ABCDEF' > src/app.kujo")
2514
+
2515
+ pack_res := run_scent_in_repo([
2516
+ "pack",
2517
+ "--task", "review hardening",
2518
+ "--out", smoke_out,
2519
+ "--include", "docs",
2520
+ "--include", "src",
2521
+ "--max-files", "5",
2522
+ "--format", "both",
2523
+ "--json"
2524
+ ])
2525
+ assert_equal(pack_res.exitcode, 0)
2526
+
2527
+ pack_out := parse_json_safe(to_string(pack_res.stdout))
2528
+ require_truthy(pack_out["ok"], "hardening pack output parses")
2529
+ context := read_file(smoke_out + "/context.json")
2530
+ require_truthy(contains(context, "[REDACTED:TOKEN]"), "token placeholder present")
2531
+ require_truthy(contains(context, "[REDACTED:ACCESS_KEY]"), "access key placeholder present")
2532
+ assert_equal(contains(context, "sk-12345678901234567890"), 0)
2533
+ assert_equal(contains(context, "ghp_123456789012345678901234567890123456"), 0)
2534
+ assert_equal(contains(context, "AKIA1234567890ABCDEF"), 0)
2535
+ require_truthy(contains(context, "\"docs/security.md\""), "first repeated include selected")
2536
+ require_truthy(contains(context, "\"src/app.kujo\""), "second repeated include selected")
2537
+
2538
+ escape_res := run_scent_in_repo([
2539
+ "pack",
2540
+ "--task", "escape",
2541
+ "--include", "../outside",
2542
+ "--dry-run",
2543
+ "--json"
2544
+ ])
2545
+ assert_equal(escape_res.exitcode, 2)
2546
+ require_truthy(contains(escape_res.stdout, "--include path cannot contain '..'"), "parent path rejected")
2547
+
2548
+ absolute_res := run_scent_in_repo([
2549
+ "pack",
2550
+ "--task", "absolute escape",
2551
+ "--include", "/tmp",
2552
+ "--dry-run",
2553
+ "--json"
2554
+ ])
2555
+ assert_equal(absolute_res.exitcode, 2)
2556
+ require_truthy(contains(absolute_res.stdout, "--include path must stay inside the repository"), "absolute outside path rejected")
2557
+ }
2558
+
2559
+ test "ten bug regressions" {
2560
+ cwd := os_getcwd()
2561
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2562
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2563
+ bug_root := "/tmp/scent_bug_regressions"
2564
+ bug_repo := bug_root + "/repo"
2565
+ bug_out := bug_repo + "/pack"
2566
+ budget_out := bug_root + "/budget-out"
2567
+ late_out := bug_root + "/late-out"
2568
+
2569
+ func shq(value) {
2570
+ text := to_string(value)
2571
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2572
+ }
2573
+
2574
+ func run_cmd(command) {
2575
+ res := execute_status(command, {"timeout_ms": 120000})
2576
+ if type(res) == "struct" {
2577
+ return res
2578
+ }
2579
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2580
+ }
2581
+
2582
+ func run_scent_in_repo(args) {
2583
+ command := "cd " + shq(bug_repo) + " && " + shq(kujo_bin) + " run " + shq(scent_script)
2584
+ i := 0
2585
+ while i < len(args) {
2586
+ command = command + " " + shq(args[i])
2587
+ i = i + 1
2588
+ }
2589
+ return run_cmd(command)
2590
+ }
2591
+
2592
+ func require_truthy(value, label) {
2593
+ if value == false || value == 0 {
2594
+ print("Assertion failed: " + label)
2595
+ exit(1)
2596
+ }
2597
+ }
2598
+
2599
+ _ = run_cmd("rm -rf " + shq(bug_root))
2600
+ _ = run_cmd("mkdir -p " + shq(bug_repo + "/docs") + " " + shq(bug_repo + "/src") + " " + shq(bug_repo + "/focus") + " " + shq(bug_out))
2601
+ _ = run_cmd("cd " + shq(bug_repo) + " && git init -q")
2602
+ _ = run_cmd("cd " + shq(bug_repo) + " && printf '%s\\n' '# Regression repo' > README.md")
2603
+ _ = run_cmd("cd " + shq(bug_repo) + " && printf '%s\\n' 'EXCLUDED_DOC_MARKER' > docs/excluded.md")
2604
+ _ = run_cmd("cd " + shq(bug_repo) + " && printf '%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n' 'password=supersecret # not redacted' 'print(\"token\", value == 3)' '\"estimated_tokens\": 5' '\"password\": \"abc=def\"' '```' '# injected heading' '```' > src/sample.txt")
2605
+ _ = run_cmd("cd " + shq(bug_repo) + " && printf 'safe\\000BINARY_SECRET_MARKER' > src/blob")
2606
+ _ = run_cmd("printf '%s\\n' 'SYMLINK_SECRET_MARKER' > " + shq(bug_root + "/outside.txt"))
2607
+ _ = run_cmd("ln -s " + shq(bug_root + "/outside.txt") + " " + shq(bug_repo + "/src/outside-link"))
2608
+ _ = run_cmd("printf '%s\\n' 'STALE_OUTPUT_MARKER' > " + shq(bug_out + "/context.md"))
2609
+
2610
+ pack_res := run_scent_in_repo([
2611
+ "pack", "--task", "review regression markers", "--out", bug_out,
2612
+ "--include", ".", "--exclude", "docs/", "--max-files", "20",
2613
+ "--format", "both", "--json"
2614
+ ])
2615
+ assert_equal(pack_res.exitcode, 0)
2616
+ context := read_file(bug_out + "/context.json")
2617
+ context_md := read_file(bug_out + "/context.md")
2618
+
2619
+ # 1: credential lines cannot bypass redaction by containing the word "redacted".
2620
+ assert_equal(contains(context, "supersecret"), 0)
2621
+ # 2: ordinary code mentioning a token is not corrupted.
2622
+ require_truthy(contains(context, "print(\\\"token\\\", value == 3)"), "benign token code preserved")
2623
+ require_truthy(contains(context, "\\\"estimated_tokens\\\": 5"), "non-secret token metadata preserved")
2624
+ # 3: separators in credential values do not corrupt the preserved key prefix.
2625
+ require_truthy(contains(context, "\\\"password\\\": [REDACTED:PASSWORD]"), "credential key prefix preserved")
2626
+ # 4: embedded Markdown fences receive a strictly longer outer fence.
2627
+ require_truthy(contains(context_md, "````text"), "longer Markdown fence emitted")
2628
+ # 5: extensionless files containing NUL bytes are not packed.
2629
+ assert_equal(contains(context, "BINARY_SECRET_MARKER"), 0)
2630
+
2631
+ # 6: root and trailing-slash selectors work as documented.
2632
+ assert_equal(contains(context, "EXCLUDED_DOC_MARKER"), 0)
2633
+ require_truthy(contains(context, "src/sample.txt"), "root include selects nested source")
2634
+ # 7: traversal never follows repository symlinks.
2635
+ assert_equal(contains(context, "SYMLINK_SECRET_MARKER"), 0)
2636
+ symlink_res := run_scent_in_repo(["pack", "--task", "symlink", "--include", "src/outside-link", "--dry-run", "--json"])
2637
+ assert_equal(symlink_res.exitcode, 2)
2638
+ require_truthy(contains(symlink_res.stdout, "cannot contain a symlink"), "explicit symlink selector rejected")
2639
+ # 8: an existing output directory is excluded from the next pack.
2640
+ assert_equal(contains(context, "STALE_OUTPUT_MARKER"), 0)
2641
+ # 9: pack is mandatory instead of silently accepting a flag-only invocation.
2642
+ missing_pack_res := run_scent_in_repo(["--task", "missing subcommand", "--dry-run", "--json"])
2643
+ assert_equal(missing_pack_res.exitcode, 2)
2644
+ require_truthy(contains(missing_pack_res.stdout, "expected subcommand: pack"), "missing subcommand rejected")
2645
+
2646
+ # 10: budget-excluded files have one manifest decision and no stale redaction audit.
2647
+ _ = run_cmd("cd " + shq(bug_repo) + " && printf '%0600d' 0 | tr '0' 'a' > focus/a.txt")
2648
+ _ = run_cmd("cd " + shq(bug_repo) + " && printf '%s\\n' 'password=budget-secret' > focus/b.txt")
2649
+ budget_res := run_scent_in_repo([
2650
+ "pack", "--task", "budget audit", "--out", budget_out,
2651
+ "--include", "focus", "--budget", "1555", "--max-files", "3",
2652
+ "--format", "json", "--json"
2653
+ ])
2654
+ assert_equal(budget_res.exitcode, 0)
2655
+ budget_manifest := read_file(budget_out + "/manifest.json")
2656
+ budget_redactions := read_file(budget_out + "/redactions.json")
2657
+ assert_equal(len(split(budget_manifest, "\"path\": \"focus/b.txt\"")) - 1, 1)
2658
+ assert_equal(contains(budget_redactions, "focus/b.txt"), 0)
2659
+
2660
+ # Explicit directories are rescanned even if the bounded baseline traversal saw them.
2661
+ _ = run_cmd("mkdir -p " + shq(bug_repo + "/a_bulk") + " " + shq(bug_repo + "/z_focus"))
2662
+ _ = run_cmd("printf '%s\\n' 'a_bulk/' > " + shq(bug_repo + "/.gitignore"))
2663
+ _ = run_cmd("i=1; while [ $i -le 2001 ]; do : > " + shq(bug_repo + "/a_bulk") + "/file-$i.txt; i=$((i + 1)); done")
2664
+ _ = run_cmd("printf '%s\\n' 'LATE_INCLUDE_MARKER' > " + shq(bug_repo + "/z_focus/wanted.txt"))
2665
+ late_res := run_scent_in_repo([
2666
+ "pack", "--task", "late include", "--out", late_out,
2667
+ "--include", "z_focus", "--exclude", "a_bulk", "--max-files", "1", "--format", "json", "--json"
2668
+ ])
2669
+ assert_equal(late_res.exitcode, 0)
2670
+ late_context := read_file(late_out + "/context.json")
2671
+ require_truthy(contains(late_context, "LATE_INCLUDE_MARKER"), "explicit include survives baseline traversal cap")
2672
+ }
2673
+
2674
+ test "redaction runs before file-size clipping" {
2675
+ audit_scope := true
2676
+ func audit_shq(value) {
2677
+ text := to_string(value)
2678
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2679
+ }
2680
+
2681
+ func audit_run(command) {
2682
+ result := execute_status(command, {"timeout_ms": 120000})
2683
+ if type(result) == "struct" {
2684
+ return result
2685
+ }
2686
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2687
+ }
2688
+
2689
+ func audit_run_scent(repo, argv) {
2690
+ cwd := os_getcwd()
2691
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2692
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2693
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2694
+ i := 0
2695
+ while i < len(argv) {
2696
+ command = command + " " + audit_shq(argv[i])
2697
+ i = i + 1
2698
+ }
2699
+ return audit_run(command)
2700
+ }
2701
+ root := "/tmp/scent_regression_redact_clip"
2702
+ repo := root + "/repo"
2703
+ out := root + "/out"
2704
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
2705
+ _ = write_file(repo + "/secret.txt", "sk-ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\n", true)
2706
+
2707
+ result := audit_run_scent(repo, [
2708
+ "pack", "--task", "secret clipping", "--include", "secret.txt",
2709
+ "--out", out, "--max-files", "1", "--max-file-bytes", "10",
2710
+ "--format", "both", "--json"
2711
+ ])
2712
+ assert_equal(result.exitcode, 0)
2713
+ context := read_file(out + "/context.json")
2714
+ assert_equal(contains(context, "sk-ABCDEFG"), 0)
2715
+ }
2716
+
2717
+ test "max-file-bytes is a UTF-8 byte limit" {
2718
+ audit_scope := true
2719
+ func audit_shq(value) {
2720
+ text := to_string(value)
2721
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2722
+ }
2723
+
2724
+ func audit_run(command) {
2725
+ result := execute_status(command, {"timeout_ms": 120000})
2726
+ if type(result) == "struct" {
2727
+ return result
2728
+ }
2729
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2730
+ }
2731
+
2732
+ func audit_run_scent(repo, argv) {
2733
+ cwd := os_getcwd()
2734
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2735
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2736
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2737
+ i := 0
2738
+ while i < len(argv) {
2739
+ command = command + " " + audit_shq(argv[i])
2740
+ i = i + 1
2741
+ }
2742
+ return audit_run(command)
2743
+ }
2744
+ root := "/tmp/scent_regression_utf8_bytes"
2745
+ repo := root + "/repo"
2746
+ out := root + "/out"
2747
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
2748
+ _ = write_file(repo + "/unicode.txt", "12345678éZ", true)
2749
+
2750
+ result := audit_run_scent(repo, [
2751
+ "pack", "--task", "unicode bytes", "--include", "unicode.txt",
2752
+ "--out", out, "--max-files", "1", "--max-file-bytes", "10",
2753
+ "--format", "json", "--json"
2754
+ ])
2755
+ assert_equal(result.exitcode, 0)
2756
+ context := parse_json(read_file(out + "/context.json"))
2757
+ assert_equal(context["selected_files"][0]["content"], "12345678é")
2758
+ assert_equal(context["selected_files"][0]["truncated"], true)
2759
+ }
2760
+
2761
+ test "output reuse removes stale optional context artifacts" {
2762
+ audit_scope := true
2763
+ func audit_shq(value) {
2764
+ text := to_string(value)
2765
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2766
+ }
2767
+
2768
+ func audit_run(command) {
2769
+ result := execute_status(command, {"timeout_ms": 120000})
2770
+ if type(result) == "struct" {
2771
+ return result
2772
+ }
2773
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2774
+ }
2775
+
2776
+ func audit_run_scent(repo, argv) {
2777
+ cwd := os_getcwd()
2778
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2779
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2780
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2781
+ i := 0
2782
+ while i < len(argv) {
2783
+ command = command + " " + audit_shq(argv[i])
2784
+ i = i + 1
2785
+ }
2786
+ return audit_run(command)
2787
+ }
2788
+ root := "/tmp/scent_regression_output_reuse"
2789
+ repo := root + "/repo"
2790
+ out := root + "/out"
2791
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
2792
+ _ = write_file(repo + "/README.md", "# fixture\n", true)
2793
+
2794
+ both_result := audit_run_scent(repo, [
2795
+ "pack", "--task", "reuse", "--include", "README.md", "--out", out,
2796
+ "--max-files", "1", "--format", "both", "--json"
2797
+ ])
2798
+ assert_equal(both_result.exitcode, 0)
2799
+ assert_equal(file_exists(out + "/context.md"), true)
2800
+ assert_equal(file_exists(out + "/context.json"), true)
2801
+
2802
+ md_result := audit_run_scent(repo, [
2803
+ "pack", "--task", "reuse", "--include", "README.md", "--out", out,
2804
+ "--max-files", "1", "--format", "md", "--json"
2805
+ ])
2806
+ assert_equal(md_result.exitcode, 0)
2807
+ assert_equal(file_exists(out + "/context.md"), true)
2808
+ assert_equal(file_exists(out + "/context.json"), false)
2809
+
2810
+ json_result := audit_run_scent(repo, [
2811
+ "pack", "--task", "reuse", "--include", "README.md", "--out", out,
2812
+ "--max-files", "1", "--format", "json", "--json"
2813
+ ])
2814
+ assert_equal(json_result.exitcode, 0)
2815
+ assert_equal(file_exists(out + "/context.md"), false)
2816
+ assert_equal(file_exists(out + "/context.json"), true)
2817
+ }
2818
+
2819
+ test "git metadata preserves unusual path names exactly" {
2820
+ audit_scope := true
2821
+ func audit_shq(value) {
2822
+ text := to_string(value)
2823
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2824
+ }
2825
+
2826
+ func audit_run(command) {
2827
+ result := execute_status(command, {"timeout_ms": 120000})
2828
+ if type(result) == "struct" {
2829
+ return result
2830
+ }
2831
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2832
+ }
2833
+
2834
+ func audit_run_scent(repo, argv) {
2835
+ cwd := os_getcwd()
2836
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2837
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2838
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2839
+ i := 0
2840
+ while i < len(argv) {
2841
+ command = command + " " + audit_shq(argv[i])
2842
+ i = i + 1
2843
+ }
2844
+ return audit_run(command)
2845
+ }
2846
+ root := "/tmp/scent_regression_git_names"
2847
+ repo := root + "/repo"
2848
+ out := root + "/out"
2849
+ newline_name := "odd\nname.txt"
2850
+ spaced_name := " leading and trailing .txt "
2851
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q && git config user.name Test && git config user.email test@example.com")
2852
+ _ = write_file(repo + "/" + newline_name, "seed", true)
2853
+ _ = write_file(repo + "/" + spaced_name, "seed", true)
2854
+ commit_result := audit_run("cd " + audit_shq(repo) + " && git add . && git commit -q -m seed")
2855
+ assert_equal(commit_result.exitcode, 0)
2856
+ _ = write_file(repo + "/" + newline_name, "changed", true)
2857
+ _ = write_file(repo + "/" + spaced_name, "changed", true)
2858
+
2859
+ result := audit_run_scent(repo, [
2860
+ "pack", "--task", "git paths", "--changed", "--out", out,
2861
+ "--max-files", "3", "--format", "json", "--json"
2862
+ ])
2863
+ assert_equal(result.exitcode, 0)
2864
+ context := parse_json(read_file(out + "/context.json"))
2865
+ assert_equal(contains(context["changed_files"], newline_name), true)
2866
+ assert_equal(contains(context["changed_files"], spaced_name), true)
2867
+ files := parse_json(read_file(out + "/files.json"))
2868
+ assert_equal(files[0]["path"], spaced_name)
2869
+ assert_equal(files[1]["path"], newline_name)
2870
+ assert_equal(files[0]["is_changed"], 1)
2871
+ assert_equal(files[1]["is_changed"], 1)
2872
+ }
2873
+
2874
+ test "recognized secrets in task text are redacted from artifacts" {
2875
+ audit_scope := true
2876
+ func audit_shq(value) {
2877
+ text := to_string(value)
2878
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2879
+ }
2880
+
2881
+ func audit_run(command) {
2882
+ result := execute_status(command, {"timeout_ms": 120000})
2883
+ if type(result) == "struct" {
2884
+ return result
2885
+ }
2886
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2887
+ }
2888
+
2889
+ func audit_run_scent(repo, argv) {
2890
+ cwd := os_getcwd()
2891
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2892
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2893
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2894
+ i := 0
2895
+ while i < len(argv) {
2896
+ command = command + " " + audit_shq(argv[i])
2897
+ i = i + 1
2898
+ }
2899
+ return audit_run(command)
2900
+ }
2901
+ root := "/tmp/scent_regression_task_secret"
2902
+ repo := root + "/repo"
2903
+ out := root + "/out"
2904
+ secret := "sk-ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
2905
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
2906
+ _ = write_file(repo + "/README.md", "# fixture\n", true)
2907
+
2908
+ result := audit_run_scent(repo, [
2909
+ "pack", "--task", "audit " + secret, "--include", "README.md", "--out", out,
2910
+ "--max-files", "1", "--format", "both", "--json"
2911
+ ])
2912
+ assert_equal(result.exitcode, 0)
2913
+ artifacts := ["context.md", "context.json", "manifest.json", "files.json", "redactions.json", "metadata.json"]
2914
+ i := 0
2915
+ while i < len(artifacts) {
2916
+ assert_equal(contains(read_file(out + "/" + artifacts[i]), secret), 0)
2917
+ i = i + 1
2918
+ }
2919
+ assert_equal(contains(read_file(out + "/context.json"), "[REDACTED:TOKEN]"), 1)
2920
+ }
2921
+
2922
+ test "files inventory agrees with selected context files" {
2923
+ audit_scope := true
2924
+ func audit_shq(value) {
2925
+ text := to_string(value)
2926
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2927
+ }
2928
+
2929
+ func audit_run(command) {
2930
+ result := execute_status(command, {"timeout_ms": 120000})
2931
+ if type(result) == "struct" {
2932
+ return result
2933
+ }
2934
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2935
+ }
2936
+
2937
+ func audit_run_scent(repo, argv) {
2938
+ cwd := os_getcwd()
2939
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2940
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2941
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2942
+ i := 0
2943
+ while i < len(argv) {
2944
+ command = command + " " + audit_shq(argv[i])
2945
+ i = i + 1
2946
+ }
2947
+ return audit_run(command)
2948
+ }
2949
+ root := "/tmp/scent_regression_files_selected"
2950
+ repo := root + "/repo"
2951
+ out := root + "/out"
2952
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
2953
+ _ = write_file(repo + "/app.txt", "selected content\n", true)
2954
+
2955
+ result := audit_run_scent(repo, [
2956
+ "pack", "--task", "app", "--include", "app.txt", "--out", out,
2957
+ "--max-files", "1", "--format", "json", "--json"
2958
+ ])
2959
+ assert_equal(result.exitcode, 0)
2960
+ context := parse_json(read_file(out + "/context.json"))
2961
+ files := parse_json(read_file(out + "/files.json"))
2962
+ assert_equal(len(context["selected_files"]), 1)
2963
+ assert_equal(files[0]["path"], context["selected_files"][0]["path"])
2964
+ assert_equal(files[0]["selected"], true)
2965
+ assert_equal(files[0]["selection_reason"], context["selected_files"][0]["reason"])
2966
+ }
2967
+
2968
+ test "selected unreadable files receive an accurate exclusion reason" {
2969
+ audit_scope := true
2970
+ func audit_shq(value) {
2971
+ text := to_string(value)
2972
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
2973
+ }
2974
+
2975
+ func audit_run(command) {
2976
+ result := execute_status(command, {"timeout_ms": 120000})
2977
+ if type(result) == "struct" {
2978
+ return result
2979
+ }
2980
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
2981
+ }
2982
+
2983
+ func audit_run_scent(repo, argv) {
2984
+ cwd := os_getcwd()
2985
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
2986
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
2987
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
2988
+ i := 0
2989
+ while i < len(argv) {
2990
+ command = command + " " + audit_shq(argv[i])
2991
+ i = i + 1
2992
+ }
2993
+ return audit_run(command)
2994
+ }
2995
+ root := "/tmp/scent_regression_unreadable"
2996
+ repo := root + "/repo"
2997
+ out := root + "/out"
2998
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
2999
+ _ = write_file(repo + "/locked.txt", "private", true)
3000
+ _ = audit_run("chmod 000 " + audit_shq(repo + "/locked.txt"))
3001
+
3002
+ result := audit_run_scent(repo, [
3003
+ "pack", "--task", "locked", "--include", "locked.txt", "--out", out,
3004
+ "--max-files", "1", "--format", "json", "--json"
3005
+ ])
3006
+ _ = audit_run("chmod 600 " + audit_shq(repo + "/locked.txt"))
3007
+ metadata := parse_json(read_file(out + "/metadata.json"))
3008
+ assert_equal(metadata["exit_code"], 1)
3009
+ manifest := parse_json(read_file(out + "/manifest.json"))
3010
+ assert_equal(manifest[0]["path"], "locked.txt")
3011
+ assert_equal(manifest[0]["decision"], "exclude")
3012
+ assert_equal(contains(manifest[0]["reason"], "could not be read"), 1)
3013
+ }
3014
+
3015
+ test "package scripts produce justified validation commands" {
3016
+ audit_scope := true
3017
+ func audit_shq(value) {
3018
+ text := to_string(value)
3019
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
3020
+ }
3021
+
3022
+ func audit_run(command) {
3023
+ result := execute_status(command, {"timeout_ms": 120000})
3024
+ if type(result) == "struct" {
3025
+ return result
3026
+ }
3027
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
3028
+ }
3029
+
3030
+ func audit_run_scent(repo, argv) {
3031
+ cwd := os_getcwd()
3032
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
3033
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
3034
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
3035
+ i := 0
3036
+ while i < len(argv) {
3037
+ command = command + " " + audit_shq(argv[i])
3038
+ i = i + 1
3039
+ }
3040
+ return audit_run(command)
3041
+ }
3042
+
3043
+ root := "/tmp/scent_regression_package_commands"
3044
+ repo := root + "/repo"
3045
+ out := root + "/out"
3046
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
3047
+ _ = write_file(repo + "/package.json", "{\"scripts\":{\"test\":\"run-tests\",\"lint\":\"run-lint\",\"build\":\"run-build\"}}", true)
3048
+
3049
+ result := audit_run_scent(repo, [
3050
+ "pack", "--task", "commands", "--include", "package.json", "--out", out,
3051
+ "--max-files", "1", "--format", "json", "--json"
3052
+ ])
3053
+ assert_equal(result.exitcode, 0)
3054
+ context := parse_json(read_file(out + "/context.json"))
3055
+ assert_equal(context["commands"], ["npm run build", "npm run lint", "npm test"])
3056
+ }
3057
+
3058
+ test "changed-file union deduplicates staged and unstaged paths" {
3059
+ audit_scope := true
3060
+ func audit_shq(value) {
3061
+ text := to_string(value)
3062
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
3063
+ }
3064
+
3065
+ func audit_run(command) {
3066
+ result := execute_status(command, {"timeout_ms": 120000})
3067
+ if type(result) == "struct" {
3068
+ return result
3069
+ }
3070
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
3071
+ }
3072
+
3073
+ func audit_run_scent(repo, argv) {
3074
+ cwd := os_getcwd()
3075
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
3076
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
3077
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
3078
+ i := 0
3079
+ while i < len(argv) {
3080
+ command = command + " " + audit_shq(argv[i])
3081
+ i = i + 1
3082
+ }
3083
+ return audit_run(command)
3084
+ }
3085
+
3086
+ root := "/tmp/scent_regression_changed_union"
3087
+ repo := root + "/repo"
3088
+ out := root + "/out"
3089
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q && git config user.name Test && git config user.email test@example.com")
3090
+ _ = write_file(repo + "/both.txt", "seed", true)
3091
+ _ = audit_run("cd " + audit_shq(repo) + " && git add both.txt && git commit -q -m seed")
3092
+ _ = write_file(repo + "/both.txt", "staged", true)
3093
+ _ = audit_run("cd " + audit_shq(repo) + " && git add both.txt")
3094
+ _ = write_file(repo + "/both.txt", "unstaged", true)
3095
+
3096
+ result := audit_run_scent(repo, [
3097
+ "pack", "--task", "git", "--changed", "--out", out,
3098
+ "--max-files", "2", "--format", "json", "--json"
3099
+ ])
3100
+ assert_equal(result.exitcode, 0)
3101
+ context := parse_json(read_file(out + "/context.json"))
3102
+ assert_equal(context["git"]["staged_files"], ["both.txt"])
3103
+ assert_equal(context["git"]["unstaged_files"], ["both.txt"])
3104
+ assert_equal(context["changed_files"], ["both.txt"])
3105
+ }
3106
+
3107
+ test "artifact writes replace symlinks instead of following them" {
3108
+ audit_scope := true
3109
+ func audit_shq(value) {
3110
+ text := to_string(value)
3111
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
3112
+ }
3113
+
3114
+ func audit_run(command) {
3115
+ result := execute_status(command, {"timeout_ms": 120000})
3116
+ if type(result) == "struct" {
3117
+ return result
3118
+ }
3119
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
3120
+ }
3121
+
3122
+ func audit_run_scent(repo, argv) {
3123
+ cwd := os_getcwd()
3124
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
3125
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
3126
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
3127
+ i := 0
3128
+ while i < len(argv) {
3129
+ command = command + " " + audit_shq(argv[i])
3130
+ i = i + 1
3131
+ }
3132
+ return audit_run(command)
3133
+ }
3134
+
3135
+ root := "/tmp/scent_regression_artifact_symlink"
3136
+ repo := root + "/repo"
3137
+ out := root + "/out"
3138
+ victim := root + "/victim.json"
3139
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " " + audit_shq(out) + " && cd " + audit_shq(repo) + " && git init -q")
3140
+ _ = write_file(repo + "/app.txt", "safe", true)
3141
+ _ = write_file(victim, "VICTIM_SENTINEL", true)
3142
+ link_result := audit_run("ln -s " + audit_shq(victim) + " " + audit_shq(out + "/context.json"))
3143
+ assert_equal(link_result.exitcode, 0)
3144
+
3145
+ result := audit_run_scent(repo, [
3146
+ "pack", "--task", "safe", "--include", "app.txt", "--out", out,
3147
+ "--max-files", "1", "--format", "json", "--json"
3148
+ ])
3149
+ assert_equal(result.exitcode, 0)
3150
+ assert_equal(read_file(victim), "VICTIM_SENTINEL")
3151
+ assert_equal(path_is_symlink(out + "/context.json"), false)
3152
+ context := parse_json(read_file(out + "/context.json"))
3153
+ assert_equal(context["selected_files"][0]["path"], "app.txt")
3154
+ }
3155
+
3156
+ test "fallback traversal uses alphabetical directory order" {
3157
+ audit_scope := true
3158
+ func audit_shq(value) {
3159
+ text := to_string(value)
3160
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
3161
+ }
3162
+
3163
+ func audit_run(command) {
3164
+ result := execute_status(command, {"timeout_ms": 120000})
3165
+ if type(result) == "struct" {
3166
+ return result
3167
+ }
3168
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
3169
+ }
3170
+
3171
+ func audit_run_scent(repo, argv) {
3172
+ cwd := os_getcwd()
3173
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
3174
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
3175
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
3176
+ i := 0
3177
+ while i < len(argv) {
3178
+ command = command + " " + audit_shq(argv[i])
3179
+ i = i + 1
3180
+ }
3181
+ return audit_run(command)
3182
+ }
3183
+
3184
+ root := "/tmp/scent_regression_fallback_sort"
3185
+ repo := root + "/repo"
3186
+ out := root + "/out"
3187
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
3188
+ _ = write_file(repo + "/z.txt", "z", true)
3189
+ _ = write_file(repo + "/a.txt", "a", true)
3190
+ _ = write_file(repo + "/m.txt", "m", true)
3191
+
3192
+ result := audit_run_scent(repo, [
3193
+ "pack", "--task", "unrelated", "--out", out,
3194
+ "--max-files", "3", "--format", "json", "--json"
3195
+ ])
3196
+ assert_equal(result.exitcode, 0)
3197
+ context := parse_json(read_file(out + "/context.json"))
3198
+ selected_paths := []
3199
+ i := 0
3200
+ while i < len(context["selected_files"]) {
3201
+ selected_paths = push(selected_paths, context["selected_files"][i]["path"])
3202
+ i = i + 1
3203
+ }
3204
+ assert_equal(selected_paths, ["a.txt", "m.txt", "z.txt"])
3205
+ }
3206
+
3207
+ test "malformed Cargo metadata does not suggest Cargo commands" {
3208
+ audit_scope := true
3209
+ func audit_shq(value) {
3210
+ text := to_string(value)
3211
+ return "'" + replace(text, "'", "'\"'\"'") + "'"
3212
+ }
3213
+
3214
+ func audit_run(command) {
3215
+ result := execute_status(command, {"timeout_ms": 120000})
3216
+ if type(result) == "struct" {
3217
+ return result
3218
+ }
3219
+ return {"success": false, "exitcode": 1, "stdout": "", "stderr": "invalid execute_status result"}
3220
+ }
3221
+
3222
+ func audit_run_scent(repo, argv) {
3223
+ cwd := os_getcwd()
3224
+ kujo_bin := env_or("KUJO_BIN", path_join(cwd, "../kujo/target/release/kujo"))
3225
+ scent_script := env_or("SCENT_SCRIPT", path_join(cwd, "scent.kujo"))
3226
+ command := "cd " + audit_shq(repo) + " && " + audit_shq(kujo_bin) + " run " + audit_shq(scent_script)
3227
+ i := 0
3228
+ while i < len(argv) {
3229
+ command = command + " " + audit_shq(argv[i])
3230
+ i = i + 1
3231
+ }
3232
+ return audit_run(command)
3233
+ }
3234
+
3235
+ root := "/tmp/scent_regression_malformed_cargo"
3236
+ repo := root + "/repo"
3237
+ out := root + "/out"
3238
+ _ = audit_run("rm -rf " + audit_shq(root) + " && mkdir -p " + audit_shq(repo) + " && cd " + audit_shq(repo) + " && git init -q")
3239
+ _ = write_file(repo + "/Cargo.toml", "not = [valid", true)
3240
+
3241
+ result := audit_run_scent(repo, [
3242
+ "pack", "--task", "commands", "--include", "Cargo.toml", "--out", out,
3243
+ "--max-files", "1", "--format", "json", "--json"
3244
+ ])
3245
+ assert_equal(result.exitcode, 0)
3246
+ context := parse_json(read_file(out + "/context.json"))
3247
+ assert_equal(context["commands"], [])
3248
+ }
3249
+
3250
+ main()