@jenga-ai/agent 3.5.0 → 4.0.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 (136) hide show
  1. package/README.md +85 -78
  2. package/agents/developer.md +1 -1
  3. package/agents/scrum-master.md +20 -2
  4. package/agents/tester.md +3 -3
  5. package/hooks/on_session_end.sh +5 -5
  6. package/lib/generate-agent-context.js +2 -2
  7. package/lib/generate-copilot-hooks.js +1 -1
  8. package/lib/generate-skill-allow-list.js +79 -8
  9. package/lib/mirror.js +1 -1
  10. package/lib/postinstall-manifest.js +1 -1
  11. package/lib/skill-allow-list.json +2 -2
  12. package/mcp/help/index.js +8 -17
  13. package/mcp/help/scan.js +73 -0
  14. package/package.json +5 -1
  15. package/project/app/api/lib/resolve-project-root.js +1 -1
  16. package/project/app/api/parsers/knowledge-graph.js +100 -9
  17. package/project/app/api/routes/health.js +36 -0
  18. package/project/app/api/scripts/capture-snapshot.js +9 -6
  19. package/project/app/package.json +4 -0
  20. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  21. package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
  22. package/project/app/ui/dist/index.html +2 -2
  23. package/project/app/ui/package.json +4 -0
  24. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  25. package/scripts/acquire-concurrency-slot.sh +35 -5
  26. package/scripts/apply-j-prefix.sh +46 -5
  27. package/scripts/build-pages-site.sh +1 -1
  28. package/scripts/check-public-playbook-steps.sh +158 -52
  29. package/scripts/check-publicignore-match.sh +2 -2
  30. package/scripts/compute-deploy-reconcile.sh +5 -5
  31. package/scripts/delete-bare-skill-dirs.sh +329 -0
  32. package/scripts/generate-legacy-shipped-paths.js +2 -2
  33. package/scripts/idea_manager.sh +273 -3
  34. package/scripts/mark-deployed.sh +2 -2
  35. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  36. package/scripts/populate-knowledge-graph.js +213 -5
  37. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  38. package/scripts/postinstall.js +1 -1
  39. package/scripts/release-concurrency-slot.sh +34 -4
  40. package/scripts/render-ranked-list.sh +270 -0
  41. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  42. package/scripts/repoint-skill-refs.sh +539 -0
  43. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  44. package/scripts/strip-polyfill-frontmatter.py +166 -0
  45. package/scripts/todo_manager.sh +16 -1
  46. package/scripts/validate-typed-object.sh +750 -0
  47. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  48. package/scripts/verify-postinstall-reconcile.sh +7 -7
  49. package/scripts/write-context-digest.sh +1 -1
  50. package/skills/j-brainstorm/SKILL.md +3 -4
  51. package/skills/j-btw/SKILL.md +3 -4
  52. package/skills/j-clearify/SKILL.md +5 -6
  53. package/skills/j-close-story/SKILL.md +11 -12
  54. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  55. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  56. package/skills/j-commit/SKILL.md +3 -4
  57. package/skills/j-continue/SKILL.md +5 -6
  58. package/skills/j-deep-dive/SKILL.md +3 -4
  59. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  60. package/skills/j-distribute/SKILL.md +3 -4
  61. package/skills/j-do/SKILL.md +100 -18
  62. package/skills/j-doc/SKILL.md +3 -4
  63. package/skills/j-doc-sync/SKILL.md +4 -4
  64. package/skills/j-dooo/SKILL.md +6 -15
  65. package/skills/j-error/SKILL.md +3 -4
  66. package/skills/j-evaluate/SKILL.md +3 -4
  67. package/skills/j-examplify/SKILL.md +3 -4
  68. package/skills/j-gitignore/SKILL.md +157 -0
  69. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  70. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  71. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  72. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  73. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  74. package/skills/j-help/SKILL.md +3 -4
  75. package/skills/j-idea/SKILL.md +80 -9
  76. package/skills/j-idea/assets/idea_template.md +1 -1
  77. package/skills/j-improve/SKILL.md +4 -5
  78. package/skills/j-init/SKILL.md +23 -14
  79. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  80. package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
  81. package/skills/j-init/scripts/init.sh +4 -4
  82. package/skills/j-jbp/SKILL.md +3 -4
  83. package/skills/j-lgtm/SKILL.md +3 -4
  84. package/skills/j-pi-plan/SKILL.md +3 -4
  85. package/skills/j-playbook/SKILL.md +1 -1
  86. package/skills/j-proceed/SKILL.md +3 -4
  87. package/skills/j-publish/SKILL.md +4 -5
  88. package/skills/j-publish/adapters/npm-ci.md +6 -1
  89. package/skills/j-publish/adapters/npm.md +1 -1
  90. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  91. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  92. package/skills/j-publish/scripts/run_gates.sh +1 -1
  93. package/skills/j-reconcile/SKILL.md +40 -7
  94. package/skills/j-reconcile/assets/report_format.md +11 -0
  95. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
  96. package/skills/j-reconcile-origin/SKILL.md +3 -4
  97. package/skills/j-redo/SKILL.md +4 -5
  98. package/skills/j-skillify/SKILL.md +3 -4
  99. package/skills/j-spinoff/SKILL.md +4 -5
  100. package/skills/j-status/SKILL.md +18 -4
  101. package/skills/j-todo/SKILL.md +44 -5
  102. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  103. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  104. package/skills/j-uncharted/SKILL.md +252 -13
  105. package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
  106. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  107. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  108. package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
  109. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  110. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  111. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  112. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  113. package/skills/j-wtf/SKILL.md +4 -5
  114. package/skills/jenga/SKILL.md +106 -11
  115. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  116. package/skills/jenga/playbooks/schema.json +73 -6
  117. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  118. package/skills/jenga/scripts/load-nl-catalog.js +5 -2
  119. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  120. package/skills/jenga/scripts/load-playbooks.sh +290 -5
  121. package/skills/jenga/scripts/match-playbook.sh +4 -4
  122. package/skills/jenga/scripts/run-playbook-step.sh +267 -1
  123. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  124. package/templates/SKILL_TEMPLATE.md +12 -0
  125. package/templates/permission-levels/level-1-locked.json +1 -1
  126. package/templates/permission-levels/level-2-guarded.json +1 -1
  127. package/templates/permission-levels/level-3-standard.json +1 -1
  128. package/templates/permission-levels/level-4-elevated.json +2 -2
  129. package/templates/permission-levels/level-5-unrestricted.json +2 -2
  130. package/templates/playbook-types.json +40 -6
  131. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  132. package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
  133. package/scripts/audit-twin-divergence.sh +0 -625
  134. package/scripts/generate-j-alias.sh +0 -333
  135. package/skills/j-dev-done/SKILL.md +0 -53
  136. package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: j.uncharted
