@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,1361 @@
1
+ #!/usr/bin/env bash
2
+ # factory-repo.sh — generate the standalone factory repo (github.com/one-ie/factory)
3
+ #
4
+ # The factory repo is OUTPUT, never a mirror. This script builds it from canon;
5
+ # --check rebuilds and diffs, so drift is reported loudly instead of rotting.
6
+ # Design: text/factory-repo-plan.md §1. Promise: text/factory-repo.md.
7
+ #
8
+ # Usage:
9
+ # factory-repo.sh <target-dir> build the repo (fresh git history)
10
+ # factory-repo.sh --check-history [dir] one commit · no env/key blob on any ref
11
+ # factory-repo.sh --check [dir] drift: canon vs a built tree
12
+ # factory-repo.sh --check-env-indirection
13
+ # factory-repo.sh --check-portability
14
+ # factory-repo.sh --check-targets
15
+ # factory-repo.sh --check-connect [dir]
16
+ # factory-repo.sh --check-no-secrets [dir]
17
+ # factory-repo.sh --check-tests [dir]
18
+ # factory-repo.sh --check-canon [dir] spine cites nothing the tree lacks
19
+ # factory-repo.sh --check-substrate live world-key round trip (needs ONE_ENV_FILE)
20
+ # factory-repo.sh --verify build to temp, install published, run the checks
21
+ # factory-repo.sh --manifest print the portability manifest
22
+ #
23
+ # Env:
24
+ # ONE_TARGET workspace | published (default: published)
25
+ # ONE_ENV_FILE credentials for --check-substrate (default: one.ie/web/.env)
26
+ # FACTORY_VERIFY_CYCLE=0 skip --verify's real /do cycle (default: it RUNS — the
27
+ # proof must not need extra env to pass; skipping still fails)
28
+
29
+ set -uo pipefail
30
+
31
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
32
+ cd "$ROOT" || exit 1
33
+
34
+ ONE_TARGET="${ONE_TARGET:-published}"
35
+
36
+ # Pinned published versions — the version IS the boundary (plan §4).
37
+ SDK_VERSION="^0.14.11"
38
+ MCP_VERSION="^0.6.0"
39
+ CLI_VERSION="^4.0.4"
40
+
41
+ # A monorepo path is a directory that must exist on disk. `https://one.ie/x`
42
+ # is a host, not a path — the factory repo reaches the substrate over HTTPS by
43
+ # design, so matching the URL would flag the very thing we want.
44
+ MONOREPO_PATH_RE='(^|[^:a-zA-Z0-9._-])(one\.ie/(web|ai)|pay/(backend|contracts|tools)|channels/src|api/src|sync/src|backup/src|schema/[a-z-]*\.tql)|\.dev\.vars'
45
+
46
+ pass() { printf ' ok %s\n' "$*"; }
47
+ fail() { printf ' FAIL %s\n' "$*" >&2; FAILED=$((FAILED + 1)); }
48
+ info() { printf ' %s\n' "$*"; }
49
+ head_() { printf '\n== %s\n' "$*"; }
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # The portability manifest — data, asserted against the scripts by
53
+ # --check-portability. Three buckets:
54
+ #
55
+ # portable no monorepo path, no credential read → ships
56
+ # needs-env needs ONE_ENV_FILE and/or names monorepo paths
57
+ # that degrade to no-ops when absent → ships
58
+ # monorepo-only hard-asserts against THIS tree; refuses
59
+ # without it → never ships
60
+ # ---------------------------------------------------------------------------
61
+ manifest() {
62
+ cat <<'MANIFEST'
63
+ portable browser-check.mjs
64
+ portable chrome.mjs
65
+ portable do-analyze.sh
66
+ portable do-learn-loop.sh
67
+ portable do-promise-lint.sh
68
+ portable do-recon-cache.sh
69
+ portable do-validate-armed.py
70
+ portable package.json
71
+ portable w1-recon.ts
72
+ portable wf-check.mjs
73
+ needs-env cc-connect.sh
74
+ needs-env do-accept.sh
75
+ needs-env do-auto.sh
76
+ needs-env do-derives-check.sh
77
+ needs-env do-fleet.sh
78
+ needs-env do-folder.sh
79
+ needs-env do-killswitch-audit.py
80
+ needs-env do-orchestrate.sh
81
+ needs-env do-promise-settle.sh
82
+ needs-env do-prove.sh
83
+ needs-env do-reconcile.sh
84
+ needs-env do-rubric.py
85
+ needs-env do-signal.sh
86
+ needs-env do-survey.sh
87
+ needs-env do-tier.sh
88
+ needs-env do-untracked-gate.sh
89
+ needs-env do-walk.sh
90
+ needs-env notify.sh
91
+ needs-env reconcile-allow.txt
92
+ needs-env rubric-weights.json
93
+ needs-env w4-rubric.ts
94
+ monorepo-only cc-events-proof.sh
95
+ monorepo-only do-consumer-sweep.sh
96
+ monorepo-only do-rank.py
97
+ monorepo-only do-smoke.sh
98
+ monorepo-only do-substrate-check.sh
99
+ monorepo-only do-tasks-wire-check.sh
100
+ monorepo-only do-ui-gate.sh
101
+ monorepo-only factory-check.sh
102
+ monorepo-only factory-repo.sh
103
+ monorepo-only factory-tasks-check.sh
104
+ monorepo-only fade-toxic.sh
105
+ monorepo-only hourly-brief.sh
106
+ monorepo-only outcome-pull.ts
107
+ monorepo-only promise-manifest.mjs
108
+ monorepo-only spine-canary.sh
109
+ monorepo-only subdomains-proof.sh
110
+ monorepo-only sync-claude-mirror.sh
111
+ monorepo-only tg-listen.sh
112
+ monorepo-only typedb-cluster-status.sh
113
+ monorepo-only typedb-env.sh
114
+ monorepo-only typedb-flap-recorder.sh
115
+ monorepo-only typedb-scratch.sh
116
+ monorepo-only typedb-probes/containment-probe.py
117
+ monorepo-only typedb-probes/panic-probe-version.py
118
+ monorepo-only typedb-probes/panic-probe.py
119
+ monorepo-only urls-lint.sh
120
+ MANIFEST
121
+ }
122
+
123
+ # The canon pack: the text/ files the loop itself reads. Everything else in
124
+ # text/ is this company's work product and never ships.
125
+ canon_text() {
126
+ cat <<'CANON'
127
+ text/CLAUDE.md
128
+ text/contracts.md
129
+ text/dictionary.md
130
+ text/do.md
131
+ text/do-engine-plan.md
132
+ text/docs.md
133
+ text/dsl.md
134
+ text/one-loop.md
135
+ text/one-ontology.md
136
+ text/patterns.md
137
+ text/promise-signals.md
138
+ text/promises.md
139
+ text/rubrics.md
140
+ text/rubric-migration-plan.md
141
+ text/templates.md
142
+ text/templates-plan.md
143
+ text/voice-and-tone.md
144
+ text/writing-style-guide.md
145
+ text/lifecycle.md
146
+ text/routing.md
147
+ text/routing-plan.md
148
+ text/promise-plan.md
149
+ text/skill-invocation.md
150
+ text/surfaces-layers.md
151
+ text/orchestrator-how-to.md
152
+ text/do-autonomous-plan.md
153
+ text/agent-template-plan.md
154
+ text/marketplace.md
155
+ text/factory.md
156
+ text/factory-plan.md
157
+ CANON
158
+ }
159
+
160
+ # text/ files the shipped spine references DELIBERATELY without shipping them.
161
+ # Two kinds, both allowed, neither silent:
162
+ # company this repo's own work product, cited as a worked example
163
+ # stale the reference is already dead upstream (monorepo doc rot) —
164
+ # listed so --check-canon reports a real gap, not a known one
165
+ allowed_dangling() {
166
+ cat <<'ALLOW'
167
+ company text/playbook.md
168
+ company text/remote-suspend-todo.md
169
+ company text/sdk-mcp-cli-schema-design-integration.md
170
+ company text/agent-api-plan.md
171
+ company text/workflow-bridge-plan.md
172
+ stale text/feature.md
173
+ stale text/loop-log-archive.md
174
+ stale text/rich-messages.md
175
+ stale text/template-frame.md
176
+ stale text/template-spec-plan.md
177
+ ALLOW
178
+ }
179
+
180
+ # Files under .claude/ that are this machine's or this company's, not the loop's.
181
+ excluded_claude() {
182
+ cat <<'EXCLUDED'
183
+ .claude/settings.json
184
+ .claude/settings.local.json
185
+ .claude/improvements.queue.md
186
+ .claude/typedb/dev.env
187
+ .claude/commands/kill.md
188
+ .claude/commands/restart.md
189
+ .claude/commands/db-sync.md
190
+ .claude/commands/deploy.md
191
+ .claude/commands/release.md
192
+ .claude/commands/oo-push.md
193
+ .claude/skills/dev.md
194
+ .claude/skills/build.md
195
+ EXCLUDED
196
+ }
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # 1. classify — resolve which tracked scripts ship
200
+ # ---------------------------------------------------------------------------
201
+ ships() { # $1 = script basename/relpath under .claude/scripts
202
+ local b="$1"
203
+ local bucket
204
+ bucket="$(manifest | awk -v n="$b" '$2 == n {print $1}')"
205
+ [ -n "$bucket" ] || return 2 # unclassified
206
+ [ "$bucket" = "monorepo-only" ] && return 1
207
+ return 0
208
+ }
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # build — the 7 steps, each failing closed
212
+ # ---------------------------------------------------------------------------
213
+ build() {
214
+ local TARGET="$1"
215
+ [ -n "$TARGET" ] || { echo "usage: factory-repo.sh <target-dir>" >&2; return 2; }
216
+
217
+ if [ -e "$TARGET" ] && [ -n "$(ls -A "$TARGET" 2>/dev/null)" ]; then
218
+ echo "refusing: $TARGET exists and is not empty" >&2; return 2
219
+ fi
220
+
221
+ local STAGE; STAGE="$(mktemp -d)"
222
+ # No RETURN trap: bash keeps a RETURN trap set inside a function as the
223
+ # shell's trap, so it fires again (with STAGE unbound) on every later
224
+ # function return. Clean up explicitly instead — _bail does both.
225
+ _bail() { rm -rf "$STAGE"; return "$1"; }
226
+
227
+ # --- 1. classify -----------------------------------------------------
228
+ local unclassified=0
229
+ while IFS= read -r f; do
230
+ local rel="${f#.claude/scripts/}"
231
+ ships "$rel"; local rc=$?
232
+ [ $rc -eq 2 ] && { echo "unclassified script (add to manifest): $rel" >&2; unclassified=1; }
233
+ done < <(git ls-files .claude/scripts)
234
+ [ $unclassified -eq 0 ] || { echo "refusing to build with an incomplete manifest" >&2; _bail 3; return 3; }
235
+
236
+ # --- 2. assemble — from the TRACKED set, never `cp -r` ---------------
237
+ # Generating from `git ls-files` makes secret-exclusion structural: an
238
+ # untracked .env, a scratch dir, or node_modules cannot enter by accident.
239
+ local excl; excl="$(excluded_claude)"
240
+ while IFS= read -r f; do
241
+ case "$f" in
242
+ .claude/scripts/*)
243
+ ships "${f#.claude/scripts/}" || continue ;;
244
+ esac
245
+ grep -qxF "$f" <<<"$excl" && continue
246
+ # launchd plists hard-code this machine's absolute paths and schedule
247
+ # monorepo-only jobs; scratch/ is one cycle's leftovers. Neither travels.
248
+ case "$f" in .claude/launchd/*|.claude/scratch/*) continue ;; esac
249
+ mkdir -p "$STAGE/$(dirname "$f")"
250
+ cp "$f" "$STAGE/$f"
251
+ done < <(git ls-files .claude)
252
+
253
+ while IFS= read -r f; do
254
+ [ -f "$f" ] || { echo "canon missing: $f" >&2; _bail 3; return 3; }
255
+ mkdir -p "$STAGE/$(dirname "$f")"
256
+ cp "$f" "$STAGE/$f"
257
+ done < <(canon_text)
258
+
259
+ # The loop's doc templates — every -todo/-plan/-docs starts from one.
260
+ while IFS= read -r f; do
261
+ mkdir -p "$STAGE/$(dirname "$f")"
262
+ cp "$f" "$STAGE/$f"
263
+ done < <(git ls-files 'text/template-*.md')
264
+
265
+ # Accumulators: the loop APPENDS to these. Ship them seeded and empty — our
266
+ # history is not the collaborator's, but the files must exist or the first
267
+ # cycle that tries to append has nothing to append to.
268
+ printf '# Learnings\n\nOne line per closed cycle. See `.claude/rules/documentation.md`\n(§ Loop Close) for the format, including the eight-field rubric vector.\n' \
269
+ > "$STAGE/text/learnings.md"
270
+ printf '# Improvements\n\nAnchor mismatches and deferred fixes land here at `/close`, for the next\ncycle'"'"'s W2 to pick up. See `.claude/rules/documentation.md` § Loop Close.\n' \
271
+ > "$STAGE/text/improvements.md"
272
+ printf '# Loop log\n\nOne entry per ONE LOOP turn — lane, move, receipt. Read and appended by\n`.claude/workflows/one-loop.js`. See `text/one-loop.md`.\n' \
273
+ > "$STAGE/text/loop-log.md"
274
+
275
+ # --- 3. strip — settings.json never ships, and no absolute path travels ---
276
+ write_settings_example "$STAGE/.claude/settings.example.json"
277
+ portabilise "$STAGE" || { _bail 3; return 3; }
278
+
279
+ # --- 4. bind — the connect layer, one file ---------------------------
280
+ write_connect "$STAGE"
281
+ write_package_json "$STAGE" "$TARGET"
282
+ write_gitignore "$STAGE"
283
+ write_readme "$STAGE"
284
+ write_contributing "$STAGE"
285
+
286
+ # --- 5. scan — refuse to emit on a key-shaped string -----------------
287
+ if ! scan_secrets "$STAGE"; then
288
+ echo "refusing to emit: key-shaped string found in the staged tree" >&2
289
+ _bail 4; return 4
290
+ fi
291
+
292
+ # --- 6. seed — tests that reach into nothing -------------------------
293
+ write_tests "$STAGE"
294
+
295
+ # --- 7. init — fresh history, one commit, no remote ------------------
296
+ mkdir -p "$TARGET"
297
+ # shellcheck disable=SC2086
298
+ ( cd "$STAGE" && tar cf - . ) | ( cd "$TARGET" && tar xf - )
299
+ (
300
+ cd "$TARGET" || exit 1
301
+ git init -q -b main
302
+ git add -A
303
+ git -c user.name="ONE" -c user.email="build@one.ie" \
304
+ commit -q -m "The factory, standalone.
305
+
306
+ Generated by .claude/scripts/factory-repo.sh from canon.
307
+ Fresh history by design: a history-preserving cut of any prefix
308
+ carries root-level blobs."
309
+ ) || return 5
310
+
311
+ rm -rf "$STAGE"
312
+ echo "built: $TARGET (ONE_TARGET=$ONE_TARGET)"
313
+ echo "files: $(cd "$TARGET" && git ls-files | wc -l | tr -d ' ')"
314
+ }
315
+
316
+ # An absolute path to the maintainer's disk is not a secret, but it is a
317
+ # dependency the clone cannot satisfy — and it tells a reader where the private
318
+ # tree lives. Rewrite what can be made relative; refuse to emit what cannot.
319
+ portabilise() {
320
+ local S="$1" home_re
321
+ home_re="$(printf '%s' "$ROOT" | sed 's/[][\.*^$/]/\\&/g')"
322
+
323
+ # The maintainer's workspace root — the directory the monorepo sits in. Paths
324
+ # under it name private sibling repos (vendoring provenance, /oo-push targets)
325
+ # and must not travel even as prose.
326
+ local WS; WS="$(dirname "$ROOT")"
327
+
328
+ local _sed
329
+ if sed --version >/dev/null 2>&1; then _sed=(sed -i); else _sed=(sed -i ''); fi
330
+
331
+ while IFS= read -r f; do
332
+ [ -f "$f" ] || continue
333
+ # The monorepo root becomes the repo root; the loop already runs from there.
334
+ "${_sed[@]}" -e "s|$ROOT/one\.ie/web|<the project you are building>|g" \
335
+ -e "s|$ROOT|.|g" \
336
+ -e "s|$WS/[A-Za-z0-9._-]*|<a private sibling repo>|g" "$f" 2>/dev/null || true
337
+ done < <(grep -rl -e "$ROOT" -e "$WS" "$S" 2>/dev/null)
338
+
339
+ # Anything still absolute under /Users or /home is a path we could not make
340
+ # portable. Fail closed rather than shipping a broken reference.
341
+ local left
342
+ left="$(grep -rlE '(/Users/[a-z]+/|/home/[a-z]+/)' "$S" 2>/dev/null | sed "s|^$S/||")"
343
+ if [ -n "$left" ]; then
344
+ echo "refusing to emit: absolute machine paths survive in:" >&2
345
+ echo "$left" | head -10 >&2
346
+ grep -rhoE '(/Users/[a-z]+/|/home/[a-z]+/)[^ "'"'"')]*' "$S" 2>/dev/null | sort -u | head -5 >&2
347
+ return 1
348
+ fi
349
+ return 0
350
+ }
351
+
352
+ write_settings_example() {
353
+ cat > "$1" <<'JSON'
354
+ {
355
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
356
+ "env": {
357
+ "FABLE_AVAILABLE": "off"
358
+ },
359
+ "permissions": {
360
+ "allow": [
361
+ "Bash(bash .claude/scripts/*)",
362
+ "Bash(git status:*)",
363
+ "Bash(git diff:*)",
364
+ "Bash(git log:*)",
365
+ "Read(//**)"
366
+ ]
367
+ }
368
+ }
369
+ JSON
370
+ }
371
+
372
+ # The connect layer: ONE file names the URL and the key, and it names them
373
+ # by ENV VAR, never by value. Nothing else in the repo knows a URL or a secret.
374
+ write_connect() {
375
+ local S="$1"
376
+ cat > "$S/.mcp.json" <<JSON
377
+ {
378
+ "mcpServers": {
379
+ "oneie": {
380
+ "type": "stdio",
381
+ "command": "npx",
382
+ "args": ["-y", "@oneie/mcp@${MCP_VERSION#^}"],
383
+ "env": {
384
+ "ONEIE_API_URL": "\${ONEIE_API_URL:-https://one.ie}",
385
+ "ONEIE_API_KEY": "\${ONEIE_API_KEY}"
386
+ }
387
+ }
388
+ }
389
+ }
390
+ JSON
391
+
392
+ cat > "$S/.env.example" <<'ENVX'
393
+ # The connect layer. Copy to .env and fill in. .env is gitignored and must stay that way.
394
+ #
395
+ # This private repo drives the PUBLIC stack — github.com/one-ie/one and the
396
+ # published @oneie/{sdk,mcp,cli} packages — over the api.one.ie substrate.
397
+ # Nothing here needs a private checkout.
398
+ #
399
+ # Mint your own key rather than being handed one — it carries exactly your
400
+ # authority and nothing more:
401
+ #
402
+ # npx -y @oneie/cli auth login
403
+ #
404
+ # ONEIE_API_URL must be the app origin, not the gateway. The MCP client calls
405
+ # /api/* paths; https://api.one.ie serves the substrate unprefixed and every
406
+ # tool 404s against it.
407
+
408
+ # IMPORTANT: this file is not loaded for you. Claude Code expands ${VAR} in
409
+ # .mcp.json from the PROCESS environment, and does not read .env. Export it
410
+ # before starting the client, every session:
411
+ #
412
+ # set -a; source .env; set +a
413
+ # claude
414
+ #
415
+ # Skip that and the MCP server starts with an empty key: every substrate tool
416
+ # fails, and nothing says why.
417
+
418
+ ONEIE_API_URL=https://one.ie
419
+ ONEIE_API_KEY=
420
+
421
+ # The loop's own scripts (signals, promise settle, the /close propagate) default
422
+ # to a localhost dev server. In a clone there isn't one — point them at the live
423
+ # substrate or every mark and warn the loop emits goes nowhere quietly.
424
+ ONE_API_URL=https://one.ie
425
+
426
+ # A dotenv file is not a shell: ${ONEIE_API_KEY} would be read literally by any
427
+ # script that parses this file. Paste the SAME key here.
428
+ ONE_API_KEY=
429
+
430
+ # The loop's own scripts read credentials through this indirection, so the
431
+ # harness never needs a one.ie/web/ on disk.
432
+ ONE_ENV_FILE=.env
433
+ ENVX
434
+ }
435
+
436
+ write_package_json() {
437
+ local S="$1" deps overrides=""
438
+ if [ "$ONE_TARGET" = "workspace" ]; then
439
+ # `workspace:*` only resolves for a member of a workspace that DECLARES it.
440
+ # A generated tree sitting outside the monorepo is not one, so a
441
+ # workspace:* dep here fails to resolve — verified: `bun install` errors on
442
+ # all three. The maintainer target therefore uses file: paths, computed at
443
+ # build time relative to this tree, which is what workspace:* was only
444
+ # pretending to be. Relative on purpose: portabilise() refuses to emit an
445
+ # absolute machine path, and a relative one is honest about being local.
446
+ # Absolute, deliberately. A relative link looked cleaner and was wrong:
447
+ # on macOS /var is a symlink to /private/var, so a path computed from an
448
+ # unresolved temp dir was one level short and every dep failed to resolve.
449
+ # The maintainer target is machine-local by definition and is never the
450
+ # tree that gets pushed, so naming the machine's path is honest here —
451
+ # --check-no-machine-paths asserts against the PUBLISHED target, which ships.
452
+ local rel="$ROOT"
453
+ deps="\"@oneie/sdk\": \"file:$rel/packages/sdk\",
454
+ \"@oneie/mcp\": \"file:$rel/packages/mcp\",
455
+ \"@oneie/cli\": \"file:$rel/packages/cli\""
456
+ # The linked packages declare workspace:* between THEMSELVES (cli → sdk,
457
+ # cli → evals). Those resolve for a workspace member and fail for anyone
458
+ # else, so every @oneie/* reachable transitively is overridden to the same
459
+ # local path. Without this, `bun install` errors on evals even though all
460
+ # three direct deps resolve — verified.
461
+ overrides=",
462
+ \"overrides\": {
463
+ \"@oneie/sdk\": \"file:$rel/packages/sdk\",
464
+ \"@oneie/mcp\": \"file:$rel/packages/mcp\",
465
+ \"@oneie/cli\": \"file:$rel/packages/cli\",
466
+ \"@oneie/evals\": \"file:$rel/packages/evals\",
467
+ \"@oneie/react\": \"file:$rel/packages/react\"
468
+ }"
469
+ else
470
+ deps="\"@oneie/sdk\": \"$SDK_VERSION\",
471
+ \"@oneie/mcp\": \"$MCP_VERSION\",
472
+ \"@oneie/cli\": \"$CLI_VERSION\""
473
+ fi
474
+ cat > "$S/package.json" <<JSON
475
+ {
476
+ "name": "@one-ie/factory",
477
+ "version": "0.1.0",
478
+ "private": true,
479
+ "description": "The ONE factory — the /do loop and its connect layer, standalone.",
480
+ "type": "module",
481
+ "scripts": {
482
+ "test": "bun test tests/",
483
+ "check": "bash .claude/scripts/wf-check.mjs 2>/dev/null; node .claude/scripts/wf-check.mjs"
484
+ },
485
+ "dependencies": {
486
+ $deps
487
+ }$overrides,
488
+ "engines": { "node": ">=20" },
489
+ "//": "ONE_TARGET=$ONE_TARGET — the version is the boundary. See README.md."
490
+ }
491
+ JSON
492
+ }
493
+
494
+ write_gitignore() {
495
+ cat > "$1/.gitignore" <<'GI'
496
+ .env
497
+ .env.*
498
+ !.env.example
499
+ .dev.vars
500
+ *.key
501
+ *.pem
502
+ .claude/settings.local.json
503
+ .claude/settings.json
504
+ .claude/scratch/
505
+ .claude/worktrees/
506
+ .do-worktrees/
507
+ node_modules/
508
+ __pycache__/
509
+ .DS_Store
510
+ GI
511
+ }
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # step 5 — the scan. Runs BEFORE git init, so a secret can never enter
515
+ # history even for one commit.
516
+ # ---------------------------------------------------------------------------
517
+ scan_secrets() {
518
+ local S="$1" hits=0
519
+ # Key shapes this substrate actually mints, plus the usual suspects.
520
+ local pat='(one-[0-9a-f]{32,})|(osk_[A-Za-z0-9]{16,})|(sk-[A-Za-z0-9_-]{20,})|(ghp_[A-Za-z0-9]{20,})|(suiprivkey[a-z0-9]{20,})|(AKIA[0-9A-Z]{16})|(-----BEGIN [A-Z ]*PRIVATE KEY-----)'
521
+ while IFS= read -r line; do
522
+ hits=$((hits + 1)); echo " secret-shaped: $line" >&2
523
+ done < <(grep -rInE "$pat" "$S" 2>/dev/null | grep -v '\.env\.example:' | head -20)
524
+
525
+ # Any real dotenv (not the example) must not exist at all.
526
+ while IFS= read -r f; do
527
+ hits=$((hits + 1)); echo " dotenv in tree: ${f#"$S"/}" >&2
528
+ done < <(find "$S" -name '.env' -o -name '.env.*' ! -name '.env.example' -o -name '.dev.vars' 2>/dev/null)
529
+
530
+ [ $hits -eq 0 ]
531
+ }
532
+
533
+ write_tests() {
534
+ local S="$1"
535
+ mkdir -p "$S/tests"
536
+ cat > "$S/tests/factory.test.ts" <<'TS'
537
+ /**
538
+ * The factory repo's own suite. It reaches into nothing — reaching tests are
539
+ * what pin the monorepo together (24 files, 43 refs) and this tree must not
540
+ * inherit that. Every assertion below reads only this repo.
541
+ */
542
+ import { describe, expect, test } from "bun:test"
543
+ import { existsSync, readFileSync, readdirSync } from "node:fs"
544
+ import { join } from "node:path"
545
+
546
+ const ROOT = join(import.meta.dir, "..")
547
+ const read = (p: string) => readFileSync(join(ROOT, p), "utf8")
548
+
549
+ describe("the loop is present", () => {
550
+ test("/do and its spine exist", () => {
551
+ for (const f of [".claude/commands/do.md", ".claude/commands/close.md", ".claude/CLAUDE.md"])
552
+ expect(existsSync(join(ROOT, f))).toBe(true)
553
+ })
554
+
555
+ test("the four build-wave agents exist", () => {
556
+ for (const w of ["w1-recon", "w2-decide", "w3-edit", "w4-verify"])
557
+ expect(existsSync(join(ROOT, `.claude/agents/${w}.md`))).toBe(true)
558
+ })
559
+
560
+ test("the promise lint ships and is executable", () => {
561
+ expect(existsSync(join(ROOT, ".claude/scripts/do-promise-lint.sh"))).toBe(true)
562
+ })
563
+
564
+ test("the doc templates ship", () => {
565
+ const t = readdirSync(join(ROOT, "text")).filter((f) => f.startsWith("template-"))
566
+ expect(t.length).toBeGreaterThan(3)
567
+ })
568
+ })
569
+
570
+ describe("the boundary holds", () => {
571
+ test("no monorepo-only script shipped", () => {
572
+ const banned = ["factory-check.sh", "do-substrate-check.sh", "spine-canary.sh", "typedb-env.sh", "do-rank.py"]
573
+ for (const b of banned) expect(existsSync(join(ROOT, `.claude/scripts/${b}`))).toBe(false)
574
+ })
575
+
576
+ test("no shipped script HARD-depends on the monorepo", () => {
577
+ // The property that matters is not "never says one.ie/web" — a comment or
578
+ // an optional fallback path is harmless and degrades to nothing here. What
579
+ // breaks a clone is an *unconditional* dependency: importing a module from
580
+ // the monorepo, sourcing a file there, or writing output into it.
581
+ const scripts = readdirSync(join(ROOT, ".claude/scripts"), { withFileTypes: true })
582
+ .filter((e) => e.isFile())
583
+ .map((e) => `.claude/scripts/${e.name}`)
584
+
585
+ const HARD = [
586
+ /^\s*(import|export)\s.*['"][^'"]*one\.ie\/web[^'"]*['"]/m, // import from the app tree
587
+ /^\s*(source|\.)\s+\S*one\.ie\/web/m, // source a file there
588
+ /resolve\([^)]*['"]one\.ie\/web/m, // resolve an output path there
589
+ ]
590
+ const offenders = scripts.filter((f) => {
591
+ let src: string
592
+ try { src = read(f) } catch { return false }
593
+ return HARD.some((re) => re.test(src))
594
+ })
595
+ expect(offenders).toEqual([])
596
+ })
597
+
598
+ test("every shipped credential read honours ONE_ENV_FILE", () => {
599
+ // A script that reads one.ie/web/.env with no override cannot find
600
+ // credentials in a clone — it fails silently, which is worse than loudly.
601
+ const scripts = readdirSync(join(ROOT, ".claude/scripts"), { withFileTypes: true })
602
+ .filter((e) => e.isFile())
603
+ .map((e) => `.claude/scripts/${e.name}`)
604
+
605
+ const offenders = scripts.filter((f) => {
606
+ let src: string
607
+ try { src = read(f) } catch { return false }
608
+ if (!/one\.ie\/web\/\.(env|secrets|dev\.vars)/.test(src)) return false
609
+ return !/ONE_ENV_FILE/.test(src)
610
+ })
611
+ expect(offenders).toEqual([])
612
+ })
613
+
614
+ test("settings.json does not ship; the example does", () => {
615
+ // Assert what is TRACKED, not what is on disk. A set-up clone is a superset
616
+ // of the shipped tree: README step 3 tells you to create .claude/settings.json,
617
+ // so an existsSync check here goes red for every collaborator who followed
618
+ // the instructions — a suite that fails when you do it right teaches people
619
+ // to ignore the suite.
620
+ expect(existsSync(join(ROOT, ".claude/settings.example.json"))).toBe(true)
621
+ const gi = read(".gitignore")
622
+ expect(gi).toMatch(/^\.claude\/settings\.json$/m)
623
+ })
624
+
625
+ test("CONTRIBUTING.md ships and names the return path", () => {
626
+ // A generated tree that doesn't say it's generated invites the one mistake
627
+ // that loses work: fixing it here and never carrying it upstream. The doc
628
+ // has to name the direction, not just invite contributions generically.
629
+ expect(existsSync(join(ROOT, "CONTRIBUTING.md"))).toBe(true)
630
+ // \s+ between words, never a literal space: prose here is hard-wrapped at
631
+ // ~78 chars, so any phrase can acquire a newline mid-sentence on an edit.
632
+ const c = read("CONTRIBUTING.md")
633
+ expect(c).toMatch(/generated\s+output/i)
634
+ expect(c).toMatch(/not\s+a\s+second\s+source/i)
635
+ expect(c).toMatch(/pull\s+requests?\s+.*canonical\s+harness/is)
636
+ expect(c).toMatch(/regenerated\s+outward/i)
637
+ })
638
+ })
639
+
640
+ describe("the connect layer is one file", () => {
641
+ test(".mcp.json names the substrate by env var, never by value", () => {
642
+ const mcp = read(".mcp.json")
643
+ expect(mcp).toContain("@oneie/mcp")
644
+ expect(mcp).toContain("ONEIE_API_KEY")
645
+ expect(/one-[0-9a-f]{32,}/.test(mcp)).toBe(false)
646
+ })
647
+
648
+ test(".env is gitignored and .env.example is not", () => {
649
+ const gi = read(".gitignore")
650
+ expect(gi).toMatch(/^\.env$/m)
651
+ expect(gi).toMatch(/^!\.env\.example$/m)
652
+ })
653
+
654
+ test("no key-shaped string anywhere in the tree", () => {
655
+ const walk = (d: string): string[] =>
656
+ readdirSync(join(ROOT, d), { withFileTypes: true }).flatMap((e) => {
657
+ if (e.name === "node_modules" || e.name === ".git") return []
658
+ return e.isDirectory() ? walk(join(d, e.name)) : [join(d, e.name)]
659
+ })
660
+ const pat = /(one-[0-9a-f]{32,})|(osk_[A-Za-z0-9]{16,})|(sk-[A-Za-z0-9_-]{20,})|(-----BEGIN [A-Z ]*PRIVATE KEY-----)/
661
+ const offenders = walk(".").filter((f) => {
662
+ if (f.endsWith(".env.example")) return false
663
+ try { return pat.test(read(f)) } catch { return false }
664
+ })
665
+ expect(offenders).toEqual([])
666
+ })
667
+ })
668
+ TS
669
+ }
670
+
671
+ # Authored by the loop itself, in a clone, on 2026-07-30 — then folded back
672
+ # into the generator. That round trip is the contribution path this file
673
+ # describes, run once for real.
674
+ write_contributing() {
675
+ cat > "$1/CONTRIBUTING.md" <<'MD'
676
+ # Contributing
677
+
678
+ This tree is **generated output**, not a second source. It is built from the
679
+ canonical harness and can be rebuilt at any time — so a fix committed only here
680
+ survives exactly until the next regeneration, then vanishes. That is not a bug
681
+ to work around; it is the one direction of truth the factory keeps.
682
+
683
+ ## The return path
684
+
685
+ An improvement travels back up, then outward again:
686
+
687
+ ```
688
+ 1. change it here prove it in this tree — the loop is runnable
689
+ 2. pull request upstream against the canonical harness, not against this tree
690
+ 3. regenerate outward merged canon is regenerated into every built tree
691
+ 4. --check reports drift between a built tree and canon
692
+ ```
693
+
694
+ Step 2 is the whole rule: **improvements return as pull requests against the
695
+ canonical harness upstream and get regenerated outward.** One direction of
696
+ truth, both ways of travelling it.
697
+
698
+ Use this tree to *prove* the change — edit the command, run the loop, watch it
699
+ close a cycle. Then carry the proven diff upstream. A patch that was never run
700
+ here is a patch nobody has tested; a patch that only landed here is a patch
701
+ nobody will keep.
702
+
703
+ If the two ever disagree, canon wins and `--check` is how you find out.
704
+
705
+ ## What a returning change must hold
706
+
707
+ The boundary is the reason a clone works at all, so contributions keep it:
708
+
709
+ - **No secrets, ever.** Not in a file, not in one commit. The generator scans
710
+ for key-shaped strings *before* `git init`; `tests/factory.test.ts` scans the
711
+ whole tree again.
712
+ - **No monorepo-only checks.** A script that asserts against the tree this was
713
+ generated from fails here for the wrong reason, so it must not ship.
714
+ - **No hard dependency on a private checkout.** A comment or an optional
715
+ fallback is fine; an unconditional import, `source`, or output path into the
716
+ application tree is not.
717
+ - **Credential reads honour `ONE_ENV_FILE`.** A script that reads a fixed
718
+ `.env` inside the app tree finds nothing in a clone and fails silently —
719
+ worse than failing loudly.
720
+ - **Tests reach into nothing.** Every assertion in `tests/` reads only this
721
+ repo. Reaching tests are what pin a monorepo together, and this tree must not
722
+ inherit that.
723
+
724
+ ## Checks this tree can run
725
+
726
+ Both run with no monorepo, no substrate, and no key:
727
+
728
+ ```bash
729
+ bun test tests/ # the boundary suite — the tree's own shape
730
+ bun run check # syntax-checks .claude/workflows/*.js
731
+ ```
732
+
733
+ Anything needing a workspace needs your own key (`.env.example` → `.env`) and
734
+ is not part of the boundary suite by design.
735
+
736
+ ---
737
+
738
+ *Details of what is deliberately absent, and why: `README.md`.*
739
+ MD
740
+ }
741
+
742
+ write_readme() {
743
+ local S="$1"
744
+ cat > "$S/README.md" <<'MD'
745
+ # The ONE Factory
746
+
747
+ A loop that takes an intention and returns shipped, verified work — with a
748
+ promise at the top that can only settle when a command exits zero.
749
+
750
+ This repo is the loop itself, standalone. It contains no secrets, no tenant
751
+ data, and no part of the system it was cut from. You point it at your own
752
+ workspace and it builds *your* projects.
753
+
754
+ **Private repo, public stack.** Everything the loop reaches for is open or
755
+ published:
756
+
757
+ | | |
758
+ |---|---|
759
+ | `github.com/one-ie/one` | the open-source starter the loop scaffolds projects from |
760
+ | `@oneie/sdk` · `@oneie/mcp` · `@oneie/cli` | published on npm |
761
+ | `api.one.ie` · `one.ie/api/*` | the substrate, over HTTPS |
762
+
763
+ Nothing here requires a private checkout. The repo is private because the
764
+ *loop* is the thing worth keeping close — not because its dependencies are.
765
+
766
+ ---
767
+
768
+ ## Clone to first closed cycle
769
+
770
+ ```bash
771
+ git clone git@github.com:one-ie/factory.git
772
+ cd factory
773
+
774
+ # 1. mint your own key — it carries exactly your authority, nothing more
775
+ npx -y @oneie/cli auth login
776
+
777
+ # 2. bind the connect layer
778
+ cp .env.example .env
779
+ $EDITOR .env # paste ONEIE_API_KEY
780
+
781
+ # 3. bring the harness up
782
+ cp .claude/settings.example.json .claude/settings.json
783
+ bun install
784
+
785
+ # 4. export — nothing loads .env for you, and an unexported
786
+ # key means every substrate tool fails without saying why
787
+ set -a; source .env; set +a
788
+
789
+ # 5. warm the npx cache once — on a cold cache the MCP
790
+ # server download outlasts startup and reads as unavailable
791
+ npx -y @oneie/mcp@0.6.0 --help
792
+
793
+ # 6. run the loop
794
+ claude
795
+ > /do add a health endpoint that reports substrate reachability
796
+ ```
797
+
798
+ `/do` walks AIM → PROMISE → SURVEY → DESIGN → PLAN → BUILD → TEST → VERIFY →
799
+ PROVE → TEACH → SHIP → LEARN. Every stage is an artifact made true; the promise
800
+ at the top settles only when its `proof:` command exits zero.
801
+
802
+ ## Building a project
803
+
804
+ The factory doesn't hold your project — it builds one, next to itself, from the
805
+ public starter:
806
+
807
+ ```bash
808
+ npx -y @oneie/cli new my-project # scaffolds from github.com/one-ie/one
809
+ cd my-project && bun install
810
+ ```
811
+
812
+ Then run the loop against it. `/do` finds the project by its `text/` canon, so
813
+ point it at the tree you want built:
814
+
815
+ ```bash
816
+ claude
817
+ > /do in ../my-project, add a pricing page that reads plans from the substrate
818
+ ```
819
+
820
+ Every cycle closes back into the substrate — `mark` on what worked, `warn` on
821
+ what didn't — so the next cycle starts from evidence rather than from scratch.
822
+
823
+ ## Adding tasks to the board
824
+
825
+ Your tasks live in the substrate, not in a file. The MCP server in `.mcp.json`
826
+ gives the loop — and you — a board:
827
+
828
+ ```
829
+ tasks_create title + notes + tags → returns a tid
830
+ tasks_list the open queue, weight-ranked
831
+ tasks_claim take one off the queue, stamps @you
832
+ tasks_status open | picked | done | verified | dissolved
833
+ ```
834
+
835
+ Or over plain HTTP, which is all the MCP server is doing:
836
+
837
+ ```bash
838
+ curl -s -X POST https://one.ie/api/ask/tasks:create \
839
+ -H "Authorization: Bearer $ONEIE_API_KEY" \
840
+ -H 'Content-Type: application/json' \
841
+ -d '{"data":{"title":"Ship the health endpoint","notes":"Done = /health returns 200 and names the substrate it reached.","tags":["factory"]}}'
842
+ ```
843
+
844
+ The `data` envelope is required. Every task you create is tagged with your
845
+ workspace, and the board ranks by *learned* weight where it has evidence — the
846
+ priority you author is the starting point, not the final order.
847
+
848
+ ## What is here
849
+
850
+ | | |
851
+ |---|---|
852
+ | `.claude/commands/` | `/do`, `/close`, `/go`, `/see`, `/create` … the operator surface |
853
+ | `.claude/agents/` | the four build waves — recon, decide, edit, verify |
854
+ | `.claude/skills/` | domain skills the loop pulls in on match |
855
+ | `.claude/rules/` | auto-loaded per file glob |
856
+ | `.claude/hooks/` | the gates — staging guards, doc-sync, outcome checks |
857
+ | `.claude/scripts/` | the deterministic checks: promise lint, reconcile, rubric, walk |
858
+ | `text/` | the canon the loop reads, and the doc templates it writes from |
859
+ | `tests/` | this repo's own suite — it reaches into nothing |
860
+
861
+ ## What is deliberately absent
862
+
863
+ Not an oversight, a boundary.
864
+
865
+ - **No secrets.** The generator refuses to emit a tree containing a key-shaped
866
+ string, and it scans *before* `git init` — so nothing can enter history even
867
+ for one commit.
868
+ - **No tenant data, no client sites, no application tree.**
869
+ - **No production credentials.** You bring your own workspace and your own key.
870
+ - **No upstream history.** This tree is born with one commit, because a
871
+ history-preserving cut of any prefix carries root-level blobs.
872
+ - **No monorepo-only checks.** Scripts that assert against the tree this was
873
+ generated from do not ship; they would fail here for the wrong reason.
874
+
875
+ ## The two targets
876
+
877
+ The repo binds to the substrate through a published interface, not a directory.
878
+
879
+ ```
880
+ ONE_TARGET=published @oneie/{sdk,mcp,cli} from npm
881
+ (no monorepo on disk — the default)
882
+
883
+ ONE_TARGET=workspace workspace links, for a maintainer
884
+ with the monorepo checked out
885
+ ```
886
+
887
+ No source edits between them. The version is the boundary.
888
+
889
+ ## Contributing back
890
+
891
+ This tree is **generated output**, not a second source. Improvements return as
892
+ pull requests against the canonical harness upstream and get regenerated
893
+ outward — one direction of truth, both ways of travelling it.
894
+
895
+ The return path, the boundary suite a PR must keep green, and what never
896
+ travels upstream: [CONTRIBUTING.md](CONTRIBUTING.md).
897
+
898
+ ---
899
+
900
+ *Generated by `factory-repo.sh`. Licence: see `.claude/LICENSE`.*
901
+ MD
902
+ }
903
+
904
+ # ---------------------------------------------------------------------------
905
+ # checks
906
+ # ---------------------------------------------------------------------------
907
+ tmpbuild() { # build into a temp dir and echo the path
908
+ local d; d="$(mktemp -d)/factory"
909
+ build "$d" >/dev/null 2>&1 || { echo ""; return 1; }
910
+ echo "$d"
911
+ }
912
+
913
+ use_tree() { # $1 = optional dir; else build one
914
+ if [ -n "${1:-}" ] && [ -d "$1" ]; then echo "$1"; else tmpbuild; fi
915
+ }
916
+
917
+ check_history() {
918
+ head_ "check-history — one commit, no env/key blob on any ref"
919
+ local T; T="$(use_tree "${1:-}")" || { fail "build failed"; return; }
920
+ local n; n="$(git -C "$T" rev-list --count --all 2>/dev/null || echo 0)"
921
+ [ "$n" = "1" ] && pass "exactly one commit on all refs" || fail "expected 1 commit, found $n"
922
+ local blobs
923
+ blobs="$(git -C "$T" log --all --full-history --name-only --format="" \
924
+ -- '*.dev.vars' '*.env' '.env.*' '*secret*' '*.pem' '*.key' 2>/dev/null \
925
+ | grep -v '\.env\.example$' | sort -u)"
926
+ [ -z "$blobs" ] && pass "no env/key blob reachable from any ref" || fail "blobs in history: $blobs"
927
+ [ -z "$(git -C "$T" remote 2>/dev/null)" ] && pass "born with no remote" || info "remote already set (expected after handover)"
928
+ }
929
+
930
+ check_drift() {
931
+ head_ "check — drift between canon and a built tree"
932
+ local T="${1:-}"
933
+ if [ -z "$T" ] || [ ! -d "$T" ]; then
934
+ # Determinism first — a generator that varies makes every drift report noise.
935
+ local A B; A="$(tmpbuild)"; B="$(tmpbuild)"
936
+ [ -n "$A" ] && [ -n "$B" ] || { fail "build failed"; return; }
937
+ local d; d="$(diff -rq --exclude=.git "$A" "$B" 2>&1)"
938
+ [ -z "$d" ] && pass "generator is deterministic" || { fail "non-deterministic build:"; echo "$d" | head -10 >&2; }
939
+
940
+ # Then the drift the deliverable actually names: canon vs the PUBLISHED
941
+ # tree. Without this, "--check reports drift" was only ever comparing the
942
+ # generator to itself — and the live remote could rot unnoticed, which is
943
+ # the exact failure mode this repo is designed to avoid.
944
+ local REMOTE="${FACTORY_REMOTE:-https://github.com/one-ie/factory.git}"
945
+ local C; C="$(mktemp -d)/published"
946
+ if git clone -q --depth 1 "$REMOTE" "$C" 2>/dev/null; then
947
+ T="$C"
948
+ info "comparing canon against the published tree at $REMOTE"
949
+ else
950
+ # FAIL, never skip. This check's entire deliverable is "drift is detected
951
+ # loudly"; letting an unreachable remote pass green means the proof
952
+ # settles KEPT on a machine that verified no drift at all. Set
953
+ # FACTORY_CHECK_OFFLINE=1 to acknowledge deliberately — it still fails,
954
+ # because a check that did not run proved nothing.
955
+ if [ "${FACTORY_CHECK_OFFLINE:-0}" = "1" ]; then
956
+ fail "drift NOT CHECKED — FACTORY_CHECK_OFFLINE=1 was set deliberately"
957
+ else
958
+ fail "drift NOT CHECKED — cannot reach $REMOTE (no access, or no network)"
959
+ info "the published tree is the thing this check exists to compare against"
960
+ fi
961
+ return
962
+ fi
963
+ fi
964
+ local F; F="$(tmpbuild)"
965
+ [ -n "$F" ] || { fail "build failed"; return; }
966
+ local d; d="$(diff -rq --exclude=.git --exclude=node_modules --exclude=.env "$F" "$T" 2>&1)"
967
+ if [ -z "$d" ]; then pass "built tree matches canon"
968
+ else fail "drift vs canon:"; echo "$d" | head -30 >&2; fi
969
+ }
970
+
971
+ check_env_indirection() {
972
+ head_ "check-env-indirection — every credential read honours ONE_ENV_FILE"
973
+ local bad=0
974
+ while IFS= read -r rel; do
975
+ local f=".claude/scripts/$rel"
976
+ [ -f "$f" ] || continue
977
+ grep -qE 'one\.ie/web/\.(env|secrets|dev\.vars)' "$f" || continue
978
+ if grep -q 'ONE_ENV_FILE' "$f"; then
979
+ pass "$rel honours ONE_ENV_FILE"
980
+ else
981
+ fail "$rel reads one.ie/web/.env with no ONE_ENV_FILE override"; bad=1
982
+ fi
983
+ done < <(manifest | awk '$1 != "monorepo-only" {print $2}')
984
+
985
+ # Behavioural: point ONE_ENV_FILE at a foreign path and prove it is read.
986
+ # Behavioural: point ONE_ENV_FILE at a path nothing in this tree knows about
987
+ # and prove the resolver actually reads it. A grep for the variable name is
988
+ # not evidence the override works.
989
+ local tmp; tmp="$(mktemp -d)/foreign.env"
990
+ mkdir -p "$(dirname "$tmp")"
991
+ printf 'SERVER_SECRET=factory-repo-probe\n' > "$tmp"
992
+ local got; got="$(ONE_ENV_FILE="$tmp" bash .claude/scripts/do-signal.sh --print-env 2>/dev/null)"
993
+ if [ "$got" = "resolved-from: $tmp" ]; then
994
+ pass "do-signal.sh resolves credentials from a foreign ONE_ENV_FILE"
995
+ else
996
+ fail "ONE_ENV_FILE override is not honoured at runtime — got: ${got:-<nothing>}"; bad=1
997
+ fi
998
+
999
+ # And it must go red: with the override pointing at nothing, the resolver
1000
+ # must not silently claim success.
1001
+ local none; none="$(ONE_ENV_FILE=/nonexistent/nope.env bash .claude/scripts/do-signal.sh --print-env 2>/dev/null)"
1002
+ if [ "$none" = "resolved-from: $tmp" ]; then
1003
+ fail "the probe reports a stale path — it is not reading the override"; bad=1
1004
+ else
1005
+ pass "the probe tracks the override (proof it is a check, not theatre)"
1006
+ fi
1007
+ [ $bad -eq 0 ] || return 1
1008
+ }
1009
+
1010
+ check_portability() {
1011
+ head_ "check-portability — the manifest is asserted against the scripts"
1012
+ local bad=0
1013
+
1014
+ # Every tracked script is classified.
1015
+ while IFS= read -r f; do
1016
+ local rel="${f#.claude/scripts/}"
1017
+ manifest | awk -v n="$rel" '$2 == n {found=1} END {exit !found}' \
1018
+ || { fail "unclassified: $rel"; bad=1; }
1019
+ done < <(git ls-files .claude/scripts)
1020
+
1021
+ # Every manifest entry names a real file.
1022
+ while IFS= read -r rel; do
1023
+ [ -f ".claude/scripts/$rel" ] || { fail "manifest names a missing file: $rel"; bad=1; }
1024
+ done < <(manifest | awk '{print $2}')
1025
+
1026
+ # `portable` means portable: no monorepo path, no credential read.
1027
+ # A URL (https://one.ie/…) is not a path — the boundary is a directory that
1028
+ # must exist on disk, not a host the script talks to over HTTPS.
1029
+ while IFS= read -r rel; do
1030
+ local f=".claude/scripts/$rel"
1031
+ if grep -qE "$MONOREPO_PATH_RE" "$f" 2>/dev/null; then
1032
+ fail "declared portable but names a monorepo path: $rel"
1033
+ grep -nE "$MONOREPO_PATH_RE" "$f" | head -3 | sed 's/^/ /' >&2
1034
+ bad=1
1035
+ fi
1036
+ done < <(manifest | awk '$1 == "portable" {print $2}')
1037
+
1038
+ [ $bad -eq 0 ] && pass "$(manifest | wc -l | tr -d ' ') scripts classified, portable set is clean"
1039
+ [ $bad -eq 0 ] || return 1
1040
+ }
1041
+
1042
+ check_targets() {
1043
+ head_ "check-targets — one flag, no source edits"
1044
+ local bad=0
1045
+ local A B
1046
+ A="$(ONE_TARGET=published tmpbuild)"; B="$(ONE_TARGET=workspace tmpbuild)"
1047
+ [ -n "$A" ] && [ -n "$B" ] || { fail "build failed under one of the targets"; return 1; }
1048
+
1049
+ grep -q '"@oneie/sdk": "\^' "$A/package.json" && pass "published → pinned npm versions" \
1050
+ || { fail "published target does not pin versions"; bad=1; }
1051
+ grep -q '"@oneie/sdk": "file:' "$B/package.json" && pass "workspace → local file: links" \
1052
+ || { fail "workspace target does not link locally"; bad=1; }
1053
+
1054
+ # A dep string is not a resolvable dep. `workspace:*` in a tree outside the
1055
+ # workspace that declares it fails to resolve — verified, all three packages —
1056
+ # so this must install, not merely parse. This is the check that caught it.
1057
+ if command -v bun >/dev/null 2>&1; then
1058
+ local out; out="$(cd "$B" && bun install --dry-run 2>&1)"
1059
+ if grep -q 'failed to resolve' <<<"$out"; then
1060
+ fail "workspace target does not install:"; grep 'failed to resolve' <<<"$out" | sed 's/^/ /' >&2; bad=1
1061
+ else
1062
+ pass "workspace target's deps actually resolve (bun install --dry-run)"
1063
+ fi
1064
+ else
1065
+ info "bun not found — workspace deps not resolution-tested"
1066
+ fi
1067
+
1068
+ local d; d="$(diff -rq --exclude=.git --exclude=package.json "$A" "$B" 2>&1)"
1069
+ [ -z "$d" ] && pass "the two targets differ in package.json only — no source edits" \
1070
+ || { fail "targets differ outside package.json:"; echo "$d" | head -10 >&2; bad=1; }
1071
+ [ $bad -eq 0 ] || return 1
1072
+ }
1073
+
1074
+ check_connect() {
1075
+ head_ "check-connect — the connect layer is one file"
1076
+ local T; T="$(use_tree "${1:-}")" || { fail "build failed"; return 1; }
1077
+ local bad=0
1078
+
1079
+ [ -f "$T/.mcp.json" ] && pass ".mcp.json present" || { fail ".mcp.json missing"; bad=1; }
1080
+
1081
+ # One file BINDS the substrate; many may READ it. The distinction is the
1082
+ # whole point of a connect layer: `${ONE_API_URL:-https://one.ie}` in a
1083
+ # script is a consumer with an overridable default, whereas a bare
1084
+ # `ONE_API_URL=https://one.ie` is a second binding that .env can no longer
1085
+ # move. Only unconditional assignments count as bindings.
1086
+ local binders
1087
+ binders="$(grep -rlE '^[[:space:]]*(export[[:space:]]+)?(ONEIE?_API_URL|ONEIE?_API_KEY)=[^$]' "$T" 2>/dev/null \
1088
+ | sed "s|^$T/||" | grep -v '^\.env\.example$' | sort)"
1089
+ [ -z "$binders" ] && pass "nothing but the connect layer binds the substrate — every other reference is an overridable default" \
1090
+ || { fail "a second file binds the substrate:"; echo "$binders" >&2; bad=1; }
1091
+
1092
+ # Every shipped origin default must be overridable, or a clone is pinned to
1093
+ # a host it cannot reach.
1094
+ local pinned
1095
+ # Overridable takes three shapes across bash, TS, and node: `${VAR:-url}`,
1096
+ # `process.env.VAR ?? 'url'`, and `argv.find(…) ?? 'url'`. All three are fine.
1097
+ # A doc example inside help text is not a binding either.
1098
+ pinned="$(grep -rInE 'https?://(one\.ie|api\.one\.ie|localhost:4321)' "$T/.claude/scripts" "$T/.claude/hooks" 2>/dev/null \
1099
+ | grep -vE '\$\{[A-Z_]+:-' \
1100
+ | grep -vE '\?\?[[:space:]]*.https?://' \
1101
+ | grep -vE '^\S+:[0-9]+:[[:space:]]*#' \
1102
+ | grep -vE 'one\.ie/foo' \
1103
+ | sed "s|^$T/||")"
1104
+ [ -z "$pinned" ] && pass "no shipped script pins an origin it cannot be moved off" \
1105
+ || { fail "a script pins an origin with no env override:"; echo "$pinned" | head -8 >&2; bad=1; }
1106
+
1107
+ # .env.example must set every origin the shipped scripts fall back on, or a
1108
+ # clone silently signals into a localhost dev server that does not exist.
1109
+ grep -q '^ONE_API_URL=' "$T/.env.example" && pass ".env.example moves the loop off the localhost default" \
1110
+ || { fail ".env.example does not set ONE_API_URL — the loop would signal into localhost"; bad=1; }
1111
+
1112
+ # Claude Code expands ${VAR} in .mcp.json from the PROCESS environment and
1113
+ # does not read dotenv files. A .env nobody exports is a connect layer that
1114
+ # silently comes up with an empty key — so the tree must say so out loud.
1115
+ grep -q 'source .env' "$T/.env.example" && pass ".env.example says how to actually load itself" \
1116
+ || { fail ".env.example does not name the export step — a clone would start with an empty key"; bad=1; }
1117
+
1118
+ grep -q 'oneie/mcp' "$T/.mcp.json" && pass ".mcp.json runs the published @oneie/mcp" \
1119
+ || { fail ".mcp.json does not reference @oneie/mcp"; bad=1; }
1120
+ grep -qE 'one-[0-9a-f]{16,}' "$T/.mcp.json" && { fail ".mcp.json carries a literal key"; bad=1; } \
1121
+ || pass ".mcp.json names the key by env var, never by value"
1122
+
1123
+ grep -q 'ONEIE_API_KEY' "$T/.env.example" && pass ".env.example names the key without a value" \
1124
+ || { fail ".env.example does not name ONEIE_API_KEY"; bad=1; }
1125
+ grep -qE '^ONEIE_API_KEY=$' "$T/.env.example" && pass "the example key is empty" \
1126
+ || { fail ".env.example has a non-empty key"; bad=1; }
1127
+ grep -qx '.env' "$T/.gitignore" && pass ".env is gitignored" || { fail ".env not gitignored"; bad=1; }
1128
+ [ $bad -eq 0 ] || return 1
1129
+ }
1130
+
1131
+ check_no_secrets() {
1132
+ head_ "check-no-secrets — the scan, and the refusal"
1133
+ local T; T="$(use_tree "${1:-}")" || { fail "build failed"; return 1; }
1134
+ if scan_secrets "$T"; then pass "no key-shaped string in the built tree"
1135
+ else fail "key-shaped string in the built tree"; return 1; fi
1136
+
1137
+ # The check must be able to go red. Plant one and prove the scan catches it.
1138
+ local canary; canary="$(mktemp -d)"
1139
+ cp -R "$T/." "$canary/" 2>/dev/null
1140
+ printf 'ONEIE_API_KEY=one-%s\n' "$(printf '0%.0s' $(seq 1 40))" > "$canary/planted.txt"
1141
+ if scan_secrets "$canary" >/dev/null 2>&1; then
1142
+ fail "the scan passed a tree containing a planted key — it cannot go red"
1143
+ return 1
1144
+ else
1145
+ pass "the scan goes red on a planted key (proof it is a check, not theatre)"
1146
+ fi
1147
+ rm -rf "$canary"
1148
+ }
1149
+
1150
+ check_tests() {
1151
+ head_ "check-tests — the repo carries its own suite, reaching into nothing"
1152
+ local T; T="$(use_tree "${1:-}")" || { fail "build failed"; return 1; }
1153
+ [ -f "$T/tests/factory.test.ts" ] && pass "suite present" || { fail "no suite"; return 1; }
1154
+
1155
+ if grep -qE '\.\./\.\./(one\.ie|pay|channels|api|schema)' "$T/tests/"*.ts 2>/dev/null; then
1156
+ fail "the suite reaches out of the repo"; return 1
1157
+ fi
1158
+ pass "the suite has no cross-tree imports"
1159
+
1160
+ if command -v bun >/dev/null 2>&1; then
1161
+ local out; out="$(cd "$T" && bun test tests/ 2>&1)"
1162
+ if grep -qE '[0-9]+ pass' <<<"$out" && ! grep -qE '[1-9][0-9]* fail' <<<"$out"; then
1163
+ pass "suite runs green in the built tree — $(grep -oE '[0-9]+ pass' <<<"$out" | head -1)"
1164
+ else
1165
+ fail "suite is red in the built tree:"; echo "$out" | tail -25 >&2; return 1
1166
+ fi
1167
+ else
1168
+ info "bun not found — suite present but not executed"
1169
+ fi
1170
+ }
1171
+
1172
+ # The shipped harness reads canon. A command that cites text/lifecycle.md in a
1173
+ # tree that doesn't carry it sends the reader — human or agent — to a file that
1174
+ # isn't there. This was real: rules/documentation.md names SIX source-of-truth
1175
+ # docs and the first build shipped four.
1176
+ check_canon() {
1177
+ head_ "check-canon — the spine cites nothing the tree does not carry"
1178
+ local T; T="$(use_tree "${1:-}")" || { fail "build failed"; return 1; }
1179
+ local bad=0 allow; allow="$(allowed_dangling | awk '{print $2}')"
1180
+
1181
+ local dangling=""
1182
+ while IFS= read -r ref; do
1183
+ [ -f "$T/$ref" ] && continue
1184
+ grep -qxF "$ref" <<<"$allow" && continue
1185
+ dangling="$dangling$ref
1186
+ "
1187
+ done < <(grep -rhoE 'text/[a-z0-9-]+\.md' \
1188
+ "$T/.claude/commands" "$T/.claude/rules" \
1189
+ "$T/.claude/agents" "$T/.claude/workflows" 2>/dev/null | sort -u)
1190
+
1191
+ if [ -z "$dangling" ]; then
1192
+ pass "every text/ file the spine cites is present, or explicitly allowed"
1193
+ else
1194
+ fail "the spine cites canon the tree does not carry:"
1195
+ printf '%s' "$dangling" | sed 's/^/ /' >&2
1196
+ bad=1
1197
+ fi
1198
+
1199
+ # The allowlist must not rot either: an entry that is now shipped, or a
1200
+ # `stale` entry that came back upstream, is a stale exemption.
1201
+ while IFS= read -r line; do
1202
+ local kind ref; kind="${line%% *}"; ref="${line##* }"
1203
+ if [ -f "$T/$ref" ]; then
1204
+ fail "allowlisted but now shipped — drop the exemption: $ref"; bad=1
1205
+ elif [ "$kind" = "stale" ] && [ -f "$ROOT/$ref" ]; then
1206
+ fail "allowlisted as stale but exists upstream now — reclassify: $ref"; bad=1
1207
+ fi
1208
+ done < <(allowed_dangling)
1209
+
1210
+ # It must be able to go red.
1211
+ local canary; canary="$(mktemp -d)/tree"
1212
+ mkdir -p "$canary/.claude/commands"
1213
+ printf 'see text/definitely-not-shipped.md\n' > "$canary/.claude/commands/probe.md"
1214
+ if grep -rhoE 'text/[a-z0-9-]+\.md' "$canary/.claude/commands" | grep -q 'definitely-not-shipped'; then
1215
+ pass "the check goes red on a planted dangling reference"
1216
+ else
1217
+ fail "the dangling-reference scan cannot detect a planted reference"; bad=1
1218
+ fi
1219
+ rm -rf "$canary"
1220
+ [ $bad -eq 0 ] || return 1
1221
+ }
1222
+
1223
+ check_no_machine_paths() {
1224
+ head_ "check-no-machine-paths — no absolute path to the maintainer's disk travels"
1225
+ local T; T="$(use_tree "${1:-}")" || { fail "build failed"; return 1; }
1226
+ local left
1227
+ left="$(grep -rlE '(/Users/[a-z]+/|/home/[a-z]+/)' "$T" 2>/dev/null | sed "s|^$T/||")"
1228
+ [ -z "$left" ] && pass "no absolute machine path in the built tree" \
1229
+ || { fail "absolute machine paths survive in:"; echo "$left" | head -8 >&2; return 1; }
1230
+
1231
+ # It must be able to go red: plant one and prove the build refuses.
1232
+ local canary; canary="$(mktemp -d)/tree"
1233
+ mkdir -p "$canary"; cp -R "$T/." "$canary/" 2>/dev/null
1234
+ printf 'cd /Users/someone/private/repo && bun run dev\n' > "$canary/planted.sh"
1235
+ if grep -rqE '(/Users/[a-z]+/|/home/[a-z]+/)' "$canary" 2>/dev/null; then
1236
+ pass "the check goes red on a planted path (proof it is a check, not theatre)"
1237
+ else
1238
+ fail "the check passed a tree containing a planted machine path"; return 1
1239
+ fi
1240
+ rm -rf "$canary"
1241
+ }
1242
+
1243
+ # Live round trip on the rail a standalone clone actually uses: a world key,
1244
+ # over HTTPS, no monorepo. Reads ONE_ENV_FILE (or the monorepo default).
1245
+ check_substrate() {
1246
+ head_ "check-substrate — a world key creates a task over plain HTTPS"
1247
+ local envf="${ONE_ENV_FILE:-$ROOT/one.ie/web/.env}"
1248
+ local key="${ONEIE_API_KEY:-}" url="${ONEIE_API_URL:-https://one.ie}"
1249
+ if [ -z "$key" ] && [ -f "$envf" ]; then
1250
+ key="$(grep -m1 -E '^(ONEIE_API_KEY|ONE_API_KEY)=' "$envf" | cut -d= -f2- | tr -d '"'"'"' ')"
1251
+ fi
1252
+ if [ -z "$key" ]; then
1253
+ # FAIL, never skip — same reasoning as --check. This is the deliverable that
1254
+ # says a clone can put work on a board; with no key the round trip never
1255
+ # happens, so passing green would settle the contract on nothing.
1256
+ if [ "${FACTORY_SKIP_SUBSTRATE:-0}" = "1" ]; then
1257
+ fail "substrate rail NOT CHECKED — FACTORY_SKIP_SUBSTRATE=1 was set deliberately"
1258
+ else
1259
+ fail "substrate rail NOT CHECKED — no ONEIE_API_KEY in env or $envf"
1260
+ info "the live round trip is the proof that a clone can add tasks; without a key there is none"
1261
+ fi
1262
+ return
1263
+ fi
1264
+
1265
+ local body; body="$(curl -s -m 20 -X POST "$url/api/ask/tasks:create" \
1266
+ -H "Authorization: Bearer $key" -H 'Content-Type: application/json' \
1267
+ -d '{"data":{"title":"factory-repo --check-substrate probe","notes":"Automated rail probe. Dissolved immediately.","tags":["factory","probe"]}}')"
1268
+ local tid; tid="$(sed -n 's/.*"tid":"\([^"]*\)".*/\1/p' <<<"$body")"
1269
+ if [ -n "$tid" ]; then
1270
+ pass "task created over the world-key rail — $tid"
1271
+ curl -s -m 20 -o /dev/null -X POST "$url/api/ask/tasks:status" \
1272
+ -H "Authorization: Bearer $key" -H 'Content-Type: application/json' \
1273
+ -d "{\"data\":{\"tid\":\"$tid\",\"status\":\"dissolved\"}}"
1274
+ pass "probe dissolved"
1275
+ else
1276
+ fail "tasks:create did not return a tid: $(head -c 200 <<<"$body")"; return 1
1277
+ fi
1278
+ }
1279
+
1280
+ verify() {
1281
+ head_ "verify — build to temp, no monorepo assumed, run every check"
1282
+ local T; T="$(mktemp -d)/factory"
1283
+ build "$T" || { fail "build failed"; return 1; }
1284
+ info "built at $T"
1285
+
1286
+ FAILED=0
1287
+ check_history "$T"
1288
+ check_connect "$T"
1289
+ check_no_secrets "$T"
1290
+ check_tests "$T"
1291
+ check_portability
1292
+ check_targets
1293
+ check_canon "$T"
1294
+
1295
+ head_ "verify — the loop in a clone"
1296
+ # Default ON. The opt-in version was a cross-surface lie: the proof passed
1297
+ # when a human ran it with FACTORY_VERIFY_CYCLE=1 and failed under
1298
+ # do-promise-settle.sh, which runs the proof string verbatim with no extra
1299
+ # env — so the promise settled BROKEN while every check was green. A proof
1300
+ # that only passes with setup its own settle harness doesn't apply is not the
1301
+ # proof in the contract. Set FACTORY_VERIFY_CYCLE=0 to skip deliberately;
1302
+ # skipping still FAILS, because a cycle that didn't run proved nothing.
1303
+ if [ "${FACTORY_VERIFY_CYCLE:-1}" = "1" ] && command -v claude >/dev/null 2>&1; then
1304
+ info "running a real /do cycle in $T …"
1305
+ # A freshly generated tree is an untrusted workspace, so Claude Code ignores
1306
+ # settings.json's permissions.allow and every bash gate in W4 goes unrun.
1307
+ # The mode must be passed here or this step silently proves less than it
1308
+ # claims — the cycle "closes" without its deterministic checks having fired.
1309
+ local log="$T/../cycle.log" after
1310
+ (
1311
+ cd "$T" || exit 1
1312
+ cp .claude/settings.example.json .claude/settings.json
1313
+ claude -p --permission-mode "${FACTORY_VERIFY_PERMS:-bypassPermissions}" \
1314
+ "${FACTORY_VERIFY_PROMPT:-/do PATCH: create docs/standalone-proof.md — a short file recording that this tree closed a /do cycle with no monorepo on disk, naming the date and what the boundary suite asserts. The file does not exist yet; create it. Then run \`bun test tests/\` and report the count. Close the cycle properly.}"
1315
+ ) > "$log" 2>&1
1316
+ after="$(cd "$T" && git status --porcelain | wc -l | tr -d ' ')"
1317
+ local suite; suite="$(cd "$T" && bun test tests/ 2>&1 | grep -oE '[0-9]+ pass' | head -1)"
1318
+ if [ "$after" -gt 0 ] && [ -n "$suite" ] && ! (cd "$T" && bun test tests/ 2>&1 | grep -qE '[1-9][0-9]* fail'); then
1319
+ pass "a real cycle ran in the clone and left the boundary suite green — $after file(s) changed, $suite"
1320
+ else
1321
+ fail "the cycle did not close — $after file(s) changed, suite: ${suite:-none}"
1322
+ info "cycle transcript: $log"
1323
+ tail -20 "$log" 2>/dev/null | sed 's/^/ | /' >&2
1324
+ fi
1325
+ else
1326
+ fail "NOT RUN — a real /do cycle in a clone is the deliverable that gates KEPT."
1327
+ if command -v claude >/dev/null 2>&1; then
1328
+ info "FACTORY_VERIFY_CYCLE=0 was set. Unset it to run the cycle."
1329
+ else
1330
+ info "\`claude\` not on PATH — the cycle cannot run here."
1331
+ fi
1332
+ fi
1333
+
1334
+ head_ "verify — result"
1335
+ [ "$FAILED" -eq 0 ] && echo " all green" || echo " $FAILED check(s) red"
1336
+ return "$FAILED"
1337
+ }
1338
+
1339
+ # ---------------------------------------------------------------------------
1340
+ FAILED=0
1341
+ case "${1:-}" in
1342
+ --manifest) manifest ;;
1343
+ --check-history) check_history "${2:-}" ;;
1344
+ --check) check_drift "${2:-}" ;;
1345
+ --check-env-indirection) check_env_indirection ;;
1346
+ --check-portability) check_portability ;;
1347
+ --check-targets) check_targets ;;
1348
+ --check-connect) check_connect "${2:-}" ;;
1349
+ --check-no-secrets) check_no_secrets "${2:-}" ;;
1350
+ --check-tests) check_tests "${2:-}" ;;
1351
+ --check-canon) check_canon "${2:-}" ;;
1352
+ --check-no-machine-paths) check_no_machine_paths "${2:-}" ;;
1353
+ --check-substrate) check_substrate ;;
1354
+ --verify) verify ;;
1355
+ -h|--help|"") sed -n '2,30p' "$0" ;;
1356
+ -*) echo "unknown flag: $1" >&2; exit 2 ;;
1357
+ *) build "$1" ;;
1358
+ esac
1359
+ rc=$?
1360
+ [ "$FAILED" -gt 0 ] && exit 1
1361
+ exit $rc