@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,88 @@
1
+ #!/usr/bin/env bash
2
+ # do-derives-check.sh — assert every artifact a promise's derives: block declares
3
+ # `true` actually EXISTS on disk. Zero LLM; the file's presence is the verdict.
4
+ #
5
+ # WHY: text/security-gates.md declared `docs: true` but text/security-gates-docs.md
6
+ # was never written — the promise settled green on a lie. This gate closes that
7
+ # class: a declared derives: artifact that is missing fails the check, so /close
8
+ # (via do-promise-settle.sh) cannot mark the promise KEPT until the file exists.
9
+ #
10
+ # Usage:
11
+ # do-derives-check.sh <slug> # exit 0 if every true file-key exists, else 1
12
+ # do-derives-check.sh --self-test # temp fixtures, zero network
13
+ # exit: 0 all present · 1 an artifact missing · 2 no promise file · 4 bad usage
14
+ #
15
+ # SCOPE — file-shaped keys ONLY. The derives: block mixes two kinds of key:
16
+ # (a) 1:1 DOC keys → a single deterministic text/<slug>-<key>.md path:
17
+ # plan · todo · docs · tutorial · how-to · reference · features · ui — CHECKED.
18
+ # (b) named-entity / multi-file keys with NO single deterministic path — SKIPPED:
19
+ # tests (per-surface files, not one text/<slug>.test.ts),
20
+ # agents (a named actor at one.ie/ai/agents/*, or the -agents-docs.md
21
+ # briefing — ambiguous; security-gates' agents:true is a compliance
22
+ # actor, NOT text/security-gates-agents.md — checking it false-fails),
23
+ # skills · tasks · tracking · routing · views (world: entries — presence-checked
24
+ # by the world: traversal + the proof's own test run, not a filename).
25
+ # v1: simple + deterministic, matched to the concrete bug (a missing DOC).
26
+ set -u
27
+
28
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
29
+
30
+ # derives keys that map 1:1 to text/<slug>-<key>.md (padded for whole-word match)
31
+ FILE_KEYS=" plan todo docs tutorial how-to reference features ui "
32
+
33
+ # print each derives sub-key whose value is exactly `true` (first frontmatter block).
34
+ # Ends the block on the first column-0 non-space, non-comment line (e.g. `rubric:` /
35
+ # `world:`); blank + col-0 comment lines inside the block are tolerated. world.agents
36
+ # is never counted — only `^derives:` opens the block.
37
+ true_derives_keys() { # $1 = promise file
38
+ awk '
39
+ /^---[[:space:]]*$/ { fm++; if (fm>=2) exit; next }
40
+ fm!=1 { next }
41
+ /^derives:[[:space:]]*$/ { ind=1; next }
42
+ ind==1 && /^[^[:space:]#]/ { ind=0 }
43
+ ind==1 && /^[[:space:]]+[A-Za-z][A-Za-z-]*:[[:space:]]+true([[:space:]]|#|$)/ {
44
+ k=$0; sub(/^[[:space:]]+/,"",k); sub(/:.*/,"",k); print k
45
+ }
46
+ ' "$1"
47
+ }
48
+
49
+ check() { # $1 = slug
50
+ local slug="$1"
51
+ local file="$ROOT/text/$slug.md" missing=0 checked=0 key
52
+ if [ ! -f "$file" ]; then
53
+ echo "[derives-check] $slug — no promise file at text/$slug.md"; return 2
54
+ fi
55
+ while IFS= read -r key; do
56
+ [ -z "$key" ] && continue
57
+ case "$FILE_KEYS" in *" $key "*) ;; *) continue ;; esac # skip named-entity keys
58
+ checked=$((checked+1))
59
+ if [ ! -f "$ROOT/text/$slug-$key.md" ]; then
60
+ echo "[derives-check] MISSING: derives.$key=true but text/$slug-$key.md does not exist"
61
+ missing=$((missing+1))
62
+ fi
63
+ done < <(true_derives_keys "$file")
64
+ if [ "$missing" -gt 0 ]; then
65
+ echo "[derives-check] $slug — $missing/$checked declared artifact(s) missing"; return 1
66
+ fi
67
+ echo "[derives-check] $slug — ok ($checked file-shaped derives artifact(s) present)"; return 0
68
+ }
69
+
70
+ if [ "${1:-}" = "--self-test" ]; then
71
+ dir="$(mktemp -d)"; trap 'rm -rf "$dir"' EXIT
72
+ mkdir -p "$dir/text"
73
+ fails=0
74
+ # green: docs:true + doc present; agents:true present but MUST be skipped (no -agents.md)
75
+ printf -- '---\nslug: green\nderives:\n plan: true\n docs: true\n agents: true\n---\n' >"$dir/text/green.md"
76
+ : >"$dir/text/green-plan.md"; : >"$dir/text/green-docs.md"
77
+ # red: docs:true but the doc is missing
78
+ printf -- '---\nslug: red\nderives:\n plan: true\n docs: true\n---\n' >"$dir/text/red.md"
79
+ : >"$dir/text/red-plan.md"
80
+ ROOT="$dir" check green >/dev/null 2>&1 || { echo "FAIL green should pass (agents skipped)"; fails=$((fails+1)); }
81
+ ROOT="$dir" check red >/dev/null 2>&1 && { echo "FAIL red should fail (missing docs)"; fails=$((fails+1)); }
82
+ echo "[derives-check] self-test: 2 fixtures, $fails failed"
83
+ exit "$fails"
84
+ fi
85
+
86
+ SLUG="${1:-}"
87
+ [ -z "$SLUG" ] && { echo "usage: do-derives-check.sh <slug> | --self-test" >&2; exit 4; }
88
+ check "$SLUG"
@@ -31,6 +31,11 @@ RANK="$ROOT/.claude/scripts/do-rank.py"
31
31
  AUTO="$ROOT/.claude/scripts/do-auto.sh"
