@litfamily/litopencode 1.0.1 → 1.0.3

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 (76) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README-Ko-KR.md +10 -7
  3. package/README.md +10 -7
  4. package/dist/activation-managed-prompts.js +4 -11
  5. package/dist/activation-primary-prompts.js +5 -12
  6. package/dist/activation-probe.d.ts +1 -0
  7. package/dist/activation-probe.js +5 -2
  8. package/dist/activation-prompt-utils.d.ts +2 -2
  9. package/dist/activation-prompt-utils.js +3 -3
  10. package/dist/activation-routing.d.ts +1 -1
  11. package/dist/activation-routing.js +1 -8
  12. package/dist/activation-workflow-prompts.d.ts +0 -1
  13. package/dist/activation-workflow-prompts.js +3 -14
  14. package/dist/activation.d.ts +4 -2
  15. package/dist/activation.js +22 -1
  16. package/dist/cli/args.d.ts +1 -1
  17. package/dist/cli/args.js +1 -31
  18. package/dist/cli/loop.js +0 -19
  19. package/dist/cli/managed-skill-assets.d.ts +1 -1
  20. package/dist/cli/managed-skill-assets.js +0 -2
  21. package/dist/cli/types.d.ts +0 -4
  22. package/dist/cli.js +0 -14
  23. package/dist/commands.d.ts +1 -10
  24. package/dist/commands.js +34 -44
  25. package/dist/features.d.ts +1 -22
  26. package/dist/features.js +0 -43
  27. package/dist/hooks.js +2 -15
  28. package/dist/ignition.js +1 -1
  29. package/dist/index.d.ts +0 -5
  30. package/dist/index.js +0 -5
  31. package/dist/scientific-visualization-banner.js +2 -2
  32. package/dist/server.js +2 -1
  33. package/dist/skills.d.ts +1 -8
  34. package/dist/skills.js +0 -13
  35. package/dist/tools.js +2 -2
  36. package/dist/workflow-families.js +3 -3
  37. package/docs/assets/cover-motion.webp +0 -0
  38. package/docs/assets/readme/badge-version.svg +1 -1
  39. package/docs/lit-mark.md +12 -8
  40. package/docs/privacy.md +2 -2
  41. package/docs/reference.md +5 -48
  42. package/package.json +2 -1
  43. package/skills/frontend-ui-ux/references/complete-contract.md +1 -1
  44. package/skills/lit-handoff/SKILL.md +3 -3
  45. package/skills/lit-plan/SKILL.md +1 -1
  46. package/skills/lit-scientific-visualization/SKILL.md +3 -3
  47. package/skills/managed-skill-manifest.json +2 -9
  48. package/skills/visual-qa/references/complete-contract.md +1 -1
  49. package/tools/check-payload-substance.mjs +74 -27
  50. package/tools/harness-speed-local.mjs +2 -1
  51. package/tools/payload-substance-parity.json +10 -25
  52. package/tools/version-manifests.json +2 -2
  53. package/dist/cli/skill-loop.d.ts +0 -3
  54. package/dist/cli/skill-loop.js +0 -649
  55. package/dist/skill-loop/apply.d.ts +0 -19
  56. package/dist/skill-loop/apply.js +0 -483
  57. package/dist/skill-loop/config.d.ts +0 -50
  58. package/dist/skill-loop/config.js +0 -215
  59. package/dist/skill-loop/curator.d.ts +0 -18
  60. package/dist/skill-loop/curator.js +0 -232
  61. package/dist/skill-loop/ledger.d.ts +0 -39
  62. package/dist/skill-loop/ledger.js +0 -344
  63. package/dist/skill-loop/proposals.d.ts +0 -48
  64. package/dist/skill-loop/proposals.js +0 -290
  65. package/dist/skill-loop/storage.d.ts +0 -72
  66. package/dist/skill-loop/storage.js +0 -817
  67. package/dist/skill-loop/time.d.ts +0 -2
  68. package/dist/skill-loop/time.js +0 -23
  69. package/dist/skill-loop/transaction.d.ts +0 -62
  70. package/dist/skill-loop/transaction.js +0 -836
  71. package/dist/skill-loop/usage.d.ts +0 -31
  72. package/dist/skill-loop/usage.js +0 -146
  73. package/dist/skill-observer.d.ts +0 -22
  74. package/dist/skill-observer.js +0 -1154
  75. package/skills/skill-observer/SKILL.md +0 -148
  76. package/skills/skill-observer/references/review-contract.md +0 -95
package/docs/lit-mark.md CHANGED
@@ -31,16 +31,20 @@ non-TTY output and JSON also disable ANSI. A non-UTF-8 locale or `TERM=dumb` sel
31
31
  Locale precedence is `LC_ALL`, `LC_CTYPE`, then `LANG`; an absent locale is conservative.
