@owlmeans/create-app 0.1.18-rc.8 → 0.1.18

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 (116) hide show
  1. package/README.md +49 -5
  2. package/THIRD-PARTY-NOTICES.txt +24 -0
  3. package/build/args/types.d.ts +9 -0
  4. package/build/args/types.d.ts.map +1 -0
  5. package/build/args/types.js +2 -0
  6. package/build/args/types.js.map +1 -0
  7. package/build/args.d.ts +3 -20
  8. package/build/args.d.ts.map +1 -1
  9. package/build/args.js +121 -88
  10. package/build/args.js.map +1 -1
  11. package/build/bin.js +3 -3
  12. package/build/bin.js.map +1 -1
  13. package/build/consts.d.ts +6 -0
  14. package/build/consts.d.ts.map +1 -0
  15. package/build/consts.js +6 -0
  16. package/build/consts.js.map +1 -0
  17. package/build/consts.local.d.ts +12 -0
  18. package/build/consts.local.d.ts.map +1 -0
  19. package/build/consts.local.js +49 -0
  20. package/build/consts.local.js.map +1 -0
  21. package/build/index.d.ts +11 -4
  22. package/build/index.d.ts.map +1 -1
  23. package/build/index.js +5 -2
  24. package/build/index.js.map +1 -1
  25. package/build/naming/types.d.ts +14 -0
  26. package/build/naming/types.d.ts.map +1 -0
  27. package/build/naming/types.js +2 -0
  28. package/build/naming/types.js.map +1 -0
  29. package/build/naming.d.ts +4 -0
  30. package/build/naming.d.ts.map +1 -0
  31. package/build/naming.js +18 -0
  32. package/build/naming.js.map +1 -0
  33. package/build/run.d.ts +1 -1
  34. package/build/run.d.ts.map +1 -1
  35. package/build/run.js +23 -17
  36. package/build/run.js.map +1 -1
  37. package/build/scaffold.d.ts +8 -0
  38. package/build/scaffold.d.ts.map +1 -0
  39. package/build/scaffold.js +19 -0
  40. package/build/scaffold.js.map +1 -0
  41. package/build/template/types.d.ts +16 -0
  42. package/build/template/types.d.ts.map +1 -0
  43. package/build/template/types.js +2 -0
  44. package/build/template/types.js.map +1 -0
  45. package/build/template.d.ts +3 -16
  46. package/build/template.d.ts.map +1 -1
  47. package/build/template.js +69 -52
  48. package/build/template.js.map +1 -1
  49. package/build/types.d.ts +61 -0
  50. package/build/types.d.ts.map +1 -0
  51. package/build/types.js +2 -0
  52. package/build/types.js.map +1 -0
  53. package/package.json +8 -6
  54. package/template/AGENTS.md +61 -2
  55. package/template/README.bare.md +70 -0
  56. package/template/README.md +9 -60
  57. package/template/_agents/scripts/link-skills.sh +327 -15
  58. package/template/_agents/skills/agent-memory/SKILL.md +12 -11
  59. package/template/_agents/skills/getting-started/SKILL.md +38 -79
  60. package/template/_agents/skills/logging/SKILL.md +41 -0
  61. package/template/_agents/skills/memory-promotion/SKILL.md +10 -4
  62. package/template/_agents/skills/memory-recompact/SKILL.md +15 -14
  63. package/template/_agents/skills/owlmeans-code-structure/SKILL.md +188 -0
  64. package/template/_agents/skills/owlmeans-code-structure/parts/code-structure-reference.md +125 -0
  65. package/template/_agents/skills/reuse-code/SKILL.md +56 -10
  66. package/template/_agents/skills/self-education/SKILL.md +25 -5
  67. package/template/_agents/skills/skill-authoring/SKILL.md +132 -14
  68. package/template/_bare.json +24 -0
  69. package/template/_gitignore +8 -0
  70. package/template/bunfig.toml +2 -0
  71. package/template/package.json +3 -2
  72. package/template/sources/api/package.json +5 -4
  73. package/template/sources/api/src/app/session/add.ts +6 -17
  74. package/template/sources/api/src/app/session/list.ts +6 -12
  75. package/template/sources/api/src/app/session/remove.ts +6 -15
  76. package/template/sources/api/src/config.ts +7 -0
  77. package/template/sources/api/src/context.bare.ts +11 -0
  78. package/template/sources/api/src/context.ts +0 -4
  79. package/template/sources/api/src/entrypoints.bare.ts +4 -0
  80. package/template/sources/api/src/entrypoints.ts +13 -0
  81. package/template/sources/api/src/index.ts +8 -2
  82. package/template/sources/api/src/models/session/types.ts +14 -0
  83. package/template/sources/api/src/models/session.ts +43 -0
  84. package/template/sources/api/src/types.bare.ts +5 -0
  85. package/template/sources/common/package.json +8 -8
  86. package/template/sources/common/src/config.ts +2 -1
  87. package/template/sources/common/src/consts.bare.ts +8 -0
  88. package/template/sources/common/src/consts.ts +0 -14
  89. package/template/sources/common/src/entrypoints.bare.ts +14 -0
  90. package/template/sources/common/src/entrypoints.ts +60 -0
  91. package/template/sources/common/src/index.bare.ts +3 -0
  92. package/template/sources/common/src/index.ts +1 -1
  93. package/template/sources/common/src/schemas.ts +7 -7
  94. package/template/sources/web/THIRD-PARTY-NOTICES.txt +24 -0
  95. package/template/sources/web/index.html +3 -1
  96. package/template/sources/web/package.json +33 -32
  97. package/template/sources/web/src/components/ui/dropdown-menu.tsx +257 -0
  98. package/template/sources/web/src/config.ts +8 -0
  99. package/template/sources/web/src/consts.ts +15 -0
  100. package/template/sources/web/src/context.bare.ts +13 -0
  101. package/template/sources/web/src/context.ts +1 -7
  102. package/template/sources/web/src/entrypoints.bare.ts +13 -0
  103. package/template/sources/web/src/entrypoints.ts +16 -0
  104. package/template/sources/web/src/index.css +1 -1
  105. package/template/sources/web/src/index.tsx +2 -2
  106. package/template/sources/web/src/layout/main.tsx +7 -2
  107. package/template/sources/web/src/nav.bare.ts +24 -0
  108. package/template/sources/web/src/nav.ts +3 -8
  109. package/template/sources/web/src/screens/home.bare.tsx +17 -0
  110. package/template/sources/web/src/screens/session.tsx +14 -16
  111. package/template/sources/web/src/vite-env.d.ts +8 -0
  112. package/template/sources/web/vite.config.ts +4 -2
  113. package/template/CLAUDE.md +0 -13
  114. package/template/sources/api/src/modules.ts +0 -11
  115. package/template/sources/common/src/modules.ts +0 -26
  116. package/template/sources/web/src/modules.ts +0 -22