32
32
  WT_BASE="$ROOT/.do-worktrees"
33
33
 
34
+ # Worktree branches cut from trunk, never HEAD — a shared tree parked on a
35
+ # feature branch must not seed fleet plans with its commits. DO_BASE_REF overrides.
36
+ BASE_REF="${DO_BASE_REF:-main}"
37
+ git -C "$ROOT" show-ref --verify --quiet "refs/heads/$BASE_REF" || BASE_REF=HEAD
38
+
34
39
  GO=false
35
40
  SLOTS=3
36
41
  TOP=40
@@ -131,11 +136,50 @@ has_conflict() { # has_conflict <slug> → exit 0 if conflict
131
136
  return 1
132
137
  }
133
138
 
139
+ # claim_task — fires tasks:claim before a worktree is cut (claim = lease, tasks-do C5).
140
+ # Best-effort like do-signal.sh's world:do-event stance: no configured secret or an
141
+ # unparseable response degrades to "ok to launch" rather than blocking the fleet. An
142
+ # explicit already-claimed/blocked response returns 1 so fill_slots skips this slug —
143
+ # two fleet runs (or a fleet run racing a human claim) never double-build the same task.
144
+ claim_task() { # claim_task <slug> → 0 ok to launch, 1 already claimed elsewhere
145
+ # GATEWAY_API_KEY, not SERVER_SECRET — /api/ask/<receiver>'s isServiceCaller checks
146
+ # env.GATEWAY_API_KEY (one.ie/web/src/pages/api/ask/[...receiver].ts). SERVER_SECRET
147
+ # is what /api/signal/ checks; the two routes don't share a secret. Also pin
148
+ # workspace so the claim's ctx.ownerSlug lands in /u/one/tasks like every other
149
+ # Claude Code task (do-signal.sh's CC_TASKS_WORKSPACE convention).
150
+ # Address the lease by the plan SLUG, never a guessed tid: real tids are minted
151
+ # task:<traceId> (resolvers/tasks.ts insertTaskRow), so "task:<slug>" resolved
152
+ # not_found for every plan and the lease could never gate. tasks:claim resolves
153
+ # data.slug via resolveTaskBySlug when present (resolvers/tasks.ts claim).
154
+ local slug="$1" secret="" f resp
155
+ # ONE_ENV_FILE first: the loop must be able to read credentials with no
156
+ # one.ie/web/ on disk. The monorepo paths stay as fallbacks and expand to
157
+ # nothing outside it.
158
+ for f in "${ONE_ENV_FILE:-${DO_ENV_FILE:-}}" "$ROOT/.env" "$ROOT/one.ie/web/.env" "$ROOT/one.ie/web/.secrets.generated.local"; do
159
+ [ -n "$f" ] || continue
160
+ [ -f "$f" ] || continue
161
+ secret=$(grep -E '^GATEWAY_API_KEY=' "$f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//' || true)
162
+ [ -n "$secret" ] && break
163
+ done
164
+ [ -z "$secret" ] && return 0
165
+ resp=$(curl -sf -X POST "${DO_SIGNAL_URL:-https://one.ie}/api/ask/tasks:claim" \
166
+ -H "Content-Type: application/json" -H "Authorization: Bearer $secret" \
167
+ -d "{\"data\":{\"slug\":\"$slug\",\"workspace\":\"${CC_TASKS_WORKSPACE:-one}\"}}" 2>/dev/null || true)
168
+ case "$resp" in
169
+ *'"ok":true'*) return 0 ;; # lease held by us — launch
170
+ *'"error":"already-claimed"'*) echo " ~ $slug — claimed by another actor, skipping" >&2; return 1 ;;
171
+ *'"error":"blocked"'*) echo " ~ $slug — blocked by an unresolved dependency, skipping" >&2; return 1 ;;
172
+ *'"error":"not_found"'*) return 0 ;; # no substrate task yet (claim-before-create) — launch
173
+ *'"error"'*) echo " ! $slug — claim degraded: $resp" >&2; return 0 ;;
174
+ *) return 0 ;; # unparseable/empty — best-effort, launch
175
+ esac
176
+ }
177
+
134
178
  launch_plan() { # launch_plan <slug> → echoes PID
135
179
  local slug="$1"
136
180
  local wt="$WT_BASE/$slug"
137
181
  if [ ! -d "$wt" ]; then
138
- git -C "$ROOT" worktree add -b "do/$slug" "$wt" HEAD >/dev/null 2>&1 \
182
+ git -C "$ROOT" worktree add -b "do/$slug" "$wt" "$BASE_REF" >/dev/null 2>&1 \
139
183
  || git -C "$ROOT" worktree add "$wt" "do/$slug" >/dev/null 2>&1 || true
140
184
  fi
141
185
  link_deps "$wt"
@@ -260,6 +304,11 @@ fill_slots() {
260
304
  while [ "${#SLOT_PIDS[@]}" -lt "$SLOTS" ]; do
261
305
  pop_next
262
306
  [ -z "$NEXT_SLUG" ] && break
307
+ if ! claim_task "$NEXT_SLUG"; then
308
+ echo " ~ $NEXT_SLUG — already claimed elsewhere, skipping"
309
+ DONE_SLUGS+=("$NEXT_SLUG")
310
+ continue
311
+ fi
263
312
  local pid; pid=$(launch_plan "$NEXT_SLUG")
264
313
  SLOT_PIDS+=("$pid")
265
314
  SLOT_SLUGS+=("$NEXT_SLUG")
@@ -54,27 +54,46 @@ def parse_outcome(text):
54
54
  lines = [re.sub(r"\\$", "", l.strip()) for l in bm.group(1).splitlines() if l.strip()]
55
55
  return " ".join(lines).strip()
56
56
  # inline form: outcome: "..." or outcome: ...
57
- im = re.search(r'^outcome:\s*"?(.+?)"?\s*$', body, re.MULTILINE)
57
+ # A double-quoted YAML scalar must be unescaped (\"", \\ → \) and may
58
+ # carry a trailing comment after the closing quote — match the quoted
59
+ # string properly first, else the escapes leak into the shell verbatim
60
+ # and grep hunts for quote characters that were never in the file.
61
+ qm = re.search(r'^outcome:\s*"((?:[^"\\]|\\.)*)"', body, re.MULTILINE)
62
+ if qm:
63
+ return re.sub(r'\\(.)', r'\1', qm.group(1)).strip()
64
+ # single-quoted YAML scalar: '' is the only escape (a literal ')
65
+ sm = re.search(r"^outcome:\s*'((?:[^']|'')*)'", body, re.MULTILINE)
66
+ if sm:
67
+ return sm.group(1).replace("''", "'").strip()
68
+ im = re.search(r'^outcome:\s*(.+?)\s*$', body, re.MULTILINE)
58
69
  return im.group(1).strip() if im else ""
59
70
 
60
71
 
61
72
  def stale_cd(cmd):
62
73
  """Walk the &&/; chain tracking cwd — `cd a && cd ../b` makes ../b relative
63
- to a, not to root. Return the first cd target that doesn't exist, else None."""
74
+ to a, not to root. A `(cd x && …)` subshell inherits the outer cwd but its
75
+ cd does NOT leak past the closing paren (outcomes are commonly chains of
76
+ subshells: `(cd web && test) && (cd channels && test)`). Parens inside
77
+ quotes (grep patterns) can unbalance the stack; that degrades to the outer
78
+ cwd, never a crash. Return the first cd target that doesn't exist, else None."""
64
79
  cwd = ROOT
80
+ stack = []
65
81
  for seg in re.split(r"&&|;|\|", cmd):
66
- m = re.search(r"\bcd\s+([^\s&|;]+)", seg)
67
- if not m:
68
- continue
69
- t = m.group(1)
70
- nxt = Path(t) if t.startswith("/") else (cwd / t)
71
- try:
72
- nxt = nxt.resolve()
73
- except Exception:
74
- return t
75
- if not nxt.exists():
76
- return t
77
- cwd = nxt
82
+ for _ in range(seg.count("(")):
83
+ stack.append(cwd)
84
+ m = re.search(r"\bcd\s+([^\s&|;)]+)", seg)
85
+ if m:
86
+ t = m.group(1)
87
+ nxt = Path(t) if t.startswith("/") else (cwd / t)
88
+ try:
89
+ nxt = nxt.resolve()
90
+ except Exception:
91
+ return t
92
+ if not nxt.exists():
93
+ return t
94
+ cwd = nxt
95
+ for _ in range(seg.count(")")):
96
+ cwd = stack.pop() if stack else ROOT
78
97
  return None
79
98
 
80
99
 
@@ -57,6 +57,31 @@ assumes_entries() { # $1 = file
57
57
  | while IFS= read -r l; do unquote "$l"; echo; done
58
58
  }
59
59
 
60
+ # exec_dry — IC6: catch a path-shape-fragile proof at PROMISE, before it dies
61
+ # at settle. Runs proof: for real (env -i, from $base) and inspects stderr for
62
+ # the classic bug: two deliverables' accept: joined by `&&`, each independently
63
+ # written assuming CWD=repo-root, e.g. "cd A && test 1 && cd A && test 2" — the
64
+ # second bare `cd A` runs from inside A (after the first leg), tries A/A, dies.
65
+ # Subshell-wrapped legs ("(cd A && test 1) && (cd A && test 2)") reset CWD
66
+ # between legs and never hit this. A non-zero exit alone is NOT a failure here —
67
+ # red-before-green proofs are legitimately red; only the path-shape death is.
68
+ exec_dry() { # $1 = file, $2 = base dir (default $ROOT)
69
+ local file="$1" base="${2:-$ROOT}" proof err
70
+ proof="$(extract_proof "$file")"
71
+ if [ -z "$proof" ]; then
72
+ echo "[promise-lint:exec-dry] FAIL — no proof: to test"
73
+ return 1
74
+ fi
75
+ err="$(cd "$base" && env -i HOME="$HOME" PATH="$PATH" bash -c "$proof" 2>&1 1>/dev/null)"
76
+ if printf '%s' "$err" | grep -qE 'cd: .*No such file or directory'; then
77
+ echo "[promise-lint:exec-dry] FAIL — path-shape death (a repeated bare 'cd X &&' leg dies once a prior leg already moved CWD into X):"
78
+ printf '%s\n' "$err" | grep -E 'cd: .*No such file or directory' | sed 's/^/ /'
79
+ return 1
80
+ fi
81
+ echo "[promise-lint:exec-dry] OK — proof is path-shape safe (a non-zero exit here is fine; red-before-green is legitimate)"
82
+ return 0
83
+ }
84
+
60
85
  lint() { # $1 = slug, $2 = file, $3 = no_run (true/false), $4 = red (true/false)
61
86
  local slug="$1" file="$2" no_run="$3" red="$4" fails=0
62
87
  echo "[promise-lint] slug=$slug file=${file#"$ROOT"/}"
@@ -106,6 +131,28 @@ lint() { # $1 = slug, $2 = file, $3 = no_run (true/false), $4 = red (true/false)
106
131
  $(printf '%s\n' "$block" | grep '^[[:space:]]*accept:')
107
132
  EOF
108
133
 
134
+ # user-visible ⇒ page: a deliverable that explicitly names a page/route/view
135
+ # (kind prefix `route:`/`page:`/`view:`/`component:`, an .astro / src/pages/
136
+ # path, or the phrase "a page") must be proven by at least one accept: that
137
+ # actually checks the page — curl / browser-check / do-prove / an http(s) URL /
138
+ # a `--route /x` flag / a src/pages/ or .astro file check.
139
+ # Heuristic is deliberately conservative (deliverables text only, NOT the
140
+ # world: block) so settled promises whose UI is proven via vitest/grep
141
+ # (brain, chat) don't retro-fail; it catches new promises that schedule a
142
+ # page but never load it.
143
+ local vis_re='(route|page|view|component):|src/pages/|\.astro| a page '
144
+ local page_proof_re='curl|browser-check|do-prove|https?://|--route[[:space:]]+/|src/pages/|\.astro'
145
+ local vis_item
146
+ vis_item="$(printf '%s\n' "$block" | grep -E '^[[:space:]]*-[[:space:]]*item:' | grep -iE "$vis_re" | head -1)"
147
+ if [ -n "$vis_item" ]; then
148
+ if ! printf '%s\n' "$block" | grep '^[[:space:]]*accept:' | grep -qiE "$page_proof_re"; then
149
+ echo "[promise-lint] FAIL — user-visible deliverable but no page-load proof:"
150
+ echo "[promise-lint] $(printf '%s' "$vis_item" | sed -E 's/^[[:space:]]*//')"
151
+ echo "[promise-lint] add a route/component deliverable whose accept: loads the page (curl / browser-check / do-prove.sh --route /x / src/pages file check)"
152
+ fails=$((fails+1))
153
+ fi
154
+ fi
155
+
109
156
  # assumes: — client-side dependencies must be green at the making
110
157
  if [ "$no_run" != true ]; then
111
158
  local a_n=0
@@ -139,6 +186,60 @@ EOF
139
186
  return 1
140
187
  }
141
188
 
189
+ # check_ui_coupling — IC9 lint coupling: when a promise's derives: block has
190
+ # `ui: true`, its proof: must invoke `do-ui-gate.sh <slug>` (that promise's own
191
+ # filename stem) — a UI-bearing promise can't settle without its screens
192
+ # actually being gate-checked.
193
+ check_ui_coupling() { # $1 = file
194
+ local file="$1" slug ui_val proof
195
+ slug="$(basename "$file" .md)"
196
+ ui_val="$(fm "$file" | awk '/^derives:/{on=1;next} on && /^[a-zA-Z_-]+:/{exit} on && /^[[:space:]]*ui:/{sub(/^[[:space:]]*ui:[[:space:]]*/,"");print;exit}')"
197
+ ui_val="$(unquote "$ui_val" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
198
+ if [ "$ui_val" != "true" ]; then
199
+ echo "[promise-lint:ui-coupling] skip — derives.ui != true (slug=$slug)"
200
+ return 0
201
+ fi
202
+ proof="$(extract_proof "$file")"
203
+ if printf '%s' "$proof" | grep -qF "do-ui-gate.sh $slug"; then
204
+ echo "[promise-lint:ui-coupling] OK — proof invokes do-ui-gate.sh $slug"
205
+ return 0
206
+ fi
207
+ echo "[promise-lint:ui-coupling] FAIL — derives.ui: true but proof: does not invoke 'do-ui-gate.sh $slug'"
208
+ return 1
209
+ }
210
+
211
+ # ── self-test: ui-coupling fixtures, zero network ────────────────────────────
212
+ if [ "${1:-}" = "--self-test-ui-coupling" ]; then
213
+ dir="$(mktemp -d)"; trap 'rm -rf "$dir"' EXIT
214
+ cat >"$dir/uinocheck.md" <<'F'
215
+ ---
216
+ derives:
217
+ ui: true
218
+ proof: "true"
219
+ ---
220
+ F
221
+ cat >"$dir/uigood.md" <<'F'
222
+ ---
223
+ derives:
224
+ ui: true
225
+ proof: "bash .claude/scripts/do-ui-gate.sh uigood"
226
+ ---
227
+ F
228
+ cat >"$dir/nouify.md" <<'F'
229
+ ---
230
+ derives:
231
+ ui: false
232
+ proof: "true"
233
+ ---
234
+ F
235
+ fails=0
236
+ check_ui_coupling "$dir/uinocheck.md" >/dev/null && { echo "FAIL: expected ui-coupling FAIL when ui:true but proof lacks do-ui-gate.sh"; fails=$((fails+1)); }
237
+ check_ui_coupling "$dir/uigood.md" >/dev/null || { echo "FAIL: expected ui-coupling PASS when proof invokes do-ui-gate.sh <slug>"; fails=$((fails+1)); }
238
+ check_ui_coupling "$dir/nouify.md" >/dev/null || { echo "FAIL: expected ui-coupling PASS (skip) when ui:false"; fails=$((fails+1)); }
239
+ echo "[promise-lint] self-test-ui-coupling: 3 fixtures, $fails failed"
240
+ exit "$fails"
241
+ fi
242
+
142
243
  # ── self-test: fixtures, zero network ────────────────────────────────────────
143
244
  if [ "${1:-}" = "--self-test" ]; then
144
245
  dir="$(mktemp -d)"; trap 'rm -rf "$dir"' EXIT
@@ -194,6 +295,22 @@ assumes:
194
295
  - "false"
195
296
  proof: "test -f a"
196
297
  ---
298
+ F
299
+ cat >"$dir/pagegap.md" <<'F'
300
+ ---
301
+ deliverables:
302
+ - item: "route: /foo — user sees the page"
303
+ accept: "grep -q foo a.ts"
304
+ proof: "grep -q foo a.ts"
305
+ ---
306
+ F
307
+ cat >"$dir/pagegood.md" <<'F'
308
+ ---
309
+ deliverables:
310
+ - item: "route: /foo — user sees the page"
311
+ accept: "curl -sf https://one.ie/foo"
312
+ proof: "curl -sf https://one.ie/foo"
313
+ ---
197
314
  F
198
315
  fails=0
199
316
  lint good "$dir/good.md" true false >/dev/null || { echo "FAIL good → 0"; fails=$((fails+1)); }
@@ -203,19 +320,48 @@ F
203
320
  lint emptyproof "$dir/emptyproof.md" true false >/dev/null && { echo "FAIL emptyproof → 1"; fails=$((fails+1)); }
204
321
  lint badassume "$dir/badassume.md" false false >/dev/null && { echo "FAIL badassume → 1"; fails=$((fails+1)); }
205
322
  lint redcheck "$dir/gap.md" true true >/dev/null && { echo "FAIL red-on-gap → 1"; fails=$((fails+1)); }
206
- echo "[promise-lint] self-test: 7 fixtures, $fails failed"
323
+ lint pagegap "$dir/pagegap.md" true false >/dev/null && { echo "FAIL pagegap 1 (user-visible, no page proof)"; fails=$((fails+1)); }
324
+ lint pagegood "$dir/pagegood.md" true false >/dev/null || { echo "FAIL pagegood → 0"; fails=$((fails+1)); }
325
+ echo "[promise-lint] self-test: 9 fixtures, $fails failed"
326
+ exit "$fails"
327
+ fi
328
+
329
+ # ── self-test: exec-dry fixtures, zero network ───────────────────────────────
330
+ if [ "${1:-}" = "--self-test-exec-dry" ]; then
331
+ dir="$(mktemp -d)"; trap 'rm -rf "$dir"' EXIT
332
+ mkdir -p "$dir/leg"
333
+ touch "$dir/leg/marker"
334
+ cat >"$dir/failcase.md" <<'F'
335
+ ---
336
+ proof: "cd leg && test -f marker && cd leg && test -f marker"
337
+ ---
338
+ F
339
+ cat >"$dir/passcase.md" <<'F'
340
+ ---
341
+ proof: "(cd leg && test -f marker) && (cd leg && test -f marker)"
342
+ ---
343
+ F
344
+ fails=0
345
+ exec_dry "$dir/failcase.md" "$dir" >/dev/null && { echo "FAIL: expected exec-dry FAIL on repeated bare 'cd leg &&' legs"; fails=$((fails+1)); }
346
+ exec_dry "$dir/passcase.md" "$dir" >/dev/null || { echo "FAIL: expected exec-dry PASS on subshell-wrapped legs"; fails=$((fails+1)); }
347
+ echo "[promise-lint] self-test-exec-dry: 2 fixtures, $fails failed"
207
348
  exit "$fails"
208
349
  fi
209
350
 
210
351
  # ── main ─────────────────────────────────────────────────────────────────────
211
352
  SLUG="${1:-}"; [ -z "$SLUG" ] && { grep '^# ' "$0" | sed 's/^# //' | sed -n '3,24p'; exit 4; }
212
353
  shift
213
- NO_RUN=false; RED=false
354
+ NO_RUN=false; RED=false; EXEC_DRY=false
214
355
  while [ $# -gt 0 ]; do
215
356
  case "$1" in
216
- --no-run) NO_RUN=true; shift ;;
217
- --red) RED=true; shift ;;
357
+ --no-run) NO_RUN=true; shift ;;
358
+ --red) RED=true; shift ;;
359
+ --exec-dry) EXEC_DRY=true; shift ;;
218
360
  *) echo "unknown flag $1"; exit 4 ;;