3
- description: Polyfill alias of the uncharted skill under a collision-safe directory name. Identical behavior to /uncharted — Investigate code that has no Jenga board provenance — a foreign file, an external source being pulled in, or an entire pre-existing codebase — and give it a consistent understanding document plus proper board representation. Use when the bare /uncharted form is shadowed by another tool's own built-in command of the same name.
3
+ description: Investigate code that has no Jenga board provenance — a foreign file, an external source being pulled in, or an entire pre-existing codebase — and give it a consistent understanding document plus proper board representation.
4
4
  metadata:
5
5
  prefered_agent: scrum-master
6
6
  output_types: text
@@ -14,7 +14,6 @@ keywords:
14
14
  - "unlinked code"
15
15
  - "no board history"
16
16
  - j-uncharted
17
- - polyfill
18
17
  examples:
19
18
  - "what does this file actually do?"
20
19
  - "help me understand this directory I inherited"
@@ -28,9 +27,9 @@ examples:
28
27
 
29
28
  # Uncharted — Investigative Workflow for Foreign & Pre-Existing Code
30
29
 
31
- This skill is a literal-directory-name duplicate of `skills/uncharted/`. It exists so that `/j-uncharted` (and `j.j-uncharted`) give a guaranteed-unshadowed way to reach the same flow as `/uncharted`, even if a host tool's own built-in command of the same name would otherwise shadow or override the bare `/uncharted` alias (Claude Code's native skill resolution is a literal-string, directory-name-based match — see `docs/skill-authoring.md`'s "Invocation Convention").
30
+ `skills/j-uncharted/` is the **canonical, hand-edited** directory for this skill, per CLAUDE.md's "The Canonical Naming Contract" (the `E50` reopening of 2026-09-09, which promoted `skills/j-uncharted/` from generated twin to sole canonical form). The `j-` prefix is there for collision safety — a real directory under a distinct name, so a host tool shipping its own same-named built-in command cannot shadow it (Claude Code's native skill resolution is a literal-string, directory-name-based match; see `docs/skill-authoring.md`'s "Invocation Convention").
32
31
 
33
- This file is generated/synced by `scripts/generate-j-alias.sh uncharted` from `skills/uncharted/SKILL.md` — do not hand-edit it; re-run the generator instead to pick up source changes.
32
+ > ⚠️ **`scripts/generate-j-alias.sh` was retired by `E50_S14` and no longer exists — there is nothing to run.** This file was previously generated from a bare `skills/uncharted/SKILL.md` source; `E50_S15` deleted that directory. This file is now the sole canonical, hand-edited source for this skill — edit it directly.
34
33
 
35
34
  `/uncharted` is the entry point for code with **no board provenance** — code that was never planned, decomposed, or executed through the Jenga workflow.
36
35
 
@@ -58,13 +57,14 @@ Every mode runs the same shared investigative engine and emits the same understa
58
57
  | `segment` | one file, directory, or feature | Two explicit modes since E20_S08_T03: `--mode delivery` (default, unchanged) analyses a target already in the repo and proposes a standard epic/story/task; `--mode investigate` opens a conversational architecture-investigation flow instead. See **`segment`** below for the mode choice. | E40_S02 / E20_S08_T03 |
59
58
  | `import` | an external source | Acquires a git URL, an out-of-repo path, or a pasted snippet into the repo at a user-confirmed location, then hands off to `segment`. | E40_S03 |
60
59
  | `onboard` | the whole codebase | Conversational by default since E20_S08_T03: discovery scripts seed a human-in-the-loop elicitation that writes `[ARCH]`-tagged board items and coarse graph nodes. `--legacy` reproduces the original fully-automated, zero-prompt, capped-**backfilled**-epic pass unchanged. Board/graph-only — never touches application code, in either mode. | E40_S04 / E20_S08_T03 |
60
+ | `refresh` | the delta since a prior scan | Incremental re-scan for an already-`onboard`ed codebase. Locates a baseline (a committed scan-record, or one inferred from board evidence, or — if neither exists — falls back to a full `onboard` run), diffs the codebase against it, and routes only `changed`/`new` candidates through `onboard`'s own mechanics — same bare-vs-`--legacy` split, same Convergence Loop, same backfilled-epic writer. `removed` candidates are never silently dropped: their graph node(s) are marked superseded and the corresponding board item is flagged for review. Board/graph-only — never touches application code, in either mode. See **`refresh`** below. | E40_S07 |
61
61
 
62
62
  **Dispatch rules:**
63
63
 
