owfadev 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.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 +68 -0
  41. package/README.md +32 -24
  42. package/README.zh-CN.md +32 -24
  43. package/bin/owfa.js +483 -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,254 @@ 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
+ // Rootless-suite conversion (kind: suite): the kernel's family-root/closure law
302
+ // does not apply, so no synthesized root plugin and no synthetic dependency
303
+ // edges — the staged package matches the canonical owfa-personal shape (21
304
+ // members, no root). The conversion stages no semantic migration (no
305
+ // migrations/ dir: nothing semantic changes — plugin bodies and projections are
306
+ // byte-identical), so there is nothing to verify-clear.
307
+ function convertCoreToPersonal(target) {
308
+ const hold = path.join(target, MIGRATE_SCRATCH, "convert", "core");
309
+ if (!fs.existsSync(hold)) return;
310
+ const pkgDir = path.join(target, ".owfa", "plugins", "domains", "personal");
311
+ if (fs.existsSync(pkgDir)) {
312
+ out("convert: domains/personal already present — rescued core treated as already converted, holding tree discarded");
313
+ return;
314
+ }
315
+ const members = fs
316
+ .readdirSync(hold)
317
+ .filter((d) => fs.existsSync(path.join(hold, d, "PLUGIN.yaml")))
318
+ .sort();
319
+ if (!members.length) fail("convert: rescued .owfa/plugins/core/ carries no plugin masters — nothing to convert");
320
+ const staging = path.join(target, MIGRATE_SCRATCH, "convert", "personal");
321
+ fs.mkdirSync(staging, { recursive: true });
322
+ for (const m of members) fs.cpSync(path.join(hold, m), path.join(staging, m), { recursive: true });
323
+
324
+ // 0.3-era rescued manifests still declare the pre-rename command ids (map /
325
+ // profile / save) while the command masters riding with this conversion carry
326
+ // the v0.4 successors — rewrite the staging copies' `commands:` lists so the
327
+ // adopted roster binds (flow list and block list both handled).
328
+ for (const m of members) {
329
+ const mf = path.join(staging, m, "PLUGIN.yaml");
330
+ const lines = fs.readFileSync(mf, "utf8").split("\n");
331
+ let inCommands = false;
332
+ for (let i = 0; i < lines.length; i++) {
333
+ const flow = lines[i].match(/^((?:commands|usage_routes):\s*)\[(.*)\](\s*)$/);
334
+ if (flow) {
335
+ const items = flow[2].split(",").map((s) => s.trim()).filter(Boolean)
336
+ .map((s) => RETIRED_MASTERS.get(s) || s);
337
+ lines[i] = `${flow[1]}[${items.join(", ")}]${flow[3]}`;
338
+ continue;
339
+ }
340
+ if (/^(commands|usage_routes):\s*$/.test(lines[i])) {
341
+ inCommands = true;
342
+ continue;
343
+ }
344
+ if (inCommands) {
345
+ const item = lines[i].match(/^(\s*-\s*)(\S+)\s*$/);
346
+ if (!item) {
347
+ inCommands = false;
348
+ continue;
349
+ }
350
+ lines[i] = item[1] + (RETIRED_MASTERS.get(item[2]) || item[2]);
351
+ }
352
+ }
353
+ fs.writeFileSync(mf, lines.join("\n"), "utf8");
354
+ }
355
+
356
+ // the suite's command masters ride along: the members adopted above declare
357
+ // them (mapping/notes/sessions/user-profile `commands:`), and the embedded
358
+ // blessed copy carries the canonical copies — without them the converted
359
+ // instance would lose map/save/profile until a manual `kernel upgrade personal`
360
+ const blessedCmd = path.join(CLI_ROOT, "payloads", "personal", "command");
361
+ if (fs.existsSync(blessedCmd)) {
362
+ const stagingCmd = path.join(staging, "command");
363
+ fs.mkdirSync(stagingCmd, { recursive: true });
364
+ for (const d of fs.readdirSync(blessedCmd)) {
365
+ fs.cpSync(path.join(blessedCmd, d), path.join(stagingCmd, d), { recursive: true });
366
+ }
367
+ }
368
+
369
+ // rootless suite: kind=suite skips the family-root/closure law entirely
370
+ // (owfa_kernel validate_packages), so no synthesized root plugin and no
371
+ // synthetic `personal` dependency edges — the converted package matches the
372
+ // canonical owfa-personal shape (21 members, no root). This also keeps the
373
+ // projected injection region within the 16 KiB budget: near-capacity 0.3
374
+ // instances (domains installed) have no headroom for an extra block.
375
+
376
+ // synthesize PKG.yaml — skeleton and anchors preserve the pre-0.4 channel
377
+ // contract mechanically (INSTANCE_DIRS and the kernel's former teardown
378
+ // anchor surface), so the adopted instance behaves exactly as before
379
+ const anchors = ["wiki/profile.md", "wiki/language.md", "wiki/calendar.md", "wiki/todo.md", "wiki/cron/"];
380
+ const lines = [
381
+ `# synthesized by the owfa channel: v0.4 core de-parasitization conversion (upgrade)`,
382
+ `id: personal`,
383
+ // kind: suite — the conversion product is a rootless-suite package: the
384
+ // canonical owfa-personal shape, replaceable via `kernel upgrade personal`
385
+ `kind: suite`,
386
+ `version: 0.1.0`,
387
+ `compat: ">= 0.4"`,
388
+ `members:`,
389
+ ...members.map((m) => ` - ${m}`),
390
+ `skeleton: [vault/, wiki/notes/, wiki/sessions/, wiki/vault/]`,
391
+ `uninstall:`,
392
+ ` anchors: [${anchors.join(", ")}]`,
393
+ ``,
394
+ ];
395
+ fs.writeFileSync(path.join(staging, "PKG.yaml"), lines.join("\n"), "utf8");
396
+
397
+ out("");
398
+ out(`convert: core suite (${members.length} members) -> package personal (kernel install --path, --yes: mechanical adoption)`);
399
+ if (kernel(target, ["install", "personal", "--path", staging, "--yes"]) !== 0) {
400
+ fail(
401
+ `core -> personal conversion failed — the rescued masters are preserved at ${rel(target, hold)}; ` +
402
+ `fix the error above and rerun \`owfa upgrade\` (the conversion resumes from the hold)`
403
+ );
404
+ }
405
+ // success: drop the conversion scratch (and the migrate root if now empty)
406
+ fs.rmSync(path.join(target, MIGRATE_SCRATCH, "convert"), { recursive: true, force: true });
407
+ const scratchRoot = path.join(target, MIGRATE_SCRATCH);
408
+ if (fs.existsSync(scratchRoot) && fs.readdirSync(scratchRoot).length === 0) fs.rmdirSync(scratchRoot);
409
+ }
410
+
411
+
412
+ // The 0.4 layout shim: 0.3-era absolute paths (.meta/...) keep working through a
413
+ // directory shim. POSIX symlink; Windows junction (no admin rights needed).
414
+ // Idempotent and self-healing (review P1/P2 on this PR):
415
+ // - written on fresh init too — released 0.3 packages (cuhksz/lark/github) still
416
+ // instruct .meta/ paths, so EVERY instance shape gets the shim until 0.5;
417
+ // - on upgrade a stale shim (instance dir renamed or copied — Windows junctions
418
+ // are absolute) is detected via realpath and re-created;
419
+ // - a real .meta directory (0.3 layout that never migrated) is never touched.
420
+ function ensureMetaShim(target) {
421
+ const metaPath = path.join(target, LEGACY_META);
422
+ const owfa = path.join(target, ".owfa");
423
+ if (!fs.existsSync(owfa)) return "absent";
424
+ let kind = "missing";
425
+ // lstat, not existsSync: a dangling shim (instance dir moved — review P2 round 2)
426
+ // makes existsSync false, which would skip the stale branch and hit EEXIST below.
427
+ let mst = null;
428
+ try {
429
+ mst = fs.lstatSync(metaPath);
430
+ } catch {
431
+ mst = null; // genuinely absent
432
+ }
433
+ if (mst) {
434
+ if (!mst.isSymbolicLink()) return "real-dir";
435
+ kind = "link";
436
+ let stale = true;
437
+ try {
438
+ stale = fs.realpathSync(metaPath) !== fs.realpathSync(owfa);
439
+ } catch {
440
+ stale = true; // dangling link
441
+ }
442
+ if (!stale) {
443
+ ensureGitignore(target, LEGACY_META);
444
+ return "ok";
445
+ }
446
+ fs.rmSync(metaPath, { force: true }); // symlink/junction only — never a real dir
447
+ }
448
+ try {
449
+ fs.symlinkSync(".owfa", metaPath, process.platform === "win32" ? "junction" : "dir");
450
+ } catch (e) {
451
+ out(`warning: could not create the .meta shim (${e.message}) — 0.3-era scripts calling .meta/... will not resolve`);
452
+ return "failed";
453
+ }
454
+ ensureGitignore(target, LEGACY_META);
455
+ return kind === "link" ? "recreated" : "created";
197
456
  }