219
361
  esac
220
362
  done
363
+ if $EXEC_DRY; then
364
+ exec_dry "${PROMISE_FILE:-$ROOT/text/$SLUG.md}"
365
+ exit $?
366
+ fi
221
367
  lint "$SLUG" "${PROMISE_FILE:-$ROOT/text/$SLUG.md}" "$NO_RUN" "$RED"
@@ -24,6 +24,35 @@ set -u
24
24
  ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
25
25
  BASE="${ONE_API_URL:-http://localhost:4321}"
26
26
 
27
+ # Credentials come from ONE_ENV_FILE (DO_ENV_FILE is the accepted alias), the same
28
+ # indirection do-signal.sh and do-fleet.sh honour and that factory-repo.sh's
29
+ # --check-env-indirection enforces for every shipped credential-reading script.
30
+ #
31
+ # This script read $GATEWAY_API_KEY from the PROCESS environment only, and nothing
32
+ # exports it — so it never sent an Authorization header and every mark/warn 401'd.
33
+ # The failure was invisible because a 401 printed "wire unreachable, broadcast-only":
34
+ # the settle blaming the network for its own missing credential. Measured 2026-08-02
35
+ # settling pages-fill-pack and tauri — both proofs exit 0, both marks 401, against a
36
+ # localhost:4321 answering 200. tauri had already settled KEPT this way once before.
37
+ ONE_ENV_FILE="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.env}}"
38
+ DO_ENV_FILE="$ONE_ENV_FILE"
39
+
40
+ # Unquoted on purpose: expands to nothing outside the monorepo, so the loop
41
+ # degrades to ONE_ENV_FILE alone rather than erroring.
42
+ _MONOREPO_ENV_CANDIDATES="$ROOT/one.ie/web/.env $ROOT/one.ie/web/.dev.vars"
43
+
44
+ # Reads a key out of the first env file that carries it. Never echoes the value
45
+ # anywhere but its own stdout, which the caller assigns straight to a local.
46
+ _env_key() { # $1 = key name
47
+ local _f _v
48
+ for _f in "$ONE_ENV_FILE" "$ROOT/.env" $_MONOREPO_ENV_CANDIDATES; do
49
+ [ -f "$_f" ] || continue
50
+ _v=$(grep -E "^$1=" "$_f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//' || true)
51
+ [ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
52
+ done
53
+ printf ''
54
+ }
55
+
27
56
  usage() { grep '^# ' "$0" | sed 's/^# //' | sed -n '3,14p'; exit 4; }
28
57
 
29
58
  # ── frontmatter proof: extractor (first frontmatter block only) ──────────────
@@ -82,6 +111,55 @@ settle() { # $1 = slug, $2 = promise file, $3 = composite ("" = none), $4 = dry-
82
111
  local rc=0
83
112
  ( cd "$ROOT" && bash -c "$proof" ) >/dev/null 2>&1 || rc=$?
84
113
 
114
+ # EXIT 3 IS "COULD NOT RUN", NOT "BROKEN". A proof: is an `&&`-join, so a leg
115
+ # that cannot reach its substrate stops the chain and the join returns 3 — and
116
+ # this script used to fold that into `else → warn`. A TypeDB blip would then
117
+ # deposit resistance on a proven path, permanently, for infrastructure noise:
118
+ # the settle would record a promise as BROKEN for a reason the maker cannot
119
+ # fix and did not cause. text/factory.md § "A note on red vs cannot run" states
120
+ # the rule — "exit 3 anywhere in the chain means re-run later, never warn" —
121
+ # and until now the tool that enforces promises did not implement the sharpest
122
+ # idea in them.
123
+ #
124
+ # An unverifiable promise closes NOTHING: no mark, no warn, no on-chain settle,
125
+ # and the loop stays open by design. It is not `dissolved` either — dissolved
126
+ # means there is no observable to run (rule 1's third exit); here the observable
127
+ # exists and was simply unreachable. The caller re-runs later. /close must treat
128
+ # exit 3 as "not settled yet", never as a close.
129
+ if [ "$rc" -eq 3 ]; then
130
+ echo "[promise-settle] proof exit=3 → UNVERIFIABLE (substrate unreachable) — no mark, no warn, promise stays open"
131
+ echo "[promise-settle] re-run when the substrate answers: $0 $slug"
132
+ if [ "$dry" = true ]; then
133
+ echo "[promise-settle] DRY would write no path and broadcast unverifiable"
134
+ return 3
135
+ fi
136
+ # Announce it anyway: an unverifiable settle is news the world can act on
137
+ # (someone can fix the cluster), and the broadcast writes no path.
138
+ # shellcheck source=/dev/null
139
+ source "$ROOT/.claude/hooks/lib/signal.sh" 2>/dev/null && \
140
+ emit_world "promise" "fyi" "promise,do,unverifiable,$slug" \
141
+ "promise $slug UNVERIFIABLE — proof could not run; no path written" "proof_exit=3"
142
+ echo "[promise-settle] broadcast world:announce queued (fire-and-forget, unconfirmed) tags=[promise,do,unverifiable,$slug]"
143
+ return 3
144
+ fi
145
+
146
+ # derives gate — a promise cannot settle KEPT while an artifact its derives: block
147
+ # declares true is missing on disk (the class that let security-gates ship a missing
148
+ # -docs.md). Zero LLM; folds into the verdict so the close still ends in mark/warn.
149
+ # Only exit 1 ("artifact missing") overrides kept→broken — exit 2 ("no promise file
150
+ # at the conventional text/<slug>.md path") is a different failure class (e.g. a
151
+ # settle() call against a custom PROMISE_FILE, or a self-test fixture slug with no
152
+ # real promise on disk) and must not be conflated with a genuinely broken promise.
153
+ if [ "$rc" -eq 0 ]; then
154
+ local derives_out derives_rc=0
155
+ derives_out="$(bash "$ROOT/.claude/scripts/do-derives-check.sh" "$slug" 2>&1)" || derives_rc=$?
156
+ if [ "$derives_rc" -eq 1 ]; then
157
+ echo "[promise-settle] derives-check FAILED — declared artifact missing; overriding kept→broken:"
158
+ echo "$derives_out" | sed 's/^/[promise-settle] /'
159
+ rc=1
160
+ fi
161
+ fi
162
+
85
163
  local verb strength verdict
86
164
  if [ "$rc" -eq 0 ]; then
87
165
  verdict=kept; verb=mark
@@ -94,7 +172,7 @@ settle() { # $1 = slug, $2 = promise file, $3 = composite ("" = none), $4 = dry-
94
172
  local edge="promise:${slug}→proof"
95
173
  if [ "$dry" = true ]; then
96
174
  echo "[promise-settle] DRY $verb $edge strength=$strength"
97
- echo "[promise-settle] DRY broadcast world:announce tags=[promise,do,$verdict,$slug]"
175
+ echo "[promise-settle] DRY broadcast world:announce queued (fire-and-forget, unconfirmed) tags=[promise,do,$verdict,$slug]"
98
176
  if [ -n "${SUI_PRIVATE_KEY:-}" ]; then
99
177
  echo "[promise-settle] DRY on-chain settle_promise kept=$([ "$verdict" = kept ] && echo true || echo false) signer=env-override"
100
178
  else
@@ -112,20 +190,46 @@ settle() { # $1 = slug, $2 = promise file, $3 = composite ("" = none), $4 = dry-
112
190
  local -a hdr=(-H 'Content-Type: application/json')
113
191
  # Secrets off argv (ps aux would leak a bare -H value) — fold both into a
114
192
  # private 0600 curl --config file instead.
193
+ # Environment wins over the env file, so an explicit export still overrides.
194
+ local gsecret gkey
195
+ gsecret="${GATEWAY_SERVICE_SECRET:-$(_env_key GATEWAY_SERVICE_SECRET)}"
196
+ gkey="${GATEWAY_API_KEY:-$(_env_key GATEWAY_API_KEY)}"
115
197
  local authcfg=""
116
- if [ -n "${GATEWAY_SERVICE_SECRET:-}" ] || [ -n "${GATEWAY_API_KEY:-}" ]; then
198
+ if [ -n "$gsecret" ] || [ -n "$gkey" ]; then
117
199
  authcfg="$(mktemp)"; chmod 600 "$authcfg"
118
- [ -n "${GATEWAY_SERVICE_SECRET:-}" ] && printf 'header = "X-Gateway-Key: %s"\n' "$GATEWAY_SERVICE_SECRET" >>"$authcfg"
119
- [ -n "${GATEWAY_API_KEY:-}" ] && printf 'header = "Authorization: Bearer %s"\n' "$GATEWAY_API_KEY" >>"$authcfg"
200
+ [ -n "$gsecret" ] && printf 'header = "X-Gateway-Key: %s"\n' "$gsecret" >>"$authcfg"
201
+ [ -n "$gkey" ] && printf 'header = "Authorization: Bearer %s"\n' "$gkey" >>"$authcfg"
120
202
  hdr+=(--config "$authcfg")
121
203
  fi
122
- http="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 -X POST "$BASE/api/$verb/$enc" \
123
- "${hdr[@]}" -d "{\"strength\":$strength,\"tags\":[\"promise\",\"do\",\"$slug\"]}" 2>/dev/null || echo 000)"
204
+ # --max-time 20, not 5. This POST is a SUBSTRATE WRITE: the route authenticates,
205
+ # then opens a TypeDB transaction against the cloud cluster, where a cold sign-in
206
+ # alone measured 4.1s (factory-check.sh header, 2026-07-29). A 5s ceiling made a
207
+ # healthy system report "wire unreachable" — the settle blaming the network for
208
+ # its own stopwatch, which is exactly what /factory's gaps board did before it
209
+ # was fixed. Measured after the change: the same call answers well inside 20s.
210
+ #
211
+ # curl's own failure is captured separately. `|| echo 000` appended a second
212
+ # "000" to the -w output, printing http=000000 — a status code that does not
213
+ # exist, in the line an operator reads to decide whether the path moved.
214
+ local curl_rc=0
215
+ http="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 -X POST "$BASE/api/$verb/$enc" \
216
+ "${hdr[@]}" -d "{\"strength\":$strength,\"tags\":[\"promise\",\"do\",\"$slug\"]}" 2>/dev/null)" || curl_rc=$?
217
+ [ "$curl_rc" -ne 0 ] && http="000 (curl rc=$curl_rc)"
124
218
  [ -n "$authcfg" ] && rm -f "$authcfg"
219
+ # Name the failure accurately. Calling a 401 "wire unreachable" is what hid the
220
+ # missing-credential bug above: an operator reading that line goes looking at the
221
+ # network for a fault that is entirely local.
125
222
  if [ "$http" = 200 ]; then
126
223
  echo "[promise-settle] $verb $edge strength=$strength http=$http"
127
224
  else
128
- echo "[promise-settle] $verb $edge strength=$strength http=$http — wire unreachable, broadcast-only"
225
+ local why
226
+ case "$http" in
227
+ 401|403) why="AUTH REJECTED — no/!bad GATEWAY_API_KEY; checked ONE_ENV_FILE=$ONE_ENV_FILE. Substrate NOT written" ;;
228
+ 000*) why="wire unreachable ($BASE)" ;;
229
+ 404) why="no such route at $BASE/api/$verb — substrate NOT written" ;;
230
+ *) why="substrate NOT written" ;;
231
+ esac
232
+ echo "[promise-settle] $verb $edge strength=$strength http=$http — $why, broadcast-only"
129
233
  fi