64
- 1. If the mode is one of `segment`, `import`, or `onboard`, dispatch to that section below.
65
- 2. If the mode is missing or unrecognised, do **not** guess. Present the three modes as a numbered choice list with a free-text option last, per the Interaction Pattern in `CLAUDE.md`.
64
+ 1. If the mode is one of `segment`, `import`, `onboard`, or `refresh`, dispatch to that section below.
65
+ 2. If the mode is missing or unrecognised, do **not** guess. Present the four modes as a numbered choice list with a free-text option last, per the Interaction Pattern in `CLAUDE.md`.
66
66
  3. If a mode is given but its target is missing, ask for the target the same way — a numbered list of plausible candidates where they can be inferred, free-text last.
67
- 4. Never widen scope across modes in one invocation. `import` may hand off to `segment` because that handoff is part of its contract (E40_S03_T03); nothing else chains implicitly.
67
+ 4. Never widen scope across modes in one invocation. Two, and only two, handoffs are part of a mode's own contract rather than a violation of this rule: `import` may hand off to `segment` (E40_S03_T03), and `refresh` may fall back to running `onboard` in full when no baseline exists for the requested root (see **`refresh`** below, and `onboard`'s own Invocation Contract row). Nothing else chains implicitly.
68
68
 
69
69
  ---
70
70
 
@@ -358,7 +358,9 @@ It returns one `results[]` record. Read three fields:
358
358
 
359
359
  **Report the linkage before doing anything else.** `unlinked` is the condition that justifies this whole workflow; say so. `linked` is a genuine finding — the target *already has* board provenance, so tell the user which items reference it and confirm they still want a segment pass rather than `/redo`. Do not proceed silently past a `linked` target.
360
360
 
361
- > **Step 1 is authoritative on linkage.** `run-engine.sh` keeps its own private substring version for the document's `Board Linkage` row, and the two can disagree: a board item mentioning only `docs/hooks/guide.md` makes the document call `hooks/` *linked* while Step 1 correctly calls it *unlinked*. Where they differ, trust Step 1 and correct the row when you fill in the document — do not hand the user a document that contradicts what you just told them.
361
+ > **Step 1 is authoritative on linkage.** `run-engine.sh` keeps its own private, minimal reimplementation of `resolve-segment-target.sh`'s in-repo linkage check for the document's `Board Linkage` row — as of `E40_S02_T05`, both use the same path-boundary rule (a board reference counts only when a board file's path-like token equals the target or is a path descendant of it, and a board reference to the target's generated `.agents/`/`.claude/` mirror counts as a reference to the canonical root too), so the two should agree for every in-repo target. They can still theoretically diverge — `run-engine.sh`'s version is a minimal reimplementation, not `resolve-segment-target.sh`'s full `BOARD_INDEX`/tokenizer, and full unification (having `run-engine.sh` call `resolve-segment-target.sh --json-only` directly) remains a deliberate, separate follow-up (see below). Where they differ, trust Step 1 and correct the row when you fill in the document — do not hand the user a document that contradicts what you just told them.
362
+ >
363
+ > **When Step 1 reports `not_checked` (e.g. the target is outside this repository), do not render an `unlinked`/`linked` claim at all.** Neither is true — nothing was searched, so nothing was verified absent or present. Render the document's Board Linkage row as `"not applicable — target outside repository"` (or the equivalent `reason` Step 1 returned, worded the same way), and say the same thing out loud per "Report the linkage before doing anything else" above. `run-engine.sh`'s own row for this case is worded consistently (`not_checked — target is outside this repository`), so the two should already agree — but Step 1 stays the source of truth if they ever don't.
362
364
  >
363
365
  > The same script is also `/reconcile`'s board-linkage check (E40_S05_T02). Collapsing the two implementations into one is a deliberate follow-up, not something to do in passing — see the script's header.
364
366
 
@@ -660,7 +662,7 @@ exists as a *board-only* mode rather than a generic migration tool. Its entire o
660
662
  without exception, is:
661
663
 
662
664
  - `project/board/` (backfilled epics in `--legacy` mode; `[ARCH]`-tagged epics/stories/tasks in conversational mode)
