@jenga-ai/agent 2.0.0 → 3.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 (177) hide show
  1. package/README.md +82 -243
  2. package/agents/developer.md +5 -5
  3. package/agents/scrum-master.md +23 -23
  4. package/agents/tester.md +5 -5
  5. package/bin/jenga.js +10 -0
  6. package/hooks/copilot_session_end.sh +7 -3
  7. package/hooks/prompt_router_helper.js +17 -5
  8. package/lib/commands/doctor.js +351 -0
  9. package/lib/commands/init.js +16 -0
  10. package/lib/generate-copilot-hooks.js +116 -0
  11. package/lib/generate-skill-allow-list.js +9 -3
  12. package/lib/legacy-shipped-paths.json +336 -0
  13. package/lib/postinstall-manifest.js +469 -0
  14. package/lib/skill-allow-list.json +2 -3
  15. package/package.json +16 -25
  16. package/scripts/apply-j-prefix.sh +25 -12
  17. package/scripts/generate-j-alias.sh +333 -0
  18. package/scripts/generate-legacy-shipped-paths.js +248 -0
  19. package/scripts/postinstall.js +205 -2
  20. package/scripts/verify-legacy-seed-reconcile.sh +254 -0
  21. package/scripts/verify-postinstall-reconcile.sh +392 -0
  22. package/skills/{brainstorm → j-brainstorm}/SKILL.md +9 -2
  23. package/skills/{btw → j-btw}/SKILL.md +9 -2
  24. package/skills/{clearify → j-clearify}/SKILL.md +9 -2
  25. package/skills/{close-story → j-close-story}/SKILL.md +17 -10
  26. package/skills/{close-story → j-close-story}/scripts/check-privatized.sh +2 -2
  27. package/skills/{close-story → j-close-story}/scripts/check-story-closeable.sh +1 -1
  28. package/skills/{close-story → j-close-story}/scripts/extract-task-diff-stats.sh +1 -1
  29. package/skills/{commit → j-commit}/SKILL.md +9 -2
  30. package/skills/j-continue/SKILL.md +36 -0
  31. package/skills/{deep-dive → j-deep-dive}/SKILL.md +9 -8
  32. package/skills/{dev-done → j-dev-done}/SKILL.md +11 -4
  33. package/skills/{dev-done → j-dev-done}/scripts/classify-commit-outcome.sh +4 -4
  34. package/skills/{distribute → j-distribute}/SKILL.md +17 -10
  35. package/skills/{distribute → j-distribute}/scripts/distribute-changes.sh +1 -1
  36. package/skills/{do → j-do}/SKILL.md +12 -5
  37. package/skills/{doc → j-doc}/README.md +5 -5
  38. package/skills/{doc → j-doc}/SKILL.md +15 -8
  39. package/skills/{doc → j-doc}/authoring-notes.md +1 -1
  40. package/skills/{doc-sync → j-doc-sync}/SKILL.md +9 -2
  41. package/skills/{dooo → j-dooo}/SKILL.md +9 -2
  42. package/skills/j-error/SKILL.md +36 -0
  43. package/skills/{evaluate → j-evaluate}/SKILL.md +9 -2
  44. package/skills/j-examplify/SKILL.md +49 -0
  45. package/skills/{help → j-help}/SKILL.md +9 -2
  46. package/skills/{idea → j-idea}/SKILL.md +10 -3
  47. package/skills/{idea → j-idea}/assets/idea_handoff_template.md +1 -1
  48. package/skills/{improve → j-improve}/SKILL.md +9 -2
  49. package/skills/j-init/SKILL.md +2 -2
  50. package/skills/j-jbp/SKILL.md +32 -0
  51. package/skills/j-lgtm/SKILL.md +28 -0
  52. package/skills/{pi-plan → j-pi-plan}/SKILL.md +10 -3
  53. package/skills/{proceed → j-proceed}/SKILL.md +9 -2
  54. package/skills/{publish → j-publish}/SKILL.md +47 -40
  55. package/skills/{publish → j-publish}/adapters/droplet.md +1 -1
  56. package/skills/{publish → j-publish}/adapters/mobile-ios.md +3 -3
  57. package/skills/{publish → j-publish}/adapters/npm-ci.md +3 -3
  58. package/skills/{publish → j-publish}/adapters/npm.md +8 -8
  59. package/skills/{publish → j-publish}/assets/ci-contract.md +2 -2
  60. package/skills/{publish → j-publish}/schemas/publish.schema.json +1 -1
  61. package/skills/{publish → j-publish}/scripts/npm_stage_inspect.sh +34 -1
  62. package/skills/{publish → j-publish}/scripts/npm_stage_pipeline.sh +9 -4
  63. package/skills/{publish → j-publish}/scripts/publish_deploy.sh +4 -4
  64. package/skills/{publish → j-publish}/scripts/validate_npm_stage_env.sh +1 -1
  65. package/skills/{publish → j-publish}/wizards/droplet.md +1 -1
  66. package/skills/{publish → j-publish}/wizards/mobile-ios.md +1 -1
  67. package/skills/{publish → j-publish}/wizards/npm-ci.md +1 -1
  68. package/skills/{publish → j-publish}/wizards/npm.md +1 -1
  69. package/skills/{reconcile → j-reconcile}/SKILL.md +12 -5
  70. package/skills/{reconcile → j-reconcile}/scripts/detect-unlinked-code.sh +2 -2
  71. package/skills/{reconcile → j-reconcile}/scripts/resolve-reconcile-scope.sh +3 -3
  72. package/skills/{reconcile-origin → j-reconcile-origin}/SKILL.md +13 -6
  73. package/skills/{redo → j-redo}/SKILL.md +9 -2
  74. package/skills/{skillify → j-skillify}/SKILL.md +10 -3
  75. package/skills/{spinoff → j-spinoff}/SKILL.md +9 -2
  76. package/skills/{status → j-status}/SKILL.md +9 -2
  77. package/skills/{todo → j-todo}/SKILL.md +10 -3
  78. package/skills/{todo → j-todo}/assets/todo_handoff_template.md +1 -1
  79. package/skills/{todo → j-todo}/scripts/add_trivial_task.sh +3 -3
  80. package/skills/{todo → j-todo}/scripts/update_story_tasks.py +2 -2
  81. package/skills/{uncharted → j-uncharted}/SKILL.md +35 -28
  82. package/skills/{uncharted → j-uncharted}/assets/UNDERSTANDING_DOC_TEMPLATE.md +2 -2
  83. package/skills/{uncharted → j-uncharted}/scripts/detect-dependencies.sh +1 -1
  84. package/skills/{uncharted → j-uncharted}/scripts/detect-tests.sh +1 -1
  85. package/skills/{uncharted → j-uncharted}/scripts/directory-triage.sh +3 -3
  86. package/skills/{uncharted → j-uncharted}/scripts/elicitation-state.sh +3 -3
  87. package/skills/{uncharted → j-uncharted}/scripts/enumerate-target.sh +1 -1
  88. package/skills/{uncharted → j-uncharted}/scripts/import-source.sh +1 -1
  89. package/skills/{uncharted → j-uncharted}/scripts/inspect-provenance.sh +1 -1
  90. package/skills/{uncharted → j-uncharted}/scripts/resolve-segment-target.sh +5 -5
  91. package/skills/{uncharted → j-uncharted}/scripts/run-engine.sh +1 -1
  92. package/skills/{uncharted → j-uncharted}/scripts/validate-proposed-items.sh +2 -2
  93. package/skills/{uncharted → j-uncharted}/scripts/write-backfilled-epics.sh +1 -1
  94. package/skills/j-wtf/SKILL.md +27 -0
  95. package/skills/jenga/SKILL.md +1 -1
  96. package/skills/jenga-permission-level/SKILL.md +1 -1
  97. package/templates/SCRUM_BOARD_SCHEMA.md +1 -1
  98. package/templates/agent-context.md.tpl +10 -10
  99. package/templates/copilot-instructions.md.tpl +89 -19
  100. package/skills/continue/SKILL.md +0 -29
  101. package/skills/error/SKILL.md +0 -29
  102. package/skills/examplify/SKILL.md +0 -42
  103. package/skills/init/SKILL.md +0 -155
  104. package/skills/init/assets/scope-thresholds_template.json +0 -7
  105. package/skills/init/assets/strategy_stub_template.md +0 -38
  106. package/skills/init/assets/workflow_template.json +0 -30
  107. package/skills/init/scripts/apply-project-visibility.sh +0 -176
  108. package/skills/init/scripts/detect-existing-codebase.sh +0 -166
  109. package/skills/init/scripts/init.sh +0 -116
  110. package/skills/jbp/SKILL.md +0 -25
  111. package/skills/lgtm/SKILL.md +0 -21
  112. package/skills/skillify/assets/init-new/assets/.gitignore_template +0 -15
  113. package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +0 -13
  114. package/skills/skillify/assets/init-new/assets/directory_structure.txt +0 -14
  115. package/skills/skillify/assets/init-new/assets/test-config_template.json +0 -4
  116. package/skills/wtf/SKILL.md +0 -20
  117. /package/skills/{close-story → j-close-story}/scripts/compute-scope-divergence.sh +0 -0
  118. /package/skills/{close-story → j-close-story}/scripts/extract-diff-stats.sh +0 -0
  119. /package/skills/{close-story → j-close-story}/scripts/update-task-frontmatter.sh +0 -0
  120. /package/skills/{commit → j-commit}/assets/user_instructions_template.md +0 -0
  121. /package/skills/{distribute → j-distribute}/CONFIG_SCHEMA.md +0 -0
  122. /package/skills/{distribute → j-distribute}/scripts/check-version.sh +0 -0
  123. /package/skills/{distribute → j-distribute}/scripts/commit-version-bump.sh +0 -0
  124. /package/skills/{do → j-do}/assets/intent-vs-diff-prompt.md +0 -0
  125. /package/skills/{do → j-do}/assets/sender_template.json +0 -0
  126. /package/skills/{doc → j-doc}/assets/path-objectives.yaml +0 -0
  127. /package/skills/{doc → j-doc}/scripts/resolve_last_update.py +0 -0
  128. /package/skills/{doc-sync → j-doc-sync}/assets/default_excludes.txt +0 -0
  129. /package/skills/{doc-sync → j-doc-sync}/assets/doc_targets.md +0 -0
  130. /package/skills/{evaluate → j-evaluate}/assets/evaluation_invokation_template.yml +0 -0
  131. /package/skills/{evaluate → j-evaluate}/assets/evaluation_rapport_template.md +0 -0
  132. /package/skills/{idea → j-idea}/assets/idea_template.md +0 -0
  133. /package/skills/{pi-plan → j-pi-plan}/assets/epic.json +0 -0
  134. /package/skills/{pi-plan → j-pi-plan}/assets/story_template.md +0 -0
  135. /package/skills/{publish → j-publish}/assets/ExportOptions.plist.template +0 -0
  136. /package/skills/{publish → j-publish}/assets/ownership-matrix.md +0 -0
  137. /package/skills/{publish → j-publish}/assets/publish.example.json +0 -0
  138. /package/skills/{publish → j-publish}/assets/publish.example.npm-ci.json +0 -0
  139. /package/skills/{publish → j-publish}/assets/publish.example.npm.json +0 -0
  140. /package/skills/{publish → j-publish}/assets/secrets-guide.md +0 -0
  141. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-minimal.json +0 -0
  142. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-empty-secrets.json +0 -0
  143. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-workflow-path.json +0 -0
  144. /package/skills/{publish → j-publish}/scripts/check_target_config.sh +0 -0
  145. /package/skills/{publish → j-publish}/scripts/droplet_pipeline.sh +0 -0
  146. /package/skills/{publish → j-publish}/scripts/finalize_changelog.sh +0 -0
  147. /package/skills/{publish → j-publish}/scripts/generate_release_notes.sh +0 -0
  148. /package/skills/{publish → j-publish}/scripts/ios_pipeline.sh +0 -0
  149. /package/skills/{publish → j-publish}/scripts/npm_ci_pipeline.sh +0 -0
  150. /package/skills/{publish → j-publish}/scripts/npm_pipeline.sh +0 -0
  151. /package/skills/{publish → j-publish}/scripts/publish_common.sh +0 -0
  152. /package/skills/{publish → j-publish}/scripts/reconcile_tags.sh +0 -0
  153. /package/skills/{publish → j-publish}/scripts/run_gates.sh +0 -0
  154. /package/skills/{publish → j-publish}/scripts/setup_wizard.sh +0 -0
  155. /package/skills/{publish → j-publish}/scripts/show_history.sh +0 -0
  156. /package/skills/{publish → j-publish}/scripts/suggest_semver_bump.sh +0 -0
  157. /package/skills/{publish → j-publish}/scripts/validate_config.sh +0 -0
  158. /package/skills/{publish → j-publish}/scripts/validate_droplet_env.sh +0 -0
  159. /package/skills/{publish → j-publish}/scripts/validate_ios_env.sh +0 -0
  160. /package/skills/{publish → j-publish}/scripts/validate_npm_ci_env.sh +0 -0
  161. /package/skills/{publish → j-publish}/scripts/validate_npm_env.sh +0 -0
  162. /package/skills/{publish → j-publish}/scripts/write_ledger_entry.sh +0 -0
  163. /package/skills/{reconcile → j-reconcile}/assets/report_format.md +0 -0
  164. /package/skills/{reconcile-origin → j-reconcile-origin}/scripts/reconcile-origin.sh +0 -0
  165. /package/skills/{skillify → j-skillify}/assets/init-new/SKILL.md +0 -0
  166. /package/skills/{init → j-skillify/assets/init-new}/assets/.gitignore_template +0 -0
  167. /package/skills/{init → j-skillify/assets/init-new}/assets/PROJECT_SUMMARY_template.md +0 -0
  168. /package/skills/{init → j-skillify/assets/init-new}/assets/directory_structure.txt +0 -0
  169. /package/skills/{init → j-skillify/assets/init-new}/assets/test-config_template.json +0 -0
  170. /package/skills/{skillify → j-skillify}/assets/init-new/assets/workflow_template.json +0 -0
  171. /package/skills/{skillify → j-skillify}/assets/init-new/scripts/init.sh +0 -0
  172. /package/skills/{skillify → j-skillify}/assets/init-old/SKILL.md +0 -0
  173. /package/skills/{status → j-status}/assets/output_format.md +0 -0
  174. /package/skills/{todo → j-todo}/assets/todo_template.md +0 -0
  175. /package/skills/{uncharted → j-uncharted}/assets/SEGMENT_PROPOSAL_TEMPLATE.md +0 -0
  176. /package/skills/{uncharted → j-uncharted}/scripts/apply-subsystem-cap.sh +0 -0
  177. /package/skills/{uncharted → j-uncharted}/scripts/discover-subsystems.sh +0 -0