130
234
 
131
235
  # public broadcast — the world hears the settlement regardless of the substrate wire
@@ -133,7 +237,14 @@ settle() { # $1 = slug, $2 = promise file, $3 = composite ("" = none), $4 = dry-
133
237
  source "$ROOT/.claude/hooks/lib/signal.sh" 2>/dev/null && \
134
238
  emit_world "promise" "fyi" "promise,do,$verdict,$slug" \
135
239
  "promise $slug $verdict — $verb $edge strength=$strength" "proof_exit=$rc"
136
- echo "[promise-settle] broadcast world:announce tags=[promise,do,$verdict,$slug]"
240
+ # NOT a confirmation. emit_world curls in a background subshell, discards the
241
+ # response and returns 0 unconditionally — by design, since it also runs from
242
+ # hooks that must never block. So this line reports what was QUEUED, not what
243
+ # the world heard. Printing it as a delivery would be a settle announcing its
244
+ # own success on no evidence, which is the failure class this whole promise
245
+ # family exists to name. Confirming it needs a synchronous emitter; until then
246
+ # the wording carries the uncertainty.
247
+ echo "[promise-settle] broadcast world:announce queued (fire-and-forget, unconfirmed) tags=[promise,do,$verdict,$slug]"
137
248
 
138
249
  # on-chain twin — settle_promise on the generated Move object (schema/sui.tql →
139
250
  # substrate.move). Fires only when armed (signer present + object minted); the
@@ -183,6 +294,10 @@ if [ "${1:-}" = "--self-test" ]; then
183
294
  printf -- '---\ntitle: no proof here\n---\n' >"$dir/noproof.md"
184
295
  printf -- '---\nproof: "[ \\"a\\" = \\"a\\" ]"\n---\n' >"$dir/escaped-quotes.md"
185
296
  printf -- '---\nproof: "true"\ncontract:\n oracle: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"\n---\n' >"$dir/unmatchable-oracle.md"
297
+ # cannot-run: the `&&`-join stops at a leg that exits 3, so the join returns 3.
298
+ # The second leg must NEVER run — if it did, this fixture would return 0 and the
299
+ # test would pass for the wrong reason.
300
+ printf -- '---\nproof: "exit 3 && true"\n---\n' >"$dir/cannotrun.md"
186
301
  fails=0
187
302
  out="$(settle kept-fixture "$dir/kept.md" 0.8 true)"
188
303
  echo "$out" | grep -q 'DRY mark .*strength=4' || { echo "FAIL kept → mark strength=4"; fails=$((fails+1)); }
@@ -195,7 +310,14 @@ if [ "${1:-}" = "--self-test" ]; then
195
310
  rc=$?
196
311
  echo "$out" | grep -q 'DRY on-chain: unmatchable oracle=0xffff.*would skip' || { echo "FAIL unmatchable-oracle → clean skip, no wrong-key attempt"; fails=$((fails+1)); }
197
312
  [ "$rc" -eq 0 ] || { echo "FAIL unmatchable-oracle → exit 0"; fails=$((fails+1)); }
198
- echo "[promise-settle] self-test: 5 fixtures, $fails failed"
313
+ # cannot-run must be its OWN verdict: exit 3, and NEITHER verb written. The bug
314
+ # this fixture pins is a cluster blip settling a promise BROKEN and warning its
315
+ # path — so asserting "not mark" is not enough; assert "not warn" explicitly.
316
+ out="$(settle cannotrun-fixture "$dir/cannotrun.md" "" true)"; rc=$?
317
+ [ "$rc" -eq 3 ] || { echo "FAIL cannot-run → exit 3 (got $rc)"; fails=$((fails+1)); }
318
+ echo "$out" | grep -q 'UNVERIFIABLE' || { echo "FAIL cannot-run → says UNVERIFIABLE"; fails=$((fails+1)); }
319
+ echo "$out" | grep -qE 'DRY (warn|mark) ' && { echo "FAIL cannot-run → wrote a path verb (must write neither)"; fails=$((fails+1)); }
320
+ echo "[promise-settle] self-test: 6 fixtures, $fails failed"
199
321
  exit "$fails"
200
322
  fi
201
323