198
457
 
199
- function ensureDirs(target) {
200
- for (const d of INSTANCE_DIRS) fs.mkdirSync(path.join(target, d), { recursive: true });
458
+ // Windows: core.symlinks=false makes git traverse the junction as a plain
459
+ // directory, so without this line the framework is tracked twice (.owfa/ plus
460
+ // every .meta/* entry) and the 0.5 shim retirement reads as a mass deletion.
461
+ // Instance-git contract: adding an ignore sweeps tracked matches out — the CLI
462
+ // writes the line, and upgrade tells the user the one-time sweep command.
463
+ function ensureGitignore(target, line) {
464
+ const p = path.join(target, ".gitignore");
465
+ let text = "";
466
+ if (fs.existsSync(p)) text = fs.readFileSync(p, "utf8");
467
+ const hit = new RegExp(`^${line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m");
468
+ if (hit.test(text)) return false;
469
+ const glue = text && !text.endsWith("\n") ? "\n" : "";
470
+ fs.writeFileSync(p, `${text}${glue}# 0.4 layout shim (retires in 0.5)\n${line}\n`, "utf8");
471
+ return true;
201
472
  }
202
473
 
203
474
  function ensureAgents(target, name) {
@@ -253,7 +524,7 @@ function pythonOrFail() {
253
524
 
254
525
  function kernel(target, kernelArgs) {
255
526
  const py = pythonOrFail();
256
- const script = path.join(target, ".meta", "scripts", "wiki_plugin_kernel.py");
527
+ const script = path.join(target, ".owfa", "scripts", "owfa_kernel.py");
257
528
  if (!fs.existsSync(script)) fail(`no kernel at ${rel(target, script)} — is this an OWFA instance?`);
258
529
  const res = spawnSync(py.cmd, [...py.args, script, ...kernelArgs], {
259
530
  cwd: target,
@@ -288,11 +559,12 @@ function timestamp() {
288
559
 
289
560
 
290
561
  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 [];
562
+ for (const dir of [".owfa", LEGACY_META]) {
563
+ try {
564
+ return Object.keys(JSON.parse(fs.readFileSync(path.join(target, dir, "migrations", "pending.json"), "utf8")));
565
+ } catch {}
295
566
  }
567
+ return [];
296
568
  }
297
569
 
298
570
  function reportPending(target) {
@@ -300,7 +572,7 @@ function reportPending(target) {
300
572
  if (!pending.length) return false;
301
573
  out("");
302
574
  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]}`);
575
+ out(` run: python .owfa/scripts/owfa_kernel.py migrate ${pending[0]}`);
304
576
  out(" (read the printed page, perform the step, then rerun with --clear)");
305
577
  return true;
306
578
  }
@@ -312,35 +584,114 @@ function reportPending(target) {
312
584
  function cmdInit(args) {
313
585
  const target = path.resolve(args._[0] || ".");
314
586
  const force = args.flags.has("force");
315
- const hasMeta = fs.existsSync(path.join(target, ".meta"));
316
- if (hasMeta && !force) {
587
+ const hasOwfa = fs.existsSync(path.join(target, ".owfa"));
588
+ const hasLegacyMeta = fs.existsSync(path.join(target, LEGACY_META));
589
+ if (hasLegacyMeta && !hasOwfa) {
317
590
  fail(
318
- `${target} already contains .meta/ — refusing to overwrite. Use \`owfa upgrade\` to update the framework, ` +
591
+ `${target} uses the 0.3 layout (.meta/) — run \`owfa upgrade\` to migrate to the .owfa layout ` +
592
+ `(the upgrade preserves all instance state and leaves a .meta shim for old paths).`
593
+ );
594
+ }
595
+ if (hasOwfa && !force) {
596
+ fail(
597
+ `${target} already contains .owfa/ — refusing to overwrite. Use \`owfa upgrade\` to update the framework, ` +
319
598
  `or pass --force to re-copy the framework trees (instance data is never touched either way).`
320
599
  );
321
600
  }
322
601
  const name = args.name || path.basename(target) || "workspace";
323
602
  fs.mkdirSync(target, { recursive: true });
324
603
 
604
+ // two-stage bootstrap (v0.4 core de-parasitization, final shape). Stage ①: the
605
+ // kernel payload. Stage ② (after convergence): the personal suite — by default
606
+ // the blessed copy embedded in this channel tarball (offline, --path channel);
607
+ // `--core none` skips it; `--core <pkg-id>` takes the suite from the registry.
608
+ // Defensive compat branch: this channel stopped shipping .owfa/plugins/core/ in
609
+ // v0.4.0 — if a future (or hand-assembled) channel tree ever carries core again,
610
+ // the suite already lives in the framework tree and stage ② must not double-
611
+ // install; saying otherwise is refused up front.
612
+ const coreShipped =
613
+ fs.existsSync(path.join(CLI_ROOT, ".owfa", "plugins", "core")) ||
614
+ fs.existsSync(path.join(CLI_ROOT, ".meta", "plugins", "core"));
615
+ if (coreShipped && args.core && args.core !== "none") {
616
+ fail(
617
+ `--core ${args.core}: this channel still ships the core suite inside the framework tree ` +
618
+ `(compat period) — suite install is unavailable until the channel stops shipping core`
619
+ );
620
+ }
621
+
325
622
  out(`OWFA init — framework ${FRAMEWORK_VERSION} → ${target}`);
326
- if (!hasMeta && fs.existsSync(path.join(target, ".agents", "skills"))) {
623
+ if (!hasOwfa && fs.existsSync(path.join(target, ".agents", "skills"))) {
327
624
  out("note: .agents/skills/ exists — framework skill copies are updated in place, other skills stay untouched");
328
625
  }
329
- if (hasMeta) out("warning: --force replaces the framework trees; instance vault/ and wiki/ are untouched");
330
- copyPayload(target, { replace: hasMeta });
331
- ensureDirs(target);
626
+ if (hasOwfa) out("warning: --force replaces the framework trees; instance vault/ and wiki/ are untouched");
627
+ copyPayload(target, { replace: hasOwfa });
628
+ // P1 (review): the shim is part of the 0.4 layout for EVERY instance shape —
629
+ // released 0.3 packages (cuhksz/lark/github) still instruct .meta/ paths,
630
+ // fresh instances included.
631
+ if (ensureMetaShim(target) === "created") {
632
+ out("shim: .meta → .owfa written — 0.3-era package paths keep working (retires in 0.5)");
633
+ }
332
634
  out(`agents.md: ${ensureAgents(target, name)}`);
333
635
 
334
636
  out("");
335
- out("converging projections (python .meta/scripts/wiki_plugin_kernel.py all)");
637
+ out("converging projections (python .owfa/scripts/owfa_kernel.py all)");
336
638
  if (kernel(target, ["all"]) !== 0) {
337
639
  const pending = pendingPackages(target);
338
640
  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>\``);
641
+ fail(`convergence blocked by pending semantic migrations (${pending.join(", ")}) — clear them with \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\``);
340
642
  }
341
643
  fail("kernel convergence failed — the framework tree is incomplete; nothing was deleted, fix and rerun");
342
644
  }
343
645
 
646
+ // ---- stage ②: the core suite ----
647
+ let suiteNote;
648
+ if (coreShipped) {
649
+ suiteNote = "core suite ships inside the framework tree (defensive compat branch — this channel does not)";
650
+ out(`suite: ${suiteNote} — no separate suite package is installed`);
651
+ } else if (args.core === "none") {
652
+ suiteNote = "no suite installed — this instance cannot install packages that depend on core capabilities until a suite is installed";
653
+ out(`suite: ${suiteNote}`);
654
+ } else if (args.core) {
655
+ out("");
656
+ out(`installing core suite ${args.core} from the registry (no --yes — the trust gate stays interactive)`);
657
+ if (kernel(target, ["install", args.core]) !== 0) {
658
+ fail(`suite package ${args.core} failed to install — the framework itself is in place; run \`owfa doctor\``);
659
+ }
660
+ suiteNote = `suite ${args.core} installed from the registry`;
661
+ if (reportPending(target)) {
662
+ fail(`framework and suite ${args.core} are installed, but the workspace is not converged until the semantic migration above is cleared`);
663
+ }
664
+ } else {
665
+ // default: the embedded blessed copy — 100% offline, local --path trust.
666
+ // A copy without plugin masters is the placeholder state (suite repo not yet
667
+ // synced into the channel): report it exactly like --core none. A --force
668
+ // re-init over an instance that already carries the suite keeps it (the
669
+ // package tree is instance state, the channel never replaces it here).
670
+ const pkgDir = path.join(target, ".owfa", "plugins", "domains", "personal");
671
+ const hasMasters =
672
+ fs.existsSync(path.join(BLESSED_SUITE, "PKG.yaml")) &&
673
+ fs.readdirSync(BLESSED_SUITE, { withFileTypes: true }).some(
674
+ (d) => d.isDirectory() && fs.existsSync(path.join(BLESSED_SUITE, d.name, "PLUGIN.yaml"))
675
+ );
676
+ if (fs.existsSync(pkgDir)) {
677
+ suiteNote = "personal suite already installed — kept";
678
+ out(`suite: ${suiteNote}`);
679
+ } else if (hasMasters) {
680
+ out("");
681
+ out("installing the blessed personal suite (embedded copy — offline)");
682
+ if (kernel(target, ["install", "personal", "--path", BLESSED_SUITE]) !== 0) {
683
+ fail("the embedded personal suite failed to install — the channel tarball is inconsistent; report this (the framework itself is in place)");
684
+ }
685
+ suiteNote = "personal suite installed (embedded blessed copy)";
686
+ if (reportPending(target)) {
687
+ fail("framework and suite are installed, but the workspace is not converged until the semantic migration above is cleared");
688
+ }
689
+ } else {
690
+ 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)";
691
+ out(`suite: ${suiteNote}`);
692
+ }
693
+ }
694
+
344
695
  let installed = 0;
345
696
  if (args.packages) {
346
697
  for (const pkg of String(args.packages)
@@ -349,7 +700,9 @@ function cmdInit(args) {
349
700
  .filter(Boolean)) {
350
701
  out("");
351
702
  out(`installing package ${pkg} from the registry`);
352
- const installArgs = ["install", pkg, "--yes"];
703
+ // no --yes forwarding (v0.4): auto-installable trust levels pass on their
704
+ // own; anything else must go through the kernel's interactive trust gate
705
+ const installArgs = ["install", pkg];
353
706
  if (args.without) installArgs.push("--without", String(args.without));
354
707
  if (kernel(target, installArgs) !== 0) fail(`package ${pkg} failed to install (earlier packages stay installed)`);
355
708
  installed++;
@@ -358,12 +711,12 @@ function cmdInit(args) {
358
711
  }
359
712
 
360
713
  out("");
361
- out(`done — framework ${FRAMEWORK_VERSION}${installed ? ` + ${installed} package(s)` : ""} in ${target}`);
714
+ out(`done — framework ${FRAMEWORK_VERSION}${installed ? ` + ${installed} package(s)` : ""}; ${suiteNote} in ${target}`);
362
715
  out("");
363
716
  out("next:");
364
717
  out(` cd ${target}`);
365
718
  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\"");
719
+ out(` skeleton dirs (vault/, wiki/...) came with the suite — its PKG.yaml \`skeleton:\` declared them`);
367
720
  out(" owfa doctor # anytime: python, framework, kernel validate");
368
721
  return 0;
369
722
  }
@@ -371,44 +724,64 @@ function cmdInit(args) {
371
724
  function cmdUpgrade(args) {
372
725
  const target = path.resolve(args._[0] || ".");
373
726
  const local = readVersion(target);
374
- if (!local) fail(`${target} is not an OWFA instance (no .meta/VERSION) — run \`owfa init\` first`);
727
+ if (!local) fail(`${target} is not an OWFA instance (no .owfa/VERSION or .meta/VERSION) — run \`owfa init\` first`);
728
+ const legacyLayout = !fs.existsSync(path.join(target, ".owfa", "VERSION"));
375
729
  const incoming = FRAMEWORK_VERSION;
376
730
  const cmp = cmpVersion(local, incoming);
377
731
  const frozen = args.flags.has("check") || args.flags.has("dry-run");
378
732
  const force = args.flags.has("force");
379
733
 
380
734
  out(`OWFA upgrade — instance ${local} → channel ${incoming} (CLI ${PKG.version})`);
735
+ if (legacyLayout) {
736
+ out(`layout migration: 0.3 instance (.meta/) → 0.4 layout (.owfa/ + .meta shim for old paths)`);
737
+ }
381
738
  if (cmp > 0) {
382
739
  if (!force) fail(`instance framework ${local} is newer than this CLI's ${incoming} — run \`npx owfa@latest upgrade\``);
383
740
  out(`warning: downgrading framework ${local} → ${incoming} (--force)`);
384
741
  }
385
- if (cmp === 0 && !force) {
742
+ if (cmp === 0 && !force && !legacyLayout) {
386
743
  out("already at this framework version — nothing to do (pass --force to re-sync the trees)");
387
744
  return 0;
388
745
  }
389
746
  if (frozen) {
390
- out(`would replace ${PAYLOAD.join(" + ")} and rerun kernel all (dry run)`);
747
+ out(`would replace ${PAYLOAD.join(" + ")}${legacyLayout ? " (migrating .meta/ → .owfa/)" : ""} and rerun kernel all (dry run)`);
391
748
  return 0;
392
749
  }
393
750
 
394
751
  if (args.flags.has("backup")) {
395
- const dir = path.join(target, ".owfa", "backup", `${local}-${timestamp()}`);
752
+ const dir = path.join(target, BACKUP_DIR, `${local}-${timestamp()}`);
396
753
  for (const entry of PAYLOAD) {
397
- const src = path.join(target, entry);
754
+ const src = path.join(target, entry === ".owfa" && legacyLayout ? LEGACY_META : entry);
398
755
  if (fs.existsSync(src)) fs.cpSync(src, path.join(dir, entry), { recursive: true, force: true });
399
756
  }
400
- out(`backup: ${rel(target, dir)} (add .owfa/ to the instance .gitignore if you do not want it tracked)`);
757
+ out(`backup: ${rel(target, dir)} (add .owfa-backup/ to the instance .gitignore if you do not want it tracked)`);
401
758
  }
402
759
 
403
760
  out(`warning: local edits under ${PAYLOAD.join("/, ")}/ are replaced by the channel's tree — keep instance hotfixes in git`);
404
761
  copyPayload(target, { replace: true });
762
+ // v0.4 core de-parasitization: if the replace rescued a parked core/ (the
763
+ // channel stopped shipping it), convert it into the personal suite package
764
+ // BEFORE converging — the conversion's own install converges the adopted set.
765
+ convertCoreToPersonal(target);
766
+ // P2 (review): self-heal a stale shim here too — instance dirs get renamed and
767
+ // copied, and Windows junction targets are absolute. The .gitignore sweep hint
768
+ // covers the "tracked twice" half only the user's git can perform.
769
+ const shimState = ensureMetaShim(target);
770
+ if (legacyLayout) {
771
+ if (shimState === "created" || shimState === "recreated") {
772
+ out(`shim: .meta → .owfa ${shimState} — 0.3-era paths keep resolving until the shim retires (0.5)`);
773
+ }
774
+ if (shimState !== "failed") {
775
+ out("instance git: if .meta/ was tracked before 0.4, sweep it once — git rm -r --cached .meta");
776
+ }
777
+ }
405
778
 
406
779
  out("");
407
- out("converging projections (python .meta/scripts/wiki_plugin_kernel.py all)");
780
+ out("converging projections (python .owfa/scripts/owfa_kernel.py all)");
408
781
  if (kernel(target, ["all"]) !== 0) {
409
782
  const pending = pendingPackages(target);
410
783
  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>\``);
784
+ fail(`trees replaced, but convergence is blocked by pending semantic migrations (${pending.join(", ")}) — run \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\``);
412
785
  }
413
786
  fail("kernel convergence failed after the tree replacement — the instance git snapshot is your rollback");
414
787
  }
@@ -416,10 +789,10 @@ function cmdUpgrade(args) {
416
789
  const pending = pendingPackages(target);
417
790
  if (pending.length) {
418
791
  out("");
419
- out(`pending semantic migrations: ${pending.join(", ")} — run \`python .meta/scripts/wiki_plugin_kernel.py migrate <pkg>\` and follow the page`);
792
+ out(`pending semantic migrations: ${pending.join(", ")} — run \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\` and follow the page`);
420
793
  }
421
794
  out("");
422
- out(`done — framework ${local} → ${incoming}`);
795
+ out(`done — framework ${local} → ${incoming}${legacyLayout ? " (layout migrated to .owfa/)" : ""}`);
423
796
  return 0;
424
797
  }
425
798
 
@@ -431,9 +804,44 @@ function cmdDoctor(args) {
431
804
  out(`node ${process.version}`);
432
805
  out(`python ${py ? py.version || `${py.cmd} ${py.args.join(" ")}` : "NOT FOUND"}`);
433
806
  out(`target ${target}`);
434
- if (!local) fail("not an OWFA instance (no .meta/VERSION) — run `owfa init` first");
807
+ if (!local) fail("not an OWFA instance (no .owfa/VERSION) — run `owfa init` first");
435
808
  out(`instance framework ${local}`);
436
809
  const problems = []; // required conditions — any hit fails doctor (review P2, PR #53)
810
+ // layout-shim health (review P2 on the layout PR): a renamed/copied instance
811
+ // leaves the Windows junction pointing at the old absolute path; a real .meta
812
+ // directory means the 0.3 layout was never migrated. doctor reports; upgrade heals.
813
+ const owfaDir = path.join(target, ".owfa");
814
+ const metaPath = path.join(target, LEGACY_META);
815
+ if (fs.existsSync(owfaDir)) {
816
+ // lstat, not existsSync: a dangling shim must reach the stale branch, not
817
+ // silently skip the whole check (review P2, PR #57 round 2).
818
+ let mst = null;
819
+ try {
820
+ mst = fs.lstatSync(metaPath);
821
+ } catch {
822
+ mst = null;
823
+ }
824
+ if (!mst) {
825
+ out(".meta: shim missing — 0.3-era .meta/... paths will not resolve; run `owfa upgrade` to re-create it");
826
+ problems.push(".meta shim is missing");
827
+ } else if (!mst.isSymbolicLink()) {
828
+ out(".meta: real directory — 0.3 layout never migrated; run `owfa upgrade`");
829
+ problems.push(".meta is not the layout shim");
830
+ } else {
831
+ let same = false;
832
+ try {
833
+ same = fs.realpathSync(metaPath) === fs.realpathSync(owfaDir);
834
+ } catch {
835
+ same = false; // dangling
836
+ }
837
+ if (!same) {
838
+ out(".meta: shim resolves outside this instance — run `owfa upgrade` to re-create it");
839
+ problems.push(".meta shim target is stale");
840
+ } else {
841
+ out(".meta shim: ok (resolves into this instance)");
842
+ }
843
+ }
844
+ }
437
845
  const agents = path.join(target, "AGENTS.md");
438
846
  if (!fs.existsSync(agents)) {
439
847
  out("AGENTS.md: MISSING — the agent will not see the structural contract");
@@ -493,10 +901,14 @@ function usage() {
493
901
  the owfa framework's install and upgrade channel
494
902
 
495
903
  usage:
496
- owfa init [dir] [--name NAME] [--packages a,b] [--force]
497
- deploy a fresh workspace (framework ${FRAMEWORK_VERSION}); additive to existing content
904
+ owfa init [dir] [--name NAME] [--core none|<pkg-id>] [--packages a,b] [--force]
905
+ deploy a fresh workspace (framework ${FRAMEWORK_VERSION}); additive to existing content;
906
+ two-stage: kernel payload, then the core suite (default: embedded blessed copy,
907
+ offline; --core none skips it; --core <pkg-id> installs from the registry)
498
908
  owfa upgrade [dir] [--check] [--backup] [--force]
499
- replace the framework trees with this channel's version and converge
909
+ replace the framework trees with this channel's version and converge;
910
+ if this channel stopped shipping core, the instance's core plugins are
911
+ converted into the personal suite package (mechanical adoption)
500
912
  owfa doctor [dir] python / framework / AGENTS.md / kernel validate
501
913
  owfa add <pkg> [--yes] [--without a,b]
502
914
  install a domain package from the registry, then converge
@@ -509,19 +921,25 @@ notes:
509
921
  - python 3.9+ is required to run the framework; node is only the installer
510
922
  - \`init\` never deletes instance data; \`upgrade\` preserves the kernel's
511
923
  migration ledger, journal and registry cache
924
+ - skeleton dirs (vault/, wiki/...) come from package declarations (PKG.yaml
925
+ \`skeleton:\`), created on install — the default init already installed the
926
+ embedded personal suite and its skeleton
512
927
  - no install/postinstall scripts: npm install executes nothing
513
928
  `);
514
929
  }
515
930
 
516
931
  const COMMAND_HELP = {
517
- init: "usage: owfa init [dir] [--name NAME] [--packages a,b] [--without m,n] [--force]\n\n" +
932
+ init: "usage: owfa init [dir] [--name NAME] [--core none|<pkg-id>] [--packages a,b] [--without m,n] [--force]\n\n" +
518
933
  "deploy a fresh workspace (additive: existing files and AGENTS.md are preserved).\n" +
519
934
  "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" +
935
+ "two-stage bootstrap: ① kernel payload ② core suite — default the blessed copy embedded\n" +
936
+ "in the channel tarball (offline); --core none skips stage ②; --core <pkg-id> installs\n" +
937
+ "the suite from the registry (online; the kernel's trust gate stays interactive).\n" +
938
+ "--packages installs further registry packages; --without prunes members (dependency-closed subsets only).\n" +
521
939
  "--force re-copies the framework trees over an existing instance (instance data is never touched).",
522
940
  upgrade: "usage: owfa upgrade [dir] [--check] [--backup] [--force]\n\n" +
523
941
  "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" +
942
+ "--check (alias --dry-run) is read-only; --backup snapshots the current trees to .owfa-backup/.\n" +
525
943
  "instance data, installed packages, pending migrations and standalone skills are preserved.",
526
944
  doctor: "usage: owfa doctor [dir]\n\n" +
527
945
  "health check: python, framework version, AGENTS.md injection markers, kernel validate.\n" +