Repository navigation
Expand file tree
/
Copy pathselftest.sh
More file actions
executable file
·3897 lines (3745 loc) · 305 KB
/
Copy pathselftest.sh
File metadata and controls
executable file
·3897 lines (3745 loc) · 305 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# Godot Director self-test. It installs the framework into temporary copies of examples/market-day
# and tests the installer, the project check, the per-clone setup, the Claude Code hooks and the
# pre-commit hook. Run it after every change to the framework.
#
# bash selftest.sh [path to Godot 4.5+] (or set GODOT_BIN)
#
# Without Godot, only the parts that don't need it are tested. KEEP=1 keeps the temporary folder.
set -u
src="$(cd "$(dirname "$0")" && pwd)"
godot="${1:-${GODOT_BIN:-}}"
work="$(mktemp -d)"
if [ "${KEEP:-0}" = "1" ]; then echo "keeping $work"; else trap 'rm -rf "$work"' EXIT; fi
passed=0; failed=0
nl='
'
ok() { passed=$((passed + 1)); echo " ok $1"; }
bad() { failed=$((failed + 1)); echo " FAIL $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | tail -n 15 | sed 's/^/ /'; }
expect_status() { # <what> <expected> <actual> [output]
if [ "$3" -eq "$2" ]; then ok "$1"; else bad "$1 (exit $3, expected $2)" "${4:-}"; fi
}
expect_output() { # <what> <extended regex> <output>
if printf '%s' "$3" | grep -qE -- "$2"; then ok "$1"; else bad "$1 (no match for /$2/)" "$3"; fi
}
expect_no_output() { # <what> <extended regex> <output>
if printf '%s' "$3" | grep -qE -- "$2"; then bad "$1 (unexpected /$2/)" "$3"; else ok "$1"; fi
}
git_q() { git -c user.name=selftest -c user.email=selftest@localhost "$@"; }
hash_of() { git hash-object --stdin < "$1"; }
finish() {
echo "selftest: $passed passed, $failed failed"
[ "$failed" -eq 0 ]
exit $?
}
new_repo() { # new_repo <folder>: an empty git repository with one commit
mkdir -p "$1" && (cd "$1" && git init -q && git config core.autocrlf false && printf '# game\n' > README.md && git add -A && git_q commit -qm init)
}
# --- framework files ----------------------------------------------------------------------------
echo "framework"
plugin_version="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$src/.claude-plugin/plugin.json")"
[ "$plugin_version" = "$(tr -d '\r\n' < "$src/VERSION")" ] && ok "plugin.json version matches VERSION" \
|| bad "plugin.json version ($plugin_version) matches VERSION ($(cat "$src/VERSION"))"
installer="$src/skills/godot-director/SKILL.md"
if [ -f "$installer" ] && grep -q '^name: godot-director$' "$installer" && grep -q '^description: ' "$installer" && grep -q 'ONBOARDING.md' "$installer" && grep -qE 'git .*clone ' "$installer" && [ "$(find "$src" -name SKILL.md -not -path "*/.git/*" -mindepth 2 -maxdepth 4 | wc -l)" -eq 1 ]; then
ok "the installer skill is the only one the skills CLI finds, and fetches Godot Director on its own"
else
bad "skills/godot-director/SKILL.md (name, description, self-contained fetch, only skill found by npx skills add)"
fi
# Every install path gets the latest release, tagged v<version>. The files that carry the version
# must agree with VERSION: the skill's metadata.version and the release it clones, and the release
# the marketplace pins the plugin to.
version="$(tr -d '\r\n' < "$src/VERSION")"
skill_version=""; branch=""; dashes=0
while IFS= read -r line; do
[ "$line" = "---" ] && { dashes=$((dashes + 1)); continue; }
if [ "$dashes" -eq 1 ]; then case "$line" in " version: "*) skill_version="${line# version: }" ;; esac; fi
[[ $line =~ --branch[[:space:]]+v([0-9.]+) ]] && branch="${BASH_REMATCH[1]}"
done < "$installer"
[ "$skill_version" = "$version" ] && [ "$branch" = "$version" ] \
&& ok "the installer skill's metadata.version and the release it clones match VERSION" \
|| bad "skills/godot-director/SKILL.md: version '$skill_version', clones v$branch, VERSION is $version"
pinned_ref="$(sed -n 's/.*"ref"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$src/.claude-plugin/marketplace.json")"
[ "$pinned_ref" = "v$version" ] && ok "the marketplace pins the plugin to the release v<VERSION>" \
|| bad "the marketplace pins the plugin to '$pinned_ref', VERSION is $version"
cmp -s "$src/LICENSE" "$src/framework/.godot-director/LICENSE" && ok "the installed LICENSE copy matches LICENSE" \
|| bad "framework/.godot-director/LICENSE differs from LICENSE"
# The import skip is one block copied into check.sh, autoplay.sh and ui_capture.sh: the copies must agree.
import_block() { sed -n '/^# >>> import skip/,/^# <<< import skip/p' "$1"; }
block="$(import_block "$src/framework/tools/check.sh")"
if [ -n "$block" ] && [ "$block" = "$(import_block "$src/framework/tools/autoplay.sh")" ] \
&& [ "$block" = "$(import_block "$src/framework/tools/ui_capture.sh")" ]; then
ok "the import skip is the same block in check.sh, autoplay.sh and ui_capture.sh"
else
bad "the import skip block differs between check.sh, autoplay.sh and ui_capture.sh"
fi
missing=""
for skill in "$src"/framework/.claude/skills/*/SKILL.md; do
name="$(basename "$(dirname "$skill")")"
[ -f "$src/framework/.godot-director/procedures/$name.md" ] || missing="$missing $name"
grep -q "^name: $name$" "$skill" || missing="$missing $name(name)"
done
[ -z "$missing" ] && ok "every skill points to an existing procedure" || bad "skills without procedures:$missing"
missing=""
for doc in "$src"/framework/.godot-director/rules.md "$src"/framework/.godot-director/procedures/*.md "$src"/project/TASKS.md; do
for ref in $(grep -o '[a-z-]*\.md' "$doc" | sort -u); do
case "$ref" in
rules.md|tasks.md|research-domain.md) [ -f "$src/framework/.godot-director/$ref" ] || missing="$missing $(basename "$doc")→$ref" ;;
next-task.md|build.md|design.md|research.md|playtest.md|function-check.md|align.md|drift-reset.md|prune.md|review.md|refresh-model-sizing.md|ui-design.md|prototype.md)
[ -f "$src/framework/.godot-director/procedures/$ref" ] || missing="$missing $(basename "$doc")→$ref" ;;
esac
done
done
[ -z "$missing" ] && ok "rules and procedures only point to files that exist" || bad "dangling references:$missing"
missing=""
for procedure in "$src"/framework/.godot-director/procedures/*.md; do
name="$(basename "$procedure")"
grep -qF "$name" "$src/framework/.godot-director/rules.md" || missing="$missing $name"
done
[ -z "$missing" ] && ok "rules.md names every procedure" || bad "procedures rules.md doesn't name:$missing"
grep -q 'drift-reset\.md' "$src/framework/.godot-director/procedures/align.md" \
&& grep -q '^disable-model-invocation: true$' "$src/framework/.claude/skills/drift-reset/SKILL.md" \
&& ok "align recommends the drift reset, and only the human can start it" \
|| bad "align.md must recommend drift-reset.md, and the drift-reset skill must set disable-model-invocation"
missing=""
for agent in "$src"/framework/.claude/agents/*.md; do
procedure="$(grep -o '\.godot-director/procedures/[a-z-]*\.md' "$agent" | head -n 1)"
procedure="${procedure##*/}"
[ -n "$procedure" ] && [ -f "$src/framework/.godot-director/procedures/$procedure" ] || missing="$missing $(basename "$agent")"
done
[ -z "$missing" ] && ok "every subagent points to an existing procedure" || bad "subagents without procedures:$missing"
# The research assistant: every piece exists and is wired in, and the researcher can read and search but not change anything.
missing=""
for file in framework/.godot-director/procedures/research.md framework/.godot-director/research-domain.md \
framework/.claude/agents/researcher.md framework/.claude/skills/research/SKILL.md \
project/design/research/README.md examples/market-day/design/research/README.md; do
[ -f "$src/$file" ] || missing="$missing $file"
done
[ -z "$missing" ] && ok "the research assistant's files exist" || bad "research assistant files missing:$missing"
researcher="$src/framework/.claude/agents/researcher.md"
settings="$src/framework/.claude/settings.json"
if [ "$(sed -n 's/^tools: //p' "$researcher" | tr -d '\r')" = "Read, Grep, Glob, WebSearch, WebFetch" ] \
&& grep -q '^model: inherit' "$researcher" \
&& grep -Eq '^ *"WebSearch",?[[:space:]]*$' "$settings" && grep -Eq '^ *"WebFetch",?[[:space:]]*$' "$settings"; then
ok "the researcher has read, search and fetch tools only, and settings.json allows web search and fetch"
else
bad "researcher.md must have exactly the tools Read, Grep, Glob, WebSearch, WebFetch (no edit, write or shell), and settings.json must allow WebSearch and WebFetch"
fi
wired=""
proc="$src/framework/.godot-director"
grep -q 'research-domain\.md' "$proc/procedures/research.md" || wired="$wired research.md>research-domain.md"
grep -q 'researcher' "$proc/procedures/next-task.md" && grep -q 'Research items' "$proc/procedures/next-task.md" || wired="$wired next-task.md"
grep -q 'research\.md' "$proc/procedures/design.md" && grep -q 'design/research/' "$proc/procedures/design.md" || wired="$wired design.md"
grep -q 'R<n>' "$proc/tasks.md" && grep -q '^- \*\*Research\.\*\*' "$proc/tasks.md" || wired="$wired tasks.md"
grep -q 'research' "$proc/procedures/align.md" || wired="$wired align.md"
grep -q '/research' "$src/README.md" && grep -q '/research' "$src/ONBOARDING.md" || wired="$wired README.md/ONBOARDING.md"
grep -q '^\*\*Next IDs:\*\* T[0-9]* · B[0-9]* · R[0-9]*$' "$src/project/TASKS.md" || wired="$wired project/TASKS.md(Next IDs)"
grep -q '^\*\*Next IDs:\*\* T[0-9]* · B[0-9]* · R[0-9]*$' "$src/examples/market-day/TASKS.md" || wired="$wired market-day/TASKS.md(Next IDs)"
[ -z "$wired" ] && ok "the research assistant is wired into the procedures, the item format, the seeds and the docs" \
|| bad "the research assistant isn't wired into:$wired"
# Every subagent launch sets its model explicitly from the mapping, so none inherits the session's model:
# next-task.md says so for the builder, the reviewer, the researcher and an escalated retry, and
# rules.md states the rule the launches apply.
launch=""
nt="$proc/procedures/next-task.md"
grep -qF 'Every subagent launch sets its model explicitly' "$nt" || launch="$launch launch-rule"
grep -qF "A launch never lets a subagent inherit the session's model" "$nt" || launch="$launch no-inherit"
grep -qF 'Where the mapping has no entry for the size, or model sizing is off, the launch passes the session model explicitly' "$nt" \
|| launch="$launch session-fallback"
grep -qF 'the builder, the reviewer (step 6), the researcher (Research items) and an escalated retry' "$nt" || launch="$launch launches"
grep -qF 'Pass that model as `model: <id>`' "$nt" || launch="$launch builder-model"
grep -qF 'or in a subagent if your tool has them, with its model set as section 3 says' "$nt" || launch="$launch builder-other-tools"
grep -qF 'subagent, with its model set as section 3 says for the item' "$nt" || launch="$launch reviewer"
grep -qF 'in the foreground (`run_in_background: false`), with its model set as section 3 says' "$nt" || launch="$launch researcher"
grep -qF 'follow `research.md` in a fresh context, or a subagent if your tool has them, with its model set as section 3 says' "$nt" \
|| launch="$launch research-other-tools"
grep -qF 'on the next-size model, set on the launch as section 3 says' "$nt" || launch="$launch escalation"
grep -qF "rerun it once on the next size's model, set on the launch as section 3 says" "$nt" || launch="$launch research-escalation"
grep -qF 'Otherwise use the session model' "$nt" && launch="$launch old-fallback"
grep -qF 'turn on model sizing' "$nt" && launch="$launch old-conditional"
grep -qF "for the item's size, as for a build" "$nt" && launch="$launch old-research"
grep -qF 'Every subagent launch sets its model explicitly' "$proc/rules.md" \
&& grep -qF 'A size without an entry, or model sizing off, passes the session model, also explicitly' "$proc/rules.md" \
|| launch="$launch rules.md"
grep -qF 'A size without an entry uses the session model.' "$proc/rules.md" && launch="$launch rules-old"
[ -z "$launch" ] && ok "every subagent launch sets its model explicitly, and next-task.md and rules.md say so" \
|| bad "the subagent model wording is missing or has drifted:$launch"
# The human-written GDD: the template ships as a seed (and in the example), is the director's own
# structure, and onboarding and the README present it.
wired=""
for file in project/design/director-gdd.md examples/market-day/design/director-gdd.md; do
grep -q '^# Game Design Document (GDD): ' "$src/$file" && grep -q '^\* \*\*Elevator Pitch:\*\*' "$src/$file" \
&& grep -q '^## 7\. Milestone Roadmap' "$src/$file" || wired="$wired $file"
done
cmp -s "$src/project/design/director-gdd.md" "$src/examples/market-day/design/director-gdd.md" || wired="$wired example copy differs from the seed"
grep -q 'design/director-gdd\.md' "$src/ONBOARDING.md" && grep -q '(from your document)' "$src/ONBOARDING.md" \
&& grep -q '(inferred, please confirm)' "$src/ONBOARDING.md" && grep -q 'alternative to the chat interview or an addition to it' "$src/ONBOARDING.md" \
&& grep -q 'sets the stage for later development' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md"
grep -q 'design/director-gdd\.md' "$src/README.md" || wired="$wired README.md"
[ -z "$wired" ] && ok "the human-written GDD template ships as a seed and in the example, and onboarding and the README present it" \
|| bad "the human-written GDD isn't wired into:$wired"
# The README's Start here step comes before Install and links the template at the release tag, and
# the template's header carries the same link: both follow VERSION, like the skill's clone line.
gdd_link="https://raw.githubusercontent.com/Freakling/Godot-Director/v$version/project/design/director-gdd.md"
start_line="$(grep -n '^### Start here' "$src/README.md" | head -n 1 | cut -d: -f1)"
install_line="$(grep -n '^### Install' "$src/README.md" | head -n 1 | cut -d: -f1)"
if grep -qF "($gdd_link)" "$src/README.md" && grep -qF "$gdd_link" "$src/project/design/director-gdd.md" \
&& [ -n "$start_line" ] && [ -n "$install_line" ] && [ "$start_line" -lt "$install_line" ]; then
ok "the README's Start here step precedes Install and links the template at v<VERSION>, and the template's header carries that link"
else
bad "the README's Start here step (before Install, linking $gdd_link) and the template header's link"
fi
# Export: the procedure and skill exist and are wired in, the skipped-step path writes a question
# and a gated setup item, and nothing here needs export templates or a Godot run.
missing=""
for file in framework/.godot-director/procedures/export.md framework/.claude/skills/export/SKILL.md; do
[ -f "$src/$file" ] || missing="$missing $file"
done
[ -z "$missing" ] && ok "the export procedure and skill exist" || bad "export files missing:$missing"
wired=""
exportproc="$proc/procedures/export.md"
grep -q 'export\.md' "$proc/rules.md" || wired="$wired rules.md"
grep -q 'export\.md' "$src/framework/.claude/skills/export/SKILL.md" || wired="$wired skills/export"
grep -q '/export' "$src/README.md" || wired="$wired README.md"
grep -q 'export\.md' "$src/ONBOARDING.md" && grep -q 'Set up exports' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md"
grep -q -e '--export-release' -e 'godot_bin.local' "$exportproc" && grep -q 'export_presets\.cfg' "$exportproc" && grep -q 'export_templates' "$exportproc" || wired="$wired export.md(command, presets, templates)"
grep -q 'When the human skips it' "$exportproc" && grep -q 'Depends on: Q<n>' "$exportproc" && grep -q 'GDD › Open Questions' "$exportproc" || wired="$wired export.md(skip path)"
grep -q 'Q<n>' "$proc/tasks.md" && grep -q 'Open Questions' "$proc/procedures/next-task.md" || wired="$wired tasks.md/next-task.md(Q<n> in Depends on)"
grep -q '"/builds/"' "$src/install.sh" && grep -q '^/builds/$' "$src/examples/market-day/.gitignore" || wired="$wired /builds/ gitignore line"
[ -z "$wired" ] && ok "export is wired into the rules, the skill, the docs, the onboarding skip path and the gitignore" \
|| bad "export isn't wired into:$wired"
# Release: the procedure, skill, workflow template and script exist and are wired in, and
# tools/release.sh runs the configured commands, with no network, templates or real credentials.
missing=""
for file in framework/.godot-director/procedures/release.md framework/.claude/skills/release/SKILL.md \
framework/.godot-director/templates/release-workflow.yml framework/tools/release.sh; do
[ -f "$src/$file" ] || missing="$missing $file"
done
[ -z "$missing" ] && ok "the release procedure, skill, workflow template and script exist" || bad "release files missing:$missing"
wired=""
releaseproc="$proc/procedures/release.md"
grep -q 'release\.md' "$proc/rules.md" || wired="$wired rules.md"
grep -q 'release\.md' "$src/framework/.claude/skills/release/SKILL.md" || wired="$wired skills/release"
grep -q '/release' "$src/README.md" || wired="$wired README.md"
grep -q 'release\.md' "$src/ONBOARDING.md" && grep -q 'Set up release' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md"
grep -q 'release\.cfg' "$releaseproc" && grep -q 'tools/release\.sh' "$releaseproc" && grep -q 'when the human asks to promote' "$releaseproc" || wired="$wired release.md(config, script, production gate)"
grep -q 'When the human skips it' "$releaseproc" && grep -q 'Depends on: Q<n>' "$releaseproc" && grep -q 'GDD › Open Questions' "$releaseproc" || wired="$wired release.md(skip path)"
grep -q 'environment: production' "$proc/templates/release-workflow.yml" && grep -q 'tools/release\.sh uat' "$proc/templates/release-workflow.yml" || wired="$wired release-workflow.yml"
[ -z "$wired" ] && ok "release is wired into the rules, the skill, the docs, the onboarding skip path and the workflow template" \
|| bad "release isn't wired into:$wired"
rel="$work/release"
mkdir -p "$rel/builds/Web" "$rel/builds/Empty"
printf 'x\n' > "$rel/builds/Web/index.html"
printf '[Web]\nuat = echo uploading-uat $RELEASE_TEST_TARGET\nproduction = echo uploading-production\nsecrets = RELEASE_TEST_SECRET\n[Empty]\nuat = echo never\n' > "$rel/release.cfg"
out="$(cd "$rel" && bash "$src/framework/tools/release.sh" uat Web 2>&1)"; expect_status "release.sh stops when a named secret is unset" 2 $? "$out"
expect_output "release.sh names the missing secret" "secret RELEASE_TEST_SECRET is not set" "$out"
expect_no_output "release.sh runs nothing before that" "uploading-uat" "$out"
out="$(cd "$rel" && bash "$src/framework/tools/release.sh" uat Web --dry-run 2>&1)"; expect_status "release.sh --dry-run needs no secrets" 0 $? "$out"
expect_output "release.sh --dry-run lists the command and uploads nothing" "dry run, nothing uploaded" "$out"
out="$(cd "$rel" && RELEASE_TEST_SECRET=hunter2 RELEASE_TEST_TARGET=here bash "$src/framework/tools/release.sh" uat Web 2>&1)"; expect_status "release.sh runs the uat command" 0 $? "$out"
expect_output "release.sh ran it with the environment" "^uploading-uat here$" "$out"
expect_no_output "release.sh never prints a secret value" "hunter2" "$out"
out="$(cd "$rel" && RELEASE_TEST_SECRET=x bash "$src/framework/tools/release.sh" production Web 2>&1)"; expect_output "release.sh runs the production command only when asked for it" "^uploading-production$" "$out"
out="$(cd "$rel" && RELEASE_TEST_SECRET=x bash "$src/framework/tools/release.sh" uat 2>&1)"; expect_status "release.sh refuses a preset with no build" 2 $? "$out"
out="$(cd "$work" && bash "$src/framework/tools/release.sh" uat 2>&1)"; expect_status "release.sh needs release.cfg" 2 $? "$out"
# UI design: the procedure, skill, capture tools and seeds exist and are wired in.
missing=""
for file in framework/.godot-director/procedures/ui-design.md framework/.claude/skills/ui-design/SKILL.md \
framework/tools/ui_capture.sh framework/tools/ui_capture.gd framework/tools/ui_catalog.gd framework/tools/ui_lint.gd \
framework/tools/ui_overlay.gd framework/tools/ui_drawing.gd project/tools/ui_screens.gd project/tools/ui_capture.cfg \
project/design/ui/README.md examples/market-day/design/ui/README.md \
tests/ui-drawing/fixture.excalidraw tests/ui-drawing/fixture-scaled.excalidraw tests/ui-drawing/fixture.elements.json \
tests/ui-drawing/fixture.expected.json tests/ui-drawing/fixture.png tests/ui-drawing/starter.expected.excalidraw \
examples/market-day/tools/ui_screens.gd examples/market-day/tools/ui_capture.cfg; do
[ -f "$src/$file" ] || missing="$missing $file"
done
[ -z "$missing" ] && ok "the ui-design procedure, skill, capture tools and seeds exist" || bad "ui-design files missing:$missing"
wired=""
uiproc="$proc/procedures/ui-design.md"
grep -q 'ui-design\.md' "$proc/rules.md" || wired="$wired rules.md(procedure)"
grep -q 'tools/ui_screens\.gd' "$proc/rules.md" && grep -q 'godot_director_capture' "$proc/rules.md" || wired="$wired rules.md(UI)"
grep -q 'ui-design\.md' "$src/framework/.claude/skills/ui-design/SKILL.md" || wired="$wired skills/ui-design"
grep -q 'ui_capture\.sh --list' "$uiproc" && grep -q 'design/ui/captures/' "$uiproc" && grep -q 'Exit 3' "$uiproc" \
&& grep -q '\.elements\.json' "$uiproc" && grep -q '\.map\.png' "$uiproc" && grep -q '\.lint\.txt' "$uiproc" || wired="$wired ui-design.md(list, capture, outputs)"
for rule in squeezed-text stretched-control off-screen overlap unreachable no-default-focus hard-coded-position; do
grep -q "\"$rule\"" "$src/framework/tools/ui_lint.gd" && grep -q "\`$rule\`" "$uiproc" || wired="$wired lint($rule)"
done
grep -q 'process <screen>' "$src/framework/.claude/skills/ui-design/SKILL.md" || wired="$wired skills/ui-design(process)"
grep -q '^- `process <screen>`' "$uiproc" && grep -q 'tools/ui_drawing\.gd' "$uiproc" && grep -q '\.drawing\.json' "$uiproc" \
&& grep -q '\.starter\.excalidraw' "$uiproc" && grep -q 'Never read the raw' "$uiproc" && grep -q '^### The screen spec' "$uiproc" || wired="$wired ui-design.md(process, reader, spec)"
grep -q '`design/ui/<screen>\.md`' "$proc/rules.md" && grep -q '`director/ui/`' "$proc/rules.md" || wired="$wired rules.md(one place)"
grep -q '^- \*\*`UI:`\*\*' "$proc/tasks.md" || wired="$wired tasks.md(UI)"
grep -q 'ui-design\.md' "$proc/procedures/design.md" || wired="$wired design.md"
grep -q 'director/\.gdignore' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md(director/.gdignore)"
grep -q 'Excalidraw' "$src/project/design/ui/README.md" && cmp -s "$src/project/design/ui/README.md" "$src/examples/market-day/design/ui/README.md" || wired="$wired design/ui/README.md seed"
grep -q '"Bash(bash tools/ui_capture.sh \*)"' "$settings" || wired="$wired settings.json"
grep -q '"/design/ui/captures/"' "$src/install.sh" && grep -q '^/design/ui/captures/$' "$src/examples/market-day/.gitignore" || wired="$wired captures gitignore line"
grep -q '"/tools/ui_\*\.gd text eol=lf"' "$src/install.sh" || wired="$wired .gitattributes line"
grep -q '^extends "res://tools/ui_catalog\.gd"$' "$src/project/tools/ui_screens.gd" || wired="$wired ui_screens.gd seed"
[ -z "$wired" ] && ok "ui-design is wired into the rules, the skill, the settings, the lint and the installer's lines" \
|| bad "ui-design isn't wired into:$wired"
# UI design, part 3: verify, the workflow hooks, the lint in the check and overlays.
wired=""
[ -f "$src/framework/tools/ui_verify.gd" ] || wired="$wired tools/ui_verify.gd"
grep -q '^- `verify <screen>`' "$uiproc" && grep -q '^## verify' "$uiproc" && grep -q 'ui_capture\.sh <id> --verify' "$uiproc" \
&& grep -q 'tools/ui_overlay\.gd -- compare' "$uiproc" && grep -q 'compare@<W>x<H>\.png' "$uiproc" || wired="$wired ui-design.md(verify)"
grep -q "^## If you can't view images" "$uiproc" && grep -q 'ui_capture\.sh --missing' "$uiproc" || wired="$wired ui-design.md(text paths, --missing)"
grep -q '^## The lint in the check' "$uiproc" && grep -q 'check_in_check' "$uiproc" && grep -q 'enforce' "$uiproc" || wired="$wired ui-design.md(lint in the check)"
grep -q '"overlays": \["/root/Hud"\]' "$uiproc" && ! grep -q "isn't in it" "$uiproc" || wired="$wired ui-design.md(overlays)"
grep -q '"overlays"' "$src/framework/tools/ui_catalog.gd" && grep -q '_show_overlays' "$src/framework/tools/ui_capture.gd" || wired="$wired overlays in capture"
grep -q '^5\. \*\*Screens\*\*' "$proc/procedures/build.md" && grep -q '^ Screen: ' "$proc/procedures/build.md" && grep -q -- '--verify' "$proc/procedures/build.md" || wired="$wired build.md"
grep -q '^3\. \*\*Screens\*\*' "$proc/procedures/review.md" && grep -q 'compare@' "$proc/procedures/review.md" || wired="$wired review.md"
grep -q 'compare@' "$proc/procedures/function-check.md" && grep -q -- '--verify' "$proc/procedures/function-check.md" || wired="$wired function-check.md"
grep -q 'ui_capture\.sh --missing' "$proc/procedures/align.md" && grep -q 'sha1' "$proc/procedures/align.md" && grep -q '`UI:`' "$proc/procedures/align.md" || wired="$wired align.md"
grep -q 'tools/check.cfg tools/ui_capture.cfg' "$src/framework/tools/check.sh" && grep -q 'check_in_check' "$src/framework/tools/check.sh" || wired="$wired check.sh(fingerprint, lint)"
grep -q '^check_in_check=true$' "$src/project/tools/ui_capture.cfg" && grep -q '^enforce=\["\*"\]$' "$src/project/tools/ui_capture.cfg" \
&& grep -q '^tolerance_pct=4.0$' "$src/project/tools/ui_capture.cfg" && cmp -s "$src/project/tools/ui_capture.cfg" "$src/examples/market-day/tools/ui_capture.cfg" || wired="$wired ui_capture.cfg seed"
grep -q 'check_in_check=false' "$src/install.sh" || wired="$wired install.sh(upgrade writes the lint off)"
grep -q 'director designs (director/ui/), agent builds' "$src/project/AGENTS.md" && grep -q '`design/ui/`' "$src/project/AGENTS.md" || wired="$wired AGENTS.md template"
grep -q '`design/ui/`' "$src/examples/market-day/AGENTS.md" && grep -q '^/tools/ui_verify\.gd$' "$src/examples/market-day/.gitignore" || wired="$wired example AGENTS.md, .gitignore"
grep -q 'director designs (director/ui/), agent builds' "$src/ONBOARDING.md" && grep -q 'ui_capture\.sh --missing' "$src/ONBOARDING.md" \
&& grep -q 'check_in_check=false' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md"
grep -q '(`/ui-design`)' "$src/README.md" && grep -q 'tools/ui_screens\.gd' "$src/README.md" && grep -q 'director/ui/' "$src/README.md" || wired="$wired README.md"
grep -q 'verify <screen>' "$src/framework/.claude/skills/ui-design/SKILL.md" || wired="$wired skills/ui-design(verify)"
[ -z "$wired" ] && ok "ui-design verify, the build, review, function check and align hooks, the lint in the check and overlays are wired in" \
|| bad "ui-design verify and the hooks aren't wired into:$wired"
# The play log: the framework's base, the game's seed (hooks marked PLACEHOLDER), the example's wiring,
# the five rules, onboarding's autoload line and the installer's lines.
wired=""
for file in framework/tools/play_log_base.gd project/tools/play_log.gd examples/market-day/tools/play_log.gd \
examples/market-day/tests/test_play_log.gd tests/play-log/selftest_play_log.gd; do
[ -f "$src/$file" ] || wired="$wired $file"
done
grep -q '^extends "res://tools/play_log_base\.gd"$' "$src/project/tools/play_log.gd" \
&& [ "$(grep -c '## PLACEHOLDER' "$src/project/tools/play_log.gd")" -ge 3 ] || wired="$wired play_log.gd seed"
for hook in '_session_extras() -> Dictionary' '_build_snapshot() -> Dictionary' 'progress_block_reason() -> String' \
'log_snapshot() -> void' 'start_capture() -> void' 'begin_run() -> String' 'continue_run(id: String) -> void'; do
grep -qF "func $hook" "$src/framework/tools/play_log_base.gd" || wired="$wired base($hook)"
done
grep -q 'OS\.add_logger' "$src/framework/tools/play_log_base.gd" && grep -q 'extends Logger' "$src/framework/tools/play_log_base.gd" || wired="$wired base(Logger)"
grep -q '^PlayLog="\*res://tools/play_log\.gd"$' "$src/examples/market-day/project.godot" \
&& grep -q '^settings/gdscript/always_track_call_stacks=true$' "$src/examples/market-day/project.godot" || wired="$wired example project.godot"
grep -q '^ settings/gdscript/always_track_call_stacks=true$' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md(call stacks)"
grep -q '^func _install_id() -> String:$' "$src/project/tools/play_log.gd" || wired="$wired play_log.gd seed(_install_id)"
grep -q 'PlayLog="\*res://tools/play_log\.gd"' "$src/ONBOARDING.md" && grep -q 'Migrate PlayLog to extend tools/play_log_base\.gd' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md"
grep -q '"/tools/play_log_base\.gd text eol=lf"' "$src/install.sh" && grep -q '^/tools/play_log_base\.gd$' "$src/examples/market-day/.gitignore" \
&& grep -q '^/tools/play_log_base\.gd text eol=lf$' "$src/examples/market-day/.gitattributes" || wired="$wired installer lines"
for rule in 'Everything is logged' 'Every action is drivable' 'Agent-visible state' 'Seeded randomness' 'Privacy'; do
grep -q "^- \*\*$rule\.\*\*" "$proc/rules.md" || wired="$wired rules.md($rule)"
done
grep -q "use the editor's Remote scene tree instead" "$proc/rules.md" && wired="$wired rules.md(the Remote tree sentence is still there)"
grep -q 'start_capture()' "$proc/rules.md" && grep -q 'dev menu' "$proc/rules.md" || wired="$wired rules.md(start_capture, dev menu)"
grep -q 'tools/play_log_base\.gd' "$src/README.md" && grep -q 'tools/play_log\.gd' "$src/README.md" || wired="$wired README.md"
[ -z "$wired" ] && ok "the play log is wired in: base, seed, example, the five rules, onboarding and the installer's lines" \
|| bad "the play log isn't wired into:$wired"
# Autoplay: the driver, the adapter and scenario bases, the game's seeds (stubs marked PLACEHOLDER,
# a smoke scenario), the example's wiring, the capture's shared element map writer and the
# installer's lines.
wired=""
for file in framework/tools/autoplay.sh framework/tools/autoplay.gd framework/tools/autoplay_adapter_base.gd \
framework/tools/autoplay_catalog.gd project/tools/autoplay_adapter.gd project/tools/autoplay_scenarios.gd \
project/tools/autoplay_invariants.gd examples/market-day/tools/autoplay_adapter.gd \
examples/market-day/tools/autoplay_scenarios.gd examples/market-day/tools/autoplay_invariants.gd; do
[ -f "$src/$file" ] || wired="$wired $file"
done
grep -q '^extends "res://tools/autoplay_adapter_base\.gd"$' "$src/project/tools/autoplay_adapter.gd" \
&& [ "$(grep -c '^## PLACEHOLDER' "$src/project/tools/autoplay_adapter.gd")" -ge 7 ] || wired="$wired autoplay_adapter.gd seed"
grep -q '^extends "res://tools/autoplay_catalog\.gd"$' "$src/project/tools/autoplay_scenarios.gd" \
&& grep -q '"id": "smoke", .*"kind": "smoke"' "$src/project/tools/autoplay_scenarios.gd" || wired="$wired autoplay_scenarios.gd seed(smoke)"
grep -q '^func check(_state: Dictionary, _adapter: Object) -> PackedStringArray:$' "$src/project/tools/autoplay_invariants.gd" \
&& grep -q 'PLACEHOLDER' "$src/project/tools/autoplay_invariants.gd" || wired="$wired autoplay_invariants.gd seed"
for hook in 'start_run(_run_seed: int) -> void' 'load_save(_path: String) -> bool' 'list_actions() -> Array\[Dictionary\]' \
'perform(_action: Dictionary) -> bool' 'state() -> Dictionary' 'progress_block_reason() -> String' 'is_run_over() -> bool'; do
grep -q "^func $hook:$" "$src/framework/tools/autoplay_adapter_base.gd" || wired="$wired adapter base($hook)"
done
grep -q 'a smoke scenario is required' "$src/framework/tools/autoplay_catalog.gd" || wired="$wired autoplay_catalog.gd(smoke required)"
grep -q 'Engine.set_meta(AUTOPLAY_META, true)' "$src/framework/tools/autoplay.gd" && grep -q 'Input.parse_input_event' "$src/framework/tools/autoplay.gd" \
&& grep -q 'lint.element_map(' "$src/framework/tools/autoplay.gd" && grep -q 'lint.element_map(' "$src/framework/tools/ui_capture.gd" \
&& ! grep -q '^func _element_map' "$src/framework/tools/ui_capture.gd" || wired="$wired autoplay.gd(meta, real input, the capture's element map writer)"
grep -q '"/playtesting/autoplay/"' "$src/install.sh" && grep -q '^/playtesting/autoplay/$' "$src/examples/market-day/.gitignore" \
&& grep -q '"/tools/autoplay\.gd text eol=lf"' "$src/install.sh" && grep -q '^/tools/autoplay_adapter_base\.gd text eol=lf$' "$src/examples/market-day/.gitattributes" \
&& grep -q '^/tools/autoplay_catalog\.gd$' "$src/examples/market-day/.gitignore" || wired="$wired installer lines"
[ -z "$wired" ] && ok "autoplay is wired in: driver, bases, seeds with a smoke scenario, the example, the shared element map and the installer's lines" \
|| bad "autoplay isn't wired into:$wired"
# Log review: the tool and its wrapper, the signatures.tsv seed (a header only, the same in the
# example), the normalisation fixture and the installer's lines.
wired=""
for file in framework/tools/log_review.sh framework/tools/log_review.gd project/playtesting/signatures.tsv \
examples/market-day/playtesting/signatures.tsv tests/log-review/signature-cases.tsv; do
[ -f "$src/$file" ] || wired="$wired $file"
done
[ "$(cat "$src/project/playtesting/signatures.tsv")" = "$(printf 'id\tkind\ttext\tfirst_seen_build')" ] \
&& cmp -s "$src/project/playtesting/signatures.tsv" "$src/examples/market-day/playtesting/signatures.tsv" || wired="$wired signatures.tsv seed(header, example copy)"
grep -q 'GODOT_BIN' "$src/framework/tools/log_review.sh" && grep -q '\[ "\$minor" -lt 5 \]' "$src/framework/tools/log_review.sh" \
&& grep -q 'tools/log_review.gd' "$src/framework/tools/log_review.sh" || wired="$wired log_review.sh(Godot lookup, 4.5 minimum)"
grep -q '"/tools/log_review\.gd text eol=lf"' "$src/install.sh" && grep -q '"/playtesting/signatures\.tsv text eol=lf"' "$src/install.sh" \
&& grep -q '^/tools/log_review\.gd text eol=lf$' "$src/examples/market-day/.gitattributes" \
&& grep -q '^/playtesting/signatures\.tsv text eol=lf$' "$src/examples/market-day/.gitattributes" \
&& grep -q '^/tools/log_review\.sh$' "$src/examples/market-day/.gitignore" && grep -q '^/tools/log_review\.gd$' "$src/examples/market-day/.gitignore" || wired="$wired installer lines"
[ -z "$wired" ] && ok "log review is wired in: tool, wrapper, the signatures.tsv seed with its header, the fixture and the installer's lines" \
|| bad "log review isn't wired into:$wired"
# The dev menu: the base, the game's seed (PLACEHOLDER marked), the example's wiring, the rules,
# onboarding's question and recorded answer, the upgrade step and the installer's lines.
wired=""
for file in framework/tools/dev_menu_base.gd project/tools/dev_menu.gd examples/market-day/tools/dev_menu.gd \
examples/market-day/tests/test_dev_menu.gd tests/dev-menu/selftest_dev_menu.gd; do
[ -f "$src/$file" ] || wired="$wired $file"
done
grep -q '^extends "res://tools/dev_menu_base\.gd"$' "$src/project/tools/dev_menu.gd" \
&& [ "$(grep -c 'PLACEHOLDER' "$src/project/tools/dev_menu.gd")" -ge 3 ] \
&& grep -q '^func setting_enabled() -> bool:$' "$src/project/tools/dev_menu.gd" || wired="$wired dev_menu.gd seed"
for hook in 'register_system(system_name: String, node: Object' 'is_available() -> bool' 'actions_of(system_name: String)' \
'inspect(system_name: String) -> Dictionary' 'inspect_text(system_name: String) -> String' 'log_text() -> String' \
'run_action(system_name: String, method: String'; do
grep -qF "func $hook" "$src/framework/tools/dev_menu_base.gd" || wired="$wired base($hook)"
done
grep -q '"debug/dev_menu"' "$src/framework/tools/dev_menu_base.gd" && grep -q '"--dev-menu"' "$src/framework/tools/dev_menu_base.gd" \
&& grep -q 'KEY_F12' "$src/framework/tools/dev_menu_base.gd" || wired="$wired base(setting, flag, F12)"
grep -q '^DevMenu="\*res://tools/dev_menu\.gd"$' "$src/examples/market-day/project.godot" \
&& grep -q '^- Dev menu: in game (F12, debug builds only)$' "$src/examples/market-day/AGENTS.md" || wired="$wired example project.godot, AGENTS.md"
grep -q 'register_system(name, node)' "$proc/rules.md" && grep -q 'tools/dev_menu_base\.gd' "$proc/rules.md" && grep -q 'debug_inspect()' "$proc/rules.md" || wired="$wired rules.md(dev menu base, registration)"
grep -q '^- Dev menu: \*\*ask the human' "$src/ONBOARDING.md" && grep -q '^ Dev menu: in game (F12, debug builds only)$' "$src/ONBOARDING.md" \
&& grep -q '`debug/dev_menu=true`' "$src/ONBOARDING.md" && grep -q -- '`-- --dev-menu`' "$src/ONBOARDING.md" \
&& grep -q 'AGENTS.md › Project rules' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md(dev menu step)"
grep -q '^7\. \*\*The dev menu\.\*\*' "$src/CHANGELOG.md" || wired="$wired CHANGELOG.md(upgrade step)"
grep -q '^- Dev menu: ' "$src/project/AGENTS.md" && grep -q 'tools/dev_menu_base\.gd' "$src/README.md" || wired="$wired project/AGENTS.md, README.md"
grep -q '"/tools/dev_menu_base\.gd text eol=lf"' "$src/install.sh" && grep -q '^/tools/dev_menu_base\.gd$' "$src/examples/market-day/.gitignore" \
&& grep -q '^/tools/dev_menu_base\.gd text eol=lf$' "$src/examples/market-day/.gitattributes" || wired="$wired installer lines"
[ -z "$wired" ] && ok "the dev menu is wired in: base, seed, example, the rule, onboarding's question and answer format, the upgrade step and the installer's lines" \
|| bad "the dev menu isn't wired into:$wired"
# /autoplay and the workflow around it: the procedure and the skill that point at each other, the
# request list's "check the logs", the (autoplay) tag, every procedure that uses autoplay, the
# settings' allowed commands, the check's [autoplay] section and the seeds, README and onboarding.
wired=""
autoproc="$proc/procedures/autoplay.md"
autoskill="$src/framework/.claude/skills/autoplay/SKILL.md"
[ -f "$autoproc" ] && [ -f "$autoskill" ] || wired="$wired autoplay.md/skill(missing)"
grep -q '^name: autoplay$' "$autoskill" && grep -q 'procedures/autoplay\.md' "$autoskill" && grep -q -- '--player' "$autoskill" \
&& grep -q -- '--replay' "$autoskill" && grep -q 'check the logs' "$autoskill" || wired="$wired skills/autoplay"
grep -q '/autoplay' "$autoproc" && grep -q 'tools/autoplay\.sh' "$autoproc" && grep -q 'tools/log_review\.sh' "$autoproc" \
&& grep -q -- '--replay <run folder>' "$autoproc" && grep -q -- '--player' "$autoproc" || wired="$wired autoplay.md(commands)"
grep -q '^## Who owns what' "$autoproc" && grep -q 'Framework-owned' "$autoproc" && grep -q 'Project-owned' "$autoproc" \
&& grep -q "can't view images" "$autoproc" && grep -q '\.elements\.json' "$autoproc" && grep -q 'review\.md' "$autoproc" \
&& grep -q '^## Report' "$autoproc" && grep -q 'File and confirm' "$autoproc" && grep -q 'Never edit `director/`' "$autoproc" || wired="$wired autoplay.md(owners, element maps, report, bugs)"
grep -q '^- \*\*autoplay\.md:\*\*' "$proc/rules.md" && grep -q '"Check the logs"' "$proc/rules.md" && grep -q '/autoplay --player' "$proc/rules.md" || wired="$wired rules.md(request list)"
for file in rules.md tasks.md procedures/next-task.md procedures/build.md procedures/review.md; do
grep -q '(autoplay)' "$proc/$file" || wired="$wired $file((autoplay))"
done
for file in next-task build review function-check playtest align; do
grep -qi 'autoplay' "$proc/procedures/$file.md" || wired="$wired $file.md(autoplay)"
done
grep -q 'build\.md` step 6' "$proc/procedures/next-task.md" && grep -q '^6\. \*\*Autoplay\*\*' "$proc/procedures/build.md" && grep -q '^ Autoplay: ' "$proc/procedures/build.md" \
&& grep -q 'plus `smoke`\|and `smoke`' "$proc/procedures/build.md" && grep -q "delta line" "$proc/procedures/build.md" || wired="$wired build.md(autoplay step, report line)"
grep -q 'Logging and drivability' "$proc/procedures/review.md" && grep -q 'list_actions()' "$proc/procedures/review.md" && grep -q 'opened by `smoke`' "$proc/procedures/review.md" || wired="$wired review.md(logging, drivability)"
grep -q 'Pre-verified by autoplay' "$proc/procedures/function-check.md" && grep -q 'run folder' "$proc/procedures/function-check.md" || wired="$wired function-check.md(pre-verified)"
grep -q 'Machine review' "$proc/procedures/playtest.md" && grep -q 'tools/autoplay\.sh smoke' "$proc/procedures/playtest.md" || wired="$wired playtest.md(machine review)"
grep -q 'Scenario gaps' "$proc/procedures/align.md" && grep -q 'Logging gaps' "$proc/procedures/align.md" && grep -q 'never fails\|none of it fails' "$proc/procedures/align.md" || wired="$wired align.md(gaps)"
for command in 'tools/autoplay.sh \*' 'tools/autoplay.sh' 'tools/log_review.sh \*' 'tools/log_review.sh'; do
grep -q "\"Bash(bash $command)\"" "$settings" || wired="$wired settings.json($command)"
done
grep -q '^\[autoplay\]$' "$src/project/tools/check.cfg" && grep -q '^check_in_check=true$' "$src/project/tools/check.cfg" && grep -q '^enforce=\["smoke"\]$' "$src/project/tools/check.cfg" \
&& grep -q '^\[autoplay\]$' "$src/examples/market-day/tools/check.cfg" || wired="$wired check.cfg seeds"
grep -q "sed .s/^/GDIR-FAIL: /." "$src/framework/tools/check.sh" && grep -q "GDIR_LOCK_HELD=1" "$src/framework/tools/check.sh" \
&& grep -q "GDIR_LOCK_HELD" "$src/framework/tools/autoplay.sh" && grep -q "tools/check.cfg > \[autoplay\]" "$src/framework/tools/check.sh" || wired="$wired check.sh([autoplay])"
grep -q 'old_version' "$src/install.sh" && grep -q "tools/check.cfg" "$src/install.sh" && grep -q 'autoplay in the check off' "$src/install.sh" || wired="$wired install.sh(check.cfg off on an upgrade)"
grep -q '^### Auto-playtesting' "$src/README.md" && grep -q '(`/autoplay`)' "$src/README.md" && grep -q 'framework-owned\|Godot Director owns' "$src/README.md" \
&& grep -q "can't view images" "$src/README.md" && grep -q 'check_in_check' "$src/README.md" || wired="$wired README.md(auto-playtesting)"
grep -q '^8\. \*\*The workflow\.\*\*' "$src/CHANGELOG.md" && grep -q '^- \*\*New: `/autoplay`' "$src/CHANGELOG.md" && grep -q 'Wire autoplay' "$src/CHANGELOG.md" || wired="$wired CHANGELOG.md"
grep -q 'autoplay_adapter\.gd' "$src/ONBOARDING.md" && grep -q '^8\. \*\*Autoplay\.\*\*' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md(autoplay)"
[ -z "$wired" ] && ok "/autoplay is wired in: procedure and skill, the request list, the (autoplay) tag, the workflow procedures, the settings, the check's [autoplay] section and the docs" \
|| bad "/autoplay isn't wired into:$wired"
# The minimum Godot is 4.5 (the engine Logger) everywhere it's stated and checked. A Godot Director
# version such as v4.3.0 (a tag or a `version:` line) isn't a Godot version.
stale="$(cd "$src" && grep -nE '4\.3([^0-9.]|$)|-lt [0-4] \]; \}|-ge [0-4] \]; \}' README.md ONBOARDING.md CLAUDE.md \
skills/godot-director/SKILL.md selftest.sh framework/tools/check.sh framework/tools/setup-clone.sh framework/tools/ui_capture.sh framework/tools/autoplay.sh \
framework/tools/log_review.sh framework/.godot-director/procedures/ui-design.md examples/market-day/AGENTS.md)"
if [ -z "$stale" ] && grep -q 'Godot 4\.5 or newer' "$src/README.md" && grep -q "compatibility: 'Godot 4\.5 or newer" "$installer" \
&& grep -q 'Godot 4\.5+' "$src/ONBOARDING.md" && grep -q '"4\.5"' "$src/examples/market-day/project.godot" \
&& grep -q '\[ "\$minor" -lt 5 \]' "$src/framework/tools/check.sh" && grep -q '\[ "\$minor" -lt 5 \]' "$src/framework/tools/ui_capture.sh" \
&& grep -q '\[ "\$minor" -lt 5 \]' "$src/framework/tools/autoplay.sh" \
&& grep -q '\[ "\$minor" -ge 5 \]' "$src/framework/tools/setup-clone.sh"; then
ok "the minimum Godot is 4.5 everywhere it's stated and checked"
else
bad "the minimum Godot is 4.5 everywhere it's stated and checked" "$stale"
fi
# The old name (Claude4Godot, 2.x and earlier) is left only in history and in the 2.x migration.
allowed=" CHANGELOG.md install.sh selftest.sh ONBOARDING.md framework/tools/check.gd framework/tools/check.sh framework/tools/setup-clone.sh "
leftovers=""
for file in $(cd "$src" && grep -rIli --exclude-dir=.git --exclude-dir=.godot -e claude4godot -e c4g . | sed 's|^\./||'); do
case "$allowed" in *" $file "*) ;; *) leftovers="$leftovers $file" ;; esac
done
[ -z "$leftovers" ] && ok "the old name is left only in the changelog and the 2.x migration" \
|| bad "the old name (claude4godot, c4g) is still in:$leftovers"
out="$(cd "$src" && git grep -P '[\x{2013}\x{2014}]' -- '*.md' '*.sh' '*.gd' '*.cfg' '*.json' '*.txt' 2>/dev/null)"
[ -z "$out" ] && ok "no en or em dashes in tracked text files" \
|| bad "en or em dashes found" "$out"
mismatch=""
for size in XS S M L XL; do
sid="$(grep "| \`$size\` |" "$src/framework/.godot-director/procedures/refresh-model-sizing.md" \
| head -n1 | awk -F'|' '{gsub(/[` ]/, "", $3); print $3}')"
oid="$(grep "^ - $size:" "$src/ONBOARDING.md" | awk '{print $NF}')"
[ "$sid" = "$oid" ] || mismatch="$mismatch $size"
done
[ -z "$mismatch" ] && ok "ONBOARDING.md model IDs match refresh-model-sizing.md" \
|| bad "ONBOARDING.md model IDs don't match refresh-model-sizing.md (sizes:$mismatch)"
# User stories, which onboarding turns on or off: the template (framework-owned, so a game with them
# off never gets the file) and the example's own file have the header line, the sections and rows in
# the story format; the item format carries Stories:; the check.cfg seed has the cadence; the rules,
# the procedures, the settings and the docs name them, each step only with them on; and Market Day
# uses them, with a Stories: line on every agent item. tools/stories.sh --due is tested under
# "user stories", with them on, off and unanswered.
stories_tpl="$src/framework/.godot-director/templates/user-stories.md"
stories_ex="$src/examples/market-day/design/user-stories.md"
ex_tasks="$src/examples/market-day/TASKS.md"
wired=""
[ -f "$stories_tpl" ] && [ -f "$src/framework/tools/stories.sh" ] && [ -f "$stories_ex" ] || wired="$wired files"
[ -e "$src/project/design/user-stories.md" ] && wired="$wired project/design/user-stories.md(a seed would reach a game with them off)"
grep -q '^<!-- Godot Director user stories template · framework-owned' "$stories_tpl" \
&& [ "$(sed -n 2p "$stories_tpl")" = '# User stories: {{PROJECT_NAME}}' ] || wired="$wired template(header, title)"
check_stories() { # check_stories <file> <label>: the header line, the sections, the rows, the IDs and the roles
local file="$1" label="$2" rows next ids role
grep -qE '^\*\*Next ID:\*\* US-[0-9]+ · \*\*Last validated:\*\* (never|[0-9]{4}-[0-9]{2}-[0-9]{2}( at `[0-9a-f]{7,40}`)?)$' "$file" \
|| wired="$wired $label(Next ID and Last validated)"
grep -q '^## The value in one paragraph per role$' "$file" && grep -q '^## Roles$' "$file" || wired="$wired $label(sections)"
rows="$(grep -E '^\| US-' "$file")"
[ -n "$rows" ] && grep -q '^| ID | Story | Confirmed |$' "$file" || wired="$wired $label(tables)"
printf '%s\n' "$rows" | grep -vqE '^\| US-[0-9]+ \| As an? [^|]+ I [^|]+, so [^|]+\. \| ([0-9]{4}-[0-9]{2}-[0-9]{2}|unconfirmed) \|$' \
&& wired="$wired $label(a row isn't | US-<n> | As a <role> I ..., so ... | <date> or unconfirmed |)"
next="$(sed -n 's/^\*\*Next ID:\*\* US-\([0-9]*\) .*/\1/p' "$file")"
ids="$(printf '%s\n' "$rows" | sed -n 's/^| US-\([0-9]*\) .*/\1/p')"
[ -n "$next" ] && [ -z "$(printf '%s\n' "$ids" | awk -v next_id="$next" '$1 + 0 >= next_id + 0 || seen[$1]++')" ] \
|| wired="$wired $label(IDs unique and below Next ID)"
for role in $(printf '%s\n' "$rows" | sed -n 's/^| US-[0-9]* | As an\{0,1\} \([A-Za-z]*\) I .*/\1/p' | tr 'A-Z' 'a-z' | awk '!seen[$0]++'); do
sed -n '/^## Roles$/,/^## [^R]/p' "$file" | tr 'A-Z' 'a-z' | grep -q "^- \*\*$role:\*\*" || wired="$wired $label(role $role not in Roles)"
done
}
check_stories "$stories_tpl" template
check_stories "$stories_ex" example
grep -q '^- User stories: on$' "$src/examples/market-day/AGENTS.md" || wired="$wired example AGENTS.md(rule)"
gdd_line='Why each feature is built, and for whom: `design/user-stories.md`.'
grep -qxF "$gdd_line" "$src/examples/market-day/design/gdd.md" && grep -qF "$gdd_line" "$proc/procedures/design.md" || wired="$wired GDD link(example, design.md)"
no_field="$(awk '/^### / { if (item != "" && !has) printf " %s", item; item = ($0 ~ / · agent( |$)/ && $2 !~ /^R/) ? $2 : ""; has = 0; next }
/^- Stories: / { has = 1 } END { if (item != "" && !has) printf " %s", item }' "$ex_tasks")"
[ -z "$no_field" ] || wired="$wired example TASKS.md(no Stories: on$no_field)"
for id in $(sed -n 's/^- Stories: //p' "$ex_tasks" | tr ',' '\n' | tr -d ' ' | grep '^US-'); do
grep -q "^| $id |" "$stories_ex" || wired="$wired example TASKS.md($id isn't a story)"
done
for id in $(grep -E '^\| US-' "$stories_ex" | sed -n 's/^| \(US-[0-9]*\) .*/\1/p'); do
grep -qE "^- Stories: (.*[ ,])?$id(,|$)" "$ex_tasks" || wired="$wired example($id served by no item)"
done
grep -q '^/tools/stories\.sh$' "$src/examples/market-day/.gitignore" || wired="$wired example .gitignore"
grep -q '^- Stories: US-[0-9]' "$proc/tasks.md" && grep -q '^- \*\*`Stories:`\.\*\* Only in a game that uses user stories' "$proc/tasks.md" || wired="$wired tasks.md"
grep -q '^- User stories: on$' "$src/project/AGENTS.md" || wired="$wired project/AGENTS.md(example rule)"
sed -n '/^\[stories\]$/,/^\[/p' "$src/project/tools/check.cfg" | grep -q '^every=10$' || wired="$wired project/tools/check.cfg"
grep -q '^| Whom the game is for, and why each feature is built (a game that uses user stories) | `design/user-stories\.md`' "$proc/rules.md" \
&& grep -q '^- \*\*Why it.s built\.\*\* User stories are the game.s choice' "$proc/rules.md" \
&& grep -q '`design/user-stories\.md` when a story was drafted for it' "$proc/rules.md" || wired="$wired rules.md"
for heading in '## User stories' '### Turning them on' '### Validate the stories' '### Still right? in a playtest or function check'; do
grep -qxF "$heading" "$proc/procedures/design.md" || wired="$wired design.md($heading)"
done
grep -qF 'skip every step about stories in the procedures, and this section except Turning them on, which runs when the human asks for it' "$proc/procedures/design.md" \
&& grep -q 'bash tools/stories\.sh --due' "$proc/procedures/design.md" && grep -q 'its `Stories:`' "$proc/procedures/design.md" \
&& grep -qF '.godot-director/templates/user-stories.md' "$proc/procedures/design.md" \
&& grep -qF -- '- [ ] Still right - [ ] Needs a change' "$proc/procedures/design.md" || wired="$wired design.md"
grep -q 'bash tools/stories\.sh --due' "$proc/procedures/next-task.md" && grep -q '\[stories\] every' "$proc/procedures/next-task.md" \
&& grep -q 'not due: user stories are off' "$proc/procedures/next-task.md" \
&& grep -q 'With user stories on, a feature item (not a research item) with no story gets one before it is built' "$proc/procedures/next-task.md" \
&& grep -q 'with `design/user-stories\.md` when the claim drafted a story' "$proc/procedures/next-task.md" || wired="$wired next-task.md"
grep -qF -- '- [ ] Still right - [ ] Needs a change' "$proc/procedures/function-check.md" && grep -q '^4\. \*\*Stories\*\* (with user stories on)' "$proc/procedures/function-check.md" \
|| wired="$wired function-check.md"
grep -q '`## User stories` section' "$proc/procedures/playtest.md" && grep -q '^6\. \*\*Stories\*\* (with user stories on)' "$proc/procedures/playtest.md" || wired="$wired playtest.md"
grep -q 'stories no item serves' "$proc/procedures/align.md" && grep -q 'feature items with no story' "$proc/procedures/align.md" \
&& grep -q 'stories unconfirmed past the cadence' "$proc/procedures/align.md" && grep -q 'bugs with no story' "$proc/procedures/align.md" \
&& grep -q 'skip it unless AGENTS.md › Project rules says `User stories: on`' "$proc/procedures/align.md" || wired="$wired align.md"
for file in design align; do
grep -q 'were written from the code on <date>, before the game kept user stories\.' "$proc/procedures/$file.md" || wired="$wired $file.md(written from the code)"
done
grep -q 'design/user-stories\.md' "$proc/procedures/build.md" || wired="$wired build.md"
for file in design next-task function-check playtest ui-design drift-reset; do
grep -q 'with user stories on\|With user stories on' "$proc/procedures/$file.md" || wired="$wired $file.md(only with them on)"
done
grep -qF '"Bash(bash tools/stories.sh --due)",' "$settings" && ! grep -qF '"Bash(bash tools/stories.sh*)"' "$settings" \
&& ! grep -qF '"Bash(bash tools/stories.sh *)"' "$settings" || wired="$wired settings.json"
grep -q '^### User stories (fresh start, existing game and migrate)$' "$src/ONBOARDING.md" \
&& grep -qF '`- User stories: on` or `- User stories: off`' "$src/ONBOARDING.md" \
&& grep -qF '`.godot-director/procedures/design.md` › User stories › Turning them on' "$src/ONBOARDING.md" \
&& grep -q '^ 5\. whether the game uses user stories' "$src/ONBOARDING.md" && grep -q '`\[stories\] every`' "$src/ONBOARDING.md" \
|| wired="$wired ONBOARDING.md"
grep -q 'design/user-stories\.md' "$src/README.md" && grep -q 'tools/stories\.sh' "$src/README.md" \
&& grep -q 'In a game that uses user stories' "$src/framework/playtesting/README.md" && grep -q 'Still right' "$src/framework/playtesting/README.md" \
|| wired="$wired README.md, playtesting/README.md"
grep -q '^- \*\*User stories, if the game wants them\.\*\*' "$src/CHANGELOG.md" || wired="$wired CHANGELOG.md"
[ -z "$wired" ] && ok "user stories: the template and the example's stories have their header, sections and rows, and Stories:, the cadence, the rules, the procedures (each only with them on), the settings and the docs name them" \
|| bad "the user stories aren't wired into:$wired"
# Prototypes: the procedure, the skill and tools/prototype.sh exist and are wired in. rules.md routes the
# procedure and says a prototype session is outside the loop and reaches main only through the harvest, the
# item format has Prototype:, the design procedure harvests one and files nothing without the human, the
# settings allow the script, the hooks know proto/ branches, and the docs name it. The script and the hooks
# run under "prototype".
_pp="$proc/procedures/prototype.md"
wired=""
for file in framework/.godot-director/procedures/prototype.md framework/.claude/skills/prototype/SKILL.md framework/tools/prototype.sh framework/.githooks/pre-push; do
[ -f "$src/$file" ] || wired="$wired $file"
done
grep -q 'procedures/prototype\.md' "$src/framework/.claude/skills/prototype/SKILL.md" || wired="$wired SKILL.md"
grep -q '^## Start$' "$_pp" && grep -q '^## In a prototype session$' "$_pp" && grep -q '^## Harvest$' "$_pp" \
&& grep -qF 'bash tools/prototype.sh start <name>' "$_pp" && grep -qF 'bash tools/prototype.sh harvest <name>' "$_pp" \
&& grep -qF 'bash tools/prototype.sh remove <name>' "$_pp" && grep -qF 'bash tools/prototype.sh list' "$_pp" \
&& grep -qF 'git diff main...proto/<name>' "$_pp" && grep -qF 'Never push a `proto/` branch' "$_pp" \
&& grep -qF 'git branch -D proto/<name>' "$_pp" && grep -qF 'Never force it' "$_pp" \
&& grep -q 'Nothing is filed without the human.s answer' "$_pp" && grep -qF 'Commits need no approval' "$_pp" \
&& grep -qF '`ui-design.md` › capture' "$_pp" && grep -q 'Run `design\.md` on them' "$_pp" \
&& grep -qF 'git pull --ff-only' "$_pp" && grep -qF 'start <name> --as-is' "$_pp" \
&& grep -q "ask the human to stop the prototype's game and close its session" "$_pp" && grep -qF 'import cache' "$_pp" \
|| wired="$wired prototype.md"
grep -qF '**The game can run.**' "$_pp" && grep -qF 'bash tools/autoplay.sh smoke' "$_pp" && grep -qF 'bash tools/log_review.sh --player' "$_pp" \
&& grep -qF 'bash tools/check.sh --only tests/<file>.gd' "$_pp" && grep -qF '**A playtest:**' "$_pp" \
&& grep -qF 'the GDD changes, decisions, user stories (with user stories on) and items' "$_pp" \
|| wired="$wired prototype.md(the game can run, the harvest)"
grep -qF "gitleaks is optional in every other commit" "$_pp" && grep -qF "it won't start, or commit, without one" "$_pp" \
&& grep -qF 'tools/godot_bin.local' "$_pp" || wired="$wired prototype.md(gitleaks, Godot path)"
grep -qF 'Unlike Record step 4, a drafted story the human doesn' "$proc/procedures/design.md" || wired="$wired design.md(unanswered story)"
grep -qF '**prototype.md:**' "$proc/rules.md" && grep -q '^- \*\*Prototypes\.\*\*' "$proc/rules.md" \
&& grep -q 'That session is outside the loop' "$proc/rules.md" && grep -q 'reaches main only through the harvest' "$proc/rules.md" \
&& grep -qF 'The game can run there.' "$proc/rules.md" || wired="$wired rules.md"
grep -q '^- \*\*`Prototype:`\*\* (optional)\. `proto/<name>`' "$proc/tasks.md" || wired="$wired tasks.md"
grep -q '^## Harvest a prototype$' "$proc/procedures/design.md" && grep -q 'or harvest a prototype (Harvest a prototype, below)' "$proc/procedures/design.md" \
&& grep -qF -- '- Prototype: proto/<name>' "$proc/procedures/design.md" && grep -qF '(prototype: proto/<name>)' "$proc/procedures/design.md" \
&& grep -qF 'came from the prototype proto/<name>' "$proc/procedures/design.md" \
&& grep -q 'Nothing is filed without the human.s answer' "$proc/procedures/design.md" || wired="$wired design.md"
grep -qF '"Bash(bash tools/prototype.sh *)",' "$settings" || wired="$wired settings.json"
grep -qF 'bash tools/prototype.sh here' "$src/framework/.githooks/pre-commit" && grep -qF 'bash tools/prototype.sh here' "$src/framework/.claude/hooks/stop-check.sh" \
&& grep -qF 'refs/heads/proto/' "$src/framework/.githooks/pre-push" && grep -qF 'pre-commit pre-push' "$src/framework/tools/setup-clone.sh" || wired="$wired hooks"
grep -qF '[ -f PROTOTYPE.md ] && [ -f tools/prototype.sh ] && bash tools/prototype.sh here' "$src/framework/.githooks/pre-commit" \
&& grep -qF '[ -f PROTOTYPE.md ] && [ -f tools/prototype.sh ] && bash tools/prototype.sh here' "$src/framework/.claude/hooks/stop-check.sh" \
&& grep -qF '[ -n "$gd" ] && [ -n "$gc" ] && [ "$gd" != "$gc" ]' "$src/framework/tools/prototype.sh" \
&& grep -qF 'gitleaks:allow' "$src/framework/.githooks/pre-commit" && grep -qF 'if [[ -f $root/PROTOTYPE.md ]]; then' "$src/framework/.claude/hooks/guard-protected.sh" \
&& grep -qF '**The gates are the human' "$_pp" && grep -qF 'a staged `gitleaks:allow` comment' "$_pp" \
&& grep -qF 'refs="$(cat)"; printf' "$src/framework/tools/prototype.sh" && grep -qF 'refs="$(cat)"; printf' "$src/framework/tools/setup-clone.sh" \
&& grep -qF 'refs="$(cat)"; printf' "$_pp" || wired="$wired hooks(prototype checks, gates, chaining)"
grep -qF 'A session in a checkout that `bash tools/prototype.sh here` calls a prototype (a linked worktree on a `proto/` branch with `PROTOTYPE.md` at its root) is a prototype session: read `prototype.md` › In a prototype session before any work.' "$proc/rules.md" \
&& grep -qF 'git diff main...proto/<name> -- <path>' "$proc/procedures/build.md" || wired="$wired rules.md(prototype session) build.md"
grep -qF '`/prototype {name}`' "$src/README.md" && grep -qF 'tools/prototype.sh' "$src/README.md" && grep -qF '· prototype ·' "$src/README.md" || wired="$wired README.md"
grep -qF '`/prototype`' "$src/ONBOARDING.md" && grep -qF '.githooks/pre-commit .githooks/pre-push' "$src/ONBOARDING.md" || wired="$wired ONBOARDING.md"
grep -q '^/tools/prototype\.sh$' "$src/examples/market-day/.gitignore" || wired="$wired example .gitignore"
grep -q '^- \*\*A prototype tries a change before it.s built against the design\.\*\*' "$src/CHANGELOG.md" || wired="$wired CHANGELOG.md"
[ -z "$wired" ] && ok "prototypes: the procedure, the skill and tools/prototype.sh exist, and the rules, the item format, the design procedure, the settings, the hooks and the docs name them" \
|| bad "prototypes aren't wired into:$wired"
# --- installer ----------------------------------------------------------------------------------
echo "installer"
game="$work/market-day"
cp -R "$src/examples/market-day" "$game"
rm -rf "$game/.godot" "$game/.claude" "$game/.godot-director" "$game/.githooks" "$game/tools/godot_bin.local"
(cd "$game" && git init -q && git config core.autocrlf false && git add -A && git_q commit -qm "example") || exit 1
cd "$game" || exit 1
out="$(bash "$src/install.sh" . 2>&1)"; expect_status "installs" 0 $? "$out"
if [ -f .godot-director/manifest ] && [ -f .godot-director/procedures/next-task.md ] && [ -f tools/check.gd ] \
&& [ -f tools/setup-clone.sh ] && [ -f .claude/skills/next-task/SKILL.md ] && [ -f .claude/hooks/guard-git.sh ]; then
ok "copies the core and the Claude adapter, and writes the manifest"
else
bad "copies the core and the Claude adapter, and writes the manifest" "$out"
fi
if [ -f .godot-director/procedures/research.md ] && [ -f .godot-director/research-domain.md ] && [ -f .claude/agents/researcher.md ] \
&& [ -f .claude/skills/research/SKILL.md ] && [ -f design/research/README.md ]; then
ok "installs the research assistant: procedure, domain file, researcher, skill and note folder"
else
bad "installs the research assistant: procedure, domain file, researcher, skill and note folder" "$out"
fi
if [ -f .godot-director/procedures/export.md ] && [ -f .claude/skills/export/SKILL.md ] && grep -q '^/builds/$' .gitignore; then
ok "installs /export: procedure, skill and the builds/ gitignore line"
else
bad "installs /export: procedure, skill and the builds/ gitignore line" "$out"
fi
if [ -f .godot-director/procedures/release.md ] && [ -f .godot-director/templates/release-workflow.yml ] && [ -f .claude/skills/release/SKILL.md ] && [ -x tools/release.sh ]; then
ok "installs /release: procedure, workflow template, skill and tools/release.sh"
else
bad "installs /release: procedure, workflow template, skill and tools/release.sh" "$out"
fi
if [ -x tools/ui_capture.sh ] && [ -f tools/ui_capture.gd ] && [ -f tools/ui_lint.gd ] && [ -f tools/ui_drawing.gd ] && [ -f design/ui/README.md ] && [ -f .claude/skills/ui-design/SKILL.md ] \
&& grep -q '^/design/ui/captures/$' .gitignore && grep -q '"shop"' tools/ui_screens.gd; then
ok "installs /ui-design: procedure, skill, capture tools and the captures gitignore line, keeping the game's catalog"
else
bad "installs /ui-design: procedure, skill, capture tools and the captures gitignore line, keeping the game's catalog" "$out"
fi
if [ -f tools/play_log_base.gd ] && cut -f2 .godot-director/manifest | grep -q '^tools/play_log_base\.gd$' && grep -q 'RunState' tools/play_log.gd; then
ok "installs the play log base, keeping the game's own PlayLog"
else
bad "installs the play log base, keeping the game's own PlayLog" "$out"
fi
if [ -x tools/autoplay.sh ] && [ -f tools/autoplay.gd ] && [ -f tools/autoplay_adapter_base.gd ] && [ -f tools/autoplay_catalog.gd ] \
&& cut -f2 .godot-director/manifest | grep -q '^tools/autoplay_adapter_base\.gd$' && grep -q 'RunState' tools/autoplay_adapter.gd \
&& grep -q '"trade_week"' tools/autoplay_scenarios.gd && grep -q '^/playtesting/autoplay/$' .gitignore; then
ok "installs autoplay and the run folders' gitignore line, keeping the game's adapter and scenarios"
else
bad "installs autoplay and the run folders' gitignore line, keeping the game's adapter and scenarios" "$out"
fi
if [ -x tools/log_review.sh ] && [ -f tools/log_review.gd ] && cut -f2 .godot-director/manifest | grep -q '^tools/log_review\.gd$' \
&& [ "$(cat playtesting/signatures.tsv)" = "$(printf 'id\tkind\ttext\tfirst_seen_build')" ] \
&& grep -q '^/tools/log_review\.gd text eol=lf$' .gitattributes && grep -q '^/playtesting/signatures\.tsv text eol=lf$' .gitattributes; then
ok "installs log review, the signatures.tsv seed and the .gitattributes lines for both"
else
bad "installs log review, the signatures.tsv seed and the .gitattributes lines for both" "$out"
fi
if [ -f tools/dev_menu_base.gd ] && cut -f2 .godot-director/manifest | grep -q '^tools/dev_menu_base\.gd$' && grep -q 'register_system("Run", RunState' tools/dev_menu.gd \
&& grep -q '^/tools/dev_menu_base\.gd text eol=lf$' .gitattributes; then
ok "installs the dev menu base and its .gitattributes line, keeping the game's own DevMenu"
else
bad "installs the dev menu base and its .gitattributes line, keeping the game's own DevMenu" "$out"
fi
if [ -x tools/stories.sh ] && [ -f .godot-director/templates/user-stories.md ] && cut -f2 .godot-director/manifest | grep -q '^tools/stories\.sh$' \
&& cut -f2 .godot-director/manifest | grep -q '^\.godot-director/templates/user-stories\.md$' && cmp -s design/user-stories.md "$stories_ex"; then
ok "installs tools/stories.sh and the user stories template, keeping the game's own stories"
else
bad "installs tools/stories.sh and the user stories template, keeping the game's own stories" "$out"
fi
if [ -f .godot-director/procedures/prototype.md ] && [ -f .claude/skills/prototype/SKILL.md ] && [ -x tools/prototype.sh ] && [ -x .githooks/pre-push ] \
&& cut -f2 .godot-director/manifest | grep -q '^tools/prototype\.sh$' && cut -f2 .godot-director/manifest | grep -q '^\.githooks/pre-push$'; then
ok "installs prototypes: the procedure, the skill, tools/prototype.sh and the pre-push hook"
else
bad "installs prototypes: the procedure, the skill, tools/prototype.sh and the pre-push hook" "$out"
fi
grep -q "Market Day" AGENTS.md && ok "keeps the project's own files" || bad "keeps the project's own files"
[ -z "$(git status --porcelain)" ] && ok "adds no duplicate .gitignore/.gitattributes lines" || bad "adds no duplicate lines" "$(git status --porcelain)"
out="$(bash "$src/install.sh" . 2>&1)"; expect_output "a second run changes nothing" "0 new, 0 updated" "$out"
echo "# my own note" >> .godot-director/procedures/prune.md
out="$(bash "$src/install.sh" . 2>&1)"
if grep -q "my own note" .godot-director/procedures/prune.md && [ ! -f .godot-director/procedures/prune.md.gdir-new ]; then
ok "keeps a local edit when this version doesn't change the file"
else
bad "keeps a local edit when this version doesn't change the file" "$out"
fi
# Pretend the installed version had a different prune.md: now both sides changed.
awk -F'\t' -v OFS='\t' '$2 == ".godot-director/procedures/prune.md" { $1 = "0000000000000000000000000000000000000000" } { print }' \
.godot-director/manifest > "$work/manifest" && cp "$work/manifest" .godot-director/manifest
out="$(bash "$src/install.sh" . 2>&1)"
if [ -f .godot-director/procedures/prune.md.gdir-new ] && grep -q "my own note" .godot-director/procedures/prune.md; then
ok "writes .gdir-new when both sides changed a file"
else
bad "writes .gdir-new when both sides changed a file" "$out"
fi
mv .godot-director/procedures/prune.md.gdir-new .godot-director/procedures/prune.md
# An unchanged file from an older version is replaced.
printf 'old version\n' > .godot-director/procedures/align.md
awk -F'\t' -v OFS='\t' -v h="$(hash_of .godot-director/procedures/align.md)" '$2 == ".godot-director/procedures/align.md" { $1 = h } { print }' \
.godot-director/manifest > "$work/manifest" && cp "$work/manifest" .godot-director/manifest
out="$(bash "$src/install.sh" . 2>&1)"
cmp -s .godot-director/procedures/align.md "$src/framework/.godot-director/procedures/align.md" \
&& ok "replaces a file unchanged since the last install" || bad "replaces a file unchanged since the last install" "$out"
printf 'extends RefCounted\n' > tools/retired.gd; printf 'uid://x\n' > tools/retired.gd.uid
printf '%s\ttools/retired.gd\n' "$(hash_of tools/retired.gd)" >> .godot-director/manifest
out="$(bash "$src/install.sh" . 2>&1)"
[ ! -f tools/retired.gd ] && [ ! -f tools/retired.gd.uid ] && ok "removes a dropped file and its .uid" || bad "removes a dropped file and its .uid" "$out"
# Upgrading a 2.x install, made by turning a fresh install back into the 2.x layout.
legacy="$work/legacy"; new_repo "$legacy"
bash "$src/install.sh" "$legacy" >/dev/null 2>&1
(
cd "$legacy" || exit 1
git_q add -A && git_q commit -qm "install" && git mv .godot-director .claude4godot || exit 1
printf 'old rules\n' > .claude4godot/rules.md # unchanged since the 2.x install
echo "# my own note" >> .claude4godot/procedures/prune.md # edited; 3.0.0 doesn't change it
echo "# my own note" >> .claude4godot/procedures/align.md # edited; 2.x had another version
awk -F'\t' -v OFS='\t' -v rules="$(hash_of .claude4godot/rules.md)" '
/^# godot-director / { print "# claude4godot 2.0.2"; next }
$2 == ".godot-director/rules.md" { $1 = rules }
$2 == ".godot-director/procedures/align.md" { $1 = "0000000000000000000000000000000000000000" }
{ sub(/^\.godot-director\//, ".claude4godot/", $2); print }' .claude4godot/manifest > "$work/manifest"
cp "$work/manifest" .claude4godot/manifest
for file in .gitignore .gitattributes; do
sed -e 's|^# Godot Director$|# Claude4Godot|' -e 's|\.gdir-new|.c4g-new|' -e 's|/\.godot-director/|/.claude4godot/|' "$file" > "$work/lines"
cp "$work/lines" "$file"
done
printf 'x\n' > tools/check.sh.c4g-new
printf '#!/usr/bin/env bash\n# Claude4Godot: runs the project'"'"'s committed pre-commit hook. Installed by tools/setup-clone.sh.\nexit 0\n' \
> "$(git rev-parse --git-path hooks)/pre-commit"
mkdir -p .godot/claude4godot && : > .godot/claude4godot/check.log
git_q add -A && git_q commit -qm "2.x layout"
)
out="$(bash "$src/install.sh" "$legacy" 2>&1)"; expect_status "upgrades a 2.x install" 0 $? "$out"
expect_output "reports the upgrade from 2.x and the migration" "upgraded from 2\.0\.2 to .*migrated from the 2\.x names" "$(printf '%s' "$out" | tr '\n' ' ')"
cd "$legacy" || exit 1
if [ ! -e .claude4godot ] && [ -f .godot-director/manifest ] && grep -qE "^# godot-director $(tr -d '\r\n' < "$src/VERSION")( [+] unreleased [(].*[)])?$" .godot-director/manifest \
&& ! grep -q claude4godot .godot-director/manifest && [ -n "$(git diff --cached --name-status -M | grep '^R.*\.claude4godot/LICENSE')" ]; then
ok "moves .claude4godot/ to .godot-director/ with git mv, and rewrites the manifest"
else
bad "moves .claude4godot/ to .godot-director/ with git mv, and rewrites the manifest" "$out$nl$(git status --short)"
fi
if grep -q "my own note" .godot-director/procedures/prune.md && [ ! -f .godot-director/procedures/prune.md.gdir-new ] \
&& grep -q "my own note" .godot-director/procedures/align.md && [ -f .godot-director/procedures/align.md.gdir-new ] \
&& cmp -s .godot-director/rules.md "$src/framework/.godot-director/rules.md"; then
ok "carries the 2.x manifest over: local edits kept or flagged, unchanged files replaced"
else
bad "carries the 2.x manifest over: local edits kept or flagged, unchanged files replaced" "$out"
fi
[ -f tools/check.sh.gdir-new ] && [ ! -f tools/check.sh.c4g-new ] && ok "renames a leftover .c4g-new to .gdir-new" \
|| bad "renames a leftover .c4g-new to .gdir-new" "$out"
if ! grep -qi "claude4godot\|c4g" .gitignore .gitattributes && [ "$(grep -c '^\*\.gdir-new$' .gitignore)" -eq 1 ] \
&& [ "$(grep -c '^/\.godot-director/\*\* text eol=lf$' .gitattributes)" -eq 1 ]; then
ok "replaces the 2.x .gitignore and .gitattributes lines without duplicates"
else
bad "replaces the 2.x .gitignore and .gitattributes lines without duplicates" "$(cat .gitignore .gitattributes)"
fi
grep -q "^# Godot Director: runs" "$(git rev-parse --git-path hooks)/pre-commit" && [ ! -e .godot/claude4godot ] \
&& ok "updates the clone's 2.x pre-commit shim and drops the old runtime folder" \
|| bad "updates the clone's 2.x pre-commit shim and drops the old runtime folder" "$out"
cd "$game" || exit 1
# Upgrading from the last release, v3.10.0 (taken from git history; skipped where the tag isn't there):
# the new files and seeds arrive, the game keeps its own AGENTS.md and check.cfg, the layout lint in
# the check ends up off, and align lists the screens the catalog doesn't cover without failing.
if git -C "$src" rev-parse -q --verify 'v3.10.0^{commit}' >/dev/null 2>&1; then
old_release="$work/v3.10.0"; mkdir -p "$old_release"
git -C "$src" archive v3.10.0 | (cd "$old_release" && tar -xf -)
upgraded="$work/upgraded"
cp -R "$old_release/examples/market-day" "$upgraded"
(cd "$upgraded" && git init -q && git config core.autocrlf false && git add -A && git_q commit -qm "example") || exit 1
bash "$old_release/install.sh" "$upgraded" >/dev/null 2>&1
(cd "$upgraded" && git add -A && { git_q commit -qm "install 3.10.0" >/dev/null 2>&1 || true; } && cp AGENTS.md "$work/up-agents" && cp tools/check.cfg "$work/up-check-cfg")
out="$(bash "$src/install.sh" "$upgraded" 2>&1)"; expect_status "upgrades a game from v3.10.0" 0 $? "$out"
if [ -f "$upgraded/tools/ui_screens.gd" ] && [ -f "$upgraded/tools/ui_capture.cfg" ] && [ -f "$upgraded/design/ui/README.md" ] && [ -f "$upgraded/design/director-gdd.md" ] \
&& grep -q '^/design/ui/captures/$' "$upgraded/.gitignore" && grep -q 'tools/ui_verify\.gd$' "$upgraded/.godot-director/manifest" \
&& grep -q 'procedures/ui-design\.md$' "$upgraded/.godot-director/manifest" && grep -q 'skills/ui-design/SKILL\.md$' "$upgraded/.godot-director/manifest"; then
ok "the upgrade adds the stubs, the human-written GDD template, the captures .gitignore line and the manifest entries"
else
bad "the upgrade adds the stubs, the human-written GDD template, the captures .gitignore line and the manifest entries" "$out"
fi
if [ -f "$upgraded/tools/autoplay_adapter.gd" ] && [ -f "$upgraded/tools/autoplay_scenarios.gd" ] && [ -f "$upgraded/tools/autoplay_invariants.gd" ] \
&& grep -q 'tools/autoplay\.gd$' "$upgraded/.godot-director/manifest" && grep -q '^/playtesting/autoplay/$' "$upgraded/.gitignore"; then
ok "the upgrade adds autoplay, its seeds and the run folders' .gitignore line"
else
bad "the upgrade adds autoplay, its seeds and the run folders' .gitignore line" "$out"
fi
if [ -x "$upgraded/tools/log_review.sh" ] && grep -q 'tools/log_review\.gd$' "$upgraded/.godot-director/manifest" \
&& cmp -s "$upgraded/playtesting/signatures.tsv" "$src/project/playtesting/signatures.tsv" \
&& grep -q '^/playtesting/signatures\.tsv text eol=lf$' "$upgraded/.gitattributes"; then
ok "the upgrade adds log review, the signatures.tsv seed and its .gitattributes line"
else
bad "the upgrade adds log review, the signatures.tsv seed and its .gitattributes line" "$out"
fi
if cmp -s "$upgraded/tools/dev_menu.gd" "$src/project/tools/dev_menu.gd" && grep -q 'tools/dev_menu_base\.gd$' "$upgraded/.godot-director/manifest" \
&& grep -q '^/tools/dev_menu_base\.gd text eol=lf$' "$upgraded/.gitattributes" && ! grep -q '^DevMenu=' "$upgraded/project.godot"; then
ok "the upgrade adds the dev menu base and its seed, and leaves it unregistered until the human answers"
else
bad "the upgrade adds the dev menu base and its seed, and leaves it unregistered until the human answers" "$out"
fi
cmp -s "$upgraded/AGENTS.md" "$work/up-agents" && cmp -s "$upgraded/tools/check.cfg" "$work/up-check-cfg" \
&& ok "the upgrade keeps the game's own AGENTS.md and tools/check.cfg" || bad "the upgrade keeps the game's own AGENTS.md and tools/check.cfg"
if grep -q '^check_in_check=false$' "$upgraded/tools/ui_capture.cfg" && grep -q '^enforce=\[\]$' "$upgraded/tools/ui_capture.cfg"; then
ok "the upgrade writes the layout lint in the check off (check_in_check=false, enforce=[])"
else
bad "the upgrade writes the layout lint in the check off" "$(cat "$upgraded/tools/ui_capture.cfg")"
fi
if [ -n "$godot" ]; then
cp "$upgraded/scenes/shop_screen.tscn" "$upgraded/scenes/extra_screen.tscn"
out="$(cd "$upgraded" && GODOT_BIN="$godot" bash tools/ui_capture.sh --missing 2>&1)"; expect_status "align's screen listing doesn't fail" 0 $? "$out"
if printf '%s\n' "$out" | grep -q '^missing: scenes/extra_screen\.tscn' && ! printf '%s\n' "$out" | grep -q '^missing: scenes/shop_screen\.tscn'; then
ok "align lists a screen scene the catalog doesn't cover, and not the one it does"
else
bad "align lists a screen scene the catalog doesn't cover, and not the one it does" "$out"
fi
rm -f "$upgraded/scenes/extra_screen.tscn" "$upgraded/scenes/extra_screen.tscn.uid"
out="$(cd "$upgraded" && GODOT_BIN="$godot" bash tools/check.sh 2>&1)"; expect_status "the upgraded game's check passes with the lint off" 0 $? "$out"
expect_no_output "and runs no layout lint" "ui lint" "$out"
fi
else
echo " skip upgrade from v3.10.0: no v3.10.0 tag in this checkout"
fi
core_only="$work/core-only"; new_repo "$core_only"
out="$(bash "$src/install.sh" --tools none "$core_only" 2>&1)"
if [ ! -e "$core_only/.claude" ] && [ ! -e "$core_only/CLAUDE.md" ] && [ -f "$core_only/AGENTS.md" ] && [ -f "$core_only/.godot-director/rules.md" ]; then
ok "--tools none installs the tool-neutral core only"
else
bad "--tools none installs the tool-neutral core only" "$out"
fi
if [ -f "$core_only/.godot-director/procedures/research.md" ] && [ -f "$core_only/.godot-director/research-domain.md" ] \
&& [ -f "$core_only/design/research/README.md" ] && [ -f "$core_only/tools/ui_screens.gd" ] && [ -f "$core_only/tools/ui_capture.cfg" ] && grep -q '^\*\*Next IDs:\*\* T[0-9]* · B[0-9]* · R[0-9]*$' "$core_only/TASKS.md"; then
ok "a new game gets the research procedure and domain file, the note folder, the screen catalog and capture settings, and an R counter in TASKS.md, even with --tools none"
else
bad "a new game gets the research procedure and domain file, the note folder, the screen catalog and capture settings, and an R counter in TASKS.md" "$out"
fi
if [ -f "$core_only/tools/play_log_base.gd" ] && cmp -s "$core_only/tools/play_log.gd" "$src/project/tools/play_log.gd"; then
ok "a new game gets the play log base and the PlayLog seed, even with --tools none"
else
bad "a new game gets the play log base and the PlayLog seed, even with --tools none" "$out"
fi
if [ -x "$core_only/tools/autoplay.sh" ] && [ -f "$core_only/tools/autoplay.gd" ] \
&& cmp -s "$core_only/tools/autoplay_adapter.gd" "$src/project/tools/autoplay_adapter.gd" \
&& cmp -s "$core_only/tools/autoplay_scenarios.gd" "$src/project/tools/autoplay_scenarios.gd" \
&& cmp -s "$core_only/tools/autoplay_invariants.gd" "$src/project/tools/autoplay_invariants.gd" && grep -q '^/playtesting/autoplay/$' "$core_only/.gitignore"; then
ok "a new game gets autoplay, the adapter, scenario and invariant seeds and the run folders' gitignore line, even with --tools none"
else
bad "a new game gets autoplay, the adapter, scenario and invariant seeds and the run folders' gitignore line, even with --tools none" "$out"
fi
if [ -x "$core_only/tools/log_review.sh" ] && [ -f "$core_only/tools/log_review.gd" ] \
&& cmp -s "$core_only/playtesting/signatures.tsv" "$src/project/playtesting/signatures.tsv"; then
ok "a new game gets log review and the signatures.tsv seed, even with --tools none"
else
bad "a new game gets log review and the signatures.tsv seed, even with --tools none" "$out"
fi
if [ -f "$core_only/tools/dev_menu_base.gd" ] && cmp -s "$core_only/tools/dev_menu.gd" "$src/project/tools/dev_menu.gd"; then
ok "a new game gets the dev menu base and the DevMenu seed, even with --tools none"
else
bad "a new game gets the dev menu base and the DevMenu seed, even with --tools none" "$out"
fi
if [ -f "$core_only/design/director-gdd.md" ] && grep -q '^# Game Design Document (GDD): ' "$core_only/design/director-gdd.md"; then
ok "a new game gets the human-written GDD template, even with --tools none"
else
bad "a new game gets the human-written GDD template, even with --tools none" "$out"
fi
if [ -x "$core_only/tools/stories.sh" ] && [ -f "$core_only/.godot-director/templates/user-stories.md" ] && [ ! -e "$core_only/design/user-stories.md" ]; then
ok "a new game gets tools/stories.sh and the user stories template, and no design/user-stories.md until it turns them on"
else
bad "a new game gets tools/stories.sh and the user stories template, and no design/user-stories.md until it turns them on" "$out"
fi
# Installing from a Godot Director folder that is itself a git clone, or sits inside the game.
gdir_clone="$work/gdir-clone"; cp -R "$src" "$gdir_clone"; rm -rf "$gdir_clone/.git"
(cd "$gdir_clone" && git init -q && git config core.autocrlf false && git add -A && git_q commit -qm gdir) >/dev/null 2>&1
from_clone="$work/from-clone"; new_repo "$from_clone"
out="$(bash "$gdir_clone/install.sh" "$from_clone" 2>&1)"; expect_status "installs from a Godot Director git clone" 0 $? "$out"
inside="$work/inside"; new_repo "$inside"; cp -R "$gdir_clone" "$inside/Godot-Director"; rm -rf "$inside/Godot-Director/.git"
printf '/Godot-Director/\n' >> "$inside/.git/info/exclude"
out="$(bash "$inside/Godot-Director/install.sh" "$inside" 2>&1)"; expect_status "installs from a Godot Director folder inside the game" 0 $? "$out"
[ -f "$inside/.godot-director/LICENSE" ] && ok "installs the license copy" || bad "installs the license copy" "$out"
# An install from a source with unreleased changes says so. A copy of the installer and its files, in
# a git repository of its own (so the commit is known), with the CHANGELOG given. The copy leaves out
# the rest of this folder (tests, node_modules) to stay quick.
snap_version="$(tr -d '\r\n' < "$src/VERSION")"
snapshot_source() { # snapshot_source <folder> <CHANGELOG to use>
rm -rf "$1" && mkdir -p "$1" \
&& cp -R "$src/framework" "$src/project" "$src/install.sh" "$src/VERSION" "$1/" \
&& cp "$2" "$1/CHANGELOG.md" \
&& (cd "$1" && git init -q && git config core.autocrlf false && git add -A && git_q commit -qm snapshot) >/dev/null 2>&1
}
printf '# Changelog\n\n## Unreleased\n\n- **New: a snapshot entry.**\n\n## %s (2026-10-17)\n\n- Earlier.\n' "$snap_version" > "$work/changelog-unreleased.md"
printf '# Changelog\n\n## Unreleased\n\n## %s (2026-10-17)\n\n- Earlier.\n' "$snap_version" > "$work/changelog-empty.md"
printf '# Changelog\n\n## %s (2026-10-17)\n\n- Earlier.\n' "$snap_version" > "$work/changelog-release.md"
snap="$work/snapshot"
snapshot_source "$snap" "$work/changelog-unreleased.md" || bad "builds a source with unreleased entries"
snap_commit="$(git -C "$snap" rev-parse --short HEAD 2>/dev/null)"
snap_app="$work/snapshot-app"; new_repo "$snap_app"
out="$(bash "$snap/install.sh" "$snap_app" 2>&1)"; expect_status "installs from a source with unreleased entries" 0 $? "$out"
if [ "$(sed -n 1p "$snap_app/.godot-director/manifest")" = "# godot-director $snap_version + unreleased ($snap_commit)" ] \
&& [ "$(sed -n 2p "$snap_app/.godot-director/manifest")" = "# source $snap_commit" ]; then
ok "the manifest says unreleased, with the source commit on its own line"
else
bad "the manifest says unreleased, with the source commit on its own line" "$(head -n 3 "$snap_app/.godot-director/manifest")"
fi
closing="Installed $snap_version + unreleased ($snap_commit): a development snapshot, not a release. The latest release is at https://github.com/Freakling/Godot-Director/releases."
printf '%s\n' "$out" | grep -qxF "$closing" \
&& ok "the closing lines say the install is a development snapshot" \
|| bad "the closing lines say the install is a development snapshot" "$out"
empty="$work/snapshot-empty"
snapshot_source "$empty" "$work/changelog-empty.md" || bad "builds a source with an empty Unreleased section"
empty_commit="$(git -C "$empty" rev-parse --short HEAD 2>/dev/null)"
empty_app="$work/empty-app"; new_repo "$empty_app"
out="$(bash "$empty/install.sh" "$empty_app" 2>&1)"; expect_status "installs from a source with an empty Unreleased section" 0 $? "$out"
if [ "$(sed -n 1p "$empty_app/.godot-director/manifest")" = "# godot-director $snap_version" ] \
&& [ "$(sed -n 2p "$empty_app/.godot-director/manifest")" = "# source $empty_commit" ]; then
ok "an empty Unreleased section records the plain version and the commit"
else
bad "an empty Unreleased section records the plain version and the commit" "$(head -n 3 "$empty_app/.godot-director/manifest")"
fi
expect_no_output "an empty Unreleased section adds no closing line" "development snapshot" "$out"
# A release records the plain version, and its commit when it comes from a git checkout. A release
# zip has no .git, and an enclosing repository's commit must not be recorded for it.
rel="$work/snapshot-release"
snapshot_source "$rel" "$work/changelog-release.md" || bad "builds a release source"
rel_commit="$(git -C "$rel" rev-parse --short HEAD 2>/dev/null)"
rel_app="$work/release-app"; new_repo "$rel_app"
out="$(bash "$rel/install.sh" "$rel_app" 2>&1)"; expect_status "installs from a release checkout" 0 $? "$out"
if [ "$(sed -n 1p "$rel_app/.godot-director/manifest")" = "# godot-director $snap_version" ] \
&& [ "$(sed -n 2p "$rel_app/.godot-director/manifest")" = "# source $rel_commit" ]; then
ok "a release checkout records the plain version and its commit"
else
bad "a release checkout records the plain version and its commit" "$(head -n 3 "$rel_app/.godot-director/manifest")"
fi
expect_no_output "a release adds no closing line about snapshots" "development snapshot" "$out"
zip_app="$work/zip-app"; new_repo "$zip_app"
mkdir -p "$zip_app/Godot-Director"
cp -R "$rel/framework" "$rel/project" "$rel/install.sh" "$rel/VERSION" "$rel/CHANGELOG.md" "$zip_app/Godot-Director/"
out="$(bash "$zip_app/Godot-Director/install.sh" "$zip_app" 2>&1)"; expect_status "installs from a release folder with no git, inside an app repository" 0 $? "$out"
if [ "$(sed -n 1p "$zip_app/.godot-director/manifest")" = "# godot-director $snap_version" ] \
&& [ "$(grep -c '^# source' "$zip_app/.godot-director/manifest")" -eq 0 ]; then
ok "a release folder with no git records no commit"
else
bad "a release folder with no git records no commit" "$(head -n 3 "$zip_app/.godot-director/manifest")"
fi
# A snapshot folder with no git: the suffix without a commit, and no # source line.
zip_snap="$work/zip-snapshot-app"; new_repo "$zip_snap"
mkdir -p "$zip_snap/Godot-Director"
cp -R "$rel/framework" "$rel/project" "$rel/install.sh" "$rel/VERSION" "$zip_snap/Godot-Director/"
cp "$work/changelog-unreleased.md" "$zip_snap/Godot-Director/CHANGELOG.md"
out="$(bash "$zip_snap/Godot-Director/install.sh" "$zip_snap" 2>&1)"; expect_status "installs from an unreleased folder with no git" 0 $? "$out"
if [ "$(sed -n 1p "$zip_snap/.godot-director/manifest")" = "# godot-director $snap_version + unreleased" ] \
&& [ "$(grep -c '^# source' "$zip_snap/.godot-director/manifest")" -eq 0 ] \
&& printf '%s\n' "$out" | grep -qxF "Installed $snap_version + unreleased: a development snapshot, not a release. The latest release is at https://github.com/Freakling/Godot-Director/releases."; then
ok "an unreleased folder with no git records the suffix without a commit, and says so"
else
bad "an unreleased folder with no git records the suffix without a commit, and says so" "$out"
fi
# An upgrade from the unreleased install reads the old version without its suffix (and skips the
# # source line): the next release, 9.9.9, upgrades it, and the message says so.
next="$work/snapshot-next"
snapshot_source "$next" "$work/changelog-release.md" && printf '9.9.9\n' > "$next/VERSION" \
|| bad "builds the next release source"