@@ -24,7 +24,86 @@
24
24
  *
25
25
  * The actual filesystem mirror is delegated to `lib/mirror.js`, which is the
26
26
  * single source of truth shared with the in-repo `/self-sync` skill. Consumer
27
- * install always uses `reconcileDeletes: false` (additive-only).
27
+ * install always uses `reconcileDeletes: false` (additive-only) — that flag diffs
28
+ * the destination's current contents against the source tree with no notion of who
29
+ * wrote a given file, so enabling it here would delete a consumer's own custom
30
+ * skills. It is deliberately left off; see the manifest mechanism below.
31
+ *
32
+ * Upgrade cleanup — manifest-based delete reconciliation (E26_S08_T01)
33
+ * ────────────────────────────────────────────────────────────────────
34
+ * Additive-only mirroring means a release that renames, removes, or excludes a
35
+ * skill leaves the previously installed copies in the consumer's mirror roots
36
+ * forever, so old and new forms both keep loading. To clean those up *without*
37
+ * risking consumer-authored files, each run records what it wrote and the next run
38
+ * removes only what it itself wrote last time and did not write again.
39
+ *
40
+ * Location : one manifest per destination root, inside that root —
41
+ * <consumer>/.agents/.jenga-postinstall-manifest.json
42
+ * <consumer>/.claude/.jenga-postinstall-manifest.json
43
+ * Keeping it inside the root it describes means it travels with that
44
+ * mirror; moving or renaming the consumer project cannot desync it.
45
+ *
46
+ * Format : {
47
+ * "manifest_version": 1,
48
+ * "package": "@jenga-ai/agent",
49
+ * "package_version": "3.0.1",
50
+ * "generated_at": "<ISO 8601>",
51
+ * "dest_root": ".agents",
52
+ * "paths": ["agents/developer.md", "skills/do/SKILL.md"]
53
+ * }
54
+ * `paths` are relative to the destination root, POSIX-separated,
55
+ * deduped and sorted, and record REGULAR FILES ONLY — never
56
+ * directories. Directory removal is derived by pruning parents that
57
+ * become empty, which is precisely why a directory still holding a
58
+ * consumer file is never removed.
59
+ *
60
+ * Rule : a path is deleted only if a manifest previously written by THIS
61
+ * package's postinstall lists it AND the current run did not write it.
62
+ * If no prior manifest exists, see the legacy-path seeding note below —
63
+ * "we don't know what we wrote before" is never treated as "delete
64
+ * everything we didn't just write". Every parse/IO failure likewise
65
+ * degrades to additive-only.
66
+ *
67
+ * Skipped : if any `copySet` entry is missing from the package (a packaging
68
+ * regression), BOTH the delete pass and the manifest write are skipped
69
+ * for that run — otherwise the run would under-report what it wrote and
70
+ * read an entire mirrored subtree as stale. The previous manifest is
71
+ * left in place because it still describes what is on disk.
72
+ *
73
+ * Legacy-path seeding — first-manifest orphan cleanup (E26_S08_T03)
74
+ * ────────────────────────────────────────────────────────────────────
75
+ * T01's rule above is purely forward-looking: a consumer already installed BEFORE any
76
+ * manifest existed takes the `no-prior-manifest` branch on their first run of a fixed
77
+ * version, and the manifest that run writes records only what that one run mirrored —
78
+ * pre-existing orphans (e.g. a retired `j:`-form skill, an excluded `j-<name>` twin)
79
+ * were never in it, so they could never become deletion candidates on any FUTURE
80
+ * upgrade either. Confirmed empirically against a throwaway fixture, not merely
81
+ * reasoned about: such a file survived two upgrades, the second with an active delete
82
+ * pass, appearing 0 times in either manifest.
83
+ *
84
+ * On the `no-prior-manifest` branch, this run now additionally seeds a SYNTHETIC prior
85
+ * path list from `lib/legacy-shipped-paths.json` — a static, package-shipped list of
86
+ * paths known to have shipped in some real prior published version (see
87
+ * `scripts/generate-legacy-shipped-paths.js`) — intersected with what is ACTUALLY a
88
+ * regular file on disk in that mirror root right now
89
+ * (`seedFromLegacyPaths` in `lib/postinstall-manifest.js`). If anything seeds, it is
90
+ * reconciled against THIS run's copy set immediately (adopt-then-reconcile), so the
91
+ * cleanup lands on the very upgrade that introduces this feature rather than one cycle
92
+ * later. Provenance stays the entire compensating control: a path absent from every
93
+ * published version's file list is never in `lib/legacy-shipped-paths.json`, so it can
94
+ * never be seeded, regardless of where it sits in the mirror root — a consumer's own
95
+ * hand-authored file was never in any published version and stays untouchable exactly
96
+ * as before. A genuine first-ever install has nothing on disk to intersect with, so the
97
+ * seed set is naturally empty and this falls straight through to the additive-only
98
+ * behaviour — no separate "is this a first install" branch is needed for that to hold.
99
+ * The seeded reconciliation reuses the EXACT SAME boundary/type-checked delete engine
100
+ * as the manifest-backed path (`reconcileWithPriorPaths` shares its core with
101
+ * `reconcileFromManifest`), so invariants 3, 4, and 5 apply unchanged over seeded
102
+ * candidates too.
103
+ *
104
+ * Implementation lives in `lib/postinstall-manifest.js` (full rationale + safety
105
+ * invariants documented there) and `scripts/generate-legacy-shipped-paths.js` (legacy
106
+ * path list generation); consumer-facing docs in `docs/distribution.md`.
28
107
  */