663
- - `project/rapports/analysis/` (understanding documents and the subsystem cap record — `--legacy` mode only; conversational mode's primary output is the graph, not this document type — see Conversational Elicitation above)
665
+ - `project/rapports/analysis/` (understanding documents and the subsystem cap record — `--legacy` mode only; conversational mode's primary output is the graph, not this document type — see Conversational Elicitation above). Both modes additionally write a committed scan-record (`*.scan-record.json`, `E40_S07_T01`) into this same directory at the end of every successful run — distinguished from an understanding document by filename suffix, never overloading one artifact with two purposes.
664
666
  - `project/PROJECT_SUMMARY.md` (populated by `E40_S04_T05`, `--legacy` mode only)
665
667
  - `project/knowledge-graph/graph.json` (coarse graph nodes/edges — conversational mode only, per the stub schema)
666
668
  - `project/queue/elicitation-state/` (multi-session persistence scratch state — conversational mode only, git-ignored, not a durable artifact)
@@ -683,10 +685,10 @@ stated here:
683
685
 
684
686
  New in **E20_S08_T03**. Runs when `onboard` is invoked without `--legacy`.
685
687
 
686
- **Step 1 — discovery stays scripted.** Run the exact same deterministic discovery chain the legacy pipeline uses — `discover-subsystems.sh` — unchanged. Evidence-gathering is not where this rework touches anything; only what happens with the output differs.
688
+ **Step 1 — discovery stays scripted.** Run the exact same deterministic discovery chain the legacy pipeline uses — `discover-subsystems.sh` — unchanged. Evidence-gathering is not where this rework touches anything; only what happens with the output differs. Capture its output — Step 5 reuses it to write the scan-record once the run completes successfully.
687
689
 
688
690
  ```bash
689
- bash skills/j-uncharted/scripts/discover-subsystems.sh <root>
691
+ DISCOVERY_JSON=$(bash skills/j-uncharted/scripts/discover-subsystems.sh <root>)
690
692
  ```
691
693
 
692
694
  **Step 2 — directory triage.** Feed the discovery output's candidate paths into the shared Directory Triage procedure (see Conversational Elicitation above), and stop at its confirmation gate before anything else happens.
@@ -701,6 +703,14 @@ bash skills/j-uncharted/scripts/elicitation-state.sh init --id "onboard-$(basena
701
703
 
702
704
  **Step 5 — on convergence, write the graph and the board item(s).** Per candidate: write the converged node(s)/edge(s) to `project/knowledge-graph/graph.json`, then present an `[ARCH]`-tagged board item proposal at whichever level fits (an individual subsystem is usually story-scale; the whole run may warrant a single `[ARCH]` epic containing one story per converged subsystem — judgement call, not a fixed rule) and stop at a confirmation gate before writing to `project/board/`, exactly as `segment --mode investigate`'s Step 4 does. Call `elicitation-state.sh complete` once every candidate has converged, been deferred, or been resolved past the turn cap.
703
705
 
706
+ **Write the scan-record once the run has completed successfully** (`elicitation-state.sh complete` has been called) — not on every candidate, and not gated on `refresh` mode existing or ever being invoked (`E40_S07_T01`). Reuse `$DISCOVERY_JSON` captured at Step 1; this is the only place in the conversational path that needs it after Step 2:
707
+
708
+ ```bash
709
+ bash skills/j-uncharted/scripts/write-scan-record.sh <<< "$DISCOVERY_JSON"
710
+ ```
711
+
712
+ This is deliberately not the elicitation state file — that persistence is transient session scratch (see Multi-Session Persistence above); the scan-record is committed and must survive indefinitely between onboard/refresh runs. Baseline discovery and diff classification against this record are separate, not-yet-implemented follow-on tasks (`E40_S07_T02`, `E40_S07_T03`); this step only writes it.
713
+
704
714
  **`PROJECT_SUMMARY.md` population still applies, unchanged in spirit.** Once the conversational pass has produced its `[ARCH]` items, hand off to the scrum-master for the same `PROJECT_SUMMARY.md` Overview/Architecture & Structure drafting-and-confirmation flow described under **Updating PROJECT_SUMMARY.md from onboard evidence** below (Steps A-D) — substituting the conversational pass's converged understanding for the legacy pipeline's `kept` array as the evidence source. Do not skip the stub-vs-real-content check in Step B just because the evidence came from a conversation instead of a script.
705
715
 
706
716
  #### Legacy mode (`--legacy`)
@@ -765,7 +775,8 @@ that actually writes backfilled epics to the board — it consumes `apply-subsys
765
775
  framed around understanding and integration, never original construction):
766
776
 
767
777
  ```bash
768
- bash skills/j-uncharted/scripts/discover-subsystems.sh <root> \
778
+ DISCOVERY_JSON=$(bash skills/j-uncharted/scripts/discover-subsystems.sh <root>)
779
+ echo "$DISCOVERY_JSON" \
769
780
  | bash skills/j-uncharted/scripts/apply-subsystem-cap.sh --rapport "$DOC" \
770
781
  | bash skills/j-uncharted/scripts/write-backfilled-epics.sh
771
782
  ```
@@ -777,6 +788,22 @@ bash skills/j-uncharted/scripts/discover-subsystems.sh <root> \
777
788
  | `--dry-run` | Compute IDs and render content without writing anything — useful for previewing what a run would produce. |
778
789
  | `--label "<text>"` | Human label for the analysed codebase, used in each epic's Purpose section. |
779
790
 
791
+ **Write the scan-record once epic generation has succeeded** (`E40_S07_T01`) — not gated on
792
+ `refresh` mode existing or ever being invoked; every successful `onboard --legacy` run leaves one,
793
+ the same as the conversational default does. Reuse `$DISCOVERY_JSON` captured above — it carries
794
+ `discover-subsystems.sh`'s own `candidates` array, which is what `write-scan-record.sh` expects;
795
+ `apply-subsystem-cap.sh`'s `kept`/`dropped` output is a different shape and is not what gets piped
796
+ here:
797
+
798
+ ```bash
799
+ bash skills/j-uncharted/scripts/write-scan-record.sh <<< "$DISCOVERY_JSON"
800
+ ```
801
+
802
+ This is a committed record (never git-ignored), distinct from `elicitation-state.sh`'s transient
803
+ session scratch — see the conversational default's own note on this above. Baseline discovery and
804
+ diff classification against it are separate, not-yet-implemented follow-on tasks (`E40_S07_T02`,
805
+ `E40_S07_T03`); this step only writes it.
806
+
780
807
  **Epic ID continuation.** The next free `E##` is one past the highest epic number found under
781
808
  both `--epics-dir` and the canonical `project/board/epics/`, so generated epics always continue
782
809
  the existing board's numbering and never collide with an epic already on it.
@@ -860,7 +887,7 @@ sections of `project/PROJECT_SUMMARY.md`:
860
887
 
861
888
  **Step B — Check both sections for existing content before proposing anything.** A section counts
862
889
  as a **stub** only if its body is empty, whitespace-only, or is (or is limited to) the literal
863
- placeholder text from `skills/init/assets/PROJECT_SUMMARY_template.md` — `_To be completed._`.
890
+ placeholder text from `skills/j-init/assets/PROJECT_SUMMARY_template.md` — `_To be completed._`.
864
891
  Anything else — a sentence, a partial list, a paragraph someone already wrote by hand — is real,
865
892
  non-stub content, however short, and is never silently overwritten. Check the Overview and
866
893
  Architecture & Structure sections independently; one can be a stub while the other is not.
@@ -910,6 +937,218 @@ Like epic generation above it, "skip" on both sections is a valid outcome, not a
910
937
  that leaves `PROJECT_SUMMARY.md` untouched still respects the read/write surface named under **The
911
938
  hard constraint** above, since that file was never a required write, only a permitted one.
912
939
 
940
+ ### `refresh`
941
+
942
+ Incremental re-scan for a codebase that has already been through `onboard` at least once. Detects
943
+ what changed since the last scan and only reinvestigates the delta, so keeping the board and graph
944
+ current does not mean re-running the whole of `onboard` every time. Like `onboard`, `refresh` has
945
+ the same bare-vs-`--legacy` split (see the Invocation Contract table above) — read **Conversational
946
+ Elicitation** above (Human-Oracle-Availability Limitation, Directory Triage, Convergence Loop,
947
+ Multi-Session Persistence) before running the conversational default. Everything below only
948
+ sequences those shared mechanics, plus the three baseline/diff scripts introduced by this story
949
+ (`E40_S07`), for `refresh`'s own scope — it does not redefine any of them. `refresh` is also bound,
950
+ without exception or restatement, by the **Constraints** section below — read-only against
951
+ application code, confirm before writing to the board, no new rapport type — the same as every
952
+ other mode.
953
+
954
+ **`refresh` introduces no new understanding mechanism.** Every node, epic, story, and task it
955
+ writes goes through exactly the same write paths `onboard` already uses — the Convergence Loop for
956
+ the conversational default, `write-backfilled-epics.sh` for `--legacy`. `refresh` only changes
957
+ *which* candidates reach those paths: `find-scan-baseline.sh` (`E40_S07_T02`) locates a baseline
958
+ and `diff-since-baseline.sh` (`E40_S07_T03`) classifies each previously-known candidate as
959
+ `unchanged`/`changed`/`new`/`removed` before either write path ever runs.
960
+
961
+ #### Baseline discovery and diff — shared by both modes
962
+
963
+ Read `find-scan-baseline.sh`'s and `diff-since-baseline.sh`'s own header comments before invoking
964
+ either — their exact flag names and exit-code numbers are load-bearing for this sequencing, and
965
+ are not restated in full here (per **Shared Investigative Engine**'s "do not restate in prose what
966
+ these scripts do step by step" convention above).
967
+
968
+ **Step 1 — run fresh discovery once, keep it.** Both the diff below and (for `--legacy`) the
969
+ backfill step later need the same fresh `discover-subsystems.sh` output, so it is captured once
970
+ here rather than re-run per step — the same pattern `onboard`'s own Step 1 already uses:
971
+
972
+ ```bash
973
+ DISCOVERY_JSON=$(bash skills/j-uncharted/scripts/discover-subsystems.sh <root>)
974
+ ```
975
+
976
+ **Step 2 — locate a baseline.**
977
+
978
+ ```bash
979
+ BASELINE_JSON=$(bash skills/j-uncharted/scripts/find-scan-baseline.sh <root>); RC=$?
980
+ ```
981
+
982
+ Three outcomes, per the script's own exit-code contract:
983
+
984
+ - **Exit 0, `"source": "scan-record"`.** The precise case — a prior `write-scan-record.sh` run for
985
+ this root was found under `project/rapports/analysis/`. Its `candidates[]` array (`path` +
986
+ `files`/`lines` per candidate) is what Step 3 below diffs against.
987
+ - **Exit 0, `"source": "inferred"`.** No scan-record exists, but a `provenance: backfilled` epic or
988
+ an `[ARCH]`-tagged board item whose `docs:` overlaps the root gave an approximate baseline commit
989
+ via git history on that board file. **This shape carries no `candidates[]` array at all** — only
990
+ `baseline_commit` and `inferred_from[]`. Say this plainly to the user before proceeding: an
991
+ inferred baseline has no prior candidate set to compare *membership* against, so `diff-since-
992
+ baseline.sh` (Step 3) can only ever classify candidates as `unchanged`/`changed` against it — its
993
+ `new`/`removed` arrays stay empty by construction. A `refresh` against an inferred baseline
994
+ cannot tell you a subsystem disappeared or a brand-new one appeared, only that a previously-known
995
+ one did or didn't change. Do not imply otherwise to the user.
996
+ - **Exit 3 — no baseline found.** Neither a scan-record nor board evidence exists for this root.
997
+ **Stop here and run `onboard`'s own conversational default in full** (its Steps 1-5 above,
998
+ including its own scan-record write at the end) — this *is* the first scan for this root, and
999
+ nothing else in this `refresh` subsection applies to that run. The *next* `refresh` invocation
1000
+ against this root then finds that scan-record at Tier 1. `--legacy refresh` takes the equivalent
1001
+ fallback: a full `onboard --legacy` run, which likewise ends by writing the first scan-record per
1002
+ its own "Write the scan-record once epic generation has succeeded" step above.
1003
+
1004
+ Any other exit (1 usage error, 2 environment error, 4 write failure) is not "no baseline" — surface
1005
+ the script's stderr and fix the invocation rather than falling back to `onboard`.
1006
+
1007
+ **Step 3 — classify what changed.**
1008
+
1009
+ ```bash
1010
+ DIFF_JSON=$(echo "$BASELINE_JSON" \
1011
+ | bash skills/j-uncharted/scripts/diff-since-baseline.sh --fresh <(echo "$DISCOVERY_JSON"))
1012
+ ```
1013
+
1014
+ `--fresh` is passed explicitly here (rather than letting the script re-run discovery itself)
1015
+ specifically so Step 1's `$DISCOVERY_JSON` is the one candidate set both this diff and the
1016
+ `--legacy` backfill step below reason about — one discovery pass per `refresh` run, not two that
1017
+ could in principle disagree if the tree changed between them. Read back `unchanged`, `changed`,
1018
+ `new`, and `removed` (each entry carrying at least `path`; `changed` entries also carry
1019
+ `changed_files`) — see the script's own header for the full shape. As Step 2 already flagged, a
1020
+ Tier 2 (`inferred`) baseline always yields empty `new`/`removed` arrays; that is documented,
1021
+ intentional behavior of the script, not a defect in this sequencing.
1022
+
1023
+ #### Conversational default
1024
+
1025
+ Runs when `refresh` is invoked without `--legacy`, once Steps 1-3 above have produced `$DIFF_JSON`.
1026
+
1027
+ 1. **`unchanged` candidates are skipped entirely.** No Familiarity Check, no Directory Triage
1028
+ entry, no prompt of any kind — they never enter the candidate set below at all.
1029
+ 2. **`changed` and `new` candidates proceed through Directory Triage (if applicable) and the
1030
+ Convergence Loop exactly as `onboard`'s conversational default already does** (its Steps 2-5
1031
+ above) — no new understanding mechanism, no different gating, no different question templates.
1032
+ The candidate set fed to Directory Triage is the union of `$DIFF_JSON`'s `changed` and `new`
1033
+ paths, in place of `onboard`'s own full `discover-subsystems.sh` output:
1034
+
1035
+ ```bash
1036
+ echo "$DIFF_JSON" | jq -r '.changed[].path, .new[].path' \
1037
+ | bash skills/j-uncharted/scripts/directory-triage.sh <root>
1038
+ ```
1039
+
1040
+ Initialize persistence the same way `onboard`'s own Step 3 does, with an id that distinguishes a
1041
+ `refresh` run from an `onboard` run over the same root:
1042
+
1043
+ ```bash
1044
+ bash skills/j-uncharted/scripts/elicitation-state.sh init \
1045
+ --id "refresh-$(basename "$(cd "$root" && pwd)")-$(date -u +%Y%m%d)" --target "<root>" --cap 5
1046
+ ```
1047
+ 3. **`removed` candidates are never silently deleted or status-changed.** For each entry in
1048
+ `$DIFF_JSON`'s `removed` array:
1049
+ - **Locate the corresponding graph node(s)** in `project/knowledge-graph/graph.json` — the
1050
+ node(s) written for that path during the run that originally converged it. The stub schema
1051
+ (`project/knowledge-graph/STUB_SCHEMA.md`) has no dedicated path field on a node, so this
1052
+ correspondence is read from what the node's own `label`/`description` and edges recorded
1053
+ about the candidate at convergence time, not looked up by a formal key. Where more than one
1054
+ node plausibly corresponds and the traces don't disambiguate, say so under the board-item
1055
+ flag below rather than guessing which one to mark.
1056
+ - **Mark it `status: superseded`**, per the stub schema's Evidence-Wins Conflict Rule. That
1057
+ rule's `superseded_by` field is documented as "required when `status: superseded`... points at
1058
+ the node that superseded this one," and every worked example in the stub schema is a real
1059
+ replacement node — there is no successor node for a subsystem that was simply removed. Rather
1060
+ than inventing a new schema field to express "no successor" (the stub schema is deliberately
1061
+ minimal per its own header, and the Human-Oracle-Availability Limitation above already
1062
+ establishes the convention for an unrepresentable nuance: say it in the node's `description`
1063
+ text, not a new field), set `superseded_by` to a plain sentinel string that cannot be mistaken
1064
+ for a real node id — `"removed-no-successor"` — and state the reason in the node's own
1065
+ `description`, e.g. "Subsystem removed from the codebase as of the `refresh` run on
1066
+ <YYYY-MM-DD>; no successor node exists." **This sentinel is `refresh`'s own documented
1067
+ convention, not a stub-schema addition** — if it's ever questioned, say so plainly rather than
1068
+ treating it as if the stub schema itself defined it.
1069
+ - **Flag the corresponding board item for review**, reusing existing mechanisms rather than a
1070
+ new frontmatter field (`templates/SCRUM_BOARD_SCHEMA.md` has no `flagged_for_review` field or
1071
+ equivalent, and adding one is a schema change for the scrum-master to make, not something this
1072
+ skill invents in passing):
1073
+ - If the item is already in a terminal status (`Passed`, `Passed with remarks`, `Rejected`,
1074
+ `Done`, `Blocked`), set `reopened_on`/`reopened_reason` per the schema's Reopen Tracking
1075
+ Fields, with a reason naming the removed path and the superseded node's id — **without**
1076
+ changing `status` itself. This keeps the flag visible on the item without the side effect of
1077
+ silently reopening it into active work; a human decides whether it actually needs reopening.
1078
+ - Regardless of status, also append a dated prose note to the board item's body — the same
1079
+ "Reconcile Note" convention already used elsewhere on this board (see e.g.
1080
+ `project/board/stories/E40_S01_shared-investigative-engine.md`'s "Reconcile Note —
1081
+ 2026-09-23" section for a live example) — naming the removed path, the superseded node id,
1082
+ and the `refresh` run's date, so a reader sees why the item needs a look even before
1083
+ checking `reopened_reason`.
1084
+ - **Never delete the board item and never change its `status` field** as part of this
1085
+ flagging step — status changes remain the tester's exclusive responsibility (per
1086
+ `agents/developer.md` and `agents/tester.md`), and this is a flag for a human to act on, not
1087
+ an automatic resolution.
1088
+
1089
+ Both the conversational path and `--legacy` (below) use this exact supersede-and-flag treatment
1090
+ for `removed` candidates — it is not conversational-only.
1091
+
1092
+ **Write the scan-record once this run completes** (`elicitation-state.sh complete` has been
1093
+ called), exactly as `onboard`'s conversational default's own step does. Reuse `$DISCOVERY_JSON`
1094
+ from Step 1 above — the full fresh discovery output, not `$DIFF_JSON`'s classified subset, and not
1095
+ only the `changed`/`new` candidates that went through the Convergence Loop — so the next baseline
1096
+ correctly reflects every candidate currently present, including the `unchanged` ones that were
1097
+ skipped this run:
1098
+
1099
+ ```bash
1100
+ bash skills/j-uncharted/scripts/write-scan-record.sh <<< "$DISCOVERY_JSON"
1101
+ ```
1102
+
1103
+ **`PROJECT_SUMMARY.md` population applies the same way `onboard`'s conversational default already
1104
+ hands it off** — see **Updating PROJECT_SUMMARY.md from onboard evidence** above (Steps A-D),
1105
+ substituting this run's converged understanding (the `changed`/`new` candidates only) as the
1106
+ evidence source, the same substitution `onboard`'s own conversational default already makes for its
1107
+ own evidence.
1108
+
1109
+ #### `--legacy` mode
1110
+
1111
+ Baseline discovery and diff are exactly the shared Steps 1-3 above — there is no `--legacy`-specific
1112
+ variant of `find-scan-baseline.sh` or `diff-since-baseline.sh`.
1113
+
1114
+ 1. **Backfill only `changed`/`new` subsystems.** `apply-subsystem-cap.sh` requires `candidates[]`
1115
+ entries carrying `rank` and `score` (see its own header) — fields `diff-since-baseline.sh`'s
1116
+ `changed`/`new` entries do not carry (they carry `path`/`files`/`lines`, plus `changed_files` for
1117
+ `changed`). Building a synthetic report from `$DIFF_JSON` directly would therefore be missing
1118
+ fields `apply-subsystem-cap.sh` actually needs. Instead, filter Step 1's full
1119
+ `$DISCOVERY_JSON` — which already carries `rank`/`score` for every candidate — down to just the
1120
+ paths `$DIFF_JSON` classified as `changed` or `new`:
1121
+
1122
+ ```bash
1123
+ CHANGED_NEW_PATHS=$(echo "$DIFF_JSON" | jq -c '[.changed[].path, .new[].path]')
1124
+ FILTERED_REPORT=$(echo "$DISCOVERY_JSON" | jq --argjson keep "$CHANGED_NEW_PATHS" \
1125
+ '.candidates |= map(select(.path as $p | $keep | index($p) != null))')
1126
+
1127
+ echo "$FILTERED_REPORT" \
1128
+ | bash skills/j-uncharted/scripts/apply-subsystem-cap.sh --rapport "$DOC" \
1129
+ | bash skills/j-uncharted/scripts/write-backfilled-epics.sh
1130
+ ```
1131
+
1132
+ This is the "backfill only" behavior the story's Design section agrees to (`E40_S07` Design,
1133
+ point 6): existing epics for `unchanged` subsystems are never touched, never re-capped, never
1134
+ renumbered — they simply never enter `apply-subsystem-cap.sh`'s input. The subsystem cap
1135
+ (default 8, `apply-subsystem-cap.sh --cap N`) still applies, but now scoped to the changed/new
1136
+ set only, not the full fresh discovery output the way a plain `onboard --legacy` run scopes it.
1137
+ 2. **`removed` candidates get the identical graph-node-supersede-and-flag treatment** described
1138
+ under the conversational default's step 3 above — mark the corresponding graph node(s)
1139
+ `status: superseded` with the `removed-no-successor` sentinel, and flag the board item via
1140
+ `reopened_on`/`reopened_reason` (terminal items) plus a dated Reconcile Note (any item), never a
1141
+ silent delete or status change. This part of the behavior is identical between the two modes.
1142
+ 3. **Write the scan-record once epic generation succeeds**, exactly as a plain `onboard --legacy`
1143
+ run's own step does. Reuse the full `$DISCOVERY_JSON` from Step 1 above — not the filtered
1144
+ changed/new-only report built for `apply-subsystem-cap.sh` — for the same reason the
1145
+ conversational default reuses the full set: the next baseline must reflect everything currently
1146
+ present, including the `unchanged` subsystems this run never touched:
1147
+
1148
+ ```bash
1149
+ bash skills/j-uncharted/scripts/write-scan-record.sh <<< "$DISCOVERY_JSON"
1150
+ ```
1151
+
913
1152
  ---