32
32
 
33
33
  OpenCode's first `experimental.text.complete` for each session displays the standard
34
- mark. Each later completion displays micro with `litopencode · <discipline>`. The
35
- completion hook is the session-start surface because plugin initialization has no
36
- session ID. Deleting a session clears both its first-completion and discipline state;
37
- plugin disposal clears all state. A new root user turn resets the prior discipline,
34
+ mark. Each later completion displays micro with `🔥 LIT IGNITED · <discipline> 🔥`
35
+ after its middle row. The completion hook is the session-start surface because plugin
36
+ initialization has no session ID. Deleting a session clears both its first-completion
37
+ and discipline state; plugin disposal clears all state. A new root user turn resets the prior discipline,
38
38
  and a trusted chat, command, or successful activation-tool result selects the current
39
39
  one. Status and blocked tool results do not change the discipline. Unactivated turns use `lit-loop`.
40
40
  Marks are fenced Markdown without ANSI because OpenCode renders completion text as
41
41
  Markdown, including when its process owns a colored terminal.
42
42
 
43
- The model must begin its reply with exactly one `▲ LIT · <discipline>` line. Prompts
44
- require this explicitly. The completion hook adds the separate harness mark and
45
- preserves the model's response, including a missing or malformed probe, so the harness
46
- cannot manufacture evidence that the model obeyed the prompt.
43
+ The model must begin its reply with exactly one `🔥 **LIT IGNITED · <discipline>** 🔥` line. Prompts
44
+ require this explicitly. Activation messages use the plain `🔥 LIT IGNITED · <discipline> 🔥`
45
+ banner. On successful activation, the plugin also requests a six-second warning toast titled
46
+ `🔥 LIT IGNITED` with the five trimmed micro rows followed by that label when the environment
47
+ supports mark glyphs; otherwise it shows only the label. Toast messages never contain ANSI. A
48
+ missing or failing TUI client never blocks prompt injection. The completion hook adds the separate
49
+ harness mark and preserves the model's response, including a missing or malformed probe, so the
50
+ harness cannot manufacture evidence that the model obeyed the prompt.
package/docs/privacy.md CHANGED
@@ -12,7 +12,7 @@ providers, npm, and other tools have their own behavior and policies.
12
12
  | --- | --- |
13
13
  | Project `.litopencode/config.json` and `state.json` | Project configuration and runtime state. |
14
14
  | Project `.litopencode/litgoal/lit-loop/` | Goals, criteria, checkpoints, session identifiers, evidence references, a brief, ledger events, and bounded lifecycle state used to resume work. Evidence files can contain commands, output, or project material. |
15
- | Project `.litopencode/knowledge/` and skill-learning state | Locally captured knowledge, reviews, proposals, and mutation/rollback records when those features are used. |
15
+ | Project `.litopencode/knowledge/` | Locally captured knowledge claims and review state. Legacy skill-learning files from earlier releases are left untouched and are no longer read. |
16
16
  | Project `.litopencode/logs/litopencode.log` | The logger's JSON-lines destination for timestamps, levels, and messages when explicitly enabled. The current server creates the logger with logging disabled by default. |
17
17
  | OpenCode config root | Plugin registration, `litopencode.json` routes and choices, and installed command/skill files. The default is `~/.config/opencode`; `XDG_CONFIG_HOME` or a CLI `--root` changes the applicable root. |
18
18
  | Home `~/.litopencode/` | Update cache, locks, transaction journal and receipt, and retained recovery backups after an unsuccessful automatic update. Backups can contain OpenCode configuration and installed command/skill content. |
