@jenga-ai/agent 1.1.0 → 1.2.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/README.md +7 -3
- package/agents/developer.md +82 -2
- package/agents/scrum-master.md +140 -21
- package/agents/tester.md +90 -8
- package/hooks/on_session_end.sh +171 -20
- package/package.json +1 -1
- package/scripts/check-permission-level.sh +107 -0
- package/scripts/check-publicignore-match.sh +122 -0
- package/scripts/check-worktree-liveness.sh +193 -0
- package/scripts/generate-rapport-manifest.sh +43 -0
- package/scripts/idea_manager.sh +47 -0
- package/scripts/install-worktree-commit-guard.sh +134 -0
- package/scripts/jenga-permission-level-switch.sh +109 -0
- package/scripts/smoke-harness.sh +139 -0
- package/scripts/validate-board.sh +62 -0
- package/scripts/with-lock.sh +158 -0
- package/scripts/worktree-remove-guard.sh +204 -0
- package/skills/clearify/SKILL.md +52 -0
- package/skills/commit/SKILL.md +13 -4
- package/skills/distribute/CONFIG_SCHEMA.md +60 -2
- package/skills/do/SKILL.md +48 -11
- package/skills/doc-sync/SKILL.md +16 -0
- package/skills/doc-sync/assets/doc_targets.md +11 -0
- package/skills/idea/SKILL.md +56 -0
- package/skills/idea/assets/idea_handoff_template.md +26 -0
- package/skills/idea/assets/idea_template.md +3 -0
- package/skills/init/SKILL.md +100 -7
- package/skills/init/assets/directory_structure.txt +1 -0
- package/skills/init/assets/workflow_template.json +1 -1
- package/skills/init/scripts/apply-project-visibility.sh +176 -0
- package/skills/init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/init/scripts/init.sh +30 -1
- package/skills/jenga/SKILL.md +160 -17
- package/skills/jenga/scripts/board-scan.sh +238 -0
- package/skills/jenga/scripts/cascade-resolve.sh +297 -0
- package/skills/jenga/scripts/render-confirmation.sh +679 -0
- package/skills/jenga/scripts/render-picker.sh +439 -0
- package/skills/jenga/scripts/resolve-id.sh +367 -0
- package/skills/jenga-permission-level/SKILL.md +81 -0
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +8 -5
- package/skills/publish/assets/ci-contract.md +2 -2
- package/skills/publish/assets/ownership-matrix.md +1 -1
- package/skills/publish/scripts/finalize_changelog.sh +115 -0
- package/skills/publish/scripts/generate_release_notes.sh +475 -28
- package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
- package/skills/publish/scripts/publish_deploy.sh +38 -8
- package/skills/publish/scripts/run_gates.sh +2 -2
- package/skills/reconcile/SKILL.md +117 -5
- package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
- package/skills/spinoff/SKILL.md +12 -7
- package/skills/todo/SKILL.md +2 -0
- package/skills/uncharted/SKILL.md +711 -0
- package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
- package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
- package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
- package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
- package/skills/uncharted/scripts/detect-tests.sh +553 -0
- package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
- package/skills/uncharted/scripts/enumerate-target.sh +470 -0
- package/skills/uncharted/scripts/import-source.sh +517 -0
- package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
- package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
- package/skills/uncharted/scripts/run-engine.sh +655 -0
- package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
- package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
- package/skills/wtf/SKILL.md +20 -0
- package/templates/CHANGELOG_TEMPLATE.md +13 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
- package/templates/permission-levels/README.md +73 -0
- package/templates/permission-levels/level-1-locked.json +71 -0
- package/templates/permission-levels/level-2-guarded.json +64 -0
- package/templates/permission-levels/level-3-standard.json +62 -0
- package/templates/permission-levels/level-4-elevated.json +60 -0
- package/templates/permission-levels/level-5-unrestricted.json +58 -0
- package/skills/convert/SKILL.md +0 -124
- package/skills/convert/convert_cli.py +0 -235
- package/skills/convert/tests/sample.csv +0 -4
- package/skills/convert/tests/sample.json +0 -5
- package/skills/convert/tests/sample.jsonl +0 -3
- package/skills/convert/tests/sample.yaml +0 -18
- package/skills/convert/tests/sample_obj.csv +0 -2
- package/skills/convert/tests/sample_obj.json +0 -9
- package/skills/mirror-public/SKILL.md +0 -237
- package/skills/mirror-public/assets/config.json +0 -5
- package/skills/mirror-public/scripts/mirror.sh +0 -374
- package/skills/self-sync/SKILL.md +0 -73
- package/skills/self-sync/scripts/run.js +0 -136
- package/skills/strategy/SKILL.md +0 -312
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: mirror-public
|
|
3
|
-
description: Mirror this private repo one-way to its public counterpart (`https://github.com/samwelmunga/jenga-npm`), applying a `.publicignore` blocklist and producing a single squash commit per run so private board, queue, log, and rapport artefacts never leak downstream.
|
|
4
|
-
keywords:
|
|
5
|
-
- mirror public
|
|
6
|
-
- public mirror
|
|
7
|
-
- jenga-npm
|
|
8
|
-
- one-way sync
|
|
9
|
-
- publicignore
|
|
10
|
-
- squash mirror
|
|
11
|
-
examples:
|
|
12
|
-
- "/mirror-public --dry-run"
|
|
13
|
-
- "/mirror-public"
|
|
14
|
-
- "/mirror-public --force"
|
|
15
|
-
- "preview what would ship to the public repo"
|
|
16
|
-
- "sync the public mirror"
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
# Mirror-Public
|
|
20
|
-
|
|
21
|
-
Push a curated subset of this private repo to the public counterpart at `https://github.com/samwelmunga/jenga-npm` as a **one-way**, **squash-committed** mirror. Private is always the source of truth; the public repo is a downstream shadow that this skill can rebuild at any time.
|
|
22
|
-
|
|
23
|
-
Distinct from `/publish` (which ships the `jenga-agent` npm package to npmjs.com) and from `/self-sync` (which mirrors root → in-repo `.claude/.agents/`). This skill is the third distribution surface: the public GitHub repo.
|
|
24
|
-
|
|
25
|
-
## Direction & Contract
|
|
26
|
-
|
|
27
|
-
- **Direction:** private → public, one-way. Public never merges back. If a public commit exists that this skill did not create, the run aborts by default (see [Safety](#safety-model)).
|
|
28
|
-
- **Trigger:** manual only. There is no CI hook, no git hook, no on-commit automation. The risk of leaking WIP outweighs the convenience.
|
|
29
|
-
- **Commit model:** each real run produces exactly **one squash commit** on the public side. Private commit messages, authors, and history never appear on the public repo.
|
|
30
|
-
- **Scope model:** ships everything **except** what `.publicignore` blocks. Additive-by-default; the blocklist is the single source of truth for what stays private.
|
|
31
|
-
|
|
32
|
-
## Invocation
|
|
33
|
-
|
|
34
|
-
| Command | Effect |
|
|
35
|
-
|---------|--------|
|
|
36
|
-
| `/mirror-public --dry-run` | Read-only preview. Prints the exact "would ship" and "would be blocked" file lists plus totals. No fetch-write, no commit, no push. |
|
|
37
|
-
| `/mirror-public` | Real run. Fetches the public tip, runs the safety check, rsyncs, squash-commits, pushes. Aborts if the public repo is ahead of the last mirror marker. |
|
|
38
|
-
| `/mirror-public --force` | Same as the real run, but overrides the safety abort — the public branch is force-updated to match private (blocklist applied). Use only for recovery; see [Recovery flow](#recovery-when-the-public-repo-is-ahead). |
|
|
39
|
-
|
|
40
|
-
All three delegate to `skills/mirror-public/scripts/mirror.sh`. This SKILL body never inlines mirror logic — everything lives in the script.
|
|
41
|
-
|
|
42
|
-
### How it is actually run
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
# preview
|
|
46
|
-
bash skills/mirror-public/scripts/mirror.sh --dry-run
|
|
47
|
-
|
|
48
|
-
# real run
|
|
49
|
-
bash skills/mirror-public/scripts/mirror.sh
|
|
50
|
-
|
|
51
|
-
# force overwrite when the public repo is ahead
|
|
52
|
-
bash skills/mirror-public/scripts/mirror.sh --force
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Blocklist — `.publicignore`
|
|
56
|
-
|
|
57
|
-
`.publicignore` lives at the **repo root** (discoverable like `.gitignore`). It is a `.gitignore`-style pattern file consumed by `rsync --exclude-from=<repo-root>/.publicignore`. Every line is a path relative to the repo root that MUST NEVER ship to the public mirror.
|
|
58
|
-
|
|
59
|
-
Current baseline (see the file for the authoritative list) blocks:
|
|
60
|
-
|
|
61
|
-
- `project/board/`, `project/queue/`, `project/logs/`, `project/rapports/`, `project/todo.md`
|
|
62
|
-
- `project/documentation/plans/`, `project/documentation/summaries/`, `project/instructions/`
|
|
63
|
-
- `.env`, `.env.*`, `*.local`, `*.pid`, `.claude/settings.local.json`
|
|
64
|
-
- `.claude/worktrees/`, `.agents/worktrees/`, `.mirror-worktrees/`
|
|
65
|
-
- `node_modules/`, `**/__pycache__/`, `*.pyc`, `.DS_Store`, `.git/`
|
|
66
|
-
|
|
67
|
-
### Adding new entries
|
|
68
|
-
|
|
69
|
-
Edit `.publicignore` and add the path(s). Rules:
|
|
70
|
-
|
|
71
|
-
- **Over-block, don't under-block.** Anything missed is a private leak; anything extra is merely an unshipped file.
|
|
72
|
-
- **Verify with `--dry-run` before pushing.** The dry-run "Files that would ship" section is the honest answer to "did my new pattern match?".
|
|
73
|
-
- Directory-globs must end in `/` (e.g. `project/scratch/`), otherwise rsync only matches a file of that exact name.
|
|
74
|
-
|
|
75
|
-
## Config — `skills/mirror-public/assets/config.json`
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"publicRepoUrl": "https://github.com/samwelmunga/jenga-npm.git",
|
|
80
|
-
"defaultBranch": "main",
|
|
81
|
-
"worktreePath": ".mirror-worktrees/public"
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
| Field | Purpose |
|
|
86
|
-
|-------|---------|
|
|
87
|
-
| `publicRepoUrl` | URL of the public downstream repo. Can be overridden at runtime by `MIRROR_PUBLIC_URL_OVERRIDE` (used by tests and by anyone rehearsing against a local bare remote). |
|
|
88
|
-
| `defaultBranch` | Branch mirrored on the public side. `main` in production. |
|
|
89
|
-
| `worktreePath` | Relative path (from the private repo root) where the script keeps its scratch clone of the public repo. Blocked from itself via `.publicignore` (`.mirror-worktrees/`). |
|
|
90
|
-
|
|
91
|
-
## Safety model
|
|
92
|
-
|
|
93
|
-
The script maintains a **local `last-mirror-sync` tag** inside its scratch clone that records the public SHA it last produced. Before every real run it compares that tag to the current tip of `origin/<defaultBranch>`:
|
|
94
|
-
|
|
95
|
-
- **Tag missing** (first ever run, or scratch clone was wiped): proceed.
|
|
96
|
-
- **Tag matches remote tip:** proceed — the public repo has not moved since our last mirror.
|
|
97
|
-
- **Tag lags remote tip:** the public repo has commits the mirror did not create. **Abort with a non-zero exit** unless `--force` is passed.
|
|
98
|
-
|
|
99
|
-
`--force` skips the abort and overwrites the public branch with the private state (post-blocklist). It is a **destructive-by-default** flag: any commit that landed on the public repo outside this skill will disappear from the public branch. Rescue that work first (see [Recovery flow](#recovery-when-the-public-repo-is-ahead)) before using `--force`.
|
|
100
|
-
|
|
101
|
-
## Squash-commit model
|
|
102
|
-
|
|
103
|
-
Every real run creates exactly one commit on the public branch:
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
chore(mirror): sync from private at <short-sha> <UTC-timestamp>
|
|
107
|
-
|
|
108
|
-
Source-Commit: <full-private-sha>
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
- **Subject** identifies the private HEAD at the time of the mirror.
|
|
112
|
-
- **`Source-Commit:` trailer** carries the full private SHA so anyone reading the public commit can cross-reference (against a private clone they have access to) which private state produced this snapshot.
|
|
113
|
-
- **No private log leaks.** Individual private commits, author names, and messages are never replayed on the public side.
|
|
114
|
-
- **Idempotent.** If the working tree matches the public tip post-blocklist, the script exits without creating a commit and prints `nothing to mirror — public tree already matches private (post-blocklist)`.
|
|
115
|
-
|
|
116
|
-
## Examples
|
|
117
|
-
|
|
118
|
-
All example outputs below were captured against a local bare remote (`/tmp/mirror-t04-test.git`) using `MIRROR_PUBLIC_URL_OVERRIDE`, so the same shape reproduces without touching the real `jenga-npm.git`.
|
|
119
|
-
|
|
120
|
-
### Dry-run
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
bash skills/mirror-public/scripts/mirror.sh --dry-run
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Tail of the output:
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
=== Files that would ship (607) ===
|
|
130
|
-
...
|
|
131
|
-
=== Files that would be blocked (599) ===
|
|
132
|
-
project/board/...
|
|
133
|
-
project/queue/...
|
|
134
|
-
project/logs/...
|
|
135
|
-
project/rapports/...
|
|
136
|
-
project/todo.md
|
|
137
|
-
...
|
|
138
|
-
|
|
139
|
-
================ mirror-public dry-run summary ================
|
|
140
|
-
would ship : 607 files
|
|
141
|
-
would block : 599 files
|
|
142
|
-
public URL : https://github.com/samwelmunga/jenga-npm.git
|
|
143
|
-
remote branch : main
|
|
144
|
-
dry run — no push, no commit, no remote mutation
|
|
145
|
-
===============================================================
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Zero remote mutation. Use this before every real run when the mirror state is uncertain, and after every `.publicignore` edit to confirm the pattern matched.
|
|
149
|
-
|
|
150
|
-
### Real run
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
bash skills/mirror-public/scripts/mirror.sh
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Tail of the output (first mirror against a fresh remote):
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
mirror.sh: no last-mirror-sync tag found (first mirror or wiped scratch) — proceeding
|
|
160
|
-
mirror.sh: rsync <repo>/ -> <repo>/.mirror-worktrees/public/ (excludes: .publicignore + .git)
|
|
161
|
-
mirror.sh: pushing b955ae3380a5d562799b0e8a75bbb758f8773104 -> origin/main
|
|
162
|
-
|
|
163
|
-
================ mirror-public summary ================
|
|
164
|
-
files changed : 607
|
|
165
|
-
new commit : b955ae3380a5d562799b0e8a75bbb758f8773104
|
|
166
|
-
remote branch : main
|
|
167
|
-
public URL : https://github.com/samwelmunga/jenga-npm.git
|
|
168
|
-
=======================================================
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
A second consecutive run reports `nothing to mirror — public tree already matches private (post-blocklist)` and exits `0`.
|
|
172
|
-
|
|
173
|
-
### `--force`
|
|
174
|
-
|
|
175
|
-
Use only when the safety abort fires and you have already rescued any external work off the public branch.
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
bash skills/mirror-public/scripts/mirror.sh --force
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Look for the WARNING line — it names the divergent SHAs so you can verify you meant to overwrite them:
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
mirror.sh: WARNING: origin/main (7524425...) has moved past last-mirror-sync (b955ae3...); proceeding due to --force
|
|
185
|
-
mirror.sh: rsync ...
|
|
186
|
-
mirror.sh: pushing 154f7b7... -> origin/main
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Recovery when the public repo is ahead
|
|
190
|
-
|
|
191
|
-
If the safety abort fires:
|
|
192
|
-
|
|
193
|
-
```
|
|
194
|
-
mirror.sh: error: origin/main (<remote-sha>) has moved past last-mirror-sync (<marker-sha>).
|
|
195
|
-
The public repo has commits the mirror did not create. Re-run with --force to overwrite.
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Do this before you reach for `--force`:
|
|
199
|
-
|
|
200
|
-
1. **Inspect the divergent commits.** In a separate clone of the public repo:
|
|
201
|
-
```bash
|
|
202
|
-
git clone https://github.com/samwelmunga/jenga-npm.git /tmp/jenga-npm-rescue
|
|
203
|
-
git -C /tmp/jenga-npm-rescue log <marker-sha>..origin/main
|
|
204
|
-
```
|
|
205
|
-
2. **Decide what to keep.** For each divergent commit that has value:
|
|
206
|
-
- If the change belongs in the private repo, port it into this repo (as a regular commit under normal review) so the next mirror carries it forward.
|
|
207
|
-
- If the change should live only on the public repo, save the patches somewhere durable (`git format-patch <marker-sha>..origin/main -o /tmp/rescue-patches`) — they will not survive `--force`.
|
|
208
|
-
3. **Re-run with `--force`** once you are confident the public branch can be overwritten:
|
|
209
|
-
```bash
|
|
210
|
-
bash skills/mirror-public/scripts/mirror.sh --force
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
The marker tag advances on success, and subsequent normal runs resume the abort-by-default behaviour.
|
|
214
|
-
|
|
215
|
-
## Environment overrides
|
|
216
|
-
|
|
217
|
-
| Variable | Purpose |
|
|
218
|
-
|----------|---------|
|
|
219
|
-
| `MIRROR_PUBLIC_URL_OVERRIDE` | Replaces `publicRepoUrl` from `config.json` at runtime. Used to rehearse against a local bare remote (e.g. `git init --bare /tmp/test-mirror.git`) without touching the real public repo. |
|
|
220
|
-
|
|
221
|
-
## Out of Scope
|
|
222
|
-
|
|
223
|
-
Per epic **E28 — Public Mirror**, these are explicitly not part of this skill:
|
|
224
|
-
|
|
225
|
-
- **Two-way sync** (PR back-flow from public → private)
|
|
226
|
-
- **Content-level scrubbing** (regex redactions inside file bodies)
|
|
227
|
-
- **Automated triggers** (git hooks, CI, watch-mode)
|
|
228
|
-
- **Cross-linking issues/PRs** between the two repos
|
|
229
|
-
|
|
230
|
-
See `project/board/epics/E28_public-mirror.md` → *Out of Scope* for the authoritative list.
|
|
231
|
-
|
|
232
|
-
## Guard rails
|
|
233
|
-
|
|
234
|
-
- **Never inline mirror logic in this SKILL body.** All filesystem, git, rsync, and push work goes through `skills/mirror-public/scripts/mirror.sh`.
|
|
235
|
-
- **Never push to `jenga-npm.git` from an ad-hoc script.** Only this skill should push. Ad-hoc pushes bypass the blocklist and the safety marker.
|
|
236
|
-
- **Never commit `.mirror-worktrees/`.** It is already in `.publicignore` and should also stay untracked in the private repo. If you see it in `git status`, the scratch clone was accidentally seeded outside the configured path — investigate before mirroring.
|
|
237
|
-
- **Never `git add` `.publicignore` matches on the private side to "hide" them from the mirror.** The private repo tracks whatever it needs to; the blocklist is the single filter and must remain the single filter.
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ---------------------------------------------------------------------------
|
|
3
|
-
# skills/mirror-public/scripts/mirror.sh
|
|
4
|
-
#
|
|
5
|
-
# One-way sync engine for /mirror-public.
|
|
6
|
-
#
|
|
7
|
-
# Reads: skills/mirror-public/assets/config.json
|
|
8
|
-
# - publicRepoUrl : URL of the public downstream repo
|
|
9
|
-
# - defaultBranch : branch to sync (typically "main")
|
|
10
|
-
# - worktreePath : scratch worktree path (relative to repo root)
|
|
11
|
-
# .publicignore : rsync blocklist (single source of truth for
|
|
12
|
-
# private-only paths — see T01)
|
|
13
|
-
#
|
|
14
|
-
# Flow:
|
|
15
|
-
# 1. Prepare a scratch clone of <publicRepoUrl> at <worktreePath>, checked
|
|
16
|
-
# out to <defaultBranch> at the tip of origin/<defaultBranch>.
|
|
17
|
-
# 2. Safety-check origin/<defaultBranch> against the local `last-mirror-sync`
|
|
18
|
-
# tag in the scratch clone. Abort if the public branch has diverged
|
|
19
|
-
# unless --force is passed.
|
|
20
|
-
# 3. rsync the private repo root into the worktree, respecting
|
|
21
|
-
# .publicignore and explicitly excluding .git/.
|
|
22
|
-
# 4. If nothing changed, exit 0 idempotently.
|
|
23
|
-
# 5. Otherwise squash everything into a single "chore(mirror): sync from
|
|
24
|
-
# private at <short-sha> <UTC-timestamp>" commit with a
|
|
25
|
-
# `Source-Commit: <full-sha>` trailer, advance the local marker tag,
|
|
26
|
-
# and push origin/<defaultBranch>.
|
|
27
|
-
# 6. Print a summary (files-changed count, new commit SHA, remote branch,
|
|
28
|
-
# public URL).
|
|
29
|
-
#
|
|
30
|
-
# Flags:
|
|
31
|
-
# --dry-run Read-only preview mode. Prepares the scratch worktree
|
|
32
|
-
# (fetch+reset only — no rsync writes, no commit, no push),
|
|
33
|
-
# then prints two labeled sections:
|
|
34
|
-
# - "Files that would ship" (from rsync --dry-run)
|
|
35
|
-
# - "Files that would be blocked" (candidates \ shipped)
|
|
36
|
-
# Followed by totals and an explicit "dry run — no push" notice.
|
|
37
|
-
# --force Override the safety-abort when the public branch is ahead
|
|
38
|
-
# of the recorded last mirror.
|
|
39
|
-
# -h|--help Print usage and exit.
|
|
40
|
-
#
|
|
41
|
-
# Env overrides:
|
|
42
|
-
# MIRROR_PUBLIC_URL_OVERRIDE Swap the configured publicRepoUrl at
|
|
43
|
-
# runtime (used for local-remote testing).
|
|
44
|
-
#
|
|
45
|
-
# The script NEVER modifies the private repo's .git/ or working tree.
|
|
46
|
-
# ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
set -euo pipefail
|
|
49
|
-
IFS=$'\n\t'
|
|
50
|
-
|
|
51
|
-
# -----------------------------------------------------------------------------
|
|
52
|
-
# Helpers
|
|
53
|
-
# -----------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
die() {
|
|
56
|
-
printf 'mirror.sh: error: %s\n' "$*" >&2
|
|
57
|
-
exit 1
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
log() {
|
|
61
|
-
printf 'mirror.sh: %s\n' "$*"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
usage() {
|
|
65
|
-
cat <<'EOF'
|
|
66
|
-
Usage: mirror.sh [--dry-run] [--force] [-h|--help]
|
|
67
|
-
|
|
68
|
-
Options:
|
|
69
|
-
--dry-run Read-only preview. Prints the exact list of files that would
|
|
70
|
-
ship and the list of files blocked by .publicignore, then
|
|
71
|
-
exits with no commit and no push.
|
|
72
|
-
--force Override the safety abort when the public branch has moved
|
|
73
|
-
since the last mirror.
|
|
74
|
-
-h, --help Show this message and exit.
|
|
75
|
-
|
|
76
|
-
Env:
|
|
77
|
-
MIRROR_PUBLIC_URL_OVERRIDE Override the configured publicRepoUrl.
|
|
78
|
-
EOF
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
# -----------------------------------------------------------------------------
|
|
82
|
-
# Locate script + repo root
|
|
83
|
-
# -----------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
|
86
|
-
# Resolve symlinks to a real path so SCRIPT_DIR points at the on-disk file.
|
|
87
|
-
while [ -h "$SCRIPT_PATH" ]; do
|
|
88
|
-
LINK_TARGET="$(readlink "$SCRIPT_PATH")"
|
|
89
|
-
case "$LINK_TARGET" in
|
|
90
|
-
/*) SCRIPT_PATH="$LINK_TARGET" ;;
|
|
91
|
-
*) SCRIPT_PATH="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)/$LINK_TARGET" ;;
|
|
92
|
-
esac
|
|
93
|
-
done
|
|
94
|
-
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
95
|
-
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
96
|
-
|
|
97
|
-
REPO_ROOT="$(git -C "$SKILL_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
98
|
-
[ -n "$REPO_ROOT" ] || die "could not locate private repo root (git rev-parse failed from $SKILL_DIR)"
|
|
99
|
-
|
|
100
|
-
CONFIG_FILE="$SKILL_DIR/assets/config.json"
|
|
101
|
-
[ -f "$CONFIG_FILE" ] || die "config not found: $CONFIG_FILE"
|
|
102
|
-
|
|
103
|
-
PUBLICIGNORE="$REPO_ROOT/.publicignore"
|
|
104
|
-
[ -f "$PUBLICIGNORE" ] || die ".publicignore not found at $PUBLICIGNORE (should have been created in T01)"
|
|
105
|
-
|
|
106
|
-
# -----------------------------------------------------------------------------
|
|
107
|
-
# Parse flags
|
|
108
|
-
# -----------------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
DRY_RUN=0
|
|
111
|
-
FORCE=0
|
|
112
|
-
|
|
113
|
-
while [ $# -gt 0 ]; do
|
|
114
|
-
case "$1" in
|
|
115
|
-
--dry-run) DRY_RUN=1 ;;
|
|
116
|
-
--force) FORCE=1 ;;
|
|
117
|
-
-h|--help) usage; exit 0 ;;
|
|
118
|
-
*) usage >&2; die "unknown argument: $1" ;;
|
|
119
|
-
esac
|
|
120
|
-
shift
|
|
121
|
-
done
|
|
122
|
-
|
|
123
|
-
# -----------------------------------------------------------------------------
|
|
124
|
-
# Load config (jq preferred, python3 fallback)
|
|
125
|
-
# -----------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
read_config_field() {
|
|
128
|
-
local field="$1"
|
|
129
|
-
if command -v jq >/dev/null 2>&1; then
|
|
130
|
-
jq -er --arg f "$field" '.[$f]' "$CONFIG_FILE" 2>/dev/null || return 1
|
|
131
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
132
|
-
python3 - "$CONFIG_FILE" "$field" <<'PY' || return 1
|
|
133
|
-
import json, sys
|
|
134
|
-
path, field = sys.argv[1], sys.argv[2]
|
|
135
|
-
with open(path) as fh:
|
|
136
|
-
data = json.load(fh)
|
|
137
|
-
if field not in data:
|
|
138
|
-
sys.exit(1)
|
|
139
|
-
sys.stdout.write(str(data[field]))
|
|
140
|
-
PY
|
|
141
|
-
else
|
|
142
|
-
die "neither jq nor python3 available to parse $CONFIG_FILE"
|
|
143
|
-
fi
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
PUBLIC_URL="$(read_config_field publicRepoUrl)" || die "config missing publicRepoUrl"
|
|
147
|
-
DEFAULT_BRANCH="$(read_config_field defaultBranch)" || die "config missing defaultBranch"
|
|
148
|
-
WORKTREE_REL="$(read_config_field worktreePath)" || die "config missing worktreePath"
|
|
149
|
-
|
|
150
|
-
# Env override for local-remote testing.
|
|
151
|
-
if [ -n "${MIRROR_PUBLIC_URL_OVERRIDE:-}" ]; then
|
|
152
|
-
log "using MIRROR_PUBLIC_URL_OVERRIDE=$MIRROR_PUBLIC_URL_OVERRIDE (config value ignored)"
|
|
153
|
-
PUBLIC_URL="$MIRROR_PUBLIC_URL_OVERRIDE"
|
|
154
|
-
fi
|
|
155
|
-
|
|
156
|
-
# Resolve worktree path against repo root if it's relative.
|
|
157
|
-
case "$WORKTREE_REL" in
|
|
158
|
-
/*) WORKTREE_PATH="$WORKTREE_REL" ;;
|
|
159
|
-
*) WORKTREE_PATH="$REPO_ROOT/$WORKTREE_REL" ;;
|
|
160
|
-
esac
|
|
161
|
-
|
|
162
|
-
log "repo root: $REPO_ROOT"
|
|
163
|
-
log "public URL: $PUBLIC_URL"
|
|
164
|
-
log "default branch: $DEFAULT_BRANCH"
|
|
165
|
-
log "worktree path: $WORKTREE_PATH"
|
|
166
|
-
|
|
167
|
-
# -----------------------------------------------------------------------------
|
|
168
|
-
# Prepare scratch worktree (clone if missing, fetch+reset if present)
|
|
169
|
-
# -----------------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
mkdir -p "$(dirname "$WORKTREE_PATH")"
|
|
172
|
-
|
|
173
|
-
if [ ! -d "$WORKTREE_PATH/.git" ]; then
|
|
174
|
-
log "cloning $PUBLIC_URL -> $WORKTREE_PATH (branch: $DEFAULT_BRANCH)"
|
|
175
|
-
git clone --branch "$DEFAULT_BRANCH" --single-branch "$PUBLIC_URL" "$WORKTREE_PATH"
|
|
176
|
-
else
|
|
177
|
-
log "refreshing existing scratch worktree at $WORKTREE_PATH"
|
|
178
|
-
git -C "$WORKTREE_PATH" remote set-url origin "$PUBLIC_URL"
|
|
179
|
-
git -C "$WORKTREE_PATH" fetch origin "$DEFAULT_BRANCH"
|
|
180
|
-
git -C "$WORKTREE_PATH" checkout -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
|
|
181
|
-
git -C "$WORKTREE_PATH" reset --hard "origin/$DEFAULT_BRANCH"
|
|
182
|
-
# Preserve the marker tag if it exists; -x would leave untracked scratch,
|
|
183
|
-
# but -fd is enough because we hard-reset above.
|
|
184
|
-
git -C "$WORKTREE_PATH" clean -fd
|
|
185
|
-
fi
|
|
186
|
-
|
|
187
|
-
# -----------------------------------------------------------------------------
|
|
188
|
-
# Safety check: last-mirror-sync tag vs origin/<branch>
|
|
189
|
-
# -----------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
MARKER_SHA="$(git -C "$WORKTREE_PATH" rev-parse -q --verify refs/tags/last-mirror-sync 2>/dev/null || true)"
|
|
192
|
-
REMOTE_HEAD="$(git -C "$WORKTREE_PATH" rev-parse "origin/$DEFAULT_BRANCH")"
|
|
193
|
-
|
|
194
|
-
if [ -z "$MARKER_SHA" ]; then
|
|
195
|
-
log "no last-mirror-sync tag found (first mirror or wiped scratch) — proceeding"
|
|
196
|
-
elif [ "$MARKER_SHA" = "$REMOTE_HEAD" ]; then
|
|
197
|
-
log "last-mirror-sync matches origin/$DEFAULT_BRANCH — safe to proceed"
|
|
198
|
-
else
|
|
199
|
-
if [ "$FORCE" -eq 1 ]; then
|
|
200
|
-
log "WARNING: origin/$DEFAULT_BRANCH ($REMOTE_HEAD) has moved past last-mirror-sync ($MARKER_SHA); proceeding due to --force"
|
|
201
|
-
else
|
|
202
|
-
die "origin/$DEFAULT_BRANCH ($REMOTE_HEAD) has moved past last-mirror-sync ($MARKER_SHA). The public repo has commits the mirror did not create. Re-run with --force to overwrite."
|
|
203
|
-
fi
|
|
204
|
-
fi
|
|
205
|
-
|
|
206
|
-
command -v rsync >/dev/null 2>&1 || die "rsync not installed"
|
|
207
|
-
|
|
208
|
-
# -----------------------------------------------------------------------------
|
|
209
|
-
# --dry-run: full read-only preview
|
|
210
|
-
#
|
|
211
|
-
# Compute + print the exact "would ship" set (from rsync --dry-run --itemize)
|
|
212
|
-
# and the "would be blocked" set (git-tracked/untracked candidates minus the
|
|
213
|
-
# ship set). Zero rsync writes, zero commits, zero pushes.
|
|
214
|
-
# -----------------------------------------------------------------------------
|
|
215
|
-
|
|
216
|
-
if [ "$DRY_RUN" -eq 1 ]; then
|
|
217
|
-
log "dry-run: computing ship + blocked sets (no rsync write, no commit, no push)"
|
|
218
|
-
|
|
219
|
-
SHIP_LIST_FILE="$(mktemp -t mirror-ship.XXXXXX)"
|
|
220
|
-
CANDIDATE_LIST_FILE="$(mktemp -t mirror-candidates.XXXXXX)"
|
|
221
|
-
BLOCKED_LIST_FILE="$(mktemp -t mirror-blocked.XXXXXX)"
|
|
222
|
-
# shellcheck disable=SC2064
|
|
223
|
-
trap "rm -f '$SHIP_LIST_FILE' '$CANDIDATE_LIST_FILE' '$BLOCKED_LIST_FILE'" EXIT
|
|
224
|
-
|
|
225
|
-
# 1. Ship list — rsync --dry-run --itemize-changes uses the SAME flag set as
|
|
226
|
-
# the real run below, so the emitted transfer list is exactly what the real
|
|
227
|
-
# run would transfer. Itemize flag format is 11 chars: YXcstpoguax
|
|
228
|
-
# Y (update type): >=receive, <=send, c=create local, h=hard link,
|
|
229
|
-
# .=no update, *=message (e.g. deleting)
|
|
230
|
-
# X (file type): f=file, d=directory, L=symlink, D=device, S=special
|
|
231
|
-
# We keep any entry that transfers/creates a file OR a symlink OR a hardlink,
|
|
232
|
-
# and drop directory-only entries (rsync recreates dirs implicitly) and any
|
|
233
|
-
# attribute-only / deletion / message lines. This matches exactly what the
|
|
234
|
-
# real rsync would push into the scratch worktree and therefore into the
|
|
235
|
-
# public remote.
|
|
236
|
-
rsync -a --delete \
|
|
237
|
-
--exclude-from="$PUBLICIGNORE" \
|
|
238
|
-
--exclude=".git" \
|
|
239
|
-
--dry-run --itemize-changes --out-format='%i %n' \
|
|
240
|
-
"$REPO_ROOT/" \
|
|
241
|
-
"$WORKTREE_PATH/" \
|
|
242
|
-
| awk '{
|
|
243
|
-
flag = $1;
|
|
244
|
-
first = substr(flag, 1, 1);
|
|
245
|
-
second = substr(flag, 2, 1);
|
|
246
|
-
# Skip directories — public tree recreates them implicitly.
|
|
247
|
-
if (second == "d") next;
|
|
248
|
-
# Keep only entries that transfer or create a file/symlink/hardlink.
|
|
249
|
-
keep = 0;
|
|
250
|
-
if (first == ">" || first == "<") keep = 1; # file transfer
|
|
251
|
-
else if (first == "c" && second == "L") keep = 1; # create symlink
|
|
252
|
-
else if (first == "h") keep = 1; # hard link
|
|
253
|
-
if (!keep) next;
|
|
254
|
-
# Rebuild path (everything after the flag token).
|
|
255
|
-
$1 = "";
|
|
256
|
-
sub(/^ /, "");
|
|
257
|
-
# Strip trailing slash just in case (should only appear on dirs, which
|
|
258
|
-
# we already skipped, but be defensive).
|
|
259
|
-
sub(/\/$/, "");
|
|
260
|
-
if (length($0) > 0) print $0;
|
|
261
|
-
}' \
|
|
262
|
-
| LC_ALL=C sort -u > "$SHIP_LIST_FILE"
|
|
263
|
-
|
|
264
|
-
# 2. Candidate list — every tracked file plus every untracked-not-gitignored
|
|
265
|
-
# file under the private repo root. This is the honest "what the maintainer
|
|
266
|
-
# thinks of as the repo" set.
|
|
267
|
-
git -C "$REPO_ROOT" ls-files -co --exclude-standard \
|
|
268
|
-
| LC_ALL=C sort -u > "$CANDIDATE_LIST_FILE"
|
|
269
|
-
|
|
270
|
-
# 3. Blocked = candidates \ shipped.
|
|
271
|
-
LC_ALL=C comm -23 "$CANDIDATE_LIST_FILE" "$SHIP_LIST_FILE" > "$BLOCKED_LIST_FILE"
|
|
272
|
-
|
|
273
|
-
SHIP_COUNT="$(wc -l < "$SHIP_LIST_FILE" | tr -d ' ')"
|
|
274
|
-
BLOCKED_COUNT="$(wc -l < "$BLOCKED_LIST_FILE" | tr -d ' ')"
|
|
275
|
-
|
|
276
|
-
printf '\n'
|
|
277
|
-
printf '=== Files that would ship (%s) ===\n' "$SHIP_COUNT"
|
|
278
|
-
if [ "$SHIP_COUNT" -eq 0 ]; then
|
|
279
|
-
printf ' (none — public tree already matches private, post-blocklist)\n'
|
|
280
|
-
else
|
|
281
|
-
cat "$SHIP_LIST_FILE"
|
|
282
|
-
fi
|
|
283
|
-
|
|
284
|
-
printf '\n'
|
|
285
|
-
printf '=== Files that would be blocked (%s) ===\n' "$BLOCKED_COUNT"
|
|
286
|
-
if [ "$BLOCKED_COUNT" -eq 0 ]; then
|
|
287
|
-
printf ' (none — .publicignore matched nothing under this repo)\n'
|
|
288
|
-
else
|
|
289
|
-
cat "$BLOCKED_LIST_FILE"
|
|
290
|
-
fi
|
|
291
|
-
|
|
292
|
-
printf '\n'
|
|
293
|
-
printf '================ mirror-public dry-run summary ================\n'
|
|
294
|
-
printf 'would ship : %s files\n' "$SHIP_COUNT"
|
|
295
|
-
printf 'would block : %s files\n' "$BLOCKED_COUNT"
|
|
296
|
-
printf 'public URL : %s\n' "$PUBLIC_URL"
|
|
297
|
-
printf 'remote branch : %s\n' "$DEFAULT_BRANCH"
|
|
298
|
-
printf 'dry run — no push, no commit, no remote mutation\n'
|
|
299
|
-
printf '===============================================================\n'
|
|
300
|
-
|
|
301
|
-
exit 0
|
|
302
|
-
fi
|
|
303
|
-
|
|
304
|
-
# -----------------------------------------------------------------------------
|
|
305
|
-
# rsync private root -> scratch worktree
|
|
306
|
-
# -----------------------------------------------------------------------------
|
|
307
|
-
|
|
308
|
-
log "rsync $REPO_ROOT/ -> $WORKTREE_PATH/ (excludes: .publicignore + .git)"
|
|
309
|
-
rsync -a --delete \
|
|
310
|
-
--exclude-from="$PUBLICIGNORE" \
|
|
311
|
-
--exclude=".git" \
|
|
312
|
-
"$REPO_ROOT/" \
|
|
313
|
-
"$WORKTREE_PATH/"
|
|
314
|
-
|
|
315
|
-
# -----------------------------------------------------------------------------
|
|
316
|
-
# Stage + detect changes (idempotency)
|
|
317
|
-
# -----------------------------------------------------------------------------
|
|
318
|
-
|
|
319
|
-
git -C "$WORKTREE_PATH" add -A
|
|
320
|
-
|
|
321
|
-
if git -C "$WORKTREE_PATH" diff --cached --quiet; then
|
|
322
|
-
log "nothing to mirror — public tree already matches private (post-blocklist)"
|
|
323
|
-
exit 0
|
|
324
|
-
fi
|
|
325
|
-
|
|
326
|
-
# -----------------------------------------------------------------------------
|
|
327
|
-
# Squash commit
|
|
328
|
-
# -----------------------------------------------------------------------------
|
|
329
|
-
|
|
330
|
-
PRIVATE_SHORT_SHA="$(git -C "$REPO_ROOT" rev-parse --short HEAD)"
|
|
331
|
-
PRIVATE_FULL_SHA="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
|
332
|
-
UTC_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
333
|
-
|
|
334
|
-
COMMIT_SUBJECT="chore(mirror): sync from private at $PRIVATE_SHORT_SHA $UTC_TS"
|
|
335
|
-
COMMIT_TRAILER="Source-Commit: $PRIVATE_FULL_SHA"
|
|
336
|
-
|
|
337
|
-
# Ensure the scratch clone has a committer identity even in ephemeral CI/test.
|
|
338
|
-
if ! git -C "$WORKTREE_PATH" config user.email >/dev/null 2>&1; then
|
|
339
|
-
git -C "$WORKTREE_PATH" config user.email "mirror-public@jenga.local"
|
|
340
|
-
fi
|
|
341
|
-
if ! git -C "$WORKTREE_PATH" config user.name >/dev/null 2>&1; then
|
|
342
|
-
git -C "$WORKTREE_PATH" config user.name "jenga mirror-public"
|
|
343
|
-
fi
|
|
344
|
-
|
|
345
|
-
git -C "$WORKTREE_PATH" commit -m "$COMMIT_SUBJECT" -m "$COMMIT_TRAILER" >/dev/null
|
|
346
|
-
|
|
347
|
-
NEW_SHA="$(git -C "$WORKTREE_PATH" rev-parse HEAD)"
|
|
348
|
-
|
|
349
|
-
# -----------------------------------------------------------------------------
|
|
350
|
-
# Advance marker tag (LOCAL to scratch clone — never pushed)
|
|
351
|
-
# -----------------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
git -C "$WORKTREE_PATH" tag -f last-mirror-sync HEAD >/dev/null
|
|
354
|
-
|
|
355
|
-
# -----------------------------------------------------------------------------
|
|
356
|
-
# Push
|
|
357
|
-
# -----------------------------------------------------------------------------
|
|
358
|
-
|
|
359
|
-
log "pushing $NEW_SHA -> origin/$DEFAULT_BRANCH"
|
|
360
|
-
git -C "$WORKTREE_PATH" push origin "$DEFAULT_BRANCH"
|
|
361
|
-
|
|
362
|
-
# -----------------------------------------------------------------------------
|
|
363
|
-
# Summary
|
|
364
|
-
# -----------------------------------------------------------------------------
|
|
365
|
-
|
|
366
|
-
FILES_CHANGED="$(git -C "$WORKTREE_PATH" diff-tree --no-commit-id --name-only -r HEAD | wc -l | tr -d ' ')"
|
|
367
|
-
|
|
368
|
-
printf '\n'
|
|
369
|
-
printf '================ mirror-public summary ================\n'
|
|
370
|
-
printf 'files changed : %s\n' "$FILES_CHANGED"
|
|
371
|
-
printf 'new commit : %s\n' "$NEW_SHA"
|
|
372
|
-
printf 'remote branch : %s\n' "$DEFAULT_BRANCH"
|
|
373
|
-
printf 'public URL : %s\n' "$PUBLIC_URL"
|
|
374
|
-
printf '=======================================================\n'
|