owfadev 0.3.1 → 0.4.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 (117) hide show
  1. package/.agents/skills/owfa-check/SKILL.md +49 -0
  2. package/.agents/skills/{docs → owfa-docs}/SKILL.md +5 -5
  3. package/.agents/skills/{wiki_plugin_kernel → owfa-kernel}/SKILL.md +13 -12
  4. package/{.meta/command/plugin → .agents/skills/owfa-plugin}/SKILL.md +10 -10
  5. package/.agents/skills/{query → owfa-query}/SKILL.md +5 -6
  6. package/.owfa/VERSION +1 -0
  7. package/.owfa/command/owfa-check/SKILL.md +49 -0
  8. package/{.meta/command/docs → .owfa/command/owfa-docs}/SKILL.md +5 -5
  9. package/{.meta/command/wiki_plugin_kernel → .owfa/command/owfa-kernel}/SKILL.md +13 -12
  10. package/{.agents/skills/plugin → .owfa/command/owfa-plugin}/SKILL.md +10 -10
  11. package/{.meta/command/query → .owfa/command/owfa-query}/SKILL.md +5 -6
  12. package/{.meta → .owfa}/docs/agent-behavior.md +3 -2
  13. package/{.meta → .owfa}/docs/agents-example.md +1 -1
  14. package/{.meta → .owfa}/docs/conversion.md +2 -2
  15. package/{.meta → .owfa}/docs/deployment.md +51 -46
  16. package/{.meta → .owfa}/docs/developer.md +4 -4
  17. package/{.meta → .owfa}/docs/distribution.md +21 -19
  18. package/.owfa/docs/home.md +61 -0
  19. package/{.meta → .owfa}/docs/intro.md +5 -5
  20. package/{.meta → .owfa}/docs/log-example.md +1 -1
  21. package/{.meta → .owfa}/docs/map.md +4 -3
  22. package/{.meta → .owfa}/docs/mechanics.md +11 -11
  23. package/{.meta → .owfa}/docs/naming.md +7 -7
  24. package/{.meta → .owfa}/docs/packages.md +80 -15
  25. package/.owfa/docs/plugin-lifecycle.md +70 -0
  26. package/{.meta → .owfa}/docs/quickstart.md +12 -12
  27. package/{.meta → .owfa}/docs/research-user-profile.md +1 -1
  28. package/{.meta → .owfa}/docs/sandbox.md +3 -3
  29. package/{.meta → .owfa}/docs/usage.md +1 -1
  30. package/{.meta → .owfa}/protocol/actions.md +6 -6
  31. package/.owfa/protocol/registry.yaml +34 -0
  32. package/.owfa/scripts/owfa_home.py +273 -0
  33. package/{.meta/scripts/wiki_plugin_kernel.py → .owfa/scripts/owfa_kernel.py} +521 -77
  34. package/{.meta → .owfa}/scripts/pipeline.py +9 -9
  35. package/.owfa/scripts/wiki_plugin_kernel.py +15 -0
  36. package/{.meta → .owfa}/scripts/wikilib.py +2 -2
  37. package/.owfa/tests/test_capabilities.py +311 -0
  38. package/{.meta → .owfa}/tests/test_install_preflight.py +11 -7
  39. package/.owfa/tests/test_owfa_home.py +127 -0
  40. package/.owfa/tests/test_version_scheme.py +63 -0
  41. package/README.md +32 -24
  42. package/README.zh-CN.md +32 -24
  43. package/bin/owfa.js +525 -65
  44. package/package.json +3 -2
  45. package/payloads/personal/PKG.yaml +15 -0
  46. package/payloads/personal/README.md +7 -0
  47. package/{.meta/plugins/core → payloads/personal}/calendar/PLUGIN.md +1 -0
  48. package/{.meta/plugins/core → payloads/personal}/calendar/PLUGIN.yaml +3 -3
  49. package/{.agents/skills/map → payloads/personal/command/owfa-map}/SKILL.md +9 -9
  50. package/{.meta/command/profile → payloads/personal/command/owfa-profile}/SKILL.md +11 -11
  51. package/{.agents/skills/save → payloads/personal/command/owfa-save}/SKILL.md +10 -10
  52. package/{.meta/plugins/core → payloads/personal}/cron/PLUGIN.md +2 -0
  53. package/{.meta/plugins/core → payloads/personal}/cron/PLUGIN.yaml +3 -3
  54. package/{.meta/plugins/core → payloads/personal}/device/PLUGIN.md +1 -0
  55. package/{.meta/plugins/core → payloads/personal}/device/PLUGIN.yaml +3 -3
  56. package/{.meta/plugins/core → payloads/personal}/domain/PLUGIN.md +2 -0
  57. package/payloads/personal/domain/PLUGIN.yaml +8 -0
  58. package/{.meta/plugins/core → payloads/personal}/hot/PLUGIN.yaml +1 -1
  59. package/{.meta/plugins/core → payloads/personal}/index/PLUGIN.md +1 -0
  60. package/{.meta/plugins/core → payloads/personal}/index/PLUGIN.yaml +4 -4
  61. package/{.meta/plugins/core → payloads/personal}/instance-git/PLUGIN.md +5 -0
  62. package/{.meta/plugins/core → payloads/personal}/instance-git/PLUGIN.yaml +3 -3
  63. package/{.meta/plugins/core → payloads/personal}/language/PLUGIN.md +1 -0
  64. package/{.meta/plugins/core → payloads/personal}/language/PLUGIN.yaml +4 -4
  65. package/{.meta/plugins/core → payloads/personal}/link/PLUGIN.md +1 -0
  66. package/{.meta/plugins/core → payloads/personal}/link/PLUGIN.yaml +3 -3
  67. package/{.meta/plugins/core → payloads/personal}/link/scripts/check.py +1 -1
  68. package/{.meta/plugins/core → payloads/personal}/log/PLUGIN.md +1 -0
  69. package/{.meta/plugins/core → payloads/personal}/log/PLUGIN.yaml +4 -4
  70. package/{.meta/plugins/core → payloads/personal}/mapping/PLUGIN.yaml +1 -1
  71. package/{.meta/plugins/core → payloads/personal}/notes/PLUGIN.md +1 -0
  72. package/{.meta/plugins/core → payloads/personal}/notes/PLUGIN.yaml +4 -4
  73. package/{.meta/plugins/core → payloads/personal}/sessions/PLUGIN.md +2 -1
  74. package/{.meta/plugins/core → payloads/personal}/sessions/PLUGIN.yaml +5 -5
  75. package/{.meta/plugins/core → payloads/personal}/structure/PLUGIN.md +2 -0
  76. package/{.meta/plugins/core → payloads/personal}/structure/PLUGIN.yaml +3 -3
  77. package/{.meta/plugins/core → payloads/personal}/structure/scripts/check.py +1 -1
  78. package/{.meta/plugins/core → payloads/personal}/tag/PLUGIN.md +2 -0
  79. package/{.meta/plugins/core → payloads/personal}/tag/PLUGIN.yaml +3 -3
  80. package/{.meta/plugins/core → payloads/personal}/tag/scripts/check.py +3 -1
  81. package/{.meta/plugins/core → payloads/personal}/tmp/PLUGIN.md +1 -0
  82. package/{.meta/plugins/core → payloads/personal}/tmp/PLUGIN.yaml +3 -3
  83. package/{.meta/plugins/core → payloads/personal}/todo/PLUGIN.md +2 -0
  84. package/{.meta/plugins/core → payloads/personal}/todo/PLUGIN.yaml +6 -5
  85. package/payloads/personal/todo/scripts/check.py +51 -0
  86. package/{.meta/plugins/core → payloads/personal}/trust/PLUGIN.md +1 -0
  87. package/{.meta/plugins/core → payloads/personal}/trust/PLUGIN.yaml +3 -3
  88. package/{.meta/plugins/core → payloads/personal}/user-profile/PLUGIN.md +2 -0
  89. package/{.meta/plugins/core → payloads/personal}/user-profile/PLUGIN.yaml +7 -6
  90. package/payloads/personal/user-profile/scripts/check.py +20 -0
  91. package/{.meta/plugins/core → payloads/personal}/vault/PLUGIN.md +1 -0
  92. package/payloads/personal/vault/PLUGIN.yaml +13 -0
  93. package/{.meta/plugins/core → payloads/personal}/wiki/PLUGIN.md +1 -0
  94. package/payloads/personal/wiki/PLUGIN.yaml +8 -0
  95. package/templates/AGENTS.template.md +2 -2
  96. package/.agents/skills/check/SKILL.md +0 -130
  97. package/.agents/skills/profile/SKILL.md +0 -75
  98. package/.meta/VERSION +0 -1
  99. package/.meta/command/check/SKILL.md +0 -130
  100. package/.meta/command/map/SKILL.md +0 -72
  101. package/.meta/command/save/SKILL.md +0 -94
  102. package/.meta/plugins/core/domain/PLUGIN.yaml +0 -8
  103. package/.meta/plugins/core/vault/PLUGIN.yaml +0 -13
  104. package/.meta/plugins/core/wiki/PLUGIN.yaml +0 -8
  105. package/.meta/protocol/registry.yaml +0 -75
  106. /package/{.meta → .owfa}/command/asset-read/SKILL.md +0 -0
  107. /package/{.meta → .owfa}/docs/research-landscape.md +0 -0
  108. /package/{.meta → .owfa}/protocol/experiments.md +0 -0
  109. /package/{.meta/plugins/core → payloads/personal}/device/scripts/check.py +0 -0
  110. /package/{.meta/plugins/core → payloads/personal}/hot/PLUGIN.md +0 -0
  111. /package/{.meta/plugins/core → payloads/personal}/instance-git/scripts/check.py +0 -0
  112. /package/{.meta/plugins/core → payloads/personal}/language/scripts/check.py +0 -0
  113. /package/{.meta/plugins/core → payloads/personal}/log/scripts/check.py +0 -0
  114. /package/{.meta/plugins/core → payloads/personal}/mapping/PLUGIN.md +0 -0
  115. /package/{.meta/plugins/core → payloads/personal}/mapping/scripts/check.py +0 -0
  116. /package/{.meta/plugins/core → payloads/personal}/sessions/scripts/check.py +0 -0
  117. /package/{.meta/plugins/core → payloads/personal}/trust/scripts/check.py +0 -0
