@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.
- package/agents/w1-recon.md +9 -4
- package/agents/w2-decide.md +51 -28
- package/agents/w3-edit.md +70 -78
- package/agents/w4-verify.md +164 -69
- package/commands/browser.md +98 -35
- package/commands/cc-connect.md +6 -0
- package/commands/chat.md +13 -1
- package/commands/close.md +45 -19
- package/commands/create.md +20 -10
- package/commands/db-sync.md +5 -1
- package/commands/deploy.md +174 -87
- package/commands/do-autonomous.md +31 -20
- package/commands/do-improve.md +4 -4
- package/commands/do-show.md +4 -3
- package/commands/do.md +53 -13
- package/commands/go.md +75 -0
- package/commands/improve.md +4 -2
- package/commands/kill.md +6 -8
- package/commands/notify.md +15 -2
- package/commands/oo-push.md +24 -9
- package/commands/rag.md +6 -0
- package/commands/release.md +46 -25
- package/commands/restart.md +5 -4
- package/commands/see.md +45 -35
- package/commands/skill-create.md +38 -5
- package/commands/sync.md +21 -11
- package/hooks/scripts/auto-continue.sh +70 -0
- package/hooks/scripts/branch-pin.sh +85 -0
- package/hooks/scripts/git-add-guard.sh +16 -5
- package/hooks/scripts/post-edit-check.sh +16 -0
- package/hooks/scripts/session-start.sh +13 -8
- package/hooks/scripts/sync-priority-todo.sh +5 -0
- package/package.json +1 -1
- package/rules/api.md +9 -0
- package/rules/design.md +13 -3
- package/rules/documentation.md +30 -71
- package/rules/engine.md +8 -1
- package/rules/ui.md +5 -1
- package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
- package/scripts/browser-check.mjs +44 -103
- package/scripts/cc-events-proof.sh +13 -12
- package/scripts/chrome.mjs +381 -0
- package/scripts/do-analyze.sh +109 -4
- package/scripts/do-auto.sh +350 -43
- package/scripts/do-consumer-sweep.sh +72 -0
- package/scripts/do-derives-check.sh +88 -0
- package/scripts/do-fleet.sh +50 -1
- package/scripts/do-killswitch-audit.py +33 -14
- package/scripts/do-promise-lint.sh +150 -4
- package/scripts/do-promise-settle.sh +131 -9
- package/scripts/do-prove.sh +197 -17
- package/scripts/do-rank.py +606 -20
- package/scripts/do-rubric.py +21 -2
- package/scripts/do-signal.sh +157 -17
- package/scripts/do-smoke.sh +9 -0
- package/scripts/do-substrate-check.sh +49 -0
- package/scripts/do-tasks-wire-check.sh +81 -0
- package/scripts/do-tier.sh +90 -5
- package/scripts/do-ui-gate.sh +332 -0
- package/scripts/do-untracked-gate.sh +231 -0
- package/scripts/do-walk.sh +344 -0
- package/scripts/factory-check.sh +747 -0
- package/scripts/factory-repo.sh +1361 -0
- package/scripts/factory-tasks-check.sh +85 -0
- package/scripts/outcome-pull.ts +279 -0
- package/scripts/promise-manifest.mjs +167 -0
- package/scripts/reconcile-allow.txt +5 -0
- package/scripts/rubric-weights.json +64 -0
- package/scripts/spine-canary.sh +20 -0
- package/scripts/typedb-cluster-status.sh +128 -0
- package/scripts/typedb-env.sh +182 -0
- package/scripts/typedb-flap-recorder.sh +85 -0
- package/scripts/typedb-probes/containment-probe.py +88 -0
- package/scripts/typedb-probes/panic-probe-version.py +92 -0
- package/scripts/typedb-probes/panic-probe.py +77 -0
- package/scripts/typedb-scratch.sh +73 -0
- package/scripts/urls-lint.sh +58 -0
- package/scripts/w4-rubric.ts +96 -12
- package/scripts/wf-check.mjs +75 -0
- package/skills/ai-ui/SKILL.md +1 -1
- package/skills/astro/SKILL.md +177 -132
- package/skills/astro/add-content-collection.md +29 -13
- package/skills/astro/check-build.md +19 -13
- package/skills/astro/create-component.md +36 -9
- package/skills/astro/create-page.md +39 -10
- package/skills/astro/optimize-performance.md +36 -8
- package/skills/cli/SKILL.md +254 -220
- package/skills/dev/SKILL.md +1 -1
- package/skills/directory-autofill/SKILL.md +68 -0
- package/skills/docs/SKILL.md +4 -4
- package/skills/mcp/SKILL.md +186 -156
- package/skills/promise-make/SKILL.md +2 -2
- package/skills/puck/SKILL.md +465 -250
- package/skills/react19/SKILL.md +196 -296
- package/skills/reactflow/SKILL.md +387 -412
- package/skills/sdk/SKILL.md +167 -106
- package/skills/shadcn/SKILL.md +245 -201
- package/skills/signal/SKILL.md +5 -0
- package/skills/sui/SKILL.md +6 -1
- package/skills/tutorial/SKILL.md +15 -0
- package/skills/typedb/SKILL.md +427 -217
- package/skills/typedb/examples/python-patterns.py +11 -4
- package/skills/typedb/examples/query-patterns.tql +8 -3
- package/skills/typedb/examples/schema-patterns.tql +4 -2
- package/skills/typedb/reference/migration-2x-3x.md +19 -1
- package/skills/typedb/reference/python-driver.md +17 -1
- package/skills/voice/SKILL.md +117 -0
- package/skills/writer/SKILL.md +29 -12
- package/templates/template-feature.md +26 -1
- package/templates/template-tests.md +4 -0
- 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
|