@@ -76,7 +76,7 @@ See the [removal reference](reference.md#remove) and [security reporting](../SEC
76
76
 
77
77
  In a source checkout, the relevant implementations are `src/state.ts`,
78
78
  `src/logger.ts`, `src/server.ts`, `src/ledger.ts`, `src/knowledge.ts`,
79
- `src/skill-loop/`, `src/cli/update-notifier.ts`, `src/cli/auto-update.ts`,
79
+ `src/cli/update-notifier.ts`, `src/cli/auto-update.ts`,
80
80
  `src/cli/model-catalog.ts`, and `src/lit-fetch*.ts`. Source files are excluded from
81
81
  the npm payload; package users receive the compiled runtime. These boundaries
82
82
  should be reviewed whenever storage, update, or retrieval behavior changes.
package/docs/reference.md CHANGED
@@ -189,54 +189,12 @@ Native goal verdict: the OpenCode host surface does not currently expose a nativ
189
189
  LitOpenCode uses `.litopencode/litgoal` instead. Static skills are installed under
190
190
  `skills/*/SKILL.md` and remain documentation unless a documented OpenCode route selects them.
191
191
 
192
- ## Skill learning loop
192
+ ## Skill learning state
193
193
 
194
- Skill Observer can turn bounded observations and a per-session tool-iteration reminder into
195
- pending skill proposals. The hook only updates project-local counters and surfaces a notice; it
196
- does not call a model or edit a skill. Review remains a foreground user action:
197
-
198
- ```sh
199
- litopencode skill-loop review
200
- litopencode skill-loop propose <proposal.json>
201
- litopencode skill-loop list
202
- litopencode skill-loop apply <proposal-id>
203
- litopencode skill-loop reject <proposal-id>
204
- litopencode skill-loop rollback <ledger-id>
205
- litopencode skill-curator status
206
- litopencode skill-curator run
207
- ```
208
-
209
- Nothing is applied without the explicit `skill-loop apply` command; shipped configuration keeps
210
- `autoApply: false`. New skills are written only to the OpenCode user skill root —
211
- `$XDG_CONFIG_HOME/opencode/skills/<name>/`, or `~/.config/opencode/skills/<name>/` when XDG is
212
- unset — with the distinct `metadata.litopencodeAgentGenerated: "true"` marker. Shipped,
213
- installer-managed, unmarked, and unsafe targets are refused.
214
-
215
- `skill-loop review` runs in an empty temporary home/config directory with tools and permissions
216
- denied, on the configured `review` category route (provider, model, and effort from
217
- `litopencode.json`) so a Grok-configured install never needs an OpenAI token. Set
218
- `LITOPENCODE_REVIEW_HOST` only to an absolute OpenCode launcher you intend LitOpenCode
219
- to pin and execute. If a host-native credential is already available as a narrow environment
220
- variable, opt in by naming it with `LITOPENCODE_REVIEW_CREDENTIAL_ENV=NAME[,NAME...]`; when that
221
- descriptor is unset, the configured review provider's own variable (for example `XAI_API_KEY`)
222
- is forwarded if present. OAuth-only hosts can opt in with `LITOPENCODE_REVIEW_AUTH=host`, which
223
- copies exactly the configured review provider's entry from the OpenCode auth store into the
224
- isolated child — nothing else. Help probes remain credential-free, and credential values are
225
- never placed in arguments, prompts, logs, or evidence.
226
-
227
- The learning loop's review, user-skill mutation, rollback, and curator boundaries currently support
228
- POSIX hosts (including macOS and Linux). On Windows those global/model boundaries fail closed with
229
- `WINDOWS_SKILL_LOOP_UNSUPPORTED` pending native handle and ACL support. Project-local listing and
230
- proposal intake remain available; this limitation does not affect install, doctor, or other passive
231
- LitOpenCode surfaces.
232
-
233
- Each successful mutation records content-addressed before/after state in the project-local
234
- `.litopencode/skill-ledger.jsonl`; rollback first verifies that current bytes still match the
235
- recorded after-image. The shipped config records a seven-day curator interval and a two-hour idle
236
- window for callers, but this release starts no background scheduler. A user-invoked
237
- `skill-curator run` marks eligible active entries stale after 30 days and moves already-stale
238
- entries to a recoverable archive after 90 days. Pinned entries are skipped, and archive transitions
239
- preserve bytes rather than deleting them.
194
+ Earlier releases wrote observation, review, and mutation records under project `.litopencode`
195
+ directories. Current LitOpenCode does not read, modify, or remove those legacy files; they are inert
196
+ and remain under the project owner's control. Other project-local state such as the goal ledger and
197
+ knowledge store continues to work independently.
240
198
 
241
199
  ## Runtime Skills
242
200
 
@@ -292,7 +250,6 @@ flowchart LR
292
250
  - <code>review-work</code>
293
251
  - <code>rules</code>
294
252
  - <code>search-workflow-ideas</code>
295
- - <code>skill-observer</code>
296
253
  - <code>start-work</code>
297
254
  - <code>structural-search</code>
298
255
  - <code>lit-korean</code>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/litopencode",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "LitOpenCode OpenCode plugin bootstrap package.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,7 @@
30
30
  "check:version": "node tools/check-version-lockstep.mjs",
31
31
  "check:managed-skill-manifest": "node tools/gen-managed-skill-manifest.mjs --check",
32
32
  "gen:managed-skill-manifest": "node tools/gen-managed-skill-manifest.mjs",
33
+ "prepublishOnly": "node tools/prepublish-test-gate.mjs",
33
34
  "prepack": "npm run build",
34
35
  "pretest": "npm run build",
35
36
  "qa:behavior-replacement": "node tools/run-behavior-replacement-probes.mjs",
@@ -254,7 +254,7 @@ $ opencode debug skill # OpenCode 1.18.4
254
254
  frontend-ui-ux -> $ISO_ROOT/skills/frontend-ui-ux/SKILL.md
255
255
 
256
256
  $ opencode debug config
257
- plugin: ["file:///.../32_litopencode"]
257
+ plugin: ["file:///path/to/litopencode"]
258
258
  mcp: []
259
259
  command count: 18
260
260
  lit-plan permission: {"edit": "deny", "bash": "deny", "task": "deny"}
@@ -6,7 +6,7 @@ description: "OpenCode-native adapter for the exact Lit Handoff continuation con
6
6
  # Lit Handoff
7
7
 
8
8
  > [!IMPORTANT]
9
- > On activation, the first model-emitted line must be exactly `▲ LIT · lit-handoff`.
9
+ > On activation, the first model-emitted line must be exactly `🔥 **LIT IGNITED · lit-handoff** 🔥`.
10
10
 
11
11
  ## #contract.activation
12
12
 
@@ -17,7 +17,7 @@ skill_name: lit-handoff
17
17
  host: OpenCode
18
18
  native_entrypoint: skills/lit-handoff/SKILL.md
19
19
  exact_source_root: ../../vendor/handoff
20
- activation_banner: "▲ LIT · lit-handoff"
20
+ activation_banner: "🔥 **LIT IGNITED · lit-handoff** 🔥"
21
21
  routes:
22
22
  - /lit-handoff
23
23
  - exact bare handoff chat message
@@ -62,7 +62,7 @@ mentioned during diagnosis or review.
62
62
 
63
63
  ## #contract.procedure
64
64
 
65
- 1. Emit `▲ LIT · lit-handoff` exactly once before any other model-emitted text. The harness renders the logo separately.
65
+ 1. Emit `🔥 **LIT IGNITED · lit-handoff** 🔥` exactly once before any other model-emitted text. The harness renders its plain activation banner and logo separately.
66
66
  2. Read `../../vendor/handoff/SKILL.md` completely. Treat its destination resolution, existing-file
67
67
  classification, evidence rules, required sections, and resumability audit as binding.
68
68
  3. Resolve `SKILL_ROOT` to `../../vendor/handoff/` and read `../../vendor/handoff/templates/HANDOFF.md` before
@@ -348,4 +348,4 @@ Do not call OpenCode's task tool from `lit-plan`, even for a nominally read-only
348
348
 
349
349
  ## Approval Language
350
350
 
351
- End planning with a clear boundary. Example: “If you approve this plan, run `/start-work`; implementation should stay within `32_litopencode`, change docs/tests only, run targeted tests, then run `npm test`, and stop before any version bump, commit, tag, push, publish, or release.” This sentence prevents accidental mutation from planning mode and gives the next agent a precise starting point.
351
+ End planning with a clear boundary. Example: “If you approve this plan, run `/start-work`; implementation should stay within this product checkout, change docs/tests only, run targeted tests, then run `npm test`, and stop before any version bump, commit, tag, push, publish, or release.” This sentence prevents accidental mutation from planning mode and gives the next agent a precise starting point.
@@ -6,7 +6,7 @@ description: "OpenCode-native adapter for the exact scientific-visualization ski
6
6
  # Lit Scientific Visualization
7
7
 
8
8
  > [!IMPORTANT]
9
- > On activation, the first model-emitted line must be exactly `▲ LIT · lit-scientific-visualization`.
9
+ > On activation, the first model-emitted line must be exactly `🔥 **LIT IGNITED · lit-scientific-visualization** 🔥`.
10
10
 
11
11
  ## #contract.activation
12
12
 
@@ -17,7 +17,7 @@ skill_name: lit-scientific-visualization
17
17
  host: OpenCode
18
18
  native_entrypoint: skills/lit-scientific-visualization/SKILL.md
19
19
  exact_source_root: ../../vendor/scientific-visualization
20
- activation_banner: "▲ LIT · lit-scientific-visualization"
20
+ activation_banner: "🔥 **LIT IGNITED · lit-scientific-visualization** 🔥"
21
21
  routes:
22
22
  - /lit-scientific-visualization
23
23
  - exact bare lit-scientific-visualization
@@ -139,7 +139,7 @@ limitations_channel: reply
139
139
  - Route safety: only `/lit-scientific-visualization`, exact bare
140
140
  `lit-scientific-visualization`, and native discovery invoke the adapter; generic,
141
141
  quoted, fenced, multipart mixed, and near-miss forms remain inert.
142
- - Probe safety: the model emits `▲ LIT · lit-scientific-visualization` exactly once as
142
+ - Probe safety: the model emits `🔥 **LIT IGNITED · lit-scientific-visualization** 🔥` exactly once as
143
143
  its first reply line. The OpenCode completion hook renders a standard mark for the
144
144
  first session completion and micro marks later, without fabricating the model probe
145
145
  or sharing activation state across sessions.
@@ -189,7 +189,7 @@
189
189
  },