package/bin/owfa.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * things the framework itself cannot own: fetching a complete framework tree,
7
7
  * initializing a directory without hand-copying it, and replacing that tree
8
8
  * on upgrade. Everything semantic stays in the Python kernel
9
- * (.meta/scripts/wiki_plugin_kernel.py) — this file only orchestrates.
9
+ * (.owfa/scripts/owfa_kernel.py) — this file only orchestrates.
10
10
  *
11
11
  * Design rules:
12
12
  * - zero runtime dependencies; Node is needed to install, not to use
@@ -28,13 +28,24 @@ const PKG = JSON.parse(fs.readFileSync(path.join(CLI_ROOT, "package.json"), "utf
28
28
  const FRAMEWORK_VERSION = readVersion(CLI_ROOT) || "?";
29
29
  const INJECT_START = "<!-- wiki-inject:start -->";
30
30
  const INJECT_END = "<!-- wiki-inject:end -->";
31
- const PAYLOAD = [".meta", ".agents"];
32
- const INSTANCE_DIRS = ["vault", path.join("wiki", "notes"), path.join("wiki", "sessions"), path.join("wiki", "vault")];
31
+ const PAYLOAD = [".owfa", ".agents"];
32
+ // v0.4 de-parasitization (final shape): the channel no longer hardcodes instance
33
+ // skeleton dirs and no longer ships .owfa/plugins/core/ — the 21 core plugins live
34
+ // in the owfa-personal suite repo, embedded at payloads/personal/, and that blessed
35
+ // copy is what a fresh init installs by default (offline, --path channel). Packages
36
+ // declare their own skeleton (`skeleton:` in PKG.yaml; the kernel mkdirs them on
37
+ // install), so the default two-stage init lands both framework and skeleton.
38
+ const BLESSED_SUITE = path.join(CLI_ROOT, "payloads", "personal");
39
+ // 0.3 instances carry the framework in .meta/; upgrade migrates them to .owfa/
40
+ // and leaves a .meta shim (symlink/junction) so old absolute paths keep working.
41
+ const LEGACY_META = ".meta";
42
+ const MIGRATE_SCRATCH = ".owfa-migrate"; // transient replace/ parking (was .owfa — the name now belongs to the framework tree)
43
+ const BACKUP_DIR = ".owfa-backup"; // durable opt-in upgrade snapshots (was .owfa/backup)
33
44
  // Kernel state that belongs to the instance, not to the framework tree.
34
45
  const PRESERVED = [
35
- path.join(".meta", "migrations", "pending.json"),
36
- path.join(".meta", "kernel-journal.log"),
37
- path.join(".meta", "registry-cache.yaml"),
46
+ path.join(".owfa", "migrations", "pending.json"),
47
+ path.join(".owfa", "kernel-journal.log"),
48
+ path.join(".owfa", "registry-cache.yaml"),
38
49
  ];
39
50
 
40
51
  // ---------------------------------------------------------------------------
@@ -42,11 +53,14 @@ const PRESERVED = [
42
53
  // ---------------------------------------------------------------------------
43
54
 
44
55
  function readVersion(root) {
45
- try {
46
- return fs.readFileSync(path.join(root, ".meta", "VERSION"), "utf8").trim();
47
- } catch {
48
- return null;
56
+ // ".meta" is the frozen 0.3 layout — a historical constant, safe to inline
57
+ // here (LEGACY_META is declared later and readVersion runs at module init).
58
+ for (const dir of [".owfa", ".meta"]) {
59
+ try {
60
+ return fs.readFileSync(path.join(root, dir, "VERSION"), "utf8").trim();
61
+ } catch {}
49
62
  }
63
+ return null;
50
64
  }
51
65
 
52
66
  function out(line = "") {
@@ -62,7 +76,7 @@ function rel(root, p) {
62
76
  return path.relative(root, p) || ".";
63
77
  }
64
78
 
65
- const FLAG_VALUE = ["name", "packages", "without", "from", "path"];
79
+ const FLAG_VALUE = ["name", "packages", "without", "from", "path", "core"];
66
80
 
67
81
  function parseArgs(argv) {
68
82
  const args = { _: [], flags: new Set(), used: new Set() };
@@ -92,6 +106,35 @@ function parseArgs(argv) {
92
106
  return args;
93
107
  }
94
108
 
109
+ // 0.3 quirk (review P1 round 3): the 0.3 channel wrote `upgrade --backup`
110
+ // snapshots to .owfa/backup/ even though its framework lived in .meta/. On
111
+ // migration that stray .owfa made replacePayloadTree park-and-delete the
112
+ // user's only pre-upgrade snapshots AND left .meta unparked (legacySwap never
113
+ // fired: no shim, no journal carry). Rescue the snapshots into the 0.4 backup
114
+ // home first; an emptied leftover .owfa is removed so the legacy swap takes
115
+ // over. A real 0.4 framework tree (.owfa/VERSION present) is never touched.
116
+ function rescueLegacyBackup(target) {
117
+ const owfa = path.join(target, ".owfa");
118
+ const meta = path.join(target, LEGACY_META);
119
+ if (!fs.existsSync(owfa) || fs.existsSync(path.join(owfa, "VERSION"))) return;
120
+ if (!fs.existsSync(path.join(meta, "VERSION"))) return; // no legacy framework — not the 0.3 shape
121
+ const stray = path.join(owfa, "backup");
122
+ if (fs.existsSync(stray)) {
123
+ const home = path.join(target, BACKUP_DIR);
124
+ fs.mkdirSync(home, { recursive: true });
125
+ for (const d of fs.readdirSync(stray)) {
126
+ if (fs.existsSync(path.join(home, d))) {
127
+ out(`warning: backup snapshot ${d} left in .owfa/backup/ — ${BACKUP_DIR}/${d} already exists`);
128
+ continue;
129
+ }
130
+ fs.renameSync(path.join(stray, d), path.join(home, d));
131
+ }
132
+ fs.rmSync(stray, { recursive: true, force: true });
133
+ out(`rescued: 0.3-era upgrade snapshots moved to ${BACKUP_DIR}/`);
134
+ }
135
+ if (fs.readdirSync(owfa).length === 0) fs.rmSync(owfa, { recursive: true });
136
+ }
137
+
95
138
  function copyPayload(target, { replace = false } = {}) {
96
139
  const skip = (src) => {
97
140
  const base = path.basename(src);
@@ -104,7 +147,12 @@ function copyPayload(target, { replace = false } = {}) {
104
147
  const dst = path.join(target, entry);
105
148
  if (!fs.existsSync(src)) fail(`package is missing ${entry}/ — the npm tarball is incomplete`);
106
149
  if (replace) {
107
- if (fs.existsSync(dst)) replacePayloadTree(target, entry, skip);
150
+ // 0.3 → 0.4 migration: the incoming .owfa/ has no counterpart on disk yet,
151
+ // but the legacy .meta/ tree must go through the same park-and-carry swap.
152
+ if (entry === ".owfa") rescueLegacyBackup(target);
153
+ const legacySwap =
154
+ entry === ".owfa" && !fs.existsSync(dst) && fs.existsSync(path.join(target, LEGACY_META));
155
+ if (fs.existsSync(dst) || legacySwap) replacePayloadTree(target, entry, skip);
108
156
  } else {
109
157
  fs.mkdirSync(path.dirname(dst), { recursive: true });
110
158
  fs.cpSync(src, dst, { recursive: true, force: true, filter: skip });
@@ -116,19 +164,36 @@ function copyPayload(target, { replace = false } = {}) {
116
164
  // files (review P1, PR #53): installed package masters (kernel install), migration
117
165
  // state (ledger / pending / dormant teardown), and skill copies the incoming tree
118
166
  // does not ship (package connector skills, user standalone skills). Everything else
119
- // under .meta/ and .agents/ is framework-owned and replaced by the channel.
167
+ // under .owfa/ and .agents/ is framework-owned and replaced by the channel.
120
168
  const META_MERGE_DIRS = [path.join("plugins", "domains"), "migrations"];
121
169
 
122
- // .meta/command/ is mixed-ownership: framework commands ship in the fresh tree,
170
+ // v0.4 renames the framework command/skill masters to owfa-* (review P1, PR #57
171
+ // round 2): a real 0.3 tree carries the retired names, and the difference-based
172
+ // carry-over would otherwise treat them as package-owned state and resurrect
173
+ // them beside their successors — 8 duplicate masters, `kernel all` blocked by
174
+ // one-sided owner-mapping errors. Retired framework names never carry; the map
175
+ // documents where each one lives now.
176
+ const RETIRED_MASTERS = new Map([
177
+ ["check", "owfa-check"],
178
+ ["docs", "owfa-docs"],
179
+ ["map", "owfa-map"],
180
+ ["plugin", "owfa-plugin"],
181
+ ["profile", "owfa-profile"],
182
+ ["query", "owfa-query"],
183
+ ["save", "owfa-save"],
184
+ ["wiki_plugin_kernel", "owfa-kernel"],
185
+ ]);
186
+
187
+ // .owfa/command/ is mixed-ownership: framework commands ship in the fresh tree,
123
188
  // package commands (kernel install) and anything the channel no longer ships are
124
189
  // carried over by difference — the fresh copy wins for shared names.
125
190
  function carryOverCommands(target, metaAttic) {
126
191
  const oldDir = path.join(metaAttic, "command");
127
192
  if (!fs.existsSync(oldDir)) return;
128
- const freshDir = path.join(target, ".meta", "command");
193
+ const freshDir = path.join(target, ".owfa", "command");
129
194
  const fresh = new Set(fs.existsSync(freshDir) ? fs.readdirSync(freshDir) : []);
130
195
  for (const d of fs.readdirSync(oldDir)) {
131
- if (fresh.has(d)) continue;
196
+ if (fresh.has(d) || RETIRED_MASTERS.has(d)) continue;
132
197
  fs.cpSync(path.join(oldDir, d), path.join(freshDir, d), { recursive: true, force: true });
133
198
  }
134
199
  }
@@ -139,11 +204,11 @@ function mergeTree(src, dst) {
139
204
  }
140
205
 
141
206
  function carryOverEntry(target, entry, attic) {
142
- if (entry === ".meta") {
143
- for (const d of META_MERGE_DIRS) mergeTree(path.join(attic, d), path.join(target, ".meta", d));
207
+ if (entry === ".owfa") {
208
+ for (const d of META_MERGE_DIRS) mergeTree(path.join(attic, d), path.join(target, ".owfa", d));
144
209
  carryOverCommands(target, attic);
145
210
  for (const p of PRESERVED) {
146
- const s = path.join(attic, path.relative(".meta", p)); // PRESERVED is target-relative; attic IS the old .meta
211
+ const s = path.join(attic, path.relative(".owfa", p)); // PRESERVED is target-relative; attic IS the old framework tree
147
212
  if (fs.existsSync(s)) {
148
213
  const d = path.join(target, p);
149
214
  fs.mkdirSync(path.dirname(d), { recursive: true });
@@ -156,48 +221,296 @@ function carryOverEntry(target, entry, attic) {
156
221
  const freshDir = path.join(target, ".agents", "skills");
157
222
  const fresh = new Set(fs.existsSync(freshDir) ? fs.readdirSync(freshDir) : []);
158
223
  for (const d of fs.readdirSync(oldSkills)) {
159
- if (fresh.has(d)) continue; // framework-shipped skill — the fresh copy wins
224
+ if (fresh.has(d) || RETIRED_MASTERS.has(d)) continue; // framework-shipped / retired-via-rename — the fresh copy wins
160
225
  fs.cpSync(path.join(oldSkills, d), path.join(freshDir, d), { recursive: true, force: true });
161
226
  }
162
227
  }
163
228
  }
164
229
 
165
- // Review round 2 (PR #53): the parked tree under .owfa/replace/ holds the only
230
+ // Review round 2 (PR #53): the parked tree under .owfa-migrate/replace/ holds the only
166
231
  // copy of the instance state between set-aside and carry-over. An interrupted
167
232
  // swap (Windows: rename of a tree with an open file) must therefore never be
168
233
  // implicitly cleaned by a retry, and a failure must roll the parked tree back.
169
234
  function replacePayloadTree(target, entry, skip) {
170
235
  const dst = path.join(target, entry);
171
- const attic = path.join(target, ".owfa", "replace", entry === ".meta" ? "meta" : "agents");
236
+ // 0.3 → 0.4 layout migration: an instance whose framework still lives in
237
+ // .meta/ parks that tree the same way, so crash semantics and carry-over
238
+ // are identical to a normal same-layout swap.
239
+ const parkedFrom =
240
+ fs.existsSync(dst)
241
+ ? dst
242
+ : entry === ".owfa" && fs.existsSync(path.join(target, LEGACY_META))
243
+ ? path.join(target, LEGACY_META)
244
+ : null;
245
+ const attic = path.join(target, MIGRATE_SCRATCH, "replace", path.basename(entry).replace(/^\./, ""));
172
246
  if (fs.existsSync(attic)) {
173
247
  fail(
174
248
  `interrupted replace detected: instance state is parked at ${attic} — ` +
175
249
  `do NOT simply rerun. Restore it manually (delete the fresh ${entry}/, ` +
176
- `rename the parked tree back to ${entry}/), then rerun.`
250
+ `rename the parked tree back to ${parkedFrom || entry}/), then rerun.`
177
251
  );
178
252
  }
253
+ if (!parkedFrom) return; // fresh init into a tree that never existed — nothing to replace
179
254
  fs.mkdirSync(path.dirname(attic), { recursive: true });
180
255
  try {
181
- fs.renameSync(dst, attic); // same volume — atomic enough for a directory swap
256
+ fs.renameSync(parkedFrom, attic); // same volume — atomic enough for a directory swap
182
257
  fs.cpSync(path.join(CLI_ROOT, entry), dst, { recursive: true, force: true, filter: skip });
183
258
  carryOverEntry(target, entry, attic);
259
+ if (entry === ".owfa") rescueCoreForConversion(target, attic, dst);
184
260
  } catch (e) {
185
261
  try {
186
262
  fs.rmSync(dst, { recursive: true, force: true }); // partial fresh copy
187
- fs.renameSync(attic, dst); // parked tree back in place
188
- fail(`replace failed (${e.message}) — rolled back: ${entry}/ restored from the parked tree; fix the underlying error before rerunning`);
263
+ fs.renameSync(attic, parkedFrom); // parked tree back in place
264
+ fail(`replace failed (${e.message}) — rolled back: ${parkedFrom} restored from the parked tree; fix the underlying error before rerunning`);
189
265
  } catch (e2) {
190
266
  fail(
191
267
  `replace failed (${e.message}) AND rollback failed (${e2.message}) — the previous tree ` +
192
- `is parked at ${attic}; do NOT rerun until it is restored to ${entry}/`
268
+ `is parked at ${attic}; do NOT rerun until it is restored to ${parkedFrom}`
193
269
  );
194
270
  }
195
271
  }
196
- fs.rmSync(path.join(target, ".owfa", "replace"), { recursive: true, force: true });
272
+ fs.rmSync(path.join(target, MIGRATE_SCRATCH, "replace"), { recursive: true, force: true });
273
+ }
274
+
275
+ // v0.4 core de-parasitization, rescue half: when the parked tree carries
276
+ // .owfa/plugins/core/ but the incoming channel tree does not (core stopped
277
+ // shipping), the parked masters are the only copy — move them out of the attic
278
+ // before it is deleted. The conversion itself (below) runs once the whole
279
+ // payload replace has finished, so it converges against the complete fresh tree.
280
+ // Inside the replace try-block: a failure here rolls the whole swap back.
281
+ function rescueCoreForConversion(target, attic, freshOwfa) {
282
+ const parkedCore = path.join(attic, "plugins", "core");
283
+ if (!fs.existsSync(parkedCore)) return;
284
+ if (fs.existsSync(path.join(freshOwfa, "plugins", "core"))) return; // compat: channel still ships core — nothing to convert
285
+ const hold = path.join(target, MIGRATE_SCRATCH, "convert", "core");
286
+ if (fs.existsSync(hold)) return; // a previous failed attempt kept the rescued masters — the convert step will rerun
287
+ fs.mkdirSync(path.dirname(hold), { recursive: true });
288
+ fs.renameSync(parkedCore, hold);
289
+ out("rescued: .owfa/plugins/core/ parked at " + rel(target, hold) + " — converting it into the personal suite package below");
290
+ }
291
+
292
+ // v0.4 core de-parasitization, convert half: mechanically adopt the rescued
293
+ // core masters as the `personal` suite package (a fact-accomplished conversion —
294
+ // the plugins are already in daily use in this instance, the package shape is
295
+ // new). Stages a synthetic package under .owfa-migrate/convert/personal/ (NOT
296
+ // directly under domains/ — `kernel install --path` owns that landing) and runs
297
+ // `kernel install --path ... --yes` for sweep symmetry + projection convergence
298
+ // (--yes is legitimate here: the channel itself is converting its own former
299
+ // payload, a mechanical adoption, not a trust decision).
300
+ //
301
+ // Family closure (kernel D5 invariant): every member must reach the family root
302
+ // through depends. The rescued set's own dependency roots (members whose depends
303
+ // has no edge into the rescued set — today: domain, instance-git, wiki) get a
304
+ // synthetic `- personal` edge in the STAGING copies only, so the adopted roster
305
+ // validates. When the real owfa-personal suite later replaces this package via
306
+ // `kernel upgrade`, the canonical manifests take over. The conversion stages no
307
+ // semantic migration (no migrations/ dir: nothing semantic changes — plugin
308
+ // bodies and projections are byte-identical), so there is nothing to verify-clear.
309
+ function convertCoreToPersonal(target) {
310
+ const hold = path.join(target, MIGRATE_SCRATCH, "convert", "core");
311
+ if (!fs.existsSync(hold)) return;
312
+ const pkgDir = path.join(target, ".owfa", "plugins", "domains", "personal");
313
+ if (fs.existsSync(pkgDir)) {
314
+ out("convert: domains/personal already present — rescued core treated as already converted, holding tree discarded");
315
+ return;
316
+ }
317
+ const members = fs
318
+ .readdirSync(hold)
319
+ .filter((d) => fs.existsSync(path.join(hold, d, "PLUGIN.yaml")))
320
+ .sort();
321
+ if (!members.length) fail("convert: rescued .owfa/plugins/core/ carries no plugin masters — nothing to convert");
322
+ const staging = path.join(target, MIGRATE_SCRATCH, "convert", "personal");
323
+ fs.mkdirSync(staging, { recursive: true });
324
+ for (const m of members) fs.cpSync(path.join(hold, m), path.join(staging, m), { recursive: true });
325
+
326
+ // 0.3-era rescued manifests still declare the pre-rename command ids (map /
327
+ // profile / save) while the command masters riding with this conversion carry
328
+ // the v0.4 successors — rewrite the staging copies' `commands:` lists so the
329
+ // adopted roster binds (flow list and block list both handled).
330
+ for (const m of members) {
331
+ const mf = path.join(staging, m, "PLUGIN.yaml");
332
+ const lines = fs.readFileSync(mf, "utf8").split("\n");
333
+ let inCommands = false;
334
+ for (let i = 0; i < lines.length; i++) {
335
+ const flow = lines[i].match(/^((?:commands|usage_routes):\s*)\[(.*)\](\s*)$/);
336
+ if (flow) {
337
+ const items = flow[2].split(",").map((s) => s.trim()).filter(Boolean)
338
+ .map((s) => RETIRED_MASTERS.get(s) || s);
339
+ lines[i] = `${flow[1]}[${items.join(", ")}]${flow[3]}`;
340
+ continue;
341
+ }
342
+ if (/^(commands|usage_routes):\s*$/.test(lines[i])) {
343
+ inCommands = true;
344
+ continue;
345
+ }
346
+ if (inCommands) {
347
+ const item = lines[i].match(/^(\s*-\s*)(\S+)\s*$/);
348
+ if (!item) {
349
+ inCommands = false;
350
+ continue;
351
+ }
352
+ lines[i] = item[1] + (RETIRED_MASTERS.get(item[2]) || item[2]);
353
+ }
354
+ }
355
+ fs.writeFileSync(mf, lines.join("\n"), "utf8");
356
+ }
357
+
358
+ // the suite's command masters ride along: the members adopted above declare
359
+ // them (mapping/notes/sessions/user-profile `commands:`), and the embedded
360
+ // blessed copy carries the canonical copies — without them the converted
361
+ // instance would lose map/save/profile until a manual `kernel upgrade personal`
362
+ const blessedCmd = path.join(CLI_ROOT, "payloads", "personal", "command");
363
+ if (fs.existsSync(blessedCmd)) {
364
+ const stagingCmd = path.join(staging, "command");
365
+ fs.mkdirSync(stagingCmd, { recursive: true });
366
+ for (const d of fs.readdirSync(blessedCmd)) {
367
+ fs.cpSync(path.join(blessedCmd, d), path.join(stagingCmd, d), { recursive: true });
368
+ }
369
+ }
370
+
371
+ // family-root edge: dependency roots of the rescued set adopt `personal`
372
+ const memberSet = new Set(members);
373
+ for (const m of members) {
374
+ const mf = path.join(staging, m, "PLUGIN.yaml");
375
+ let text = fs.readFileSync(mf, "utf8");
376
+ const depMatch = text.match(/^depends:\s*(.*)$/m);
377
+ const deps = depMatch
378
+ ? depMatch[1].replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^"|"$/g, "")).filter(Boolean)
379
+ : [];
380
+ if (deps.some((d) => memberSet.has(d) || d === "personal")) continue; // reaches the family through the set already
381
+ if (depMatch && depMatch[1].trim() === "[]") {
382
+ text = text.replace(/^depends:\s*\[\]\s*$/m, "depends:\n - personal");
383
+ } else if (depMatch) {
384
+ text = text.replace(/^depends:.*$/m, (line) => line + "\n - personal");
385
+ } else {
386
+ text = text.replace(/^(id:.*)$/m, "$1\ndepends:\n - personal");
387
+ }
388
+ fs.writeFileSync(mf, text, "utf8");
389
+ }
390
+
391
+ // synthesize the family root plugin
392
+ const root = path.join(staging, "personal");
393
+ fs.mkdirSync(root, { recursive: true });
394
+ const today = new Date().toISOString().slice(0, 10);
395
+ fs.writeFileSync(
396
+ path.join(root, "PLUGIN.yaml"),
397
+ [
398
+ `# synthesized by the owfa channel: v0.4 core de-parasitization conversion (upgrade)`,
399
+ `# adopts the framework-shipped core masters as a package; replaced by the`,
400
+ `# canonical owfa-personal suite on the next \`kernel upgrade personal\`.`,
401
+ `id: personal`,
402
+ `schema_version: 1`,
403
+ `version: 0.1`,
404
+ `depends: []`,
405
+ `updated: ${today}`,
406
+ `attachment: []`,
407
+ `fields: {}`,
408
+ `inject: ${JSON.stringify("personal suite (converted from framework core by the 0.4 channel upgrade) — family identity anchor; members, skeleton and teardown anchors are declared in PKG.yaml; canonical source: owfadev/owfa-personal")}`,
409
+ ``,
410
+ ].join("\n"),
411
+ "utf8"
412
+ );
413
+ fs.writeFileSync(path.join(root, "PLUGIN.md"), "# personal\n\nSynthesized family root for the converted core suite (0.4 channel upgrade).\nSee PKG.yaml for members, skeleton and teardown anchors.\n", "utf8");
414
+
415
+ // synthesize PKG.yaml — skeleton and anchors preserve the pre-0.4 channel
416
+ // contract mechanically (INSTANCE_DIRS and the kernel's former teardown
417
+ // anchor surface), so the adopted instance behaves exactly as before
418
+ const anchors = ["wiki/profile.md", "wiki/language.md", "wiki/calendar.md", "wiki/todo.md", "wiki/cron/"];
419
+ const lines = [
420
+ `# synthesized by the owfa channel: v0.4 core de-parasitization conversion (upgrade)`,
421
+ `id: personal`,
422
+ // kind: suite — the conversion product is a rootless-suite package (the
423
+ // owfa-personal shape): the synthesized root below is a family-identity
424
+ // anchor only, the closure law does not apply, and the canonical rootless
425
+ // suite can replace this package via `kernel upgrade personal` cleanly
426
+ `kind: suite`,
427
+ `version: 0.1.0`,
428
+ `compat: ">= 0.4"`,
429
+ `members:`,
430
+ ` - personal`,
431
+ ...members.map((m) => ` - ${m}`),
432
+ `skeleton: [vault/, wiki/notes/, wiki/sessions/, wiki/vault/]`,
433
+ `uninstall:`,
434
+ ` anchors: [${anchors.join(", ")}]`,
435
+ ``,
436
+ ];
437
+ fs.writeFileSync(path.join(staging, "PKG.yaml"), lines.join("\n"), "utf8");
438
+
439
+ out("");
440
+ out(`convert: core suite (${members.length} members) -> package personal (kernel install --path, --yes: mechanical adoption)`);
441
+ if (kernel(target, ["install", "personal", "--path", staging, "--yes"]) !== 0) {
442
+ fail(
443
+ `core -> personal conversion failed — the rescued masters are preserved at ${rel(target, hold)}; ` +
444
+ `fix the error above and rerun \`owfa upgrade\` (the conversion resumes from the hold)`
445
+ );
446
+ }
447
+ // success: drop the conversion scratch (and the migrate root if now empty)
448
+ fs.rmSync(path.join(target, MIGRATE_SCRATCH, "convert"), { recursive: true, force: true });
449
+ const scratchRoot = path.join(target, MIGRATE_SCRATCH);
450
+ if (fs.existsSync(scratchRoot) && fs.readdirSync(scratchRoot).length === 0) fs.rmdirSync(scratchRoot);
451
+ }
452
+
453
+
454
+ // The 0.4 layout shim: 0.3-era absolute paths (.meta/...) keep working through a
455
+ // directory shim. POSIX symlink; Windows junction (no admin rights needed).
456
+ // Idempotent and self-healing (review P1/P2 on this PR):
457
+ // - written on fresh init too — released 0.3 packages (cuhksz/lark/github) still
458
+ // instruct .meta/ paths, so EVERY instance shape gets the shim until 0.5;
459
+ // - on upgrade a stale shim (instance dir renamed or copied — Windows junctions
460
+ // are absolute) is detected via realpath and re-created;
461
+ // - a real .meta directory (0.3 layout that never migrated) is never touched.
462
+ function ensureMetaShim(target) {
463
+ const metaPath = path.join(target, LEGACY_META);
464
+ const owfa = path.join(target, ".owfa");
465
+ if (!fs.existsSync(owfa)) return "absent";
466
+ let kind = "missing";
467
+ // lstat, not existsSync: a dangling shim (instance dir moved — review P2 round 2)
468
+ // makes existsSync false, which would skip the stale branch and hit EEXIST below.
469
+ let mst = null;
470
+ try {
471
+ mst = fs.lstatSync(metaPath);
472
+ } catch {
473
+ mst = null; // genuinely absent
474
+ }
475
+ if (mst) {
476
+ if (!mst.isSymbolicLink()) return "real-dir";
477
+ kind = "link";
478
+ let stale = true;
479
+ try {
480
+ stale = fs.realpathSync(metaPath) !== fs.realpathSync(owfa);
481
+ } catch {
482
+ stale = true; // dangling link
483
+ }
484
+ if (!stale) {
485
+ ensureGitignore(target, LEGACY_META);
486
+ return "ok";
487
+ }
488
+ fs.rmSync(metaPath, { force: true }); // symlink/junction only — never a real dir
489
+ }
490
+ try {
491
+ fs.symlinkSync(".owfa", metaPath, process.platform === "win32" ? "junction" : "dir");
492
+ } catch (e) {
493
+ out(`warning: could not create the .meta shim (${e.message}) — 0.3-era scripts calling .meta/... will not resolve`);
494
+ return "failed";
495
+ }
496
+ ensureGitignore(target, LEGACY_META);
497
+ return kind === "link" ? "recreated" : "created";
197
498
  }
198
499
 
199
- function ensureDirs(target) {
200
- for (const d of INSTANCE_DIRS) fs.mkdirSync(path.join(target, d), { recursive: true });
500
+ // Windows: core.symlinks=false makes git traverse the junction as a plain
501
+ // directory, so without this line the framework is tracked twice (.owfa/ plus
502
+ // every .meta/* entry) and the 0.5 shim retirement reads as a mass deletion.
503
+ // Instance-git contract: adding an ignore sweeps tracked matches out — the CLI
504
+ // writes the line, and upgrade tells the user the one-time sweep command.
505
+ function ensureGitignore(target, line) {
506
+ const p = path.join(target, ".gitignore");
507
+ let text = "";
508
+ if (fs.existsSync(p)) text = fs.readFileSync(p, "utf8");
509
+ const hit = new RegExp(`^${line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m");
510
+ if (hit.test(text)) return false;
511
+ const glue = text && !text.endsWith("\n") ? "\n" : "";
512
+ fs.writeFileSync(p, `${text}${glue}# 0.4 layout shim (retires in 0.5)\n${line}\n`, "utf8");
513
+ return true;
201
514
  }
202
515
 
203
516
  function ensureAgents(target, name) {
@@ -253,7 +566,7 @@ function pythonOrFail() {
253
566
 
254
567
  function kernel(target, kernelArgs) {
255
568
  const py = pythonOrFail();
256
- const script = path.join(target, ".meta", "scripts", "wiki_plugin_kernel.py");
569
+ const script = path.join(target, ".owfa", "scripts", "owfa_kernel.py");
257
570
  if (!fs.existsSync(script)) fail(`no kernel at ${rel(target, script)} — is this an OWFA instance?`);
258
571
  const res = spawnSync(py.cmd, [...py.args, script, ...kernelArgs], {
259
572
  cwd: target,
@@ -288,11 +601,12 @@ function timestamp() {
288
601
 
289
602
 
290
603
  function pendingPackages(target) {
291
- try {
292
- return Object.keys(JSON.parse(fs.readFileSync(path.join(target, ".meta", "migrations", "pending.json"), "utf8")));
293
- } catch {
294
- return [];
604
+ for (const dir of [".owfa", LEGACY_META]) {
605
+ try {
606
+ return Object.keys(JSON.parse(fs.readFileSync(path.join(target, dir, "migrations", "pending.json"), "utf8")));
607
+ } catch {}
295
608
  }
609
+ return [];
296
610
  }
297
611
 
298
612
  function reportPending(target) {
@@ -300,7 +614,7 @@ function reportPending(target) {
300
614
  if (!pending.length) return false;
301
615
  out("");
302
616
  out(`semantic migration pending: ${pending.join(", ")} — the package is installed, but its first-run step is an agent act, not a mechanical one.`);
303
- out(` run: python .meta/scripts/wiki_plugin_kernel.py migrate ${pending[0]}`);
617
+ out(` run: python .owfa/scripts/owfa_kernel.py migrate ${pending[0]}`);
304
618
  out(" (read the printed page, perform the step, then rerun with --clear)");
305
619
  return true;
306
620
  }
@@ -312,35 +626,114 @@ function reportPending(target) {
312
626
  function cmdInit(args) {
313
627
  const target = path.resolve(args._[0] || ".");
314
628
  const force = args.flags.has("force");
315
- const hasMeta = fs.existsSync(path.join(target, ".meta"));
316
- if (hasMeta && !force) {
629
+ const hasOwfa = fs.existsSync(path.join(target, ".owfa"));
630
+ const hasLegacyMeta = fs.existsSync(path.join(target, LEGACY_META));
631
+ if (hasLegacyMeta && !hasOwfa) {
317
632
  fail(
318
- `${target} already contains .meta/ — refusing to overwrite. Use \`owfa upgrade\` to update the framework, ` +
633
+ `${target} uses the 0.3 layout (.meta/) — run \`owfa upgrade\` to migrate to the .owfa layout ` +
634
+ `(the upgrade preserves all instance state and leaves a .meta shim for old paths).`
635
+ );
636
+ }
637
+ if (hasOwfa && !force) {
638
+ fail(
639
+ `${target} already contains .owfa/ — refusing to overwrite. Use \`owfa upgrade\` to update the framework, ` +
319
640
  `or pass --force to re-copy the framework trees (instance data is never touched either way).`
320
641
  );
321
642
  }
322
643
  const name = args.name || path.basename(target) || "workspace";
323
644
  fs.mkdirSync(target, { recursive: true });
324
645
 
646
+ // two-stage bootstrap (v0.4 core de-parasitization, final shape). Stage ①: the
647
+ // kernel payload. Stage ② (after convergence): the personal suite — by default
648
+ // the blessed copy embedded in this channel tarball (offline, --path channel);
649
+ // `--core none` skips it; `--core <pkg-id>` takes the suite from the registry.
650
+ // Defensive compat branch: this channel stopped shipping .owfa/plugins/core/ in
651
+ // v0.4.0 — if a future (or hand-assembled) channel tree ever carries core again,
652
+ // the suite already lives in the framework tree and stage ② must not double-
653
+ // install; saying otherwise is refused up front.
654
+ const coreShipped =
655
+ fs.existsSync(path.join(CLI_ROOT, ".owfa", "plugins", "core")) ||
656
+ fs.existsSync(path.join(CLI_ROOT, ".meta", "plugins", "core"));
657
+ if (coreShipped && args.core && args.core !== "none") {
658
+ fail(
659
+ `--core ${args.core}: this channel still ships the core suite inside the framework tree ` +
660
+ `(compat period) — suite install is unavailable until the channel stops shipping core`
661
+ );
662
+ }
663
+
325
664
  out(`OWFA init — framework ${FRAMEWORK_VERSION} → ${target}`);
326
- if (!hasMeta && fs.existsSync(path.join(target, ".agents", "skills"))) {
665
+ if (!hasOwfa && fs.existsSync(path.join(target, ".agents", "skills"))) {
327
666
  out("note: .agents/skills/ exists — framework skill copies are updated in place, other skills stay untouched");
328
667
  }
329
- if (hasMeta) out("warning: --force replaces the framework trees; instance vault/ and wiki/ are untouched");
330
- copyPayload(target, { replace: hasMeta });
331
- ensureDirs(target);
668
+ if (hasOwfa) out("warning: --force replaces the framework trees; instance vault/ and wiki/ are untouched");
669
+ copyPayload(target, { replace: hasOwfa });
670
+ // P1 (review): the shim is part of the 0.4 layout for EVERY instance shape —
671
+ // released 0.3 packages (cuhksz/lark/github) still instruct .meta/ paths,
672
+ // fresh instances included.
673
+ if (ensureMetaShim(target) === "created") {
674
+ out("shim: .meta → .owfa written — 0.3-era package paths keep working (retires in 0.5)");
675
+ }
332
676
  out(`agents.md: ${ensureAgents(target, name)}`);
333
677
 
334
678
  out("");
335
- out("converging projections (python .meta/scripts/wiki_plugin_kernel.py all)");
679
+ out("converging projections (python .owfa/scripts/owfa_kernel.py all)");
336
680
  if (kernel(target, ["all"]) !== 0) {
337
681
  const pending = pendingPackages(target);
338
682
  if (pending.length) {
339
- fail(`convergence blocked by pending semantic migrations (${pending.join(", ")}) — clear them with \`python .meta/scripts/wiki_plugin_kernel.py migrate <pkg>\``);
683
+ fail(`convergence blocked by pending semantic migrations (${pending.join(", ")}) — clear them with \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\``);
340
684
  }
341
685
  fail("kernel convergence failed — the framework tree is incomplete; nothing was deleted, fix and rerun");
342
686
  }
343
687
 
688
+ // ---- stage ②: the core suite ----
689
+ let suiteNote;
690
+ if (coreShipped) {
691
+ suiteNote = "core suite ships inside the framework tree (defensive compat branch — this channel does not)";
692
+ out(`suite: ${suiteNote} — no separate suite package is installed`);
693
+ } else if (args.core === "none") {
694
+ suiteNote = "no suite installed — this instance cannot install packages that depend on core capabilities until a suite is installed";
695
+ out(`suite: ${suiteNote}`);
696
+ } else if (args.core) {
697
+ out("");
698
+ out(`installing core suite ${args.core} from the registry (no --yes — the trust gate stays interactive)`);
699
+ if (kernel(target, ["install", args.core]) !== 0) {
700
+ fail(`suite package ${args.core} failed to install — the framework itself is in place; run \`owfa doctor\``);
701
+ }
702
+ suiteNote = `suite ${args.core} installed from the registry`;
703
+ if (reportPending(target)) {
704
+ fail(`framework and suite ${args.core} are installed, but the workspace is not converged until the semantic migration above is cleared`);
705
+ }
706
+ } else {
707
+ // default: the embedded blessed copy — 100% offline, local --path trust.
708
+ // A copy without plugin masters is the placeholder state (suite repo not yet
709
+ // synced into the channel): report it exactly like --core none. A --force
710
+ // re-init over an instance that already carries the suite keeps it (the
711
+ // package tree is instance state, the channel never replaces it here).
712
+ const pkgDir = path.join(target, ".owfa", "plugins", "domains", "personal");
713
+ const hasMasters =
714
+ fs.existsSync(path.join(BLESSED_SUITE, "PKG.yaml")) &&
715
+ fs.readdirSync(BLESSED_SUITE, { withFileTypes: true }).some(
716
+ (d) => d.isDirectory() && fs.existsSync(path.join(BLESSED_SUITE, d.name, "PLUGIN.yaml"))
717
+ );
718
+ if (fs.existsSync(pkgDir)) {
719
+ suiteNote = "personal suite already installed — kept";
720
+ out(`suite: ${suiteNote}`);
721
+ } else if (hasMasters) {
722
+ out("");
723
+ out("installing the blessed personal suite (embedded copy — offline)");
724
+ if (kernel(target, ["install", "personal", "--path", BLESSED_SUITE]) !== 0) {
725
+ fail("the embedded personal suite failed to install — the channel tarball is inconsistent; report this (the framework itself is in place)");
726
+ }
727
+ suiteNote = "personal suite installed (embedded blessed copy)";
728
+ if (reportPending(target)) {
729
+ fail("framework and suite are installed, but the workspace is not converged until the semantic migration above is cleared");
730
+ }
731
+ } else {
732
+ suiteNote = "no suite installed — this instance cannot install packages that depend on core capabilities until a suite is installed (this channel build carries no populated embedded suite)";
733
+ out(`suite: ${suiteNote}`);
734
+ }
735
+ }
736
+
344
737
  let installed = 0;
345
738
  if (args.packages) {
346
739
  for (const pkg of String(args.packages)
@@ -349,7 +742,9 @@ function cmdInit(args) {
349
742
  .filter(Boolean)) {
350
743
  out("");
351
744
  out(`installing package ${pkg} from the registry`);
352
- const installArgs = ["install", pkg, "--yes"];
745
+ // no --yes forwarding (v0.4): auto-installable trust levels pass on their
746
+ // own; anything else must go through the kernel's interactive trust gate
747
+ const installArgs = ["install", pkg];
353
748
  if (args.without) installArgs.push("--without", String(args.without));
354
749
  if (kernel(target, installArgs) !== 0) fail(`package ${pkg} failed to install (earlier packages stay installed)`);
355
750
  installed++;
@@ -358,12 +753,12 @@ function cmdInit(args) {
358
753
  }
359
754
 
360
755
  out("");
361
- out(`done — framework ${FRAMEWORK_VERSION}${installed ? ` + ${installed} package(s)` : ""} in ${target}`);
756
+ out(`done — framework ${FRAMEWORK_VERSION}${installed ? ` + ${installed} package(s)` : ""}; ${suiteNote} in ${target}`);
362
757
  out("");
363
758
  out("next:");
364
759
  out(` cd ${target}`);
365
760
  out(" git init && git add -A && git commit -m \"init: OWFA workspace\" # recommended: instance snapshots");
366
- out(" put any file into vault/, open your agent here, say \"map\"");
761
+ out(` skeleton dirs (vault/, wiki/...) came with the suite — its PKG.yaml \`skeleton:\` declared them`);
367
762
  out(" owfa doctor # anytime: python, framework, kernel validate");
368
763
  return 0;
369
764
  }
@@ -371,44 +766,64 @@ function cmdInit(args) {
371
766
  function cmdUpgrade(args) {
372
767
  const target = path.resolve(args._[0] || ".");
373
768
  const local = readVersion(target);
374
- if (!local) fail(`${target} is not an OWFA instance (no .meta/VERSION) — run \`owfa init\` first`);
769
+ if (!local) fail(`${target} is not an OWFA instance (no .owfa/VERSION or .meta/VERSION) — run \`owfa init\` first`);
770
+ const legacyLayout = !fs.existsSync(path.join(target, ".owfa", "VERSION"));
375
771
  const incoming = FRAMEWORK_VERSION;
376
772
  const cmp = cmpVersion(local, incoming);
377
773
  const frozen = args.flags.has("check") || args.flags.has("dry-run");
378
774
  const force = args.flags.has("force");
379
775
 
380
776
  out(`OWFA upgrade — instance ${local} → channel ${incoming} (CLI ${PKG.version})`);
777
+ if (legacyLayout) {
778
+ out(`layout migration: 0.3 instance (.meta/) → 0.4 layout (.owfa/ + .meta shim for old paths)`);
779
+ }
381
780
  if (cmp > 0) {
382
781
  if (!force) fail(`instance framework ${local} is newer than this CLI's ${incoming} — run \`npx owfa@latest upgrade\``);
383
782
  out(`warning: downgrading framework ${local} → ${incoming} (--force)`);
384
783
  }
385
- if (cmp === 0 && !force) {
784
+ if (cmp === 0 && !force && !legacyLayout) {
386
785
  out("already at this framework version — nothing to do (pass --force to re-sync the trees)");
387
786
  return 0;
388
787
  }
389
788
  if (frozen) {
390
- out(`would replace ${PAYLOAD.join(" + ")} and rerun kernel all (dry run)`);
789
+ out(`would replace ${PAYLOAD.join(" + ")}${legacyLayout ? " (migrating .meta/ → .owfa/)" : ""} and rerun kernel all (dry run)`);
391
790
  return 0;
392
791
  }
393
792
 
394
793
  if (args.flags.has("backup")) {
395
- const dir = path.join(target, ".owfa", "backup", `${local}-${timestamp()}`);
794
+ const dir = path.join(target, BACKUP_DIR, `${local}-${timestamp()}`);
396
795
  for (const entry of PAYLOAD) {
397
- const src = path.join(target, entry);
796
+ const src = path.join(target, entry === ".owfa" && legacyLayout ? LEGACY_META : entry);
398
797
  if (fs.existsSync(src)) fs.cpSync(src, path.join(dir, entry), { recursive: true, force: true });
399
798
  }
400
- out(`backup: ${rel(target, dir)} (add .owfa/ to the instance .gitignore if you do not want it tracked)`);
799
+ out(`backup: ${rel(target, dir)} (add .owfa-backup/ to the instance .gitignore if you do not want it tracked)`);
401
800
  }
402
801
 
403
802
  out(`warning: local edits under ${PAYLOAD.join("/, ")}/ are replaced by the channel's tree — keep instance hotfixes in git`);
404
803
  copyPayload(target, { replace: true });
804
+ // v0.4 core de-parasitization: if the replace rescued a parked core/ (the
805
+ // channel stopped shipping it), convert it into the personal suite package
806
+ // BEFORE converging — the conversion's own install converges the adopted set.
807
+ convertCoreToPersonal(target);
808
+ // P2 (review): self-heal a stale shim here too — instance dirs get renamed and
809
+ // copied, and Windows junction targets are absolute. The .gitignore sweep hint
810
+ // covers the "tracked twice" half only the user's git can perform.
811
+ const shimState = ensureMetaShim(target);
812
+ if (legacyLayout) {
813
+ if (shimState === "created" || shimState === "recreated") {
814
+ out(`shim: .meta → .owfa ${shimState} — 0.3-era paths keep resolving until the shim retires (0.5)`);
815
+ }
816
+ if (shimState !== "failed") {
817
+ out("instance git: if .meta/ was tracked before 0.4, sweep it once — git rm -r --cached .meta");
818
+ }
819
+ }
405
820
 
406
821
  out("");
407
- out("converging projections (python .meta/scripts/wiki_plugin_kernel.py all)");
822
+ out("converging projections (python .owfa/scripts/owfa_kernel.py all)");
408
823
  if (kernel(target, ["all"]) !== 0) {
409
824
  const pending = pendingPackages(target);
410
825
  if (pending.length) {
411
- fail(`trees replaced, but convergence is blocked by pending semantic migrations (${pending.join(", ")}) — run \`python .meta/scripts/wiki_plugin_kernel.py migrate <pkg>\``);
826
+ fail(`trees replaced, but convergence is blocked by pending semantic migrations (${pending.join(", ")}) — run \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\``);
412
827
  }
413
828
  fail("kernel convergence failed after the tree replacement — the instance git snapshot is your rollback");
414
829
  }
@@ -416,10 +831,10 @@ function cmdUpgrade(args) {
416
831
  const pending = pendingPackages(target);
417
832
  if (pending.length) {
418
833
  out("");
419
- out(`pending semantic migrations: ${pending.join(", ")} — run \`python .meta/scripts/wiki_plugin_kernel.py migrate <pkg>\` and follow the page`);
834
+ out(`pending semantic migrations: ${pending.join(", ")} — run \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\` and follow the page`);
420
835
  }
421
836
  out("");
422
- out(`done — framework ${local} → ${incoming}`);
837
+ out(`done — framework ${local} → ${incoming}${legacyLayout ? " (layout migrated to .owfa/)" : ""}`);
423
838
  return 0;
424
839
  }
425
840
 
@@ -431,9 +846,44 @@ function cmdDoctor(args) {
431
846
  out(`node ${process.version}`);
432
847
  out(`python ${py ? py.version || `${py.cmd} ${py.args.join(" ")}` : "NOT FOUND"}`);
433
848
  out(`target ${target}`);
434
- if (!local) fail("not an OWFA instance (no .meta/VERSION) — run `owfa init` first");
849
+ if (!local) fail("not an OWFA instance (no .owfa/VERSION) — run `owfa init` first");
435
850
  out(`instance framework ${local}`);
436
851
  const problems = []; // required conditions — any hit fails doctor (review P2, PR #53)
852
+ // layout-shim health (review P2 on the layout PR): a renamed/copied instance
853
+ // leaves the Windows junction pointing at the old absolute path; a real .meta
854
+ // directory means the 0.3 layout was never migrated. doctor reports; upgrade heals.
855
+ const owfaDir = path.join(target, ".owfa");
856
+ const metaPath = path.join(target, LEGACY_META);
857
+ if (fs.existsSync(owfaDir)) {
858
+ // lstat, not existsSync: a dangling shim must reach the stale branch, not
859
+ // silently skip the whole check (review P2, PR #57 round 2).
860
+ let mst = null;
861
+ try {
862
+ mst = fs.lstatSync(metaPath);
863
+ } catch {
864
+ mst = null;
865
+ }
866
+ if (!mst) {
867
+ out(".meta: shim missing — 0.3-era .meta/... paths will not resolve; run `owfa upgrade` to re-create it");
868
+ problems.push(".meta shim is missing");
869
+ } else if (!mst.isSymbolicLink()) {
870
+ out(".meta: real directory — 0.3 layout never migrated; run `owfa upgrade`");
871
+ problems.push(".meta is not the layout shim");
872
+ } else {
873
+ let same = false;
874
+ try {
875
+ same = fs.realpathSync(metaPath) === fs.realpathSync(owfaDir);
876
+ } catch {
877
+ same = false; // dangling
878
+ }
879
+ if (!same) {
880
+ out(".meta: shim resolves outside this instance — run `owfa upgrade` to re-create it");
881
+ problems.push(".meta shim target is stale");
882
+ } else {
883
+ out(".meta shim: ok (resolves into this instance)");
884
+ }
885
+ }
886
+ }
437
887
  const agents = path.join(target, "AGENTS.md");
438
888
  if (!fs.existsSync(agents)) {
439
889
  out("AGENTS.md: MISSING — the agent will not see the structural contract");
@@ -493,10 +943,14 @@ function usage() {
493
943
  the owfa framework's install and upgrade channel
494
944
 
495
945
  usage:
496
- owfa init [dir] [--name NAME] [--packages a,b] [--force]
497
- deploy a fresh workspace (framework ${FRAMEWORK_VERSION}); additive to existing content
946
+ owfa init [dir] [--name NAME] [--core none|<pkg-id>] [--packages a,b] [--force]
947
+ deploy a fresh workspace (framework ${FRAMEWORK_VERSION}); additive to existing content;
948
+ two-stage: kernel payload, then the core suite (default: embedded blessed copy,
949
+ offline; --core none skips it; --core <pkg-id> installs from the registry)
498
950
  owfa upgrade [dir] [--check] [--backup] [--force]
499
- replace the framework trees with this channel's version and converge
951
+ replace the framework trees with this channel's version and converge;
952
+ if this channel stopped shipping core, the instance's core plugins are
953
+ converted into the personal suite package (mechanical adoption)
500
954
  owfa doctor [dir] python / framework / AGENTS.md / kernel validate
501
955
  owfa add <pkg> [--yes] [--without a,b]
502
956
  install a domain package from the registry, then converge
@@ -509,19 +963,25 @@ notes:
509
963
  - python 3.9+ is required to run the framework; node is only the installer
510
964
  - \`init\` never deletes instance data; \`upgrade\` preserves the kernel's
511
965
  migration ledger, journal and registry cache
966
+ - skeleton dirs (vault/, wiki/...) come from package declarations (PKG.yaml
967
+ \`skeleton:\`), created on install — the default init already installed the
968
+ embedded personal suite and its skeleton
512
969
  - no install/postinstall scripts: npm install executes nothing
513
970
  `);
514
971
  }
515
972
 
516
973
  const COMMAND_HELP = {
517
- init: "usage: owfa init [dir] [--name NAME] [--packages a,b] [--without m,n] [--force]\n\n" +
974
+ init: "usage: owfa init [dir] [--name NAME] [--core none|<pkg-id>] [--packages a,b] [--without m,n] [--force]\n\n" +
518
975
  "deploy a fresh workspace (additive: existing files and AGENTS.md are preserved).\n" +
519
976
  "python 3.9+ required for the framework; node 16.7+ runs this installer only.\n" +
520
- "--packages installs registry packages; --without prunes members from them (dependency-closed subsets only).\n" +
977
+ "two-stage bootstrap: ① kernel payload ② core suite — default the blessed copy embedded\n" +
978
+ "in the channel tarball (offline); --core none skips stage ②; --core <pkg-id> installs\n" +
979
+ "the suite from the registry (online; the kernel's trust gate stays interactive).\n" +
980
+ "--packages installs further registry packages; --without prunes members (dependency-closed subsets only).\n" +
521
981
  "--force re-copies the framework trees over an existing instance (instance data is never touched).",
522
982
  upgrade: "usage: owfa upgrade [dir] [--check] [--backup] [--force]\n\n" +
523
983
  "replace the framework trees with this channel's version and converge.\n" +
524
- "--check (alias --dry-run) is read-only; --backup snapshots the current trees to .owfa/backup/.\n" +
984
+ "--check (alias --dry-run) is read-only; --backup snapshots the current trees to .owfa-backup/.\n" +
525
985
  "instance data, installed packages, pending migrations and standalone skills are preserved.",
526
986
  doctor: "usage: owfa doctor [dir]\n\n" +
527
987
  "health check: python, framework version, AGENTS.md injection markers, kernel validate.\n" +