@oxy-hq/cli 0.1.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 (39) hide show
  1. package/README.md +163 -0
  2. package/dist/main.mjs +6688 -0
  3. package/dist/main.mjs.map +1 -0
  4. package/dist/mcp-VdontkUr.mjs +316 -0
  5. package/dist/mcp-VdontkUr.mjs.map +1 -0
  6. package/json-schemas/agent-test.json +104 -0
  7. package/json-schemas/agentic.json +406 -0
  8. package/json-schemas/app.json +1884 -0
  9. package/json-schemas/config.json +2215 -0
  10. package/json-schemas/flow-test.json +124 -0
  11. package/json-schemas/global-semantics.json +782 -0
  12. package/json-schemas/workflow.json +1907 -0
  13. package/package.json +65 -0
  14. package/skills/oxy-airway-pipelines/SKILL.md +338 -0
  15. package/skills/oxy-automations/SKILL.md +67 -0
  16. package/skills/oxy-cli/SKILL.md +134 -0
  17. package/skills/oxy-custom-apps/SKILL.md +330 -0
  18. package/skills/oxy-data-architecture/SKILL.md +225 -0
  19. package/skills/oxy-semantic-modeling/SKILL.md +216 -0
  20. package/template/.gitattributes +2 -0
  21. package/template/.github/scripts/ci-publish.sh +77 -0
  22. package/template/.github/scripts/discover-apps.sh +175 -0
  23. package/template/.github/scripts/publish-env.sh +55 -0
  24. package/template/.github/workflows/publish.yaml +653 -0
  25. package/template/.github/workflows/validate.yaml +264 -0
  26. package/template/.nvmrc +1 -0
  27. package/template/.oxyc-managed +297 -0
  28. package/template/README.md +414 -0
  29. package/template/_gitignore +32 -0
  30. package/template/apps/.gitkeep +0 -0
  31. package/template/config.yml +53 -0
  32. package/template/memory/.gitkeep +0 -0
  33. package/template/package.json +17 -0
  34. package/template/pipelines/.gitkeep +0 -0
  35. package/template/pnpm-workspace.yaml +32 -0
  36. package/template/scripts/dev.sh +443 -0
  37. package/template/semantics/topics/.gitkeep +0 -0
  38. package/template/semantics/views/.gitkeep +0 -0
  39. package/template/workflows/.gitkeep +0 -0