914
1153
 
915
1154
  ## Constraints
@@ -97,7 +97,7 @@ Options:
97
97
  -h, --help Show this help and exit.
98
98
 
99
99
  Examples:
100
- $(basename "$0") skills/reconcile/
100
+ $(basename "$0") skills/j-reconcile/
101
101
  $(basename "$0") scripts/board_resolver.sh
102
102
  EOF
103
103
  }
@@ -183,6 +183,7 @@ EXT_LANG = {
183
183
  ".cc": "cpp", ".cpp": "cpp", ".cxx": "cpp", ".hpp": "cpp", ".hh": "cpp",
184
184
  ".java": "java", ".kt": "kotlin", ".kts": "kotlin",
185
185
  ".php": "php",
186
+ ".cs": "csharp",
186
187
  }
187
188
 
188
189
  SHEBANG_LANG = [
@@ -539,6 +540,33 @@ def scan_php(text, path, base_dir, language, source):
539
540
  add_external(spec.lstrip("\\").split("\\")[0], language, "package", source)
540
541
 
541
542
 
543
+ CSHARP_USING = re.compile(r"^\s*using\s+(?:static\s+)?([\w.]+)\s*;", re.M)
544
+ # Dot-bounded, matching scan_jvm's JVM_STDLIB_PREFIXES convention: a bare prefix match
545
+ # (e.g. "System") would also swallow unrelated namespaces like "SystemUnderTest.Helpers"
546
+ # or "Systemantics.Foo". "System" and "Microsoft" are also valid bare (undotted) using
547
+ # targets on their own (e.g. "using System;"), so they're checked as an exact match too.
548
+ CSHARP_STDLIB_PREFIXES = ("System.", "Microsoft.")
549
+ CSHARP_STDLIB_EXACT = ("System", "Microsoft")
550
+ CSHARP_SRC_ROOTS = ("", "src", "Source")
551
+
552
+
553
+ def scan_csharp(text, path, base_dir, language, source):
554
+ for spec in CSHARP_USING.findall(text):
555
+ if spec in CSHARP_STDLIB_EXACT or spec.startswith(CSHARP_STDLIB_PREFIXES):
556
+ add_external(spec, language, "stdlib", source)
557
+ continue
558
+ as_path = spec.replace(".", "/")
559
+ resolved = None
560
+ for src_root in CSHARP_SRC_ROOTS:
561
+ resolved = resolve_path(os.path.join(REPO_ROOT, src_root), as_path, [".cs"])
562
+ if resolved:
563
+ break
564
+ if resolved:
565
+ add_internal(spec, language, resolved, source)
566
+ else:
567
+ add_external(spec, language, "package", source)
568
+
569
+
542
570
  SCANNERS = {
543
571
  "javascript": scan_js, "typescript": scan_js,
544
572
  "python": scan_python,
@@ -549,6 +577,7 @@ SCANNERS = {
549
577
  "c": scan_c, "cpp": scan_c,
550
578
  "java": scan_jvm, "kotlin": scan_jvm,
551
579
  "php": scan_php,
580
+ "csharp": scan_csharp,
552
581
  }
553
582
 
554
583
  # ---------------------------------------------------------------------------
@@ -559,6 +588,7 @@ MANIFEST_NAMES = [
559
588
  "package.json", "pyproject.toml", "requirements.txt", "go.mod", "Cargo.toml", "Gemfile",
560
589
  "setup.py", "setup.cfg", "Pipfile", "composer.json", "pom.xml",
561
590
  "build.gradle", "build.gradle.kts", "Package.swift", "mix.exs", "pubspec.yaml",
591
+ "packages.config", ".csproj",
562
592
  ]
563
593
 
564
594
 
@@ -620,41 +650,69 @@ def _declared_pyproject(text):
620
650
  return sorted({n for n in names if n})
621
651
 
622
652
 
653
+ CSPROJ_PACKAGE_REF = re.compile(r"""<PackageReference\s+[^>]*\bInclude\s*=\s*["']([^"']+)["']""")
654
+ PACKAGES_CONFIG_PACKAGE = re.compile(r"""<package\s+[^>]*\bid\s*=\s*["']([^"']+)["']""")
655
+
656
+
657
+ def _declared_csproj(text):
658
+ return sorted(set(CSPROJ_PACKAGE_REF.findall(text)))
659
+
660
+
661
+ def _declared_packages_config(text):
662
+ return sorted(set(PACKAGES_CONFIG_PACKAGE.findall(text)))
663
+
664
+
623
665
  DECLARED_PARSERS = {
624
666
  "package.json": _declared_package_json,
625
667
  "requirements.txt": _declared_requirements,
626
668
  "go.mod": _declared_go_mod,
627
669
  "Cargo.toml": _declared_cargo,
628
670
  "pyproject.toml": _declared_pyproject,
671
+ ".csproj": _declared_csproj,
672
+ "packages.config": _declared_packages_config,
629
673
  }
630
674
 
631
675
 
632
676
  def collect_manifests(start_dir):
633
677
  found = []
634
678
  for directory, distance in find_upwards(start_dir, None):
679
+ dir_entries = None
635
680
  for name in MANIFEST_NAMES:
636
- candidate = os.path.join(directory, name)
637
- if not os.path.isfile(candidate):
638
- continue
639
- declared = None
640
- parser = DECLARED_PARSERS.get(name)
641
- if parser:
642
- text = read_text(candidate)
643
- if text is not None:
681
+ # Entries starting with "." (e.g. ".csproj") are extension globs rather than
682
+ # fixed filenames, since a .csproj's basename varies per project. Everything
683
+ # else keeps the original exact-filename lookup.
684
+ if name.startswith("."):
685
+ if dir_entries is None:
644
686
  try:
645
- declared = parser(text)
646
- except Exception:
647
- notices.append(
648
- "Could not parse declared dependencies from %s; manifest is listed "
649
- "but declared_dependencies is null." % rel(candidate)
650
- )
651
- found.append({
652
- "name": name,
653
- "path": rel(candidate),
654
- "location": "target" if distance == 0 else "ancestor",
655
- "distance": distance,
656
- "declared_dependencies": declared,
657
- })
687
+ dir_entries = sorted(os.listdir(directory))
688
+ except OSError:
689
+ dir_entries = []
690
+ matches = [e for e in dir_entries if e.endswith(name)]
691
+ else:
692
+ matches = [name] if os.path.isfile(os.path.join(directory, name)) else []
693
+ for match_name in matches:
694
+ candidate = os.path.join(directory, match_name)
695
+ if not os.path.isfile(candidate):
696
+ continue
697
+ declared = None
698
+ parser = DECLARED_PARSERS.get(name)
699
+ if parser:
700
+ text = read_text(candidate)
701
+ if text is not None:
702
+ try:
703
+ declared = parser(text)
704
+ except Exception:
705
+ notices.append(
706
+ "Could not parse declared dependencies from %s; manifest is listed "
707
+ "but declared_dependencies is null." % rel(candidate)
708
+ )
709
+ found.append({
710
+ "name": match_name,
711
+ "path": rel(candidate),
712
+ "location": "target" if distance == 0 else "ancestor",
713
+ "distance": distance,
714
+ "declared_dependencies": declared,
715
+ })
658
716
  return found
659
717
 
660
718
 
@@ -104,7 +104,7 @@ Options:
104
104
  -h, --help Show this help and exit.
105
105
 
106
106
  Examples:
107
- $(basename "$0") skills/reconcile/
107
+ $(basename "$0") skills/j-reconcile/
108
108
  $(basename "$0") scripts/board_resolver.sh
109
109
  EOF
110
110
  }