190
190
  {
191
191
  "path": "references/complete-contract.md",
192
- "sha256": "792df3962f252cb0625879aaa87daf362e290b4ca19ef92f852f4315fe3f4fb5"
192
+ "sha256": "74732c1ae7c720133a7c20c2cc1836453393a5a61ef556f62dcc8162116b8401"
193
193
  },
194
194
  {
195
195
  "path": "references/composition.md",
@@ -992,7 +992,7 @@
992
992
  },
993
993
  {
994
994
  "path": "references/complete-contract.md",
995
- "sha256": "c9f472ddd692851601414cb1eef2564fa9935edb9f382e33b3cd38650e97cd3b"
995
+ "sha256": "0dd57b7340d9d0488aa88909f7bafcd8aac859c6fc36344e994f71f9c5667ecf"
996
996
  },
997
997
  {
998
998
  "path": "schemas/evidence-manifest-v1alpha1.json",
@@ -1364,13 +1364,6 @@
1364
1364
  "sha256": "f624ad714f2c2420d9b1050dfecfa6dfa8384197bc2ea3822d8e73cc3fd7f28e"
1365
1365
  }
1366
1366
  ]
1367
- },
1368
- "skill-observer": {
1369
- "distributionFiles": [
1370
- "SKILL.md",
1371
- "references/review-contract.md"
1372
- ],
1373
- "canonicalFiles": []
1374
1367
  }
