@oneie/claude 0.6.0 → 0.7.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 (111) hide show
  1. package/agents/w1-recon.md +9 -4
  2. package/agents/w2-decide.md +51 -28
  3. package/agents/w3-edit.md +70 -78
  4. package/agents/w4-verify.md +164 -69
  5. package/commands/browser.md +98 -35
  6. package/commands/cc-connect.md +6 -0
  7. package/commands/chat.md +13 -1
  8. package/commands/close.md +45 -19
  9. package/commands/create.md +20 -10
  10. package/commands/db-sync.md +5 -1
  11. package/commands/deploy.md +174 -87
  12. package/commands/do-autonomous.md +31 -20
  13. package/commands/do-improve.md +4 -4
  14. package/commands/do-show.md +4 -3
  15. package/commands/do.md +53 -13
  16. package/commands/go.md +75 -0
  17. package/commands/improve.md +4 -2
  18. package/commands/kill.md +6 -8
  19. package/commands/notify.md +15 -2
  20. package/commands/oo-push.md +24 -9
  21. package/commands/rag.md +6 -0
  22. package/commands/release.md +46 -25
  23. package/commands/restart.md +5 -4
  24. package/commands/see.md +45 -35
  25. package/commands/skill-create.md +38 -5
  26. package/commands/sync.md +21 -11
  27. package/hooks/scripts/auto-continue.sh +70 -0
  28. package/hooks/scripts/branch-pin.sh +85 -0
  29. package/hooks/scripts/git-add-guard.sh +16 -5
  30. package/hooks/scripts/post-edit-check.sh +16 -0
  31. package/hooks/scripts/session-start.sh +13 -8
  32. package/hooks/scripts/sync-priority-todo.sh +5 -0
  33. package/package.json +1 -1
  34. package/rules/api.md +9 -0
  35. package/rules/design.md +13 -3
  36. package/rules/documentation.md +30 -71
  37. package/rules/engine.md +8 -1
  38. package/rules/ui.md +5 -1
  39. package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
  40. package/scripts/browser-check.mjs +44 -103
  41. package/scripts/cc-events-proof.sh +13 -12
  42. package/scripts/chrome.mjs +381 -0
  43. package/scripts/do-analyze.sh +109 -4
  44. package/scripts/do-auto.sh +350 -43
  45. package/scripts/do-consumer-sweep.sh +72 -0
  46. package/scripts/do-derives-check.sh +88 -0
  47. package/scripts/do-fleet.sh +50 -1
  48. package/scripts/do-killswitch-audit.py +33 -14
  49. package/scripts/do-promise-lint.sh +150 -4
  50. package/scripts/do-promise-settle.sh +131 -9
  51. package/scripts/do-prove.sh +197 -17
  52. package/scripts/do-rank.py +606 -20
  53. package/scripts/do-rubric.py +21 -2
  54. package/scripts/do-signal.sh +157 -17
  55. package/scripts/do-smoke.sh +9 -0
  56. package/scripts/do-substrate-check.sh +49 -0
  57. package/scripts/do-tasks-wire-check.sh +81 -0
  58. package/scripts/do-tier.sh +90 -5
  59. package/scripts/do-ui-gate.sh +332 -0
  60. package/scripts/do-untracked-gate.sh +231 -0
  61. package/scripts/do-walk.sh +344 -0
  62. package/scripts/factory-check.sh +747 -0
  63. package/scripts/factory-repo.sh +1361 -0
  64. package/scripts/factory-tasks-check.sh +85 -0
  65. package/scripts/outcome-pull.ts +279 -0
  66. package/scripts/promise-manifest.mjs +167 -0
  67. package/scripts/reconcile-allow.txt +5 -0
  68. package/scripts/rubric-weights.json +64 -0
  69. package/scripts/spine-canary.sh +20 -0
  70. package/scripts/typedb-cluster-status.sh +128 -0
  71. package/scripts/typedb-env.sh +182 -0
  72. package/scripts/typedb-flap-recorder.sh +85 -0
  73. package/scripts/typedb-probes/containment-probe.py +88 -0
  74. package/scripts/typedb-probes/panic-probe-version.py +92 -0
  75. package/scripts/typedb-probes/panic-probe.py +77 -0
  76. package/scripts/typedb-scratch.sh +73 -0
  77. package/scripts/urls-lint.sh +58 -0
  78. package/scripts/w4-rubric.ts +96 -12
  79. package/scripts/wf-check.mjs +75 -0
  80. package/skills/ai-ui/SKILL.md +1 -1
  81. package/skills/astro/SKILL.md +177 -132
  82. package/skills/astro/add-content-collection.md +29 -13
  83. package/skills/astro/check-build.md +19 -13
  84. package/skills/astro/create-component.md +36 -9
  85. package/skills/astro/create-page.md +39 -10
  86. package/skills/astro/optimize-performance.md +36 -8
  87. package/skills/cli/SKILL.md +254 -220
  88. package/skills/dev/SKILL.md +1 -1
  89. package/skills/directory-autofill/SKILL.md +68 -0
  90. package/skills/docs/SKILL.md +4 -4
  91. package/skills/mcp/SKILL.md +186 -156
  92. package/skills/promise-make/SKILL.md +2 -2
  93. package/skills/puck/SKILL.md +465 -250
  94. package/skills/react19/SKILL.md +196 -296
  95. package/skills/reactflow/SKILL.md +387 -412
  96. package/skills/sdk/SKILL.md +167 -106
  97. package/skills/shadcn/SKILL.md +245 -201
  98. package/skills/signal/SKILL.md +5 -0
  99. package/skills/sui/SKILL.md +6 -1
  100. package/skills/tutorial/SKILL.md +15 -0
  101. package/skills/typedb/SKILL.md +427 -217
  102. package/skills/typedb/examples/python-patterns.py +11 -4
  103. package/skills/typedb/examples/query-patterns.tql +8 -3
  104. package/skills/typedb/examples/schema-patterns.tql +4 -2
  105. package/skills/typedb/reference/migration-2x-3x.md +19 -1
  106. package/skills/typedb/reference/python-driver.md +17 -1
  107. package/skills/voice/SKILL.md +117 -0
  108. package/skills/writer/SKILL.md +29 -12
  109. package/templates/template-feature.md +26 -1
  110. package/templates/template-tests.md +4 -0
  111. package/templates/template-todo.md +65 -10
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env bash
2
+ # do-ui-gate.sh — IC9: executable proof rows for a promise's screens/states.
3
+ #
4
+ # Reads the ```ui fenced blocks out of text/<slug>-ui.md — same fenced-block
5
+ # convention as do-walk.sh's ```walk blocks — and executes them:
6
+ #
7
+ # ```ui
8
+ # row: 1
9
+ # component: LoginButton
10
+ # state: default
11
+ # route: /login
12
+ # expect_text: "Sign in" # optional — substring in the served HTML
13
+ # expect_cmd: test -f x.tsx # optional bash, exit 0 = pass
14
+ # check: renders a blue primary button
15
+ # ```
16
+ #
17
+ # Rows may be moved under a `## Deferred` heading, but that ONLY skips them
18
+ # if the Deferred section's body contains a line `promise: <slug>` (ties the
19
+ # deferral to a tracked promise — otherwise the gate FAILs; UI work can't be
20
+ # silently waived).
21
+ #
22
+ # Usage:
23
+ # do-ui-gate.sh <slug> [--list] # print parsed rows (active vs deferred), run nothing
24
+ # do-ui-gate.sh <slug> # run active rows against a live/self-started dev server
25
+ # do-ui-gate.sh --self-test
26
+ #
27
+ # Env:
28
+ # UI_GATE_BASE_URL force the base URL (skips sentinel detection + self-start)
29
+ #
30
+ # Base URL resolution (when UI_GATE_BASE_URL unset):
31
+ # 1. derive port = 4400 + (cksum of slug) % 100 (same derivation as do-auto.sh's
32
+ # _sentinel_port, IC1) and check .do-worktrees/<slug>/.do-loop-running for a
33
+ # live pid on that port
34
+ # 2. else self-start `astro dev --port <port>` from one.ie/web, poll for 200
35
+ # up to 60s, run rows, then kill the server it started
36
+ #
37
+ # Exit codes: 0 = every active row passed (or --list) · 1 = a row failed, or an
38
+ # undeferred waive (rows after ## Deferred with no `promise: <slug>` line).
39
+ set -euo pipefail
40
+
41
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
42
+
43
+ # ---------- parse helpers (copied from do-walk.sh, fence tag `ui`) ----------
44
+
45
+ # extract_stops <file> → lines "N<TAB>key: value" for every ```ui block
46
+ extract_stops() {
47
+ awk '
48
+ /^```ui[[:space:]]*$/ { inb=1; n++; next }
49
+ /^```[[:space:]]*$/ { inb=0; next }
50
+ inb { print n "\t" $0 }
51
+ ' "$1"
52
+ }
53
+
54
+ # field <stops> <n> <key> → value, empty if absent (fence-agnostic)
55
+ field() {
56
+ printf '%s\n' "$1" | awk -F'\t' -v n="$2" -v k="$3" '
57
+ $1 == n {
58
+ line = $2
59
+ prefix = k ": "
60
+ if (index(line, prefix) == 1) {
61
+ v = substr(line, length(prefix) + 1)
62
+ sub(/[ \t]+$/, "", v)
63
+ len = length(v)
64
+ if (len >= 2) {
65
+ first = substr(v, 1, 1); last = substr(v, len, 1)
66
+ if ((first == "\"" && last == "\"") || (first == "'"'"'" && last == "'"'"'")) {
67
+ v = substr(v, 2, len - 2)
68
+ }
69
+ }
70
+ print v
71
+ exit
72
+ }
73
+ }'
74
+ }
75
+
76
+ # _sentinel_port <slug> — must match do-auto.sh's derivation exactly
77
+ _sentinel_port() {
78
+ echo $(( 4400 + $(cksum <<<"$1" | cut -d' ' -f1) % 100 ))
79
+ }
80
+
81
+ # split_rows <file> <stops> <ids> → prints two files: $1.active $1.deferred
82
+ # (as global vars ACTIVE_IDS / DEFERRED_IDS, newline-separated row numbers)
83
+ split_deferred() { # $1 = ui.md file, $2 = stops
84
+ local file="$1"
85
+ awk '/^##[[:space:]]*Deferred/{print NR; exit}' "$file"
86
+ }
87
+
88
+ deferred_section_names_promise() { # $1 = file, $2 = slug
89
+ local file="$1" slug="$2" ln
90
+ ln="$(awk '/^##[[:space:]]*Deferred/{print NR; exit}' "$file")"
91
+ [ -z "$ln" ] && return 1
92
+ tail -n "+$ln" "$file" | grep -qE "^promise:[[:space:]]*${slug}[[:space:]]*$"
93
+ }
94
+
95
+ row_line_number() { # $1 = file, $2 = row n → line number of the ```ui fence that starts row n
96
+ awk -v want="$2" '
97
+ /^```ui[[:space:]]*$/ { n++; if (n == want) { print NR; exit } }
98
+ ' "$1"
99
+ }
100
+
101
+ # ---------- self-test ----------
102
+
103
+ if [ "${1:-}" = "--self-test" ]; then
104
+ tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT
105
+ fails=0
106
+
107
+ # fixture 1: one passing row, one failing row (both expect_cmd, no route) — expect exit 1
108
+ f1="$tmpdir/f1-ui.md"
109
+ cat > "$f1" <<'EOF'
110
+ ## Row 1
111
+ ```ui
112
+ row: 1
113
+ component: A
114
+ state: default
115
+ expect_cmd: true
116
+ check: always passes
117
+ ```
118
+ ## Row 2
119
+ ```ui
120
+ row: 2
121
+ component: B
122
+ state: default
123
+ expect_cmd: false
124
+ check: always fails
125
+ ```
126
+ EOF
127
+ if UI_GATE_SELF_TEST=1 bash "${BASH_SOURCE[0]}" _fixture_f1 --file "$f1" >/dev/null 2>&1; then
128
+ echo "FAIL: fixture1 expected exit 1 (one failing row), got 0"; fails=$((fails+1))
129
+ fi
130
+
131
+ # fixture 2: failing row moved under ## Deferred, no `promise: <slug>` line — expect exit 1 (undeferred waive)
132
+ f2="$tmpdir/f2-ui.md"
133
+ cat > "$f2" <<'EOF'
134
+ ## Row 1
135
+ ```ui
136
+ row: 1
137
+ component: A
138
+ state: default
139
+ expect_cmd: true
140
+ check: always passes
141
+ ```
142
+ ## Deferred
143
+ Some other note, no promise line here.
144
+ ```ui
145
+ row: 2
146
+ component: B
147
+ state: default
148
+ expect_cmd: false
149
+ check: always fails
150
+ ```
151
+ EOF
152
+ if UI_GATE_SELF_TEST=1 bash "${BASH_SOURCE[0]}" _fixture_f2 --file "$f2" >/dev/null 2>&1; then
153
+ echo "FAIL: fixture2 expected exit 1 (undeferred waive), got 0"; fails=$((fails+1))
154
+ fi
155
+
156
+ # fixture 3: same deferred row, but section names `promise: <slug>` — expect exit 0
157
+ f3="$tmpdir/f3-ui.md"
158
+ cat > "$f3" <<'EOF'
159
+ ## Row 1
160
+ ```ui
161
+ row: 1
162
+ component: A
163
+ state: default
164
+ expect_cmd: true
165
+ check: always passes
166
+ ```
167
+ ## Deferred
168
+ promise: _fixture_f3
169
+ ```ui
170
+ row: 2
171
+ component: B
172
+ state: default
173
+ expect_cmd: false
174
+ check: always fails
175
+ ```
176
+ EOF
177
+ if ! UI_GATE_SELF_TEST=1 bash "${BASH_SOURCE[0]}" _fixture_f3 --file "$f3" >/dev/null 2>&1; then
178
+ echo "FAIL: fixture3 expected exit 0 (properly deferred), got nonzero"; fails=$((fails+1))
179
+ fi
180
+
181
+ echo "[ui-gate] self-test: 3 fixtures, $fails failed"
182
+ exit "$fails"
183
+ fi
184
+
185
+ # ---------- args ----------
186
+
187
+ slug="" list=0 file_override=""
188
+ while [ "$#" -gt 0 ]; do
189
+ case "$1" in
190
+ --list) list=1; shift ;;
191
+ --file) [ "$#" -ge 2 ] || { echo "UI-GATE: --file requires a value"; exit 1; }
192
+ file_override="$2"; shift 2 ;; # internal: used by --self-test fixtures
193
+ -*) echo "UI-GATE: unknown flag $1"; exit 1 ;;
194
+ *) slug="$1"; shift ;;
195
+ esac
196
+ done
197
+ [ -n "$slug" ] || { echo "UI-GATE: usage: do-ui-gate.sh <slug> [--list]"; exit 1; }
198
+
199
+ file="${file_override:-$ROOT/text/${slug}-ui.md}"
200
+ [ -f "$file" ] || { echo "UI-GATE: no ui doc at text/${slug}-ui.md"; exit 1; }
201
+
202
+ stops="$(extract_stops "$file")"
203
+ [ -n "$stops" ] || { echo "UI-GATE: text/${slug}-ui.md has no \`\`\`ui blocks"; exit 1; }
204
+ ids="$(printf '%s\n' "$stops" | awk -F'\t' '{print $1}' | sort -un)"
205
+
206
+ deferred_at_line="$(awk '/^##[[:space:]]*Deferred/{print NR; exit}' "$file")"
207
+
208
+ active_ids=""; deferred_ids=""
209
+ for n in $ids; do
210
+ rln="$(row_line_number "$file" "$n")"
211
+ if [ -n "$deferred_at_line" ] && [ "$rln" -gt "$deferred_at_line" ]; then
212
+ deferred_ids="$deferred_ids $n"
213
+ else
214
+ active_ids="$active_ids $n"
215
+ fi
216
+ done
217
+
218
+ if [ -n "$deferred_ids" ]; then
219
+ if ! deferred_section_names_promise "$file" "$slug"; then
220
+ echo "UI-GATE: FAIL — rows$deferred_ids are under ## Deferred but that section does not name \`promise: $slug\` — UI work cannot be silently waived"
221
+ exit 1
222
+ fi
223
+ echo "UI-GATE: rows$deferred_ids deferred to promise: $slug (skipped)"
224
+ fi
225
+
226
+ echo "UI-GATE: $slug · $(printf '%s\n' "$active_ids" | wc -w | tr -d ' ') active row(s)"
227
+
228
+ # ---------- list mode ----------
229
+
230
+ if [ "$list" -eq 1 ]; then
231
+ for n in $active_ids; do
232
+ c="$(field "$stops" "$n" component)"; s="$(field "$stops" "$n" state)"
233
+ r="$(field "$stops" "$n" route)"; ck="$(field "$stops" "$n" check)"
234
+ echo " active $n. $c/$s${r:+ → $r}"
235
+ [ -n "$ck" ] && echo " $ck"
236
+ done
237
+ for n in $deferred_ids; do
238
+ c="$(field "$stops" "$n" component)"; s="$(field "$stops" "$n" state)"
239
+ echo " deferred $n. $c/$s"
240
+ done
241
+ exit 0
242
+ fi
243
+
244
+ # ---------- zero-network short-circuit (self-test fixtures) ----------
245
+ # If no active row needs a route/expect_text (only expect_cmd rows), skip the
246
+ # base-URL bootstrap entirely — keeps --self-test zero-network.
247
+ needs_network=0
248
+ for n in $active_ids; do
249
+ r="$(field "$stops" "$n" route)"
250
+ [ -n "$r" ] && needs_network=1
251
+ done
252
+
253
+ base=""
254
+ started_server=0
255
+ server_pid=""
256
+
257
+ if [ "$needs_network" -eq 1 ]; then
258
+ base="${UI_GATE_BASE_URL:-}"
259
+ port="$(_sentinel_port "$slug")"
260
+
261
+ if [ -z "$base" ]; then
262
+ sentinel="$ROOT/.do-worktrees/$slug/.do-loop-running"
263
+ if [ -f "$sentinel" ]; then
264
+ spid="$(grep -o '"pid":[0-9]*' "$sentinel" | head -1 | cut -d: -f2 || true)"
265
+ sport="$(grep -o '"port":[0-9]*' "$sentinel" | head -1 | cut -d: -f2 || true)"
266
+ if [ -n "$spid" ] && kill -0 "$spid" 2>/dev/null && [ -n "$sport" ]; then
267
+ if curl -sf -o /dev/null --max-time 3 "http://localhost:$sport/" 2>/dev/null; then
268
+ base="http://localhost:$sport"
269
+ fi
270
+ fi
271
+ fi
272
+ fi
273
+
274
+ if [ -z "$base" ]; then
275
+ echo "UI-GATE: self-starting dev server on port $port"
276
+ ( cd "$ROOT/one.ie/web" && exec ./node_modules/.bin/astro dev --port "$port" ) >/tmp/ui-gate-$slug.log 2>&1 &
277
+ server_pid=$!
278
+ started_server=1
279
+ base="http://localhost:$port"
280
+ ok=0
281
+ for _ in $(seq 1 30); do
282
+ if curl -sf -o /dev/null --max-time 2 "$base/" 2>/dev/null; then ok=1; break; fi
283
+ sleep 2
284
+ done
285
+ if [ "$ok" -ne 1 ]; then
286
+ echo "UI-GATE: FAIL — dev server on $base did not come up within 60s"
287
+ kill "$server_pid" 2>/dev/null || true
288
+ exit 1
289
+ fi
290
+ fi
291
+ echo "UI-GATE: base=$base"
292
+ fi
293
+
294
+ cleanup() {
295
+ if [ "$started_server" -eq 1 ] && [ -n "$server_pid" ]; then
296
+ kill "$server_pid" 2>/dev/null || true
297
+ fi
298
+ }
299
+ trap cleanup EXIT
300
+
301
+ # ---------- run ----------
302
+
303
+ fail=0
304
+ for n in $active_ids; do
305
+ c="$(field "$stops" "$n" component)"; s="$(field "$stops" "$n" state)"
306
+ r="$(field "$stops" "$n" route)"
307
+ et="$(field "$stops" "$n" expect_text)"
308
+ ec="$(field "$stops" "$n" expect_cmd)"
309
+
310
+ ok=1; why=""
311
+ if [ -n "$r" ] && [ -n "$base" ]; then
312
+ body="$(curl -s --max-time 20 "$base$r" 2>/dev/null || true)"
313
+ if [ -z "$body" ]; then ok=0; why="no response from $base$r"; fi
314
+ if [ "$ok" -eq 1 ] && [ -n "$et" ] && ! printf '%s' "$body" | grep -qiF "$et"; then
315
+ ok=0; why="text '$et' not in served HTML"
316
+ fi
317
+ fi
318
+ if [ "$ok" -eq 1 ] && [ -n "$ec" ]; then
319
+ if ! (cd "$ROOT" && bash -c "$ec" >/dev/null 2>&1); then ok=0; why="expect_cmd failed: $ec"; fi
320
+ fi
321
+
322
+ if [ "$ok" -eq 1 ]; then
323
+ echo "UI-GATE: ok row $n — $c/$s"
324
+ else
325
+ echo "UI-GATE: FAIL row $n — $c/$s — $why"
326
+ fail=1
327
+ fi
328
+ done
329
+
330
+ echo ""
331
+ if [ "$fail" -eq 1 ]; then echo "UI-GATE: fail"; exit 1; fi
332
+ echo "UI-GATE: pass"; exit 0
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ # do-untracked-gate.sh — a deliverable that exists on disk but is NOT in git has
3
+ # not shipped. This gate is the only check in the harness that can say so.
4
+ #
5
+ # WHY (the real failure, 2026-07-28, plan `factory`): two shipped deliverables —
6
+ # packages/sdk/src/fn-allowlist.ts (C8's single source of truth)
7
+ # one.ie/web/tests/unit/fn-entity-args.test.ts (C7's proof)
8
+ # sat UNTRACKED for hours while every acceptance check stayed GREEN, because
9
+ # every check reads the WORKING TREE. Built, tested, passing, and on a path to
10
+ # never reach main. A human running `git status` caught it; no gate did.
11
+ #
12
+ # That is the promise's own failure class — "built but not wired" — one level
13
+ # lower than it was being looked for. factory-check.sh asks "is it live in the
14
+ # substrate?"; this asks the question underneath it: "is it even in the repo?"
15
+ #
16
+ # Usage:
17
+ # do-untracked-gate.sh <slug> [--verbose] # e.g. do-untracked-gate.sh factory
18
+ # do-untracked-gate.sh --self-test # temp git fixtures, zero network
19
+ #
20
+ # Exit: 0 = green (every existing deliverable file is tracked)
21
+ # 1 = red (a deliverable exists on disk and is untracked or gitignored)
22
+ # 3 = cannot run (not a git repo, or neither text/<slug>.md nor -todo.md)
23
+ # 4 = bad usage
24
+ #
25
+ # 1 vs 3 is the same distinction factory-check.sh draws: red sends someone to
26
+ # `git add`; cannot-run sends them to fix the environment, and proves nothing
27
+ # either way. Never widen a cannot-run into a red to make a chain simpler.
28
+ #
29
+ # WHAT IT READS — the `deliverables:` frontmatter block of BOTH text/<slug>.md
30
+ # (the promise: `- item:` / `accept:` prose) and text/<slug>-todo.md (the plan:
31
+ # `- <kind>: "path — description"`). Paths live in prose, so every path-shaped
32
+ # token in the block is a candidate. Comments inside the block are scanned too:
33
+ # a path worth naming in the schedule is a path worth having in git.
34
+ #
35
+ # WHAT IT DOES NOT DO — it never reports a MISSING file. A deliverable path that
36
+ # does not exist is simply not built yet; that is factory-check.sh's / the
37
+ # promise proof's job. This gate has exactly one question: of the files that
38
+ # DO exist, is any of them invisible to git?
39
+ set -uo pipefail
40
+
41
+ ROOT="${DO_UNTRACKED_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
42
+ VERBOSE=0
43
+ say() { [ "$VERBOSE" -eq 1 ] && echo " $*" >&2; return 0; }
44
+ red() { echo "RED $1" >&2; return 1; }
45
+ cannot() { echo "CANNOT RUN: $1" >&2; return 3; }
46
+
47
+ # Paths that are legitimately absent from git and are never a deliverable.
48
+ # A `#` comment inside a deliverables block may name one (e.g. an assumes-style
49
+ # credential file); reporting those as red would be noise, not a finding.
50
+ # `.env` / `.dev.vars` are named inside real `accept:` command lines (sui-upgrade's
51
+ # accept sources a key from pay/backend/.dev.vars) — a local secrets file is never
52
+ # a deliverable, and flagging it as "can never ship" is noise that trains the
53
+ # reader to ignore the gate.
54
+ SKIP_RE='(^|/)(node_modules|dist|build|\.wrangler|\.astro)/|(^|/)\.env|(^|/)\.dev\.vars|\.local\.md$|\.keys\.local'
55
+
56
+ # ── the deliverables: frontmatter block ──────────────────────────────────
57
+ # First frontmatter block only. Opens on `^deliverables:` — WITH an optional
58
+ # trailing comment: text/marketplace-deal.md and text/template-feature.md both
59
+ # write `deliverables: # THE SCHEDULE …`, and an anchored
60
+ # `[[:space:]]*$` never opened the block for them. That produced the worst
61
+ # possible verdict — `ok (0 files checked)`, a green earned by reading nothing.
62
+ #
63
+ # Closes on the first column-0 KEY (`assumes:`, `ux_before:`, `proof:`), not on
64
+ # any column-0 non-space char: the equally-valid YAML style puts `- item:` at
65
+ # column 0, and closing on `-` would end the block at its first entry — the same
66
+ # read-nothing green by a different door. Blank lines and comments inside the
67
+ # block are tolerated; the closing `---` ends it via the fm counter.
68
+ deliverables_block() { # $1 = file
69
+ awk '
70
+ /^---[[:space:]]*$/ { fm++; if (fm>=2) exit; next }
71
+ fm!=1 { next }
72
+ /^deliverables:[[:space:]]*(#.*)?$/ { ind=1; next }
73
+ ind==1 && /^[A-Za-z_][A-Za-z0-9_.-]*:/ { ind=0 }
74
+ ind==1 { print }
75
+ ' "$1"
76
+ }
77
+
78
+ # ── candidate path extraction ────────────────────────────────────────────
79
+ # A candidate is a repo-relative-looking token with a file extension. It must
80
+ # contain a `/` — a bare `one.tql` or `do-walk.sh` names a file the writer did
81
+ # not locate, and resolving it by basename alone across 7k files invites a
82
+ # false positive worse than the miss.
83
+ #
84
+ # Globs (`text/template-*.md`), brace sets and URLs are dropped: a glob is not a
85
+ # path, and a URL is not in the repo.
86
+ candidates() { # stdin = block text
87
+ grep -oE '[A-Za-z0-9_@.][A-Za-z0-9_@./+-]*\.[A-Za-z0-9]{1,6}' \
88
+ | grep '/' \
89
+ | grep -vE '://' \
90
+ | grep -vE "$SKIP_RE" \
91
+ | sort -u
92
+ }
93
+
94
+ # ── resolution ───────────────────────────────────────────────────────────
95
+ # Two shapes appear in real plans, and the gate is worthless unless it handles
96
+ # both — the 2026-07-28 failure had one of each:
97
+ # (a) repo-relative packages/sdk/src/fn-allowlist.ts → direct hit
98
+ # (b) package-relative tests/unit/fn-entity-args.test.ts → suffix match
99
+ # (that file lives at one.ie/web/tests/unit/…)
100
+ # (b) is resolved against the repo's file universe (tracked + untracked-not-
101
+ # ignored, one `git ls-files` call). An AMBIGUOUS suffix — more than one hit —
102
+ # is NOT skipped: every hit is checked. "Ambiguous → skip" is exactly how this
103
+ # bug walks through the gate a second time.
104
+ resolve() { # $1 = candidate; prints 0+ repo-relative paths
105
+ local cand="$1"
106
+ if [ -e "$ROOT/$cand" ]; then printf '%s\n' "$cand"; return 0; fi
107
+ printf '%s\n' "$INDEX" | awk -v c="$cand" '
108
+ BEGIN { n=length(c) }
109
+ $0==c { print; next }
110
+ length($0) > n && substr($0, length($0)-n) == "/" c { print }
111
+ '
112
+ }
113
+
114
+ check() { # $1 = slug
115
+ local slug="$1"
116
+ local promise="$ROOT/text/$slug.md" todo="$ROOT/text/$slug-todo.md"
117
+ local block="" found=0 bad=0 checked=0 unresolved=0 cand path
118
+
119
+ git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 \
120
+ || { cannot "$ROOT is not a git repo — trackedness is unanswerable here"; return 3; }
121
+
122
+ if [ -f "$promise" ]; then block+=$(deliverables_block "$promise")$'\n'; found=1
123
+ else say "no promise at text/$slug.md"; fi
124
+ if [ -f "$todo" ]; then block+=$(deliverables_block "$todo")$'\n'; found=1
125
+ else say "no todo at text/$slug-todo.md"; fi
126
+ [ "$found" -eq 1 ] || { cannot "$slug — neither text/$slug.md nor text/$slug-todo.md exists"; return 3; }
127
+
128
+ INDEX="$(git -C "$ROOT" ls-files --cached --others --exclude-standard)"
129
+
130
+ while IFS= read -r cand; do
131
+ [ -z "$cand" ] && continue
132
+ local hits; hits="$(resolve "$cand")"
133
+ if [ -z "$hits" ]; then
134
+ unresolved=$((unresolved+1)); say "skip (not on disk): $cand"; continue
135
+ fi
136
+ while IFS= read -r path; do
137
+ [ -z "$path" ] && continue
138
+ [ -d "$ROOT/$path" ] && { say "skip (directory): $path"; continue; }
139
+ checked=$((checked+1))
140
+ if git -C "$ROOT" ls-files --error-unmatch -- "$path" >/dev/null 2>&1; then
141
+ say "tracked: $path"; continue
142
+ fi
143
+ # tracked-first is deliberate: a file that is both tracked and matched by
144
+ # .gitignore is tracked, and git ships it.
145
+ if git -C "$ROOT" check-ignore -q -- "$path" 2>/dev/null; then
146
+ red "GITIGNORED deliverable: $path — exists, matched by .gitignore, CAN NEVER SHIP (named by $slug)"
147
+ else
148
+ red "UNTRACKED deliverable: $path — exists on disk, absent from git; \`git add $path\` or it never reaches main"
149
+ fi
150
+ bad=$((bad+1))
151
+ done <<<"$hits"
152
+ done < <(printf '%s' "$block" | candidates)
153
+
154
+ if [ "$bad" -gt 0 ]; then
155
+ echo "[untracked-gate] $slug — $bad/$checked existing deliverable file(s) not in git" >&2
156
+ return 1
157
+ fi
158
+ echo "[untracked-gate] $slug — ok ($checked existing deliverable file(s) tracked, $unresolved path(s) not on disk / not resolvable)"
159
+ return 0
160
+ }
161
+
162
+ # ── self-test ────────────────────────────────────────────────────────────
163
+ # Drives a GREEN fixture and a RED fixture through a real temp git repo. No
164
+ # commit is needed: `git ls-files --error-unmatch` answers from the INDEX, so
165
+ # `git add` alone makes a file tracked and the test needs no user.name/email.
166
+ # The red fixture also exercises the package-relative suffix resolver — the
167
+ # shape that carried half the real 2026-07-28 failure.
168
+ self_test() {
169
+ local fails=0 rc out
170
+ dir="$(mktemp -d)"; trap 'rm -rf "$dir"' EXIT
171
+ git -C "$dir" init -q
172
+ mkdir -p "$dir/text" "$dir/packages/sdk/src" "$dir/web/tests/unit"
173
+
174
+ # A promise + todo naming two deliverables: one repo-relative, one package-relative.
175
+ printf -- '---\nslug: green\ndeliverables:\n - item: "the allowlist"\n accept: "test -f packages/sdk/src/fn-allowlist.ts"\n---\n' \
176
+ >"$dir/text/green.md"
177
+ printf -- '---\nslug: green\ndeliverables:\n - test: "tests/unit/fn-entity-args.test.ts — entity-param binding proven (C7)"\n\nux_before: "x"\n---\n' \
178
+ >"$dir/text/green-todo.md"
179
+ sed 's/green/red/' "$dir/text/green.md" >"$dir/text/red.md"
180
+ sed 's/green/red/' "$dir/text/green-todo.md" >"$dir/text/red-todo.md"
181
+
182
+ : >"$dir/packages/sdk/src/fn-allowlist.ts"
183
+ : >"$dir/web/tests/unit/fn-entity-args.test.ts"
184
+ git -C "$dir" add text packages/sdk/src/fn-allowlist.ts >/dev/null 2>&1
185
+
186
+ # RED: the package-relative test file exists but was never `git add`ed —
187
+ # the exact 2026-07-28 shape.
188
+ ROOT="$dir" VERBOSE=0 check red >/dev/null 2>&1; rc=$?
189
+ [ "$rc" -eq 1 ] || { echo "FAIL red fixture: expected exit 1, got $rc"; fails=$((fails+1)); }
190
+ # captured, never piped into `grep -q`: pipefail + grep's early exit SIGPIPEs
191
+ # the producer and reports 141 for a pipeline that actually matched.
192
+ out="$(ROOT="$dir" VERBOSE=0 check red 2>&1)"
193
+ case "$out" in *fn-entity-args.test.ts*) ;; *)
194
+ echo "FAIL red fixture: reason must name the untracked file"; fails=$((fails+1)) ;; esac
195
+
196
+ # GREEN: same fixture once the file is tracked.
197
+ git -C "$dir" add web/tests/unit/fn-entity-args.test.ts >/dev/null 2>&1
198
+ ROOT="$dir" VERBOSE=0 check green >/dev/null 2>&1; rc=$?
199
+ [ "$rc" -eq 0 ] || { echo "FAIL green fixture: expected exit 0, got $rc"; fails=$((fails+1)); }
200
+
201
+ # GITIGNORED: a deliverable git can never ship must be loud, not silent.
202
+ printf 'packages/sdk/src/fn-allowlist.ts\n' >"$dir/.gitignore"
203
+ git -C "$dir" rm --cached -q packages/sdk/src/fn-allowlist.ts >/dev/null 2>&1
204
+ out="$(ROOT="$dir" VERBOSE=0 check green 2>&1)"
205
+ case "$out" in *GITIGNORED*) ;; *)
206
+ echo "FAIL gitignored fixture: expected a GITIGNORED reason"; fails=$((fails+1)) ;; esac
207
+
208
+ # BLOCK BOUNDARY: a `deliverables: # trailing comment` header with its entries
209
+ # at COLUMN 0. Both shapes exist in text/ and both used to yield an empty block
210
+ # — i.e. exit 0 having checked nothing. The fixture must go RED.
211
+ mkdir -p "$dir/text" "$dir/src"
212
+ : >"$dir/src/lonely.ts"
213
+ printf -- '---\nslug: shape\ndeliverables: # THE SCHEDULE\n- item: "a thing"\n accept: "test -f src/lonely.ts"\n\nproof: "x"\n---\n' \
214
+ >"$dir/text/shape.md"
215
+ ROOT="$dir" VERBOSE=0 check shape >/dev/null 2>&1; rc=$?
216
+ [ "$rc" -eq 1 ] || { echo "FAIL block-boundary: comment header + column-0 entries must still parse (got $rc)"; fails=$((fails+1)); }
217
+
218
+ # CANNOT RUN: no promise and no todo → 3, never 1.
219
+ ROOT="$dir" check nosuchslug >/dev/null 2>&1; rc=$?
220
+ [ "$rc" -eq 3 ] || { echo "FAIL missing-slug: expected exit 3, got $rc"; fails=$((fails+1)); }
221
+
222
+ echo "[untracked-gate] self-test: 6 assertions, $fails failed"
223
+ return "$fails"
224
+ }
225
+
226
+ case "${1:-}" in
227
+ --self-test) self_test; exit $? ;;
228
+ ""|-*) echo "usage: do-untracked-gate.sh <slug> [--verbose] | --self-test" >&2; exit 4 ;;
229
+ esac
230
+ [ "${2:-}" = "--verbose" ] && VERBOSE=1
231
+ check "$1"