@jenga-ai/agent 1.0.1 → 1.1.1

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 (117) hide show
  1. package/README.md +10 -7
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +215 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/mcp/router/embedder.js +1 -1
  7. package/mcp/training_runner/index.js +239 -0
  8. package/mcp/training_runner/package-lock.json +1065 -0
  9. package/mcp/training_runner/package.json +15 -0
  10. package/package.json +14 -16
  11. package/scripts/check-permission-level.sh +107 -0
  12. package/scripts/check-publicignore-match.sh +122 -0
  13. package/scripts/check-worktree-liveness.sh +193 -0
  14. package/scripts/generate-rapport-manifest.sh +43 -0
  15. package/scripts/idea_manager.sh +47 -0
  16. package/scripts/install-worktree-commit-guard.sh +134 -0
  17. package/scripts/jenga-permission-level-switch.sh +109 -0
  18. package/scripts/smoke-harness.sh +139 -0
  19. package/scripts/validate-board.sh +62 -0
  20. package/scripts/with-lock.sh +158 -0
  21. package/scripts/worktree-remove-guard.sh +204 -0
  22. package/skills/clearify/SKILL.md +52 -0
  23. package/skills/close-story/SKILL.md +203 -0
  24. package/skills/close-story/scripts/check-story-closeable.sh +195 -0
  25. package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
  26. package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
  27. package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
  28. package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
  29. package/skills/commit/SKILL.md +30 -3
  30. package/skills/distribute/CONFIG_SCHEMA.md +148 -0
  31. package/skills/distribute/SKILL.md +173 -0
  32. package/skills/distribute/scripts/check-version.sh +74 -0
  33. package/skills/distribute/scripts/commit-version-bump.sh +108 -0
  34. package/skills/distribute/scripts/distribute-changes.sh +381 -0
  35. package/skills/do/SKILL.md +352 -1
  36. package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
  37. package/skills/doc/assets/path-objectives.yaml +13 -0
  38. package/skills/doc-sync/SKILL.md +16 -0
  39. package/skills/doc-sync/assets/doc_targets.md +11 -0
  40. package/skills/idea/SKILL.md +56 -0
  41. package/skills/idea/assets/idea_handoff_template.md +26 -0
  42. package/skills/idea/assets/idea_template.md +3 -0
  43. package/skills/init/SKILL.md +101 -7
  44. package/skills/init/assets/directory_structure.txt +1 -0
  45. package/skills/init/assets/strategy_stub_template.md +38 -0
  46. package/skills/init/assets/workflow_template.json +1 -1
  47. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  48. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  49. package/skills/init/scripts/init.sh +35 -1
  50. package/skills/jenga/SKILL.md +206 -14
  51. package/skills/jenga/scripts/board-scan.sh +238 -0
  52. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  53. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  54. package/skills/jenga/scripts/render-picker.sh +439 -0
  55. package/skills/jenga/scripts/resolve-id.sh +367 -0
  56. package/skills/jenga-permission-level/SKILL.md +81 -0
  57. package/skills/proceed/SKILL.md +1 -1
  58. package/skills/publish/SKILL.md +8 -5
  59. package/skills/publish/assets/ci-contract.md +2 -2
  60. package/skills/publish/assets/ownership-matrix.md +1 -1
  61. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  62. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  63. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  64. package/skills/publish/scripts/publish_deploy.sh +38 -8
  65. package/skills/publish/scripts/run_gates.sh +2 -2
  66. package/skills/reconcile/SKILL.md +117 -5
  67. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  68. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  69. package/skills/spinoff/SKILL.md +12 -7
  70. package/skills/todo/SKILL.md +2 -0
  71. package/skills/uncharted/SKILL.md +711 -0
  72. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  73. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  74. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  75. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  76. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  77. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  78. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  79. package/skills/uncharted/scripts/import-source.sh +517 -0
  80. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  81. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  82. package/skills/uncharted/scripts/run-engine.sh +655 -0
  83. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  84. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  85. package/skills/wtf/SKILL.md +20 -0
  86. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  87. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  88. package/templates/SCRUM_BOARD_SCHEMA.md +206 -10
  89. package/templates/permission-levels/README.md +73 -0
  90. package/templates/permission-levels/level-1-locked.json +71 -0
  91. package/templates/permission-levels/level-2-guarded.json +64 -0
  92. package/templates/permission-levels/level-3-standard.json +62 -0
  93. package/templates/permission-levels/level-4-elevated.json +60 -0
  94. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  95. package/skills/convert/SKILL.md +0 -124
  96. package/skills/convert/convert_cli.py +0 -235
  97. package/skills/convert/tests/sample.csv +0 -4
  98. package/skills/convert/tests/sample.json +0 -5
  99. package/skills/convert/tests/sample.jsonl +0 -3
  100. package/skills/convert/tests/sample.yaml +0 -18
  101. package/skills/convert/tests/sample_obj.csv +0 -2
  102. package/skills/convert/tests/sample_obj.json +0 -9
  103. package/skills/mirror-public/SKILL.md +0 -237
  104. package/skills/mirror-public/assets/config.json +0 -5
  105. package/skills/mirror-public/scripts/mirror.sh +0 -374
  106. package/skills/self-sync/SKILL.md +0 -73
  107. package/skills/self-sync/scripts/run.js +0 -136
  108. package/skills/train/SKILL.md +0 -116
  109. package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
  110. package/skills/train/assets/dashboard-templates/nlp.html +0 -102
  111. package/skills/train/assets/dashboard-templates/transformers.html +0 -98
  112. package/skills/train/assets/results-parsers/__init__.py +0 -9
  113. package/skills/train/assets/results-parsers/classifiers.py +0 -84
  114. package/skills/train/assets/results-parsers/nlp.py +0 -88
  115. package/skills/train/assets/results-parsers/reporter.py +0 -154
  116. package/skills/train/assets/results-parsers/transformers.py +0 -120
  117. package/skills/train/train_cli.py +0 -786
@@ -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,5 +0,0 @@
1
- {
2
- "publicRepoUrl": "https://github.com/samwelmunga/jenga-npm.git",
3
- "defaultBranch": "main",
4
- "worktreePath": ".mirror-worktrees/public"
5
- }
@@ -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'