1375
1368
  },
1376
1369
  "renamedSkills": {
@@ -354,7 +354,7 @@ $ opencode debug skill # OpenCode 1.18.4
354
354
  visual-qa -> $ISO_ROOT/skills/visual-qa/SKILL.md
355
355
 
356
356
  $ opencode debug config
357
- plugin: ["file:///.../32_litopencode"]
357
+ plugin: ["file:///path/to/litopencode"]
358
358
  mcp: []
359
359
  command count: 18
360
360
  lit-plan permission: {"edit": "deny", "bash": "deny", "task": "deny"}
@@ -33,13 +33,66 @@ const PACKED_REFERENCE_ROOTS = new Set(["references", "scripts", "assets", "temp
33
33
  export const PARITY_FRACTION = 0.5;
34
34
  const PRODUCT_ID = "p32";
35
35
  const P31_PUBLISHED_PACKAGE_RELATIVE = join("packages", "litcodex-ai");
36
- const FAMILY_LAYOUT = Object.freeze([
37
- { id: "p27", packageRelative: ["27_PRIVATE_litclaude"], skillRootRelative: join("plugins", "litclaude", "skills"), packPrefix: "plugins/litclaude/skills" },
38
- { id: "p28", packageRelative: ["28_PRIVATE_lithermes", "packages", "lithermes-installer"], skillRootRelative: join("assets", "lithermes-plugin", "skills"), packPrefix: "assets/lithermes-plugin/skills" },
39
- { id: "p31", packageRelative: ["31_litcodex", "litcodex"], packRootRelative: ["31_litcodex", "litcodex", "packages", "litcodex-ai"], packWithScripts: true, skillRootRelative: join("plugins", "litcodex", "skills") },
40
- { id: "p32", packageRelative: ["32_litopencode"], skillRootRelative: "skills", packPrefix: "skills" },
41
- { id: "p33", packageRelative: ["33_litgrok"], skillRootRelative: join(".grok", "skills"), packPrefix: ".grok/skills" },
42
- ]);
36
+ const PRODUCT_IDS = Object.freeze(["p27", "p28", "p31", "p32", "p33"]);
37
+
38
+ function validateRelativePath(value, field) {
39
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\")) {
40
+ throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: ${field} must be a relative path`);
41
+ }
42
+ const parts = value.split("/");
43
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
44
+ throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: ${field} must not contain empty or parent segments`);
45
+ }
46
+ return value;
47
+ }
48
+
49
+ function validateRelativeParts(value, field) {
50
+ if (!Array.isArray(value) || value.length === 0 || value.some((part) => typeof part !== "string" || part.length === 0 || part === "." || part === ".." || part.includes("/") || part.includes("\\"))) {
51
+ throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: ${field} must be a non-empty relative path-part array`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ function normalizeFamilyLayout(value) {
57
+ if (!Array.isArray(value)) throw new Error("PAYLOAD_FAMILY_LAYOUT_INVALID: expected an array");
58
+ const seen = new Set();
59
+ const layout = value.map((entry, index) => {
60
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: entry ${index} must be an object`);
61
+ const id = entry.id;
62
+ if (!PRODUCT_IDS.includes(id) || seen.has(id)) throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: unknown or duplicate product id ${id}`);
63
+ seen.add(id);
64
+ const normalized = {
65
+ id,
66
+ packageRelative: validateRelativeParts(entry.packageRelative, `${id}.packageRelative`),
67
+ skillRootRelative: validateRelativePath(entry.skillRootRelative, `${id}.skillRootRelative`),
68
+ };
69
+ if (entry.packWithScripts === true) normalized.packWithScripts = true;
70
+ if (entry.packRootRelative !== undefined) normalized.packRootRelative = validateRelativeParts(entry.packRootRelative, `${id}.packRootRelative`);
71
+ if (!normalized.packWithScripts) normalized.packPrefix = validateRelativePath(entry.packPrefix, `${id}.packPrefix`);
72
+ return normalized;
73
+ });
74
+ if (seen.size !== PRODUCT_IDS.length || PRODUCT_IDS.some((id) => !seen.has(id))) {
75
+ throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: expected product ids ${PRODUCT_IDS.join(",")}`);
76
+ }
77
+ return Object.freeze(layout);
78
+ }
79
+
80
+ export function loadFamilyLayout(source) {
81
+ if (typeof source !== "string" || source.trim().length === 0) {
82
+ throw new Error("PAYLOAD_FAMILY_LAYOUT_REQUIRED: pass --family-layout or set LITOPENCODE_FAMILY_LAYOUT");
83
+ }
84
+ const candidate = source.trim();
85
+ let raw = candidate;
86
+ const candidatePath = resolve(process.cwd(), candidate);
87
+ if (existsSync(candidatePath) && lstatSync(candidatePath).isFile()) raw = readFileSync(candidatePath, "utf8");
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(raw);
91
+ } catch (error) {
92
+ throw new Error(`PAYLOAD_FAMILY_LAYOUT_INVALID: expected JSON text or a JSON file (${error.message})`);
93
+ }
94
+ return normalizeFamilyLayout(parsed);
95
+ }
43
96
 