@@ -0,0 +1,264 @@
1
+ # Validate __NAME__'s semantic model on every pull request.
2
+ #
3
+ # `oxy compile` is the gate. It parses every workspace YAML file and writes the
4
+ # result into a Postgres schema, so this job needs a throwaway database — and
5
+ # it needs one more thing that is easy to miss: a row in `workspaces` for the
6
+ # workspace id it compiles under. That row is created at `oxy serve --local`
7
+ # boot, NOT by migrations, so a CI job that only migrates dies on a foreign-key
8
+ # violation *before reading a single line of YAML*.
9
+ #
10
+ # Every step below was run against a real Postgres 16 container before it was
11
+ # written here, on the pinned CLI version. If you change a step, run it — and
12
+ # run it in BOTH layouts, since `OXY_WORKSPACE` below makes this file one that
13
+ # has two correct answers rather than one.
14
+ #
15
+ # What this gate catches and what it does not: see "What CI checks" in README.md.
16
+
17
+ name: validate
18
+
19
+ on:
20
+ pull_request:
21
+ # Also on a push to the default branch, and the reason is not belt-and-braces.
22
+ # With `pull_request` alone this gate never ran at all on the workflow this
23
+ # template actually ships with: a scaffolded repo starts unprotected, work
24
+ # goes straight to main, and the compile check sat there being triggered by
25
+ # nothing. It stays useful once PRs are required too — a merge is a push, and
26
+ # a branch protected today can be unprotected tomorrow.
27
+ #
28
+ # Deliberately no `paths:` filter. `oxy compile` reads the whole workspace,
29
+ # and a filter that missed a path would skip validation silently, which is
30
+ # worse than compiling on a README change.
31
+ push:
32
+ branches: [main]
33
+
34
+ # This gate reads the tree and compiles it into a throwaway database. It never
35
+ # writes to the repository, so it does not need a token that can. Stated
36
+ # explicitly because the DEFAULT is whatever the repo or org is set to, which
37
+ # for many repos is read/write on every scope.
38
+ permissions:
39
+ contents: read
40
+
41
+ env:
42
+ # Pinned deliberately. `get.oxy.tech` honours OXY_VERSION, and this is the
43
+ # version the sequence below was proven against — bump it only after
44
+ # re-running the sequence, not on sight.
45
+ OXY_VERSION: "0.5.126"
46
+
47
+ # WHERE THE OXY WORKSPACE IS, relative to the repo root.
48
+ #
49
+ # . the repo IS the workspace — every repo `oxyc new` scaffolds
50
+ # oxy the repo CONTAINS one — the shape of every repo that was imported
51
+ #
52
+ # Substituted at render time from `__WORKSPACE__`, by the same machinery that
53
+ # writes __SLUG__ and __NAME__, so `oxyc update` re-renders it and there is no
54
+ # special case anywhere for a repo shaped the second way.
55
+ #
56
+ # It exists because this gate cannot be layout-agnostic and must not be
57
+ # layout-wrong: `oxy compile` at the repo root of a repo whose workspace is
58
+ # `oxy/` reads no config.yml, compiles nothing, and fails. A CI file that
59
+ # installs cleanly and then fails on every run is worse than one that was
60
+ # never installed.
61
+ #
62
+ # The TOOLING adapts to the repo, never the other way round. The platform
63
+ # registered this subdirectory when the workspace was onboarded and there is
64
+ # no UI or CLI to change it, so flattening a repo to match this template
65
+ # would silently break its registration with no repair short of re-onboarding.
66
+ OXY_WORKSPACE: "__WORKSPACE__"
67
+
68
+ jobs:
69
+ compile:
70
+ name: oxy compile
71
+ runs-on: ubuntu-latest
72
+
73
+ services:
74
+ postgres:
75
+ image: postgres:16
76
+ env:
77
+ POSTGRES_PASSWORD: ci
78
+ ports:
79
+ - 5432:5432
80
+ options: >-
81
+ --health-cmd "pg_isready -U postgres"
82
+ --health-interval 5s
83
+ --health-timeout 5s
84
+ --health-retries 10
85
+
86
+ env:
87
+ OXY_DATABASE_URL: postgres://postgres:ci@localhost:5432/postgres
88
+ # OXY_STATE_DIR is set per step, not here: the `runner` context is not
89
+ # available in a job-level `env:` block, and the expression would expand
90
+ # to the empty string — putting the CLI's state dir at "/oxy-state".
91
+ # actionlint catches this; a job that "just ran" would not.
92
+
93
+ steps:
94
+ - uses: actions/checkout@v4
95
+ with:
96
+ # This job runs a downloaded installer script. Without this, checkout
97
+ # leaves a GITHUB_TOKEN in .git/config for that script to find. The
98
+ # job-level `permissions: contents: read` already narrows what such a
99
+ # token could do; not persisting it at all is the cheaper half of the
100
+ # same argument, and it costs nothing here — nothing in this job
101
+ # pushes.
102
+ persist-credentials: false
103
+
104
+ - name: Install the pinned oxy CLI
105
+ run: |
106
+ set -euo pipefail
107
+ # NOT `bash <(curl ...)`, the form the docs use: process substitution
108
+ # hands bash an empty file when the download fails, and bash then
109
+ # exits 0 — a step that reports "installed" having installed nothing.
110
+ # Download to a file first so curl's own -f failure is the step's.
111
+ curl -sSfL https://get.oxy.tech -o "$RUNNER_TEMP/get-oxy.sh"
112
+ OXY_VERSION="$OXY_VERSION" bash "$RUNNER_TEMP/get-oxy.sh"
113
+
114
+ # And check the installer left something behind: it downloads the
115
+ # binary with `curl -L` (no -f) and exits 0 regardless.
116
+ if [ ! -x "$HOME/.local/bin/oxy" ]; then
117
+ echo "::error::the installer exited 0 but left no executable at $HOME/.local/bin/oxy." >&2
118
+ exit 1
119
+ fi
120
+
121
+ # The installer drops the binary in ~/.local/bin for a non-root user
122
+ # and edits shell rc files, which a GitHub step never reads. Put it
123
+ # on PATH explicitly instead of hoping.
124
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
125
+
126
+ - name: Verify the oxy CLI
127
+ run: |
128
+ set -euo pipefail
129
+ # Not ceremony. Two silent failures make this load-bearing:
130
+ # 1. the installer runs `curl -L` WITHOUT -f, so a tag that does
131
+ # not exist writes GitHub's 404 HTML page to the binary path,
132
+ # chmod +x's it, and still exits 0;
133
+ # 2. this CLI falls through to `--help` with exit 0 on a subcommand
134
+ # it does not have, so an old binary "succeeds" at doing nothing.
135
+ if ! raw="$(oxy --version 2>&1)"; then
136
+ echo "::error::'oxy --version' failed — the installed binary is not runnable." >&2
137
+ printf '%s\n' "$raw" >&2
138
+ exit 1
139
+ fi
140
+
141
+ have="$(printf '%s\n' "$raw" | sed -n 's/^oxy-app version: \([0-9][0-9.]*\).*/\1/p' | head -1)"
142
+ if [ -z "$have" ]; then
143
+ echo "::error::could not read a version out of 'oxy --version' — the install probably wrote a 404 page." >&2
144
+ printf '%s\n' "$raw" >&2
145
+ exit 1
146
+ fi
147
+
148
+ oldest="$(printf '%s\n%s\n' "$OXY_VERSION" "$have" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)"
149
+ if [ "$oldest" != "$OXY_VERSION" ]; then
150
+ echo "::error::oxy $have is older than the pinned $OXY_VERSION." >&2
151
+ exit 1
152
+ fi
153
+
154
+ echo "oxy $have (pin $OXY_VERSION)"
155
+
156
+ - name: Migrate the throwaway database
157
+ env:
158
+ OXY_STATE_DIR: ${{ runner.temp }}/oxy-state
159
+ run: oxy migrate
160
+
161
+ - name: Seed the workspace row oxy compile needs
162
+ run: |
163
+ set -euo pipefail
164
+ # `oxy compile --workspace-id` defaults to the nil UUID, and inserting
165
+ # a revision for it needs the matching `workspaces` row to exist.
166
+ # Migrations do not create it. Seeding it here is one INSERT against a
167
+ # database that is destroyed with the job; the alternative — booting
168
+ # `oxy serve --local` just to trigger its boot-time seed — costs ~25s,
169
+ # binds two ports, and has no "seed and exit" mode.
170
+ if ! command -v psql >/dev/null 2>&1; then
171
+ echo "::error::psql is not on this runner. GitHub's ubuntu-latest image ships it; a self-hosted runner may not. Install postgresql-client, or run this INSERT with: docker exec ${{ job.services.postgres.id }} psql -U postgres" >&2
172
+ exit 1
173
+ fi
174
+ psql "$OXY_DATABASE_URL" -v ON_ERROR_STOP=1 -q -c \
175
+ "INSERT INTO workspaces (id, name) VALUES ('00000000-0000-0000-0000-000000000000', 'ci-validate') ON CONFLICT (id) DO NOTHING;"
176
+
177
+ - name: Compile the workspace
178
+ env:
179
+ OXY_STATE_DIR: ${{ runner.temp }}/oxy-state
180
+ run: |
181
+ set -euo pipefail
182
+
183
+ # --workspace-path MUST be absolute. See the floor check below.
184
+ #
185
+ # $OXY_WORKSPACE is repo-RELATIVE, so it is joined to $GITHUB_WORKSPACE
186
+ # and then CANONICALISED rather than concatenated: `cd` + `pwd -P`
187
+ # collapses the trailing `/.` that a root workspace would otherwise
188
+ # leave in the path, and it fails HERE, naming the directory, instead
189
+ # of handing oxy a path that is not there. One expression, correct for
190
+ # a repo that is the workspace and for one that contains it.
191
+ ws=""
192
+ if ! ws="$(cd "$GITHUB_WORKSPACE/$OXY_WORKSPACE" 2>/dev/null && pwd -P)"; then
193
+ echo "::error::no directory at \$GITHUB_WORKSPACE/$OXY_WORKSPACE. OXY_WORKSPACE says where this repo's Oxy workspace is, relative to the repo root: '.' when the repo IS the workspace, a subdirectory name when it contains one." >&2
194
+ exit 1
195
+ fi
196
+ echo "workspace: $ws (OXY_WORKSPACE=$OXY_WORKSPACE)"
197
+
198
+ status=0
199
+ oxy compile --workspace-path "$ws" --skip-migrations --json \
200
+ >"$RUNNER_TEMP/compile.stdout" 2>"$RUNNER_TEMP/compile.stderr" || status=$?
201
+
202
+ cat "$RUNNER_TEMP/compile.stderr" >&2
203
+
204
+ # `--json` does NOT put only JSON on stdout: a human-readable
205
+ # "Compiling workspace id=... path=..." line comes first, so piping
206
+ # stdout straight into jq fails ("Invalid numeric literal", exit 4).
207
+ # Extract from the line that opens the JSON object instead.
208
+ sed -n '/^{/,$p' "$RUNNER_TEMP/compile.stdout" > "$RUNNER_TEMP/compile.json"
209
+
210
+ if [ ! -s "$RUNNER_TEMP/compile.json" ]; then
211
+ echo "::error::oxy compile printed no JSON — it failed before compiling anything (database unreachable, or no workspaces row for --workspace-id)." >&2
212
+ exit 1
213
+ fi
214
+
215
+ # An empty .failures array is a legitimate result and jq exits 0 for
216
+ # it, so no `|| true` is needed — and `|| true` here would be wrong,
217
+ # since it would also swallow a real jq failure.
218
+ jq -r '.failures[] | "::error file=\(.path)::[\(.kind)] \(.message)"' "$RUNNER_TEMP/compile.json"
219
+ jq -r '"compile: status=\(.status) seen=\(.file_count_seen) compiled=\(.file_count_compiled) failed=\(.file_count_failed)"' "$RUNNER_TEMP/compile.json"
220
+
221
+ # Floor check, and it earns its place: `oxy compile --workspace-path .`
222
+ # finds config.yml and nothing else — every recursive kind is globbed
223
+ # as "<root>/**/*.view.yml", which matches nothing when <root> is "."
224
+ # — and it exits 0 while doing it. A green gate that validated no
225
+ # views is worse than no gate, so assert the walker saw at least the
226
+ # semantic files that are on disk.
227
+ seen="$(jq -r '.file_count_seen' "$RUNNER_TEMP/compile.json")"
228
+ floor=0
229
+ # Under $ws, not the checkout directory. In a repo whose workspace is
230
+ # `oxy/` the root has no config.yml, so a root-relative floor would
231
+ # sit one lower than the truth — quietly agreeing with a walk that saw
232
+ # nothing, which is the exact silence this check exists to break.
233
+ if [ -f "$ws/config.yml" ]; then
234
+ floor=$((floor + 1))
235
+ fi
236
+ # Count the WHOLE workspace tree, not just semantics/: the walker
237
+ # globs `**/*.view.yml` from the workspace root, so a view under
238
+ # models/ counts too, and a floor that only looked at semantics/ would
239
+ # sit at 1 and agree with a walk that saw nothing — the same silent
240
+ # agreement it exists to catch. Rooted at $ws for the same reason the
241
+ # config.yml check is: what the walker will descend into is the
242
+ # workspace, and a repo that contains one may hold .view.yml files
243
+ # outside it (a vendored fixture, an example) that the walker will
244
+ # never see and that would make this floor unreachably high.
245
+ # The prunes mirror the walker's own skip list; pruning a directory
246
+ # the walker would have descended can only UNDER-count, and a floor
247
+ # that is too low never fails a healthy repo.
248
+ # find prints nothing and exits 0 when there is no match, which is
249
+ # this repo's day-one state — a legitimate result, not an error to
250
+ # swallow. (On that empty tree floor=1=seen, so the check has nothing
251
+ # to detect and correctly says nothing.)
252
+ n="$(find "$ws" \( -name .git -o -name node_modules -o -name target \
253
+ -o -name dist -o -name build -o -name .oxy_state \
254
+ -o -name .semantics -o -name .preagg -o -name .worktrees \
255
+ -o -name .cubestore -o -name .airlayer_cache \) -prune \
256
+ -o -type f \( -name '*.view.yml' -o -name '*.topic.yml' \) \
257
+ ! -name '*.test.*' -print | wc -l | tr -d ' ')"
258
+ floor=$((floor + n))
259
+ if [ "$seen" -lt "$floor" ]; then
260
+ echo "::error::oxy compile saw $seen file(s) but at least $floor are on disk under $ws — the walker did not descend into the workspace. Check that --workspace-path is absolute, and that OXY_WORKSPACE ('$OXY_WORKSPACE') names the directory holding config.yml." >&2
261
+ exit 1
262
+ fi
263
+
264
+ exit "$status"
@@ -0,0 +1 @@
1
+ 20
@@ -0,0 +1,297 @@
1
+ # Who owns each file in a repo scaffolded from this template, and which of a
2
+ # repo's two trees each one describes.
3
+ #
4
+ # `oxyc update <customer>` brings an existing customer repo back in line with
5
+ # `template/`, and the only question that matters when it does is which files it
6
+ # may rewrite. The live customer repos are not scaffolds any more: one of them
7
+ # carries 7 views, 4 topics, a pipeline, an app and five memory facts, and its
8
+ # `package.json` differs from this template's because the customer added
9
+ # dependencies for that app. A sync that overwrote what it did not own would
10
+ # have eaten all of it.
11
+ #
12
+ # So the answer is written down HERE, next to the files, where whoever adds the
13
+ # next template file has to walk past it. It was written a phase BEFORE the
14
+ # command that reads it, on the same bet as `.oxyc-template.json`: the
15
+ # classification is cheap to record now and impossible to recover later, once
16
+ # repos exist that nobody can classify from memory.
17
+ #
18
+ # lib/sync.sh is the reader, and it decides nothing on its own — every role
19
+ # below is taken from this file and `unmatched` is the default it falls back to.
20
+ #
21
+ # --- format -----------------------------------------------------------------
22
+ #
23
+ # <directive> <glob> one per line, whitespace-separated
24
+ # # ... comment, and blank lines are ignored
25
+ #
26
+ # The glob is matched against a path relative to the repo root, with a shell
27
+ # `case`, so `*` spans `/` — `.github/workflows/*` covers any depth beneath it
28
+ # and `*.gitkeep` covers every placeholder wherever it sits.
29
+ #
30
+ # There are TWO families of directive and they answer different questions:
31
+ #
32
+ # managed / scaffold / mixed who owns the bytes
33
+ # scope-repo / scope-workspace which tree the file describes
34
+ #
35
+ # Each family has its own reader with its own allowlist, so a line from one is
36
+ # invisible to the other and a typo in either (`manged`, `scope-wrokspace`) is
37
+ # invisible to both — it falls to that family's default and fails the
38
+ # completeness test in tests/template_ownership.bats rather than quietly
39
+ # classifying a file under a word nothing recognises.
40
+ #
41
+ # --- the three roles, and what a wrong one costs ----------------------------
42
+ #
43
+ # managed The TOOL owns the bytes. A sync may overwrite the file outright,
44
+ # and a customer edit to it is a mistake that a sync is entitled to
45
+ # undo. Classifying wrongly here is DATA LOSS.
46
+ #
47
+ # scaffold Written once by `oxyc new` and never touched again. The customer
48
+ # owns it from the moment it lands. A sync may report that it has
49
+ # drifted; it may not rewrite it. Classifying wrongly here costs
50
+ # someone a manual copy — which is why anything genuinely uncertain
51
+ # belongs in this row rather than in `managed`.
52
+ #
53
+ # mixed Both, in different PARTS of one file. A sync must merge, never
54
+ # replace, and until it can do that surgically it must report and
55
+ # leave the file alone.
56
+ #
57
+ # That is the rule for a file that is THERE, and it is the only rule
58
+ # `oxyc update` has: a repo the tool scaffolded was given both halves
59
+ # on day one, so a mixed file missing from it was deleted on purpose
60
+ # and restoring it would undo somebody's decision. Update also runs
61
+ # over and over, and a command you run weekly must not quietly
62
+ # recreate what you removed.
63
+ #
64
+ # `oxyc adopt` meets the other case, and answers it differently. An
65
+ # imported repo never had the file, so there is no customer half to
66
+ # protect — and withholding it strands the managed files it pairs
67
+ # with. The first real adopt did exactly that: pokehouse-oxy got
68
+ # `scripts/dev.sh` and no package.json, so the `"dev"` entry that
69
+ # runs the script was never created and `pnpm dev` could not work.
70
+ # It installed the tool and withheld the handle.
71
+ #
72
+ # So adopt — a one-time act somebody types on purpose — installs a
73
+ # mixed file that is ABSENT and still never touches one that is
74
+ # PRESENT. Per FILE, not per role: pokehouse has a .gitignore, and
75
+ # that one is left exactly alone.
76
+ #
77
+ # --- and the role that is never written down: everything else ---------------
78
+ #
79
+ # unmatched The CUSTOMER'S. A sync must not rewrite it, must not delete it,
80
+ # and must not ask — never touch it.
81
+ #
82
+ # That is a DEFAULT, not an omission, and it is the single most important rule
83
+ # here. A scaffolded repo becomes mostly files this template never had: views,
84
+ # topics, pipelines, an app bundle, pnpm-lock.yaml, memory facts. Every one of
85
+ # them is unmatched BY CONSTRUCTION, and the file a sync does not recognise is
86
+ # exactly the file it did not create.
87
+ #
88
+ # So the safe direction is built into the shape rather than remembered: a rule
89
+ # has to be WRITTEN before anything may be rewritten, and forgetting to write
90
+ # one can only ever make a sync do less.
91
+ #
92
+ # `mixed` exists because of one concrete case, and it is worth stating: the
93
+ # root `package.json` holds a `"dev"` script entry that runs `scripts/dev.sh`.
94
+ # That entry is tool-owned — it is the entry point to a tool-owned script, and
95
+ # a customer deleting it has broken the local dev flow — while the
96
+ # `dependencies` beside it are the customer's app's, and are exactly what a
97
+ # blind overwrite would destroy. Two owners, one file. A two-way split would
98
+ # have forced a choice between losing the entry and losing the dependencies.
99
+ #
100
+ # --- the second axis: which of a repo's TWO trees does a file describe? -----
101
+ #
102
+ # A role says who owns a file's bytes. It does not say what the file is ABOUT,
103
+ # and in a repo `oxyc new` scaffolded there was never a difference to say: the
104
+ # repo IS the workspace, so every path above is simply "at the root".
105
+ #
106
+ # An IMPORTED repo splits that in two, and the template's files land on both
107
+ # sides of the split:
108
+ #
109
+ # config.yml and README.md describe the OXY WORKSPACE. pokehouse-oxy keeps
110
+ # its workspace in `oxy/`, where a config.yml and a readme already sit,
111
+ # written by somebody before oxyc saw the repo. The template's copies
112
+ # describe nothing at that repo's root — and writing them there would build a
113
+ # second, half-populated workspace beside the real one.
114
+ #
115
+ # package.json, pnpm-workspace.yaml and .gitignore describe the REPO ROOT,
116
+ # and are not Oxy workspace files at all. The first two are the PNPM
117
+ # workspace — a different workspace, which is where `scripts/dev.sh` is
118
+ # installed and where its `"dev"` entry has to live — and the third is the
119
+ # git working tree's ignore rules. All three belong at the root in both
120
+ # layouts, exactly as the managed machinery does.
121
+ #
122
+ # That distinction had nowhere to be written down, so lib/sync.sh inferred it
123
+ # from the ROLE — `scaffold` and `mixed` were skipped together as "the
124
+ # workspace's" — and the inference was wrong for all three of the second group.
125
+ # pokehouse-oxy is what that cost. Adopted before the mixed-and-absent rule
126
+ # landed, it has `scripts/dev.sh` and no package.json; adopt then refused it as
127
+ # already-stamped, and update called package.json a workspace file and would
128
+ # not so much as name the gap. The file could not arrive by any path.
129
+ #
130
+ # So it is written down, on its own lines, in its own vocabulary:
131
+ #
132
+ # scope-repo describes the REPO ROOT. Always in scope.
133
+ # scope-workspace describes the OXY WORKSPACE. Out of scope in a repo that
134
+ # keeps its workspace in a subdirectory; identical to
135
+ # scope-repo in a repo that IS its workspace.
136
+ #
137
+ # A SECOND AXIS, and not a fourth role or a modifier on the three. The two
138
+ # questions are independent — package.json is `mixed` AND repo-scoped,
139
+ # config.yml is `scaffold` AND workspace-scoped — so a file that could answer
140
+ # only one of them would lose the other. A fourth role would take config.yml's
141
+ # `scaffold` away, which is the statement that a sync may report its drift and
142
+ # never rewrite it, and buy nothing back. A modifier (`scaffold@workspace`)
143
+ # keeps one line per file and breaks every reader instead: the
144
+ # `case "$role" in managed)` arms in lib/sync.sh, lib/adopt.sh and lib/doctor.sh
145
+ # stop matching a compound token, and in adopt_plan the fall-through is
146
+ # `continue` — a file silently not installed, which is precisely the failure
147
+ # this axis exists to end.
148
+ #
149
+ # THE DEFAULT IS `scope-repo`, and it deliberately points the OTHER WAY from
150
+ # `unmatched` above. A wrong role costs DATA LOSS, so the role default has to
151
+ # be the quiet one and forgetting a role can only make a sync do less. Scope
152
+ # cannot cost data: it never widens what a role permits, and `scaffold` and
153
+ # `mixed` are not written in any layout. What scope trades is NOISE against
154
+ # SILENCE — and silence is the whole defect. Forget a `scope-workspace` line
155
+ # and a file this repo will never have is reported on every run until somebody
156
+ # writes it. Forget a `scope-repo` line under the opposite default and the file
157
+ # can never arrive and nothing anywhere says so. The unwritten answer is
158
+ # therefore the LOUD one.
159
+ #
160
+ # tests/template_ownership.bats requires both answers for every file the
161
+ # template ships, so that default is unreachable from this manifest — it
162
+ # governs only a manifest already failing its own test.
163
+
164
+ # --- managed: the tool's own machinery --------------------------------------
165
+
166
+ # CI. The workflows and the shell they call are versioned with the tooling,
167
+ # pinned together (both files carry the same OXY_VERSION and a test enforces
168
+ # it), and a customer editing them by hand is how a fleet of repos ends up on
169
+ # eight different CI definitions.
170
+ # The globs below match file EXTENSIONS rather than whole directories, and
171
+ # that is deliberate. `scripts/*` would silently absorb a `scripts/NOTES.txt`
172
+ # somebody adds next year into `managed` — a file nobody classified, in the
173
+ # data-loss direction. Narrowed, that file is simply unmatched, which is both
174
+ # the safe answer and a failing completeness test until a human decides.
175
+ managed .github/workflows/*.yaml
176
+ managed .github/workflows/*.yml
177
+ managed .github/scripts/*.sh
178
+
179
+ # Local dev. Same argument: `scripts/dev.sh` is the tooling's answer to "run an
180
+ # app locally", and it is meant to improve for everyone at once.
181
+ managed scripts/*.sh
182
+
183
+ # The merge rule for the generated memory index. It is a statement about how
184
+ # oxyc's own generated file behaves, so oxyc owns it.
185
+ managed .gitattributes
186
+
187
+ # This file.
188
+ managed .oxyc-managed
189
+
190
+ # Not a template file at all — `oxyc new` writes it at render time, and
191
+ # `oxyc update --apply` re-points it at the commit it synced from. Listed here
192
+ # so that the sync's right to rewrite it is recorded in the same place as
193
+ # everything else's, rather than being a special case buried in code.
194
+ managed .oxyc-template.json
195
+
196
+ # DELIBERATELY ABSENT: `.oxyc.json`, the repo's own statement about where its
197
+ # Oxy workspace is (see lib/workspace.sh). No rule is written for it, which
198
+ # means it falls to `unmatched` — the customer's, never touch — and that is
199
+ # exactly right twice over. It is hand-written, so a sync rewriting it would
200
+ # undo a decision somebody made; and it is the file that tells the sync what
201
+ # shape the repo is, so a sync that overwrote it would be sawing off the branch
202
+ # it stands on. Named here only so that the absence reads as a decision.
203
+ #
204
+ # This template does not ship one, either: a scaffolded repo is a workspace at
205
+ # its root, detection settles that unaided, and shipping an override would make
206
+ # the exception look like the norm.
207
+
208
+ # --- scaffold: the customer's from the moment it lands ----------------------
209
+
210
+ # Both are edited immediately and permanently: config.yml gains the models and
211
+ # databases of a real workspace, README.md gains whatever the team needs to say
212
+ # about this customer.
213
+ scaffold config.yml
214
+ scaffold README.md
215
+
216
+ # Node version. A single line, and the customer may legitimately move it — an
217
+ # app that needs a newer runtime is their call, not the tooling's. There is no
218
+ # tool-owned structure inside it to merge, so `mixed` would mean nothing here;
219
+ # a sync that forced it back would be an unrequested downgrade, so `managed` is
220
+ # wrong too. Report the drift and let a human decide.
221
+ scaffold .nvmrc
222
+
223
+ # Placeholders that exist only so git carries an empty directory. Once a real
224
+ # view or pipeline lands beside one, the customer usually deletes it — and a
225
+ # sync that recreated it would be resurrecting a file somebody removed on
226
+ # purpose. Written once, then never ours again.
227
+ scaffold *.gitkeep
228
+
229
+ # --- mixed: two owners, one file --------------------------------------------
230
+
231
+ # The case the third role exists for; see the note at the top.
232
+ mixed package.json
233
+
234
+ # The `.gitignore` rules this template ships are load-bearing — dropping
235
+ # `node_modules/` or `out/` commits a build tree on the customer's next
236
+ # `git add -A`, and a test in customer-tooling pins the exact rule lines — so
237
+ # those lines are the tool's. Anything the customer adds below them is theirs
238
+ # and is expected: a scratch directory, an app-specific artifact. Both at once,
239
+ # which is `mixed`, not a judgement call between the two.
240
+ mixed .gitignore
241
+
242
+ # Same shape, and the file says so itself: the `onlyBuiltDependencies` block
243
+ # carries the three entries the shipped app bundles need, with an instruction
244
+ # to ADD to the list rather than replace it. The `packages:` globs are the
245
+ # tool's; a fourth dependency that needs its install script run is the
246
+ # customer's.
247
+ mixed pnpm-workspace.yaml
248
+
249
+ # --- scope: which tree each file describes ----------------------------------
250
+ #
251
+ # Grouped by answer rather than written beside the role above, so that the two
252
+ # axes are never read as one column of five roles. Every file the template
253
+ # ships appears here exactly once, the same rule the roles are held to.
254
+
255
+ # THE REPO ROOT. All of the managed machinery first: CI runs from the repo root
256
+ # in both layouts, `.github/scripts/` is what it calls, and the stamp and this
257
+ # manifest describe the repo oxyc adopted rather than the workspace inside it.
258
+ scope-repo .github/workflows/*.yaml
259
+ scope-repo .github/workflows/*.yml
260
+ scope-repo .github/scripts/*.sh
261
+ scope-repo scripts/*.sh
262
+ scope-repo .gitattributes
263
+ scope-repo .oxyc-managed
264
+ scope-repo .oxyc-template.json
265
+
266
+ # The PNPM workspace and the git working tree, which are root things in a repo
267
+ # whose Oxy workspace is somewhere else. `pnpm dev` is run from the repo root;
268
+ # `packages: apps/*` names directories at the repo root, which is also where
269
+ # `.github/scripts/discover-apps.sh` globs for bundles; `.gitignore` governs
270
+ # what `git add -A` stages across the whole repo, `node_modules/` and `out/`
271
+ # included. `.nvmrc` is the node version all of that builds under.
272
+ scope-repo package.json
273
+ scope-repo pnpm-workspace.yaml
274
+ scope-repo .gitignore
275
+ scope-repo .nvmrc
276
+
277
+ # Two placeholders whose directories are also the repo's, not the workspace's.
278
+ # `memory/` is about the CUSTOMER and sits at the repo root in both shapes —
279
+ # lib/workspace.sh says so beside memory_dir, and it is the one directory oxyc
280
+ # creates in a repo it did not scaffold. `apps/` is where publish.yaml looks
281
+ # for bundles, from the repo root, and it is what pnpm-workspace.yaml declares.
282
+ scope-repo memory/.gitkeep
283
+ scope-repo apps/.gitkeep
284
+
285
+ # THE OXY WORKSPACE. config.yml IS the workspace — it is the file
286
+ # workspace_resolve looks for, and finding it one level down is what makes a
287
+ # repo a subdirectory repo in the first place — and README.md is that
288
+ # workspace's own readme, describing the layout below it. An imported repo has
289
+ # both already, inside `oxy/`, and its root README is the project's.
290
+ scope-workspace config.yml
291
+ scope-workspace README.md
292
+
293
+ # The placeholders holding open the workspace's own directories. They live
294
+ # beside config.yml in a scaffolded repo and inside `oxy/` in an imported one.
295
+ scope-workspace pipelines/.gitkeep
296
+ scope-workspace workflows/.gitkeep
297
+ scope-workspace semantics/*/.gitkeep