29
108
 
30
109
  import fs from 'node:fs';
@@ -32,7 +111,16 @@ import path from 'node:path';
32
111
  import { fileURLToPath } from 'node:url';
33
112
 
34
113
  import { mirror } from '../lib/mirror.js';
114
+ import {
115
+ toRelativePaths,
116
+ reconcileFromManifest,
117
+ reconcileWithPriorPaths,
118
+ seedFromLegacyPaths,
119
+ writeManifest,
120
+ } from '../lib/postinstall-manifest.js';
121
+ import { readLegacyShippedPaths } from './generate-legacy-shipped-paths.js';
35
122
  import { generateCopilotInstructions } from '../lib/generate-copilot-instructions.js';
123
+ import { generateCopilotHooks } from '../lib/generate-copilot-hooks.js';
36
124
  import { generateSkillAllowList } from '../lib/generate-skill-allow-list.js';
37
125
 
38
126
  // ESM equivalent of __dirname
@@ -73,10 +161,12 @@ function main() {
73
161
 
74
162
  // Read this package's version
75
163
  let packageVersion = '0.0.0';
164
+ let packageName = '';
76
165
  try {
77
166
  const pkgPath = path.join(packageRoot, 'package.json');
78
167
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
79
168
  packageVersion = pkg.version || '0.0.0';
169
+ packageName = pkg.name || '';
80
170
  } catch (_) {
81
171
  // non-fatal — proceed with default
82
172
  }
@@ -120,14 +210,36 @@ function main() {
120
210
  // Warn about any copySet entries that are missing from the package — the
121
211
  // helper silently skips missing sources, so we surface it here for parity
122
212
  // with the previous UX.
213
+ const missingEntries = [];
123
214
  for (const entry of copySet) {
124
215
  if (!fs.existsSync(path.join(packageRoot, entry))) {
125
216
  console.log(` ⚠ ${entry}/ not found in package — skipped`);
217
+ missingEntries.push(entry);
126
218
  }
127
219
  }
128
220
 
221
+ // A copySet entry missing from the package is a PACKAGING regression, not a signal
222
+ // that the consumer should lose those files. mirror() silently skips a missing
223
+ // source, so `currentPaths` would under-report everything under that entry and the
224
+ // delete pass would read the whole mirrored subtree as stale and wipe it — turning
225
+ // a bad publish into mass deletion on every consumer that installs it.
226
+ //
227
+ // So when anything is missing we skip BOTH the delete pass and the manifest write.
228
+ // Leaving the previous manifest untouched is deliberate: it still accurately
229
+ // describes what is on disk (those files are still there, just not refreshed), so a
230
+ // later healthy release reconciles correctly against it. Writing an under-reporting
231
+ // manifest here would merely defer the same mass delete to the next run.
232
+ const reconcileSafe = missingEntries.length === 0;
233
+ if (!reconcileSafe) {
234
+ console.log(
235
+ ` ⚠ Upgrade cleanup skipped — ${missingEntries.join(', ')} missing from the package. ` +
236
+ 'Existing mirrored files are left untouched.'
237
+ );
238
+ }
239
+
129
240
  let totalCopied = 0;
130
241
  let totalSkipped = 0;
242
+ let totalDeleted = 0;
131
243
 
132
244
  for (const targetRoot of targetRoots) {
133
245
  const destRoot = path.join(consumerRoot, targetRoot);
@@ -151,6 +263,82 @@ function main() {
151
263
  console.log(` ✓ ${targetRoot}/${entry}/ — ${wrote} file(s) copied`);
152
264
  }
153
265
 
266
+ // ── Manifest-based delete reconciliation (E26_S08_T01) ───────────────────
267
+ // The copy set for manifest purposes is added + overwritten + SKIPPED. Including
268
+ // `skipped` is essential, not incidental: a file that was byte-identical and
269
+ // therefore skipped by mirror() is still a package-owned path, and leaving it out
270
+ // would make the very next run classify it as stale and delete it.
271
+ const currentPaths = toRelativePaths(destRoot, [
272
+ ...result.added,
273
+ ...result.overwritten,
274
+ ...result.skipped,
275
+ ]);
276
+
277
+ // Deletes only ever touch paths a manifest we ourselves wrote lists and this run
278
+ // did not rewrite. No prior manifest => see the E26_S08_T03 seeding branch below.
279
+ try {
280
+ let recon = reconcileSafe
281
+ ? reconcileFromManifest({ destRoot, currentPaths })
282
+ : { deleted: [], prunedDirs: [], refused: [], reason: 'skipped-incomplete-package' };
283
+
284
+ // ── Legacy-path seeding (E26_S08_T03) ───────────────────────────────────
285
+ // reconcileFromManifest returned 'no-prior-manifest' — this is either a genuine
286
+ // first-ever install, OR a consumer already installed before this manifest
287
+ // mechanism (E26_S08_T01) existed. The two are indistinguishable from a manifest
288
+ // alone, which is exactly the gap T03 closes: seed a SYNTHETIC prior-path list from
289
+ // this package's known-shipped legacy paths, intersected with what is ACTUALLY a
290
+ // regular file on disk right now (seedFromLegacyPaths never seeds a path that isn't
291
+ // really there), then reconcile against that seed in THIS SAME run
292
+ // (adopt-then-reconcile) rather than waiting one more upgrade cycle. A genuine
293
+ // first-ever install has nothing on disk to intersect with, so the seed set is
294
+ // naturally empty and this falls straight through to the additive-only branch below
295
+ // — no separate first-install check is needed for that to hold.
296
+ if (reconcileSafe && recon.reason === 'no-prior-manifest') {
297
+ const legacyPathsFile = path.join(packageRoot, 'lib', 'legacy-shipped-paths.json');
298
+ const legacyPaths = readLegacyShippedPaths(legacyPathsFile);
299
+ const seeded = seedFromLegacyPaths({ destRoot, legacyPaths });
300
+ if (seeded.length > 0) {
301
+ const seedRecon = reconcileWithPriorPaths({ destRoot, priorPaths: seeded, currentPaths });
302
+ // Only surface this as a distinct outcome if the seed actually produced
303
+ // something to report. A seeded path that is STILL part of this run's own
304
+ // currentPaths (e.g. a legacy path this release still ships) is neither
305
+ // stale nor refused — nothing happened, so the plain "no previous install
306
+ // manifest" additive-only message stays accurate and must not be hidden by
307
+ // an outcome-less 'seeded-reconciled' relabel.
308
+ if (seedRecon.deleted.length > 0 || seedRecon.prunedDirs.length > 0 || seedRecon.refused.length > 0) {
309
+ recon = { ...seedRecon, reason: 'seeded-reconciled' };
310
+ }
311
+ }
312
+ }
313
+
314
+ if (recon.reason === 'skipped-incomplete-package') {
315
+ // already reported once above, per-run rather than per-root
316
+ } else if (recon.reason === 'no-prior-manifest') {
317
+ console.log(` ℹ ${targetRoot}/ — no previous install manifest; additive copy only (no deletes)`);
318
+ } else if (recon.deleted.length > 0 || recon.prunedDirs.length > 0) {
319
+ const suffix = recon.reason === 'seeded-reconciled' ? ' (seeded from known-shipped legacy paths)' : '';
320
+ console.log(
321
+ ` ✓ ${targetRoot}/ — ${recon.deleted.length} stale file(s) removed${suffix}` +
322
+ (recon.prunedDirs.length ? `, ${recon.prunedDirs.length} empty dir(s) pruned` : '')
323
+ );
324
+ totalDeleted += recon.deleted.length;
325
+ }
326
+ for (const r of recon.refused) {
327
+ console.log(` ⚠ ${targetRoot}/${r.path} — left in place (${r.reason})`);
328
+ }
329
+ } catch (e) {
330
+ // Cleanup is best-effort: a reconciliation failure must never fail the install.
331
+ console.log(` ⚠ ${targetRoot}/ — upgrade cleanup skipped (${e.message})`);
332
+ }
333
+
334
+ try {
335
+ if (reconcileSafe) {
336
+ writeManifest({ destRoot, currentPaths, packageName, packageVersion });
337
+ }
338
+ } catch (e) {
339
+ console.log(` ⚠ ${targetRoot}/ — could not write install manifest (${e.message})`);
340
+ }
341
+
154
342
  totalCopied += result.added.length + result.overwritten.length;
155
343
  totalSkipped += result.skipped.length;
156
344
  }
@@ -199,11 +387,26 @@ function main() {
199
387
  console.log(` ⚠ Could not bootstrap .github/copilot-instructions.md — ${e.message}`);
200
388
  }
201
389
 
390
+ // Bootstrap .github/hooks/jenga.json unconditionally, same pattern as the
391
+ // copilot-instructions.md bootstrap immediately above (E16_S03_T04). Wires Copilot CLI's
392
+ // native sessionEnd/userPromptSubmitted hooks to this package's hooks/copilot_session_end.sh
393
+ // and hooks/prompt_router.sh, using absolute node_modules/@jenga-ai/agent paths (packageRoot
394
+ // !== consumerRoot here, so lib/generate-copilot-hooks.js bakes in the stable installed-package
395
+ // path rather than a runtime git-root lookup — see that file's own header comment for why).
396
+ // Entirely Jenga-owned output (unlike copilot-instructions.md), so this is a plain
397
+ // idempotent overwrite — no marker-merge needed.
398
+ try {
399
+ const result = generateCopilotHooks(consumerRoot, packageRoot);
400
+ console.log(` ✓ .github/hooks/jenga.json bootstrapped (${result.path})`);
401
+ } catch (e) {
402
+ console.log(` ⚠ Could not bootstrap .github/hooks/jenga.json — ${e.message}`);
403
+ }
404
+
202
405
  // Write .jenga-version to record the installed version at consumer root
203
406
  fs.writeFileSync(versionFile, packageVersion + '\n', 'utf8');
204
407
 
205
408
  console.log('\n──────────────────────────────────────────────────────');
206
- console.log(` Summary: ${totalCopied} file(s) copied, ${totalSkipped} skipped`);
409
+ console.log(` Summary: ${totalCopied} file(s) copied, ${totalSkipped} skipped, ${totalDeleted} stale file(s) removed`);
207
410
  console.log(` .jenga-version written: ${packageVersion}`);
208
411
  console.log('──────────────────────────────────────────────────────\n');
209
412
  }
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # verify-legacy-seed-reconcile.sh — fixture rehearsal for E26_S08_T03
4
+ #
5
+ # Rehearses the legacy-path-seeding mechanism (lib/postinstall-manifest.js's
6
+ # seedFromLegacyPaths/reconcileWithPriorPaths, wired into scripts/postinstall.js's
7
+ # no-prior-manifest branch) against THROWAWAY fixture consumer projects, as required by
8
+ # this task's `crucial_level: gated` note ("must be rehearsed against a throwaway
9
+ # fixture consumer directory before being considered done, not just unit-reasoned
10
+ # about"). Kept as a SEPARATE script from scripts/verify-postinstall-reconcile.sh
11
+ # (E26_S08_T01's already-Passed rehearsal harness) so that harness is not touched by
12
+ # this task.
13
+ #
14
+ # SAFETY: this script creates its own fixture root via `mktemp -d` and writes and
15
+ # deletes ONLY inside it. It takes no path argument and will never operate on an
16
+ # existing directory, so it cannot touch this repo or a real consumer project.
17
+ # Set KEEP_FIXTURE=1 to leave the fixture on disk for inspection.
18
+ #
19
+ # Scenarios covered
20
+ # A. Pre-manifest orphan cleanup — THE fix this task delivers. A consumer already
21
+ # carrying an orphan predating any manifest (e.g. a retired j-<name> twin) gets it
22
+ # cleaned up on the VERY FIRST run of a fixed version (adopt-then-reconcile in one
23
+ # pass), while a consumer-authored sibling file in the same directory survives
24
+ # along with the directory itself.
25
+ # B. Genuine first-ever install (nothing on disk to intersect with) stays purely
26
+ # additive — no deletes, regardless of what the legacy path list contains.
27
+ # C. A legacy-listed path that is STILL currently shipped survives (it's part of this
28
+ # run's own copy set, so it is never "stale").
29
+ # D. A legacy-listed path that isn't actually present on disk is never seeded (no
30
+ # phantom delete attempt, no error).
31
+ # E. Boundary/type safety over SEEDED paths: a traversal entry and a symlink entry in
32
+ # the legacy list are refused exactly like a manifest-based candidate would be —
33
+ # seeding narrows which paths are eligible, it does not relax how a candidate is
34
+ # validated.
35
+ # F. Regression: a consumer with a VALID prior manifest reconciles exactly as T01
36
+ # already verifies — seeding must never fire once a real manifest exists.
37
+ #
38
+ # Usage: bash scripts/verify-legacy-seed-reconcile.sh
39
+ # Exit: 0 = all assertions passed, 1 = at least one failed.
40
+
41
+ set -uo pipefail
42
+
43
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
44
+ FIXTURE="$(mktemp -d "${TMPDIR:-/tmp}/jenga-legacy-seed-fixture.XXXXXX")"
45
+
46
+ PASS=0
47
+ FAIL=0
48
+
49
+ # When RESULTS_FILE is set, every verdict is also appended in a machine-readable
50
+ # `PASS<TAB><name>` / `FAIL<TAB><name>` form, so tests/postinstall-legacy-seed.bats can
51
+ # assert on individual named checks instead of on one opaque exit status. Mirrors the
52
+ # exact convention scripts/verify-postinstall-reconcile.sh already established.
53
+ record() { if [ -n "${RESULTS_FILE:-}" ]; then printf '%s\t%s\n' "$1" "$2" >> "$RESULTS_FILE"; fi; }
54
+
55
+ pass() { PASS=$((PASS + 1)); record PASS "$1"; printf ' \033[32mPASS\033[0m %s\n' "$1"; }
56
+ fail() { FAIL=$((FAIL + 1)); record FAIL "$1"; printf ' \033[31mFAIL\033[0m %s\n' "$1"; }
57
+
58
+ assert_file() { if [ -f "$1" ]; then pass "$2"; else fail "$2 (missing: $1)"; fi; }
59
+ assert_absent() { if [ ! -e "$1" ]; then pass "$2"; else fail "$2 (still present: $1)"; fi; }
60
+ assert_dir() { if [ -d "$1" ]; then pass "$2"; else fail "$2 (missing dir: $1)"; fi; }
61
+ assert_grep() { if grep -q "$1" "$2" 2>/dev/null; then pass "$3"; else fail "$3"; fi; }
62
+ assert_nogrep_re() { if grep -qE "$1" "$2" 2>/dev/null; then fail "$3"; else pass "$3"; fi; }
63
+
64
+ cleanup() {
65
+ if [ "${KEEP_FIXTURE:-0}" = "1" ]; then
66
+ printf '\n Fixture retained at: %s\n' "$FIXTURE"
67
+ else
68
+ rm -rf "$FIXTURE"
69
+ fi
70
+ }
71
+ trap cleanup EXIT
72
+
73
+ # ── build a synthetic "current package" ──────────────────────────────────────
74
+ # Real lib/ + scripts/ (the code under test), a small synthetic skills/+agents/ tree,
75
+ # and a hand-authored lib/legacy-shipped-paths.json standing in for the real generated
76
+ # artifact — this fixture's package version doesn't need to match any real published
77
+ # version; only the LOGIC under test needs to be real.
78
+ make_pkg() {
79
+ local dir="$1"
80
+ mkdir -p "$dir"
81
+ cp -R "$REPO_ROOT/lib" "$dir/lib"
82
+ cp -R "$REPO_ROOT/scripts" "$dir/scripts"
83
+ cat > "$dir/package.json" <<'JSON'
84
+ { "name": "@jenga-ai/agent", "version": "3.0.0", "type": "module" }
85
+ JSON
86
+ mkdir -p "$dir/skills/do" "$dir/agents"
87
+ printf '# do skill (still shipped)\n' > "$dir/skills/do/SKILL.md"
88
+ printf '# developer agent (still shipped)\n' > "$dir/agents/developer.md"
89
+ }
90
+
91
+ write_legacy_paths() {
92
+ local dir="$1"; shift
93
+ local paths_json
94
+ paths_json=$(printf '"%s",' "$@")
95
+ paths_json="[${paths_json%,}]"
96
+ cat > "$dir/lib/legacy-shipped-paths.json" <<JSON
97
+ {
98
+ "generated_at": "2026-01-01T00:00:00.000Z",
99
+ "package": "@jenga-ai/agent",
100
+ "source": "test-fixture",
101
+ "paths": $paths_json
102
+ }
103
+ JSON
104
+ }
105
+
106
+ run_install() {
107
+ local pkg="$1" consumer="$2" log="$3"
108
+ ( cd "$pkg" && INIT_CWD="$consumer" node scripts/postinstall.js ) > "$log" 2>&1
109
+ }
110
+
111
+ echo
112
+ echo "══ Jenga legacy-path seeding rehearsal (E26_S08_T03) ══"
113
+ echo " fixture: $FIXTURE"
114
+
115
+ PKG="$FIXTURE/pkg"
116
+ make_pkg "$PKG"
117
+ write_legacy_paths "$PKG" \
118
+ "agents/developer.md" \
119
+ "skills/do/SKILL.md" \
120
+ "skills/j-legacy-twin/SKILL.md" \
121
+ "../OUTSIDE-THE-FIXTURE.txt" \
122
+ "skills/trap/LINK.md"
123
+
124
+ # ═══ Scenario A — pre-manifest orphan cleanup (THE fix) ══════════════════════
125
+ echo
126
+ echo "── Scenario A: pre-manifest orphan cleaned on the very first run ──"
127
+
128
+ CONSUMER_A="$FIXTURE/consumer-a"
129
+ mkdir -p "$CONSUMER_A/.agents/skills/j-legacy-twin" "$CONSUMER_A/.claude/skills/j-legacy-twin"
130
+ printf '# retired twin, predates any manifest\n' > "$CONSUMER_A/.agents/skills/j-legacy-twin/SKILL.md"
131
+ printf 'my own notes\n' > "$CONSUMER_A/.agents/skills/j-legacy-twin/my-notes.md"
132
+ printf '# retired twin, predates any manifest\n' > "$CONSUMER_A/.claude/skills/j-legacy-twin/SKILL.md"
133
+ printf 'my own notes\n' > "$CONSUMER_A/.claude/skills/j-legacy-twin/my-notes.md"
134
+
135
+ run_install "$PKG" "$CONSUMER_A" "$FIXTURE/a-install.log"
136
+
137
+ assert_absent "$CONSUMER_A/.agents/skills/j-legacy-twin/SKILL.md" \
138
+ "pre-manifest orphan removed on first run (.agents)"
139
+ assert_absent "$CONSUMER_A/.claude/skills/j-legacy-twin/SKILL.md" \
140
+ "pre-manifest orphan removed on first run (.claude)"
141
+ assert_file "$CONSUMER_A/.agents/skills/j-legacy-twin/my-notes.md" \
142
+ "consumer-authored sibling survives (.agents)"
143
+ assert_file "$CONSUMER_A/.claude/skills/j-legacy-twin/my-notes.md" \
144
+ "consumer-authored sibling survives (.claude)"
145
+ assert_dir "$CONSUMER_A/.agents/skills/j-legacy-twin" \
146
+ "directory survives because it still holds a consumer file (.agents)"
147
+ assert_grep "seeded from known-shipped legacy paths" "$FIXTURE/a-install.log" \
148
+ "install log attributes the cleanup to legacy-path seeding"
149
+ assert_file "$CONSUMER_A/.agents/skills/do/SKILL.md" \
150
+ "currently-shipped skill untouched"
151
+ assert_file "$CONSUMER_A/.agents/agents/developer.md" \
152
+ "currently-shipped agent file untouched"
153
+
154
+ # Manifest written this run must describe reality: current package paths only, never
155
+ # the just-deleted orphan and never the consumer's own file.
156
+ python3 - "$CONSUMER_A/.agents/.jenga-postinstall-manifest.json" <<'PYEOF'
157
+ import json, sys
158
+ paths = json.load(open(sys.argv[1]))["paths"]
159
+ assert "skills/j-legacy-twin/SKILL.md" not in paths, "deleted orphan must not be in the fresh manifest"
160
+ assert "skills/j-legacy-twin/my-notes.md" not in paths, "consumer file must never be in the manifest"
161
+ assert "skills/do/SKILL.md" in paths and "agents/developer.md" in paths, "current package paths must be present"
162
+ PYEOF
163
+ if [ $? -eq 0 ]; then pass "fresh manifest describes only what is actually on disk post-cleanup"; else fail "fresh manifest is inaccurate"; fi
164
+
165
+ # ═══ Scenario B — genuine first-ever install stays additive-only ═════════════
166
+ echo
167
+ echo "── Scenario B: genuine first-ever install (nothing to intersect with) ──"
168
+
169
+ CONSUMER_B="$FIXTURE/consumer-b"
170
+ mkdir -p "$CONSUMER_B"
171
+ run_install "$PKG" "$CONSUMER_B" "$FIXTURE/b-install.log"
172
+
173
+ assert_nogrep_re "[1-9][0-9]* stale file" "$FIXTURE/b-install.log" \
174
+ "first-ever install deletes nothing despite a non-empty legacy path list"
175
+ assert_grep "no previous install manifest" "$FIXTURE/b-install.log" \
176
+ "first-ever install reports plain additive-only, not a seeded outcome"
177
+ assert_file "$CONSUMER_B/.agents/skills/do/SKILL.md" \
178
+ "first-ever install still copies current package files"
179
+
180
+ # ═══ Scenario C — a legacy path still currently shipped is never touched ═════
181
+ # (Implicitly exercised by Scenario A's "do"/"developer.md" already surviving, but
182
+ # asserted here as its own named check for clarity.)
183
+ assert_file "$CONSUMER_A/.claude/skills/do/SKILL.md" \
184
+ "a legacy-listed path that is STILL shipped survives untouched (not stale)"
185
+
186
+ # ═══ Scenario D — a legacy path absent from disk is never phantom-seeded ═════
187
+ # The legacy list includes "skills/j-legacy-twin/SKILL.md" for consumer-b too, but
188
+ # consumer-b never had that file on disk at all. No error, no crash, nothing reported.
189
+ assert_absent "$CONSUMER_B/.agents/skills/j-legacy-twin" \
190
+ "a legacy path never present on disk produces no phantom entries"
191
+
192
+ # ═══ Scenario E — boundary/type safety over SEEDED paths ═════════════════════
193
+ echo
194
+ echo "── Scenario E: adversarial legacy-listed paths are refused, not exploited ──"
195
+
196
+ CONSUMER_E="$FIXTURE/consumer-e"
197
+ mkdir -p "$CONSUMER_E/.agents/skills/trap"
198
+ printf 'precious\n' > "$FIXTURE/OUTSIDE-THE-FIXTURE.txt"
199
+ ln -sf "$FIXTURE/OUTSIDE-THE-FIXTURE.txt" "$CONSUMER_E/.agents/skills/trap/LINK.md"
200
+
201
+ run_install "$PKG" "$CONSUMER_E" "$FIXTURE/e-install.log"
202
+
203
+ assert_file "$FIXTURE/OUTSIDE-THE-FIXTURE.txt" \
204
+ "traversal entry in the legacy list cannot delete outside the mirror root"
205
+ if [ -L "$CONSUMER_E/.agents/skills/trap/LINK.md" ]; then
206
+ pass "symlink entry in the legacy list is refused, not followed or unlinked"
207
+ else
208
+ fail "symlink entry in the legacy list was removed"
209
+ fi
210
+
211
+ # ═══ Scenario F — regression: a VALID prior manifest reconciles normally, unseeded ═══
212
+ # Uses a genuine two-version upgrade (like T01's own harness) so a REAL manifest-backed
213
+ # stale entry exists to clean up — proving the seeding branch never fires once a real
214
+ # manifest is present, not merely that nothing happened to need it.
215
+ echo
216
+ echo "── Scenario F: a consumer with a real prior manifest reconciles normally, unseeded ──"
217
+
218
+ PKG_F1="$FIXTURE/pkg-f1"
219
+ make_pkg "$PKG_F1"
220
+ mkdir -p "$PKG_F1/skills/renamed-old"
221
+ printf '# renamed-old — removed in the next version\n' > "$PKG_F1/skills/renamed-old/SKILL.md"
222
+ write_legacy_paths "$PKG_F1" "agents/developer.md" "skills/do/SKILL.md"
223
+
224
+ PKG_F2="$FIXTURE/pkg-f2"
225
+ make_pkg "$PKG_F2"
226
+ python3 -c "
227
+ import json
228
+ p = '$PKG_F2/package.json'
229
+ d = json.load(open(p)); d['version'] = '3.0.1'; json.dump(d, open(p, 'w'))
230
+ "
231
+ write_legacy_paths "$PKG_F2" "agents/developer.md" "skills/do/SKILL.md" "skills/renamed-old/SKILL.md"
232
+
233
+ CONSUMER_F="$FIXTURE/consumer-f"
234
+ mkdir -p "$CONSUMER_F"
235
+ run_install "$PKG_F1" "$CONSUMER_F" "$FIXTURE/f-install-1.log"
236
+ assert_grep "no previous install manifest" "$FIXTURE/f-install-1.log" \
237
+ "first run for consumer-f is a normal, unseeded additive install"
238
+ assert_file "$CONSUMER_F/.agents/skills/renamed-old/SKILL.md" \
239
+ "v1 skill mirrored before the upgrade that removes it"
240
+
241
+ run_install "$PKG_F2" "$CONSUMER_F" "$FIXTURE/f-install-2.log"
242
+ assert_absent "$CONSUMER_F/.agents/skills/renamed-old/SKILL.md" \
243
+ "genuinely stale entry cleaned up via the REAL manifest-backed path"
244
+ assert_grep "stale file(s) removed" "$FIXTURE/f-install-2.log" \
245
+ "the real manifest-backed delete pass ran and reported a removal"
246
+ assert_nogrep_re "seeded from known-shipped legacy paths" "$FIXTURE/f-install-2.log" \
247
+ "seeding branch never fires when a real prior manifest is present"
248
+
249
+ echo
250
+ echo "──────────────────────────────────────────────────────"
251
+ printf ' %d passed, %d failed\n' "$PASS" "$FAIL"
252
+ echo "──────────────────────────────────────────────────────"
253
+
254
+ [ "$FAIL" -eq 0 ]