44
97
  function regularFiles(directory, relativePath = "") {
45
98
  const files = [];
@@ -117,7 +170,7 @@ export function loadParityManifest(filePath) {
117
170
  if (!validIsoDate(parsed.generatedAt)) throw new Error("PARITY_MANIFEST_DATE_INVALID");
118
171
  if (typeof parsed.fraction !== "number" || !Number.isFinite(parsed.fraction) || parsed.fraction !== PARITY_FRACTION) throw new Error("PARITY_MANIFEST_FRACTION_INVALID");
119
172
  if (parsed.scope !== "parity-enrolled") throw new Error("PARITY_MANIFEST_SCOPE_INVALID");
120
- const productIds = FAMILY_LAYOUT.map(({ id }) => id);
173
+ const productIds = PRODUCT_IDS;
121
174
  const actualProductIds = Object.keys(parsed.products).sort();
122
175
  if (actualProductIds.join(",") !== [...productIds].sort().join(",")) throw new Error("PARITY_MANIFEST_PRODUCTS_INVALID");
123
176
  const products = new Map();
@@ -583,8 +636,8 @@ function measureProduct(familyRoot, layout) {
583
636
  return { id: layout.id, productRoot, skills: resolveClosures(productRoot, skillRoot, skills, packPaths, packPrefix), divergences };
584
637
  }
585
638
 
586
- export function measureFamily(familyRoot) {
587
- return FAMILY_LAYOUT.map((layout) => measureProduct(familyRoot, layout));
639
+ export function measureFamily(familyRoot, familyLayout) {
640
+ return familyLayout.map((layout) => measureProduct(familyRoot, layout));
588
641
  }
589
642
 
590
643
  function median(values) {
@@ -661,19 +714,9 @@ export function evaluateManifestFreshness({ manifest, measurements }) {
661
714
  return { failures };
662
715
  }
663
716
 
664
- function findFamilyRoot(start) {
665
- let cursor = resolve(start);
666
- for (;;) {
667
- if (FAMILY_LAYOUT.every((layout) => existsSync(resolve(cursor, ...layout.packageRelative)))) return cursor;
668
- const parent = dirname(cursor);
669
- if (parent === cursor) return undefined;
670
- cursor = parent;
671
- }
672
- }
673
-
674
- function matchingProductId(familyRoot, productRoot) {
717
+ function matchingProductId(familyRoot, productRoot, familyLayout) {
675
718
  const resolvedProduct = resolve(productRoot);
676
- return FAMILY_LAYOUT.find((layout) => resolve(familyRoot, ...layout.packageRelative) === resolvedProduct)?.id;
719
+ return familyLayout.find((layout) => resolve(familyRoot, ...layout.packageRelative) === resolvedProduct)?.id;
677
720
  }
678
721
 
679
722
  function option(argv, name) {
@@ -685,7 +728,7 @@ function option(argv, name) {
685
728
  }
686
729
 
687
730
  export function run(argv = process.argv.slice(2)) {
688
- const allowed = new Set(["--skill-root", "--allowlist-file", "--pack-json", "--family-root", "--product-root", "--product-id", "--parity-manifest", "--reference-exemptions"]);
731
+ const allowed = new Set(["--skill-root", "--allowlist-file", "--pack-json", "--family-root", "--family-layout", "--product-root", "--product-id", "--parity-manifest", "--reference-exemptions"]);
689
732
  for (const arg of argv) if (arg.startsWith("--") && !allowed.has(arg)) throw new Error(`UNKNOWN_OPTION: ${arg}`);
690
733
  const productRoot = resolve(option(argv, "--product-root") ?? PRODUCT_ROOT);
691
734
  const skillRoot = resolve(productRoot, option(argv, "--skill-root") ?? SKILL_ROOT_RELATIVE);
@@ -706,12 +749,15 @@ export function run(argv = process.argv.slice(2)) {
706
749
  if (!targetId) throw new Error("PAYLOAD_PARITY_TARGET_UNKNOWN: pass --product-id for a product outside the family root");
707
750
  parity = evaluateManifestParity({ manifest, targetId, productRoot, skillRoot, packPaths, packPrefix: PACK_PREFIX, divergences: loadParityDivergences(allowlistPath) });
708
751
  }
709
- const requestedFamilyRoot = option(argv, "--family-root");
710
- const familyRoot = requestedFamilyRoot ? resolve(process.cwd(), requestedFamilyRoot) : (customInputs ? undefined : findFamilyRoot(productRoot));
752
+ const requestedFamilyRoot = option(argv, "--family-root") ?? process.env.LITOPENCODE_FAMILY_ROOT;
753
+ const requestedFamilyLayout = option(argv, "--family-layout") ?? process.env.LITOPENCODE_FAMILY_LAYOUT;
754
+ if (Boolean(requestedFamilyRoot) !== Boolean(requestedFamilyLayout)) throw new Error("FAMILY_LAYOUT_REQUIRED: provide both --family-root and --family-layout");
755
+ const familyRoot = requestedFamilyRoot ? resolve(process.cwd(), requestedFamilyRoot) : undefined;
711
756
  if (!familyRoot) return { ...report, references, parity, freshness: undefined };
712
- const targetId = option(argv, "--product-id") ?? matchingProductId(familyRoot, productRoot) ?? (productRoot === PRODUCT_ROOT ? PRODUCT_ID : undefined);
757
+ const familyLayout = loadFamilyLayout(requestedFamilyLayout);
758
+ const targetId = option(argv, "--product-id") ?? matchingProductId(familyRoot, productRoot, familyLayout) ?? (productRoot === PRODUCT_ROOT ? PRODUCT_ID : undefined);
713
759
  if (!targetId) throw new Error("PAYLOAD_PARITY_TARGET_UNKNOWN: pass --product-id for a product outside the family root");
714
- const measurements = measureFamily(familyRoot);
760
+ const measurements = measureFamily(familyRoot, familyLayout);
715
761
  return { ...report, references, parity, freshness: evaluateManifestFreshness({ manifest: loadParityManifest(parityManifestPath), measurements }) };
716
762
  }
717
763
 
@@ -730,6 +776,7 @@ if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
730
776
  for (const row of report.parity.rows) process.stdout.write(`${row}\n`);
731
777
  }
732
778
  if (report.freshness) process.stdout.write("PAYLOAD_PARITY_FRESHNESS_PASS: family-manifest\n");
779
+ else process.stdout.write("PAYLOAD_PARITY_FRESHNESS_SKIP: family-layout not configured\n");
733
780
  }
734
781
  } catch (error) {
735
782
  process.stderr.write(`PAYLOAD_SUBSTANCE_ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -5,10 +5,11 @@ import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { validateScenarioBytes } from "../qa/harness-speed-contract.mjs";
8
+ import { activationBanner as renderActivationBanner } from "../src/activation-probe.ts";
8
9
 
9
10
  const receiptSchema = "litfamily.harness-speed-local/v1";
10
11
  const expectedProduct = "litopencode";
11
- const activationBanner = "▲ LIT · lit-loop";
12
+ const activationBanner = renderActivationBanner("lit-loop");
12
13
  const phaseNames = Object.freeze([
13
14
  "packed_startup_config",
14
15
  "rules_transform",
@@ -35,15 +35,14 @@
35
35
  "refactor",
36
36
  "review-work",
37
37
  "rules",
38
- "skill-observer",
39
38
  "start-work",
40
39
  "structural-search",
41
40
  "visual-qa",
42
41
  "wikify"
43
42
  ],
44
43
  "inventory": {
45
- "count": 34,
46
- "sha256": "9eecdbe41a48709db65188f0c6f4eee2247d27aaa33112977b49219d248fae29"
44
+ "count": 33,
45
+ "sha256": "e89f1e4756229b543d5852a7e30264a7aa119d2ed37516db8a8cde10660ddda0"
47
46
  }
48
47
  },
49
48
  "p28": {
@@ -75,15 +74,14 @@
75
74
  "refactor",
76
75
  "review-work",
77
76
  "rules",
78
- "skill-observer",
79
77
  "start-work",
80
78
  "structural-search",
81
79
  "visual-qa",
82
80
  "wikify"
83
81
  ],
84
82
  "inventory": {
85
- "count": 32,
86
- "sha256": "0bef1ca66b2c8b7d54f175bc0b3f6046cd854dbd34c637819b697ed776858cc6"
83
+ "count": 31,
84
+ "sha256": "9676a2699c14e7c2901e6ae2bbe8a5b9df4357dc6dd8de79ff8f766d09c72b6c"
87
85
  }
88
86
  },
89
87
  "p31": {
@@ -122,15 +120,14 @@
122
120
  "refactor",
123
121
  "review-work",
124
122
  "rules",
125
- "skill-observer",
126
123
  "start-work",
127
124
  "structural-search",
128
125
  "visual-qa",
129
126
  "wikify"
130
127
  ],
131
128
  "inventory": {
132
- "count": 39,
133
- "sha256": "79975bab9d38c00e3289d307a5e12543556f11262cd00054ef00cb8df2afba79"
129
+ "count": 38,
130
+ "sha256": "42876fd3b6b0a269ddc97a8c378866211b863100fac2a1f79fb1c49ea61973ae"
134
131
  }
135
132
  },
136
133
  "p32": {
@@ -169,7 +166,6 @@
169
166
  "review-work",
170
167
  "rules",
171
168
  "search-workflow-ideas",
172
- "skill-observer",
173
169
  "start-work",
174
170
  "structural-search",
175
171
  "tool-guards",
@@ -178,8 +174,8 @@
178
174
  "workflow-loop"
179
175
  ],
180
176
  "inventory": {
181
- "count": 41,
182
- "sha256": "2ad9db6ffd956813ad489233343f1e91f52b2cf21a7a206880da011eb9995ccb"
177
+ "count": 40,
178
+ "sha256": "526fa688a02750abda27581bd3058c55a7445f825bd8445067ce2abe13758ebd"
183
179
  }
184
180
  },
185
181
  "p33": {
@@ -213,15 +209,14 @@
213
209
  "refactor",
214
210
  "review-work",
215
211
  "rules",
216
- "skill-observer",
217
212
  "start-work",
218
213
  "structural-search",
219
214
  "visual-qa",
220
215
  "wikify"
221
216
  ],
222
217
  "inventory": {
223
- "count": 34,
224
- "sha256": "e8f5cf6ae90df8bf89d0bd6b6a4634644c602d047025832a0ace2a3e93186d82"
218
+ "count": 33,
219
+ "sha256": "0a21d1b6314c3f1f6e20d78bcd7a2d236aa8944158246e82542012c954c497d3"
225
220
  }
226
221
  }
227
222
  },
@@ -517,16 +512,6 @@
517
512
  "p33": 3
518
513
  }
519
514
  },
520
- "skill-observer": {
521
- "median": 3,
522
- "closures": {
523
- "p27": 3,
524
- "p28": 2,
525
- "p31": 3,
526
- "p32": 2,
527
- "p33": 6
528
- }
529
- },
530
515
  "start-work": {
531
516
  "median": 1,
532
517
  "closures": {
@@ -11,13 +11,13 @@
11
11
  {
12
12
  "path": "README-Ko-KR.md",
13
13
  "kind": "pinned",
14
- "occurrences": 9,
14
+ "occurrences": 11,
15
15
  "why": "The landing page identifies the current checkout version; the repository-owned cover is version-free."
16
16
  },
17
17
  {
18
18
  "path": "README.md",
19
19
  "kind": "pinned",
20
- "occurrences": 9,
20
+ "occurrences": 11,
21
21
  "why": "The landing page identifies the current checkout version; the repository-owned cover is version-free."
22
22
  },
23
23
  {
@@ -1,3 +0,0 @@
1
- import type { CliResult } from "./types.ts";
2
- export declare function runSkillLoopCli(argv: readonly string[]): Promise<CliResult>;
3
- export declare function runSkillCuratorCli(argv: readonly string[]): Promise<CliResult>;