@@ -0,0 +1,70 @@
1
+ # __APP_NAME__
2
+
3
+ __APP_DESCRIPTION__
4
+
5
+ A fullstack [OwlMeans Common](https://github.com/owlmeans/common) app, scaffolded with
6
+ [`@owlmeans/create-app`](https://www.npmjs.com/package/@owlmeans/create-app) as a bare shell —
7
+ the wiring is complete and there is no example code to delete.
8
+
9
+ ## Workspaces
10
+
11
+ ```
12
+ sources/
13
+ ├── common/ # shared entrypoints (routes) and config — used by api AND web
14
+ ├── api/ # @owlmeans/server-app backend
15
+ └── web/ # @owlmeans/web-panel + shadcn UI: navigation, layout and screens
16
+ ```
17
+
18
+ ## Develop
19
+
20
+ ```sh
21
+ bun install
22
+ bun run dev
23
+ ```
24
+
25
+ - API: http://localhost:3000
26
+ - Web: http://localhost:3001
27
+
28
+ > **Note:** The API runs over plain HTTP in dev (`cfg.security = { unsecure: true }` in
29
+ > `sources/common/src/config.ts`). If you edit `sources/common`, restart `bun run dev` to
30
+ > rebuild it before the API and web pick up the changes.
31
+
32
+ ## Logging
33
+
34
+ Log through `@owlmeans/log` (`const log = logger('<scope>')`), never `console.*`. The level is set
35
+ per environment, not in code:
36
+
37
+ | Runtime | Variables | Default |
38
+ |---|---|---|
39
+ | api (`sources/api/src/config.ts`) | `LOG_LEVEL`, `LOG_DEBUG` | `info` |
40
+ | web (`sources/web/src/config.ts`, build time) | `VITE_LOG_LEVEL`, `VITE_LOG_DEBUG` | `debug` under `vite`, `info` in a build |
41
+
42
+ `*_DEBUG` lists scopes forced to debug (`*` for all). Local values go in `sources/api/.env` /
43
+ `sources/web/.env` (git-ignored).
44
+
45
+ ## Adding your first feature
46
+
47
+ 1. **`sources/common/src/entrypoints.ts`** — declare the route as an OwlMeans *entrypoint* and
48
+ give it an AJV filter. Both sides import this one declaration; neither re-declares the route.
49
+ 2. **`sources/api`** — register a resource in `src/context.ts` (widening `Context` in
50
+ `src/types.ts`), then `bind()` the entrypoint with a handler in `src/entrypoints.ts`.
51
+ 3. **`sources/web`** — add a screen under `src/screens`, hang it off a frontend route in
52
+ `src/entrypoints.ts`, list it in `src/nav.ts`, and call the backend with
53
+ `context.entrypoint(protocol).call({ params, body })`.
54
+
55
+ See the [OwlMeans getting-started guide](https://github.com/owlmeans/common/blob/main/docs/getting-started.md)
56
+ for a worked example of all three steps.
57
+
58
+ ## Agent guidance
59
+
60
+ This project ships agent context in `AGENTS.md`, with skills in
61
+ `.agents/skills/` and a shared memory store at `.agents/memory/`.
62
+
63
+ Files carrying an `AUTO-GENERATED` banner are managed by
64
+ [`@owlmeans/agent-skills`](https://www.npmjs.com/package/@owlmeans/agent-skills) — don't hand-edit
65
+ them; write your own guidance as separate, un-bannered files. Refresh after adding or upgrading
66
+ `@owlmeans/*` packages:
67
+
68
+ ```sh
69
+ npx @owlmeans/agent-skills@^0.1.18
70
+ ```
@@ -1,65 +1,14 @@
1
1
  # __APP_NAME__
2
2
 
3
- A minimal fullstack [OwlMeans Common](https://github.com/owlmeans/common) app, scaffolded with
4
- [`@owlmeans/create-app`](https://www.npmjs.com/package/@owlmeans/create-app).
3
+ __APP_DESCRIPTION__
5
4
 
6
- No authentication; session data lives in an **in-memory static resource** on the backend.
5
+ An OwlMeans application has shared protocol declarations in `sources/common`, server bindings in
6
+ `sources/api`, and browser bindings/screens in `sources/web`. Contracts, request schemas, guards
7
+ and gates are declared once in common; runtime packages bind them without changing the declaration.
7
8
 
8
- ## Workspaces
9
+ Use `bun run build` from the project root to build every workspace.
9
10
 
10
- ```
11
- sources/
12
- ├── common/ # shared entrypoints (routes), schemas and types — used by api AND web
13
- ├── api/ # @owlmeans/server-app backend; session items in @owlmeans/static-resource
14
- └── web/ # @owlmeans/web-panel + shadcn UI: navigation, layout and screens
15
- ```
16
-
17
- ## Develop
18
-
19
- ```sh
20
- bun install
21
- bun run dev
22
- ```
23
-
24
- - API: http://localhost:3000
25
- - Web: http://localhost:3001
26
-
27
- Open the **Session** page and add/remove items — they are stored per browser session
28
- (a `sid` kept in `localStorage`) in an in-memory resource on the API. Restarting the API
29
- clears them; opening a different browser/incognito window gets an isolated session.
30
-
31
- > **Note:** The API runs over plain HTTP in dev (`cfg.security = { unsecure: true }` in
32
- > `sources/common/src/config.ts`). If you edit `sources/common`, restart `bun run dev` to
33
- > rebuild it before the API and web pick up the changes.
34
-
35
- ## How it fits together
36
-
37
- 1. **`sources/common`** declares the API routes as OwlMeans *entrypoints* (`session.list`,
38
- `session.add`, `session.remove`) plus their AJV schemas and shared types.
39
- 2. **`sources/api`** registers a `@owlmeans/static-resource` in its context and `elevate()`s
40
- each entrypoint with a handler that does CRUD against it, keyed by the session id.
41
- 3. **`sources/web`** `elevate()`s the same entrypoints to screen components and calls them with
42
- `context.entrypoint(alias).call({ params, body })`.
43
-
44
- See [OwlMeans getting-started guide](https://github.com/owlmeans/common/blob/main/docs/getting-started.md)
45
- for a full walkthrough and the manual (non-scaffolded) version of this project.
46
-
47
- ## Agent guidance
48
-
49
- This project ships agent context in `CLAUDE.md` and `.github/copilot-instructions.md`. Both carry the
50
- same four mandatory sections a real OwlMeans monorepo uses — **Git Workflow**, **Reporting**,
51
- **Memory**, **Self-Education** — plus a project-purpose placeholder the agent fills in on its first
52
- session.
53
-
54
- Project memory is a single shared graph store at `.agents/memory/` (index `MEMORY.md`), used by both
55
- Claude Code and Copilot — never write memory anywhere else.
56
-
57
- Reusable guidance lives in `.claude/skills/` (Claude Code) and `.github/instructions/` (Copilot).
58
- Files carrying an `AUTO-GENERATED` banner are managed by
59
- [`@owlmeans/agent-skills`](https://www.npmjs.com/package/@owlmeans/agent-skills) — don't hand-edit
60
- them; write your own guidance as separate, un-bannered files. Refresh after adding or upgrading
61
- `@owlmeans/*` packages:
62
-
63
- ```sh
64
- npx @owlmeans/agent-skills
65
- ```
11
+ Logging goes through `@owlmeans/log`. The api's level comes from `LOG_LEVEL` (default `info`) and
12
+ `LOG_DEBUG` (scopes forced to debug, `*` for all); the web build's from `VITE_LOG_LEVEL` (default
13
+ `debug` under `vite`, `info` in a build) and `VITE_LOG_DEBUG`. Put local values in
14
+ `sources/api/.env` / `sources/web/.env` (git-ignored).
@@ -1,12 +1,16 @@
1
1
  #!/bin/sh
2
2
  # link-skills.sh
3
3
  #
4
- # Bridge the canonical skill store (.agents/skills/) into .claude/skills/ for
5
- # Claude Code, which discovers skills only under .claude/skills/. Copilot and
6
- # Codex read .agents/skills/ natively and need nothing from this script.
4
+ # Bridge every skill an agent should see into the two places agents look:
7
5
  #
8
- # Each skill becomes its own symlink — Claude Code supports per-skill symlink
9
- # entries, but not a symlinked skills directory.
6
+ # 1. LOCAL .agents/skills/<name> -> .claude/skills/<name>
7
+ # 2. LINKED <upstream>/.agents/skills/<n> -> .agents/linked-skills/<n>
8
+ # -> .claude/skills/<n>
9
+ #
10
+ # Copilot and Codex read .agents/skills/ and .agents/linked-skills/ natively;
11
+ # Claude Code discovers skills only under .claude/skills/, so both passes land
12
+ # there too. Each skill becomes its own symlink — Claude Code supports
13
+ # per-skill symlink entries, but not a symlinked skills directory.
10
14
  #
11
15
  # USAGE
12
16
  # sh .agents/scripts/link-skills.sh
@@ -14,28 +18,63 @@
14
18
  # BEHAVIOUR
15
19
  # - creates .claude/skills/<name> -> ../../.agents/skills/<name> for every
16
20
  # .agents/skills/<name>/SKILL.md
17
- # - prunes symlinks that dangle or no longer have a SKILL.md behind them
21
+ # - resolves the upstream repos this one depends on from the root
22
+ # package.json workspace entries of the form "libraries/<dep>/packages/...",
23
+ # recursing into each upstream's own package.json (visited list, depth cap
24
+ # of 4). A repo that declares no libraries/* workspace entries falls back
25
+ # to the layout a standalone npm consumer gets: the generated
26
+ # agent-meta/skills/<name> copies that ship inside the installed @owlmeans
27
+ # packages. That fallback scans node_modules/@owlmeans at the project root
28
+ # AND in every directory up to two levels below it (sources/api/node_modules
29
+ # and the like), because a workspace package keeps its own node_modules
30
+ # whenever the linker does not hoist its dependencies to the root. It never
31
+ # descends into a node_modules tree, skips any directory whose real path
32
+ # leaves the root (a projects/<repo> or libraries/<repo> symlink is another
33
+ # checkout, not this project), and records each physical package once, keyed
34
+ # by resolved path, so a package reachable through several workspaces
35
+ # contributes its skills a single time.
36
+ # - links every upstream skill that no local skill shadows into BOTH
37
+ # .agents/linked-skills/<name> and .claude/skills/<name>, pointing at the
38
+ # upstream's real directory using a relative target (never another symlink).
39
+ # Relative targets survive a hostPath checkout mounted under another root.
40
+ # A local skill
41
+ # always wins; a nearer dependency wins over a farther one.
42
+ # - writes .agents/linked-skills/INDEX.md — skill / origin repo / description
43
+ # in deterministic order — and rewrites it only when its content changes
44
+ # - prunes symlinks in both directories that dangle or no longer have a
45
+ # SKILL.md behind them, and removes .agents/linked-skills/ entirely when
46
+ # the repo has no upstream skills
18
47
  # - never touches real directories or files (warns instead), never removes
19
48
  # .gitkeep, and always exits 0 so it can run as a SessionStart hook
20
49
  #
21
50
  # The committed .claude/settings.json runs this at SessionStart. Run it by
22
51
  # hand after creating, renaming, or deleting a skill mid-session.
52
+ #
53
+ # .agents/linked-skills/ is generated and git-ignored: the upstream repos are
54
+ # themselves symlinks, so the links only make sense inside a linked checkout.
23
55
 
24
56
  set -u
25
57
 
26
- SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
27
- ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
58
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd -P)
59
+ ROOT=$(cd "$SCRIPT_DIR/../.." && pwd -P)
28
60
  SRC="$ROOT/.agents/skills"
29
61
  DST="$ROOT/.claude/skills"
62
+ LNK="$ROOT/.agents/linked-skills"
63
+ MAX_DEPTH=4
64
+
65
+ NL='
66
+ '
67
+ TAB=$(printf '\t')
30
68
 
31
69
  linked=0
70
+ dep_linked=0
32
71
  pruned=0
33
72
  skipped=0
34
73
 
35
74
  mkdir -p "$DST" 2>/dev/null || true
36
75
  [ -f "$DST/.gitkeep" ] || : > "$DST/.gitkeep" 2>/dev/null || true
37
76
 
38
- # ---- link pass -------------------------------------------------------------
77
+ # ---- local link pass -------------------------------------------------------
39
78
  if [ -d "$SRC" ]; then
40
79
  for skill_dir in "$SRC"/*/; do
41
80
  [ -d "$skill_dir" ] || continue
@@ -59,6 +98,255 @@ if [ -d "$SRC" ]; then
59
98
  done
60
99
  fi
61
100
 
101
+ # ---- dependency resolution -------------------------------------------------
102
+ # An OwlMeans monorepo declares a linked upstream by listing that upstream's
103
+ # packages explicitly as "libraries/<dep>/packages/<pkg>" workspace entries.
104
+ # The dep name is therefore the second path segment of any such entry.
105
+ deps_of() {
106
+ pkg="$1/package.json"
107
+ [ -f "$pkg" ] || return 0
108
+ grep -o '"libraries/[A-Za-z0-9._-][A-Za-z0-9._-]*/packages' "$pkg" 2>/dev/null \
109
+ | sed 's|^"libraries/||; s|/packages$||' \
110
+ | awk '!seen[$0]++'
111
+ }
112
+
113
+ # Resolve libraries/<dep> to the real directory it points at, so every link we
114
+ # create later targets a real path rather than a chain of symlinks.
115
+ resolve_repo() {
116
+ p="$1/libraries/$2"
117
+ [ -d "$p" ] || return 0
118
+ (cd "$p" 2>/dev/null && pwd -P)
119
+ }
120
+
121
+ # Breadth-first walk: the frontier holds one depth level, so the first repo to
122
+ # claim a skill name is always the nearest dependency.
123
+ visited="$NL$ROOT$NL"
124
+ order=""
125
+ frontier=""
126
+ while IFS= read -r dep; do
127
+ [ -n "$dep" ] || continue
128
+ rp=$(resolve_repo "$ROOT" "$dep")
129
+ [ -n "$rp" ] && frontier="$frontier$rp$NL"
130
+ done <<EOF
131
+ $(deps_of "$ROOT")
132
+ EOF
133
+
134
+ depth=1
135
+ while [ "$depth" -le "$MAX_DEPTH" ] && [ -n "$frontier" ]; do
136
+ next=""
137
+ while IFS= read -r rp; do
138
+ [ -n "$rp" ] || continue
139
+ case "$visited" in
140
+ *"$NL$rp$NL"*) continue ;;
141
+ esac
142
+ visited="$visited$rp$NL"
143
+ order="$order$rp$NL"
144
+ while IFS= read -r dep; do
145
+ [ -n "$dep" ] || continue
146
+ crp=$(resolve_repo "$rp" "$dep")
147
+ [ -n "$crp" ] && next="$next$crp$NL"
148
+ done <<EOF
149
+ $(deps_of "$rp")
150
+ EOF
151
+ done <<EOF
152
+ $frontier
153
+ EOF
154
+ frontier="$next"
155
+ depth=$((depth + 1))
156
+ done
157
+
158
+ # ---- candidate collection --------------------------------------------------
159
+ TMPD=$(mktemp -d 2>/dev/null) || TMPD="${TMPDIR:-/tmp}/link-skills.$$"
160
+ mkdir -p "$TMPD" 2>/dev/null || true
161
+ CAND="$TMPD/candidates"
162
+ : > "$CAND" 2>/dev/null || CAND=""
163
+
164
+ seen_names="$NL"
165
+ chain=""
166
+
167
+ record_candidate() {
168
+ cname="$1"
169
+ corigin="$2"
170
+ cpath="$3"
171
+ [ -f "$cpath/SKILL.md" ] || return 0
172
+ # A local skill always shadows an upstream one of the same name.
173
+ [ -f "$SRC/$cname/SKILL.md" ] && return 0
174
+ case "$seen_names" in
175
+ *"$NL$cname$NL"*) return 0 ;;
176
+ esac
177
+ seen_names="$seen_names$cname$NL"
178
+ printf '%s\t%s\t%s\n' "$cname" "$corigin" "$cpath" >> "$CAND"
179
+ }
180
+
181
+ if [ -n "$CAND" ] && [ -n "$order" ]; then
182
+ while IFS= read -r rp; do
183
+ [ -n "$rp" ] || continue
184
+ origin=$(basename "$rp")
185
+ chain="$chain$origin -> "
186
+ for skill_dir in "$rp"/.agents/skills/*/; do
187
+ [ -d "$skill_dir" ] || continue
188
+ skill_dir="${skill_dir%/}"
189
+ record_candidate "$(basename "$skill_dir")" "$origin" "$skill_dir"
190
+ done
191
+ done <<EOF
192
+ $order
193
+ EOF
194
+ chain="${chain% -> }"
195
+ elif [ -n "$CAND" ]; then
196
+ # Standalone consumer: no libraries/* upstreams, so the skills ship inside
197
+ # the installed packages as generated agent-meta/ copies. Those packages
198
+ # are not necessarily at the project root: a workspace whose @owlmeans deps
199
+ # belong to its sources/* packages keeps them in each package's own
200
+ # node_modules unless the linker hoists. So collect the @owlmeans scope
201
+ # directory of the root and of every directory up to two levels below it.
202
+ # A directory is scanned only when it is part of THIS project: node_modules
203
+ # trees are never descended into, and a directory whose real path leaves the
204
+ # root — a projects/<repo> or libraries/<repo> symlink into a neighbouring
205
+ # checkout — belongs to that other repository, not to this one.
206
+ in_tree() {
207
+ rp=$(cd "$1" 2>/dev/null && pwd -P) || return 1
208
+ case "$rp/" in
209
+ "$ROOT"/*) return 0 ;;
210
+ esac
211
+ return 1
212
+ }
213
+
214
+ scope_dirs="$ROOT/node_modules/@owlmeans$NL"
215
+ for lvl1 in "$ROOT"/*/; do
216
+ [ -d "$lvl1" ] || continue
217
+ lvl1="${lvl1%/}"
218
+ [ "$(basename "$lvl1")" = "node_modules" ] && continue
219
+ in_tree "$lvl1" || continue
220
+ scope_dirs="$scope_dirs$lvl1/node_modules/@owlmeans$NL"
221
+ for lvl2 in "$lvl1"/*/; do
222
+ [ -d "$lvl2" ] || continue
223
+ lvl2="${lvl2%/}"
224
+ [ "$(basename "$lvl2")" = "node_modules" ] && continue
225
+ in_tree "$lvl2" || continue
226
+ scope_dirs="$scope_dirs$lvl2/node_modules/@owlmeans$NL"
227
+ done
228
+ done
229
+
230
+ # The same physical package is reachable through several workspaces once the
231
+ # linker hoists or symlinks it, so key the dedup on the resolved path.
232
+ seen_pkgs="$NL"
233
+ while IFS= read -r scope_dir; do
234
+ [ -n "$scope_dir" ] || continue
235
+ [ -d "$scope_dir" ] || continue
236
+ for pkg_dir in "$scope_dir"/*/; do
237
+ [ -d "$pkg_dir" ] || continue
238
+ pkg_dir="${pkg_dir%/}"
239
+ real=$(cd "$pkg_dir" 2>/dev/null && pwd -P) || continue
240
+ [ -n "$real" ] || continue
241
+ case "$seen_pkgs" in
242
+ *"$NL$real$NL"*) continue ;;
243
+ esac
244
+ seen_pkgs="$seen_pkgs$real$NL"
245
+ origin="@owlmeans/$(basename "$pkg_dir")"
246
+ for skill_dir in "$real"/agent-meta/skills/*/; do
247
+ [ -d "$skill_dir" ] || continue
248
+ skill_dir="${skill_dir%/}"
249
+ record_candidate "$(basename "$skill_dir")" "$origin" "$skill_dir"
250
+ done
251
+ done
252
+ done <<EOF
253
+ $scope_dirs
254
+ EOF
255
+ [ -s "$CAND" ] && chain="node_modules/@owlmeans/*/agent-meta/skills"
256
+ fi
257
+
258
+ cand_has() {
259
+ [ -n "$CAND" ] && [ -f "$CAND" ] || return 1
260
+ awk -F"$TAB" -v n="$1" '$1 == n { found = 1 } END { exit found ? 0 : 1 }' "$CAND"
261
+ }
262
+
263
+ # Both directories are resolved absolute paths. Store their relative relationship,
264
+ # so a prepare hook inside /var/sources does not break the host's skill links.
265
+ relative_target() {
266
+ awk -v base="$1" -v dest="$2" 'BEGIN {
267
+ nb = split(base, b, "/")
268
+ nd = split(dest, d, "/")
269
+ i = 1
270
+ while (i <= nb && i <= nd && b[i] == d[i]) i++
271
+ result = ""
272
+ for (j = i; j <= nb; j++) result = result "../"
273
+ for (j = i; j <= nd; j++) result = result d[j] (j < nd ? "/" : "")
274
+ print result == "" ? "." : result
275
+ }'
276
+ }
277
+
278
+ # ---- dependency link pass --------------------------------------------------
279
+ if [ -n "$CAND" ] && [ -s "$CAND" ]; then
280
+ mkdir -p "$LNK" 2>/dev/null || true
281
+ while IFS="$TAB" read -r name origin path; do
282
+ [ -n "$name" ] || continue
283
+ ok=1
284
+ for target in "$LNK/$name" "$DST/$name"; do
285
+ if [ -e "$target" ] && [ ! -L "$target" ]; then
286
+ echo "link-skills: skip '$name' — a real file/directory shadows it in $(dirname "$target")" >&2
287
+ ok=0
288
+ continue
289
+ fi
290
+ ln -sfn "$(relative_target "$(dirname "$target")" "$path")" "$target" 2>/dev/null || {
291
+ echo "link-skills: failed to link '$name' from $origin" >&2
292
+ ok=0
293
+ }
294
+ done
295
+ if [ "$ok" -eq 1 ]; then
296
+ dep_linked=$((dep_linked + 1))
297
+ else
298
+ skipped=$((skipped + 1))
299
+ fi
300
+ done < "$CAND"
301
+ fi
302
+
303
+ # ---- index -----------------------------------------------------------------
304
+ # Read the description straight out of the SKILL.md frontmatter; fall back to
305
+ # the first heading. Pipes would break the table, so they are folded away.
306
+ describe() {
307
+ f="$1/SKILL.md"
308
+ [ -f "$f" ] || return 0
309
+ d=$(sed -n '/^---[[:space:]]*$/,/^---[[:space:]]*$/p' "$f" \
310
+ | sed -n 's/^description:[[:space:]]*//p' | head -1)
311
+ [ -n "$d" ] || d=$(sed -n 's/^#[[:space:]][[:space:]]*//p' "$f" | head -1)
312
+ d=$(printf '%s' "$d" | sed "s/^[\"']//; s/[\"']\$//; s/|/\//g")
313
+ if [ "${#d}" -gt 160 ]; then
314
+ d=$(printf '%s' "$d" | cut -c1-157)...
315
+ fi
316
+ printf '%s' "$d"
317
+ }
318
+
319
+ if [ -n "$CAND" ] && [ -s "$CAND" ]; then
320
+ {
321
+ echo "# Linked Skills"
322
+ echo ""
323
+ echo "Generated by \`.agents/scripts/link-skills.sh\` — never edit by hand, and never"
324
+ echo "commit: the entries are symlinks into checkouts that only exist locally."
325
+ echo ""
326
+ echo "These skills belong to the repositories this one depends on. They are readable at"
327
+ echo "\`.agents/linked-skills/<name>/SKILL.md\` and are loaded by Claude Code through"
328
+ echo "\`.claude/skills/\` like any local skill. A local skill of the same name always wins,"
329
+ echo "and a nearer dependency wins over a farther one."
330
+ echo ""
331
+ echo "Dependency order: $chain"
332
+ echo ""
333
+ echo "| skill | origin repo | description |"
334
+ echo "|---|---|---|"
335
+ LC_ALL=C sort -t"$TAB" -k1,1 "$CAND" | while IFS="$TAB" read -r name origin path; do
336
+ [ -n "$name" ] || continue
337
+ printf '| %s | %s | %s |\n' "$name" "$origin" "$(describe "$path")"
338
+ done
339
+ } > "$TMPD/INDEX.md" 2>/dev/null
340
+
341
+ if [ -f "$TMPD/INDEX.md" ]; then
342
+ if cmp -s "$TMPD/INDEX.md" "$LNK/INDEX.md" 2>/dev/null; then
343
+ :
344
+ else
345
+ cp "$TMPD/INDEX.md" "$LNK/INDEX.md" 2>/dev/null || true
346
+ fi
347
+ fi
348
+ fi
349
+
62
350
  # ---- prune pass ------------------------------------------------------------
63
351
  if [ -d "$DST" ]; then
64
352
  for entry in "$DST"/* "$DST"/.[!.]*; do
@@ -67,16 +355,40 @@ if [ -d "$DST" ]; then
67
355
  [ "$name" = ".gitkeep" ] && continue
68
356
  [ -L "$entry" ] || continue
69
357
 
70
- if [ ! -e "$entry" ] || [ ! -f "$SRC/$name/SKILL.md" ]; then
71
- rm -f "$entry" 2>/dev/null && pruned=$((pruned + 1))
358
+ if [ -f "$SRC/$name/SKILL.md" ]; then
359
+ continue
360
+ fi
361
+ if cand_has "$name" && [ -f "$entry/SKILL.md" ]; then
362
+ continue
72
363
  fi
364
+ rm -f "$entry" 2>/dev/null && pruned=$((pruned + 1))
73
365
  done
74
366
  fi
75
367
 
76
- if [ "$skipped" -gt 0 ]; then
77
- echo "link-skills: $linked linked, $pruned pruned, $skipped skipped"
78
- else
79
- echo "link-skills: $linked linked, $pruned pruned"
368
+ if [ -d "$LNK" ]; then
369
+ for entry in "$LNK"/*; do
370
+ [ -e "$entry" ] || [ -L "$entry" ] || continue
371
+ name=$(basename "$entry")
372
+ [ "$name" = "INDEX.md" ] && continue
373
+ [ -L "$entry" ] || continue
374
+
375
+ if cand_has "$name" && [ -f "$entry/SKILL.md" ]; then
376
+ continue
377
+ fi
378
+ rm -f "$entry" 2>/dev/null && pruned=$((pruned + 1))
379
+ done
380
+ # Nothing linked at all — leave no empty generated directory behind.
381
+ if [ -z "${CAND:-}" ] || [ ! -s "$CAND" ]; then
382
+ rm -f "$LNK/INDEX.md" 2>/dev/null || true
383
+ rmdir "$LNK" 2>/dev/null || true
384
+ fi
80
385
  fi
81
386
 
387
+ rm -rf "$TMPD" 2>/dev/null || true
388
+
389
+ summary="link-skills: $linked linked, $pruned pruned"
390
+ [ "$dep_linked" -gt 0 ] && summary="$summary, $dep_linked from dependencies"
391
+ [ "$skipped" -gt 0 ] && summary="$summary, $skipped skipped"
392
+ echo "$summary"
393
+
82
394
  exit 0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agent-memory
3
- description: Shared agent memory protocol — the .agents/memory/ graph store every agent reads and writes: MEMORY.md index, subsystem nodes, compact-on-write merging, size caps, and the read protocol. Use when reading or writing project memory, when asked to remember something, or to decide where knowledge belongs.
3
+ description: "Shared agent memory protocol — the .agents/memory/ graph store every agent reads and writes: MEMORY.md index, subsystem nodes, compact-on-write merging, size caps, and the read protocol. Use when reading or writing project memory, when asked to remember something, or to decide where knowledge belongs."
4
4
  user-invocable: true
5
5
  metadata:
6
6
  scope: general
@@ -14,8 +14,8 @@ the `MEMORY.md` index. Compactness is the core value — every write
14
14
  merges and compacts; nothing is ever appended as a log.
15
15
 
16
16
  Never write memory to `.claude/memory/`, `.github/memory/`, `~/.claude/`, `~/.copilot/`, or
17
- anywhere outside the repository. Legacy `.claude/memory/` / `.github/memory/` stores are
18
- retired — if one exists, flag it for `memory-recompact` migration instead of writing there.
17
+ anywhere outside the repository. Records found in any such place are folded into
18
+ `.agents/memory/` by `memory-recompact`, never added to.
19
19
 
20
20
  ## Store layout
21
21
 
@@ -105,8 +105,8 @@ Every write is a merge, never an append:
105
105
  4. **Compact** — reread the node; collapse redundancy; delete anything the code or git history
106
106
  now states; prune resolved `Status` lines.
107
107
  5. **Cap** — over soft cap: compact harder. Over hard cap: split by sub-scope into a linked child
108
- node, or promote procedure-shaped overflow (`memory-promotion` where present, else
109
- `skill-authoring`).
108
+ node, or promote procedure-shaped overflow (`memory-promotion` where present, else the
109
+ project's `skill-authoring` or `create-skill` skill).
110
110
  6. **Index** — update the hook line if the node's center of gravity moved; bump `updated:`.
111
111
 
112
112
  ## Knowledge, not events
@@ -116,10 +116,10 @@ correctly, stated without reference to this session?* Keep invariants, cause→e
116
116
  counter-moves, recognition fingerprints (symptoms). Drop dates, phase numbers, who did what,
117
117
  attempt sequences, and anything recoverable from code or git.
118
118
 
119
- Before (event log): "Verification 2026-06-11 … Bug found: script missing embedded-count logic.
120
- Fix deployed to all copies; second run idempotent."
121
- After (knowledge): "`nested-agent-context.sh` exists as byte-identical copies in the archive and
122
- each repo — fan every fix out to all copies; verify with `diff`. Correct re-runs are no-ops."
119
+ Before (event log): "2026-06-11 — CI red on the second run; the migration step was not idempotent,
120
+ patched it, re-ran green."
121
+ After (knowledge): "The migration step is idempotent — run against an already-migrated database
122
+ it writes nothing; a second run that still reports work means the first did not converge."
123
123
 
124
124
  ## Size caps (hard)
125
125
 
@@ -135,8 +135,9 @@ each repo — fan every fix out to all copies; verify with `diff`. Correct re-ru
135
135
  ## Memory vs skill
136
136
 
137
137
  Fact-shaped ("what is true") stays here. Procedure-shaped ("to do X, do Y") becomes a skill —
138
- follow `memory-promotion` where present, otherwise `skill-authoring`. Repeated use of a node to
139
- *perform* tasks, and over-cap nodes full of steps, are promotion triggers.
138
+ follow `memory-promotion` where present, otherwise the project's `skill-authoring` or
139
+ `create-skill` skill. Repeated use of a node to *perform* tasks, and over-cap nodes full of
140
+ steps, are promotion triggers.
140
141
 
141
142
  Promotion is a **rewrite, never a move**: never paste node text into a skill. "Record the rule,
142
143
  not the story" and the size caps above bind skill bodies at least as tightly as they bind nodes