session-orchestrator 4.1.0 → 4.2.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 (137) hide show
  1. package/.agents/skills/session-plan/SKILL.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
  6. package/.cursor/skills/session-plan/SKILL.md +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/CHANGELOG.md +34 -0
  9. package/README.md +8 -8
  10. package/agents/ux-evaluator.md +1 -1
  11. package/commands/close.md +3 -3
  12. package/commands/go.md +2 -0
  13. package/commands/memory-cleanup.md +4 -3
  14. package/commands/persona-panel.md +1 -1
  15. package/commands/session.md +3 -2
  16. package/docs/README.md +4 -4
  17. package/docs/USER-GUIDE.md +115 -48
  18. package/docs/agent-authoring.md +2 -2
  19. package/docs/baseline.md +55 -1
  20. package/docs/ci-setup.md +1 -1
  21. package/docs/codex-setup.md +1 -0
  22. package/docs/components.md +2 -2
  23. package/docs/cursor-setup.md +1 -0
  24. package/docs/events-schema.md +4 -1
  25. package/docs/instruction-delivery.md +1 -1
  26. package/docs/memory-proposal-flow.md +3 -3
  27. package/docs/migration-v4.md +2 -2
  28. package/docs/owner-config-schema.md +74 -90
  29. package/docs/persona-panel.md +4 -4
  30. package/docs/pi-setup.md +1 -0
  31. package/docs/rule-authoring.md +13 -6
  32. package/docs/scope-collision-guard.md +2 -0
  33. package/docs/session-config-reference.md +55 -22
  34. package/docs/session-config-template.md +9 -5
  35. package/docs/vault-docs-architecture.md +4 -2
  36. package/hooks/_lib/hook-import-set.json +28 -3
  37. package/hooks/_lib/vcs-create-matcher.mjs +214 -16
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks.json +1 -1
  40. package/hooks/pre-bash-issue-budget.mjs +123 -26
  41. package/hooks/subagent-telemetry.mjs +106 -20
  42. package/package.json +4 -4
  43. package/scripts/baseline-archetypes.mjs +28 -0
  44. package/scripts/lib/auto-dialectic.mjs +0 -68
  45. package/scripts/lib/baseline-archetypes.mjs +439 -0
  46. package/scripts/lib/build-live-signals.mjs +5 -6
  47. package/scripts/lib/config/issue-budget.mjs +68 -8
  48. package/scripts/lib/config/private-config-dir.mjs +3 -2
  49. package/scripts/lib/config/remote-hosts.mjs +2 -2
  50. package/scripts/lib/config-schema.mjs +79 -0
  51. package/scripts/lib/file-lock.mjs +47 -5
  52. package/scripts/lib/issue-budget-reconcile.mjs +392 -0
  53. package/scripts/lib/issue-budget.mjs +76 -3
  54. package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
  55. package/scripts/lib/maintenance-due-banner.mjs +440 -0
  56. package/scripts/lib/owner-config.example.yaml +29 -46
  57. package/scripts/lib/owner-yaml.mjs +14 -13
  58. package/scripts/lib/quality-gate.mjs +13 -6
  59. package/scripts/lib/resource-probe/evaluate.mjs +19 -21
  60. package/scripts/lib/rules-sync.mjs +34 -4
  61. package/scripts/lib/session-close-backfill.mjs +182 -40
  62. package/scripts/lib/session-end/phase-skip.mjs +85 -86
  63. package/scripts/lib/session-end/tail-runner.mjs +178 -0
  64. package/scripts/lib/session-schema/constants.mjs +6 -0
  65. package/scripts/lib/session-schema/validator.mjs +20 -0
  66. package/scripts/lib/session-shape.mjs +558 -0
  67. package/scripts/lib/session-start-probes.mjs +10 -3
  68. package/scripts/lib/session-token-rollup.mjs +95 -10
  69. package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
  70. package/scripts/lib/state-md.mjs +1 -0
  71. package/scripts/lib/subagents-schema.mjs +77 -9
  72. package/scripts/lib/telemetry/pricing.mjs +197 -0
  73. package/scripts/lib/telemetry/sync.mjs +50 -1
  74. package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
  75. package/scripts/lib/validate/check-unwired-features.mjs +8 -7
  76. package/scripts/lib/vault-mirror/process.mjs +2 -1
  77. package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
  78. package/scripts/lib/wave-resource-gate.mjs +23 -27
  79. package/scripts/lib/wave-sizing.mjs +10 -3
  80. package/scripts/materialize-wave-scope.mjs +68 -14
  81. package/scripts/print-applicable-rules.mjs +7 -6
  82. package/scripts/print-learnings-index.mjs +3 -2
  83. package/scripts/session-shape.mjs +266 -0
  84. package/skills/_shared/config-reading.md +15 -9
  85. package/skills/_shared/private-capability-context.md +89 -0
  86. package/skills/bootstrap/SKILL.md +61 -13
  87. package/skills/bootstrap/_shared-template.md +99 -14
  88. package/skills/bootstrap/deep-template.md +36 -26
  89. package/skills/bootstrap/fast-template.md +44 -8
  90. package/skills/bootstrap/intensity-heuristic.md +10 -4
  91. package/skills/bootstrap/private-contract.md +119 -0
  92. package/skills/bootstrap/public-fallback.md +30 -18
  93. package/skills/bootstrap/standard-template.md +39 -24
  94. package/skills/discovery/probes-ui.md +1 -1
  95. package/skills/docs-orchestrator/audience-mapping.md +1 -1
  96. package/skills/evolve/SKILL.md +2 -2
  97. package/skills/gitlab-ops/SKILL.md +3 -3
  98. package/skills/grill/SKILL.md +1 -1
  99. package/skills/memory-cleanup/SKILL.md +2 -2
  100. package/skills/plan/mode-new.md +9 -0
  101. package/skills/reconcile/SKILL.md +1 -1
  102. package/skills/session-end/SKILL.md +3 -2
  103. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  104. package/skills/session-end/phase-3-6-tail.md +23 -65
  105. package/skills/session-end/phase-3-7a-recommendations.md +2 -2
  106. package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
  107. package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
  108. package/skills/session-end/session-metrics-write.md +31 -12
  109. package/skills/session-plan/SKILL.md +56 -48
  110. package/skills/session-plan/wave-template.md +8 -15
  111. package/skills/session-start/SKILL.md +18 -2
  112. package/skills/session-start/phase-2-5-docs-planning.md +1 -1
  113. package/skills/session-start/phase-8-5-express-path.md +12 -9
  114. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
  115. package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
  116. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
  117. package/skills/test-runner/rubric-v1.md +2 -2
  118. package/skills/wave-executor/SKILL.md +42 -12
  119. package/skills/wave-executor/circuit-breaker.md +3 -1
  120. package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
  121. package/skills/wave-executor/references/wave-loop-review.md +1 -1
  122. package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
  123. package/templates/nextjs-minimal/package.json +1 -1
  124. package/templates/node-minimal/package.json +1 -1
  125. package/scripts/lib/multi-provider-build/providers.mjs +0 -64
  126. package/scripts/lib/multi-provider-build/templating.mjs +0 -130
  127. package/scripts/lib/owner-config/coerce.mjs +0 -29
  128. package/scripts/lib/owner-config/constants.mjs +0 -21
  129. package/scripts/lib/owner-config/defaults.mjs +0 -50
  130. package/scripts/lib/owner-config/error.mjs +0 -19
  131. package/scripts/lib/owner-config/index.mjs +0 -13
  132. package/scripts/lib/owner-config/merge.mjs +0 -52
  133. package/scripts/lib/owner-config/validate.mjs +0 -259
  134. package/scripts/lib/owner-config-loader.mjs +0 -170
  135. package/scripts/lib/owner-config.mjs +0 -28
  136. package/scripts/lib/soul-resolve.mjs +0 -130
  137. package/scripts/lib/vault-mirror/render.mjs +0 -8
@@ -1,13 +0,0 @@
1
- /**
2
- * owner-config/index.mjs — Re-export hub for all 10 public symbols.
3
- *
4
- * DAG: constants/error (leaves) → defaults → merge; constants/error → validate → coerce.
5
- * This hub is the single import target for `scripts/lib/owner-config.mjs` (top wrapper).
6
- */
7
-
8
- export { CURRENT_OWNER_SCHEMA_VERSION, VALID_TONE_STYLES, VALID_OUTPUT_LEVELS, VALID_PREAMBLE_LEVELS, VALID_COMMENTS_LEVELS } from './constants.mjs';
9
- export { OwnerConfigError } from './error.mjs';
10
- export { defaults } from './defaults.mjs';
11
- export { validate } from './validate.mjs';
12
- export { coerce } from './coerce.mjs';
13
- export { merge } from './merge.mjs';
@@ -1,52 +0,0 @@
1
- /**
2
- * owner-config/merge.mjs — Deep merge for owner persona configs.
3
- *
4
- * Imports: constants + defaults. One-level-deep section merge because the
5
- * schema has no nested object leaves beyond top-level sections. The output is
6
- * always a full default-filled config — either input may be partial.
7
- */
8
-
9
- import { CURRENT_OWNER_SCHEMA_VERSION } from './constants.mjs';
10
- import { defaults } from './defaults.mjs';
11
-
12
- function isPlainObject(v) {
13
- return v !== null && typeof v === 'object' && !Array.isArray(v);
14
- }
15
-
16
- /**
17
- * Deep merge two owner configs. `override` values win on every leaf key
18
- * where they are defined (i.e. not undefined). The returned value is a
19
- * full default-filled config — either input may be partial.
20
- *
21
- * Used by D3 (`soul.md` runtime-merge) and D4 (baseline-propagation +
22
- * per-session override). The merge is one level deep on each top-level
23
- * section (owner, tone, efficiency, hardware-sharing, defaults, metadata)
24
- * because the schema has no nested object leaves beyond that.
25
- *
26
- * @param {object|null|undefined} base
27
- * @param {object|null|undefined} override
28
- * @returns {object}
29
- */
30
- export function merge(base, override) {
31
- const baseSafe = isPlainObject(base) ? base : {};
32
- const overSafe = isPlainObject(override) ? override : {};
33
- const def = defaults();
34
-
35
- const sections = ['owner', 'tone', 'efficiency', 'hardware-sharing', 'defaults', 'metadata'];
36
- const out = {
37
- 'schema-version': CURRENT_OWNER_SCHEMA_VERSION,
38
- };
39
-
40
- for (const section of sections) {
41
- const baseSection = isPlainObject(baseSafe[section]) ? baseSafe[section] : {};
42
- const overSection = isPlainObject(overSafe[section]) ? overSafe[section] : {};
43
- out[section] = { ...def[section], ...baseSection };
44
- for (const [k, v] of Object.entries(overSection)) {
45
- if (v !== undefined) {
46
- out[section][k] = v;
47
- }
48
- }
49
- }
50
-
51
- return out;
52
- }
@@ -1,259 +0,0 @@
1
- /**
2
- * owner-config/validate.mjs — Pure schema validator for the owner persona config.
3
- * Never throws — returns {ok, value, errors}. Unknown top-level sections dropped.
4
- */
5
-
6
- import {
7
- CURRENT_OWNER_SCHEMA_VERSION,
8
- VALID_TONE_STYLES,
9
- VALID_OUTPUT_LEVELS,
10
- VALID_PREAMBLE_LEVELS,
11
- VALID_COMMENTS_LEVELS,
12
- } from './constants.mjs';
13
-
14
- // Internal constants
15
- const NAME_MAX = 100;
16
- const TONALITY_MAX = 200;
17
- const TEST_COMMAND_MAX = 200;
18
- const EDITOR_MAX = 50;
19
- const HEX64_RE = /^[a-f0-9]{64}$/i;
20
- const ISO_639_1_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
21
-
22
- // Private helpers
23
- const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
24
- const isNonEmptyString = (v, max) => typeof v === 'string' && v.length > 0 && (max === undefined || v.length <= max);
25
-
26
- // Section validators (module-private)
27
- function _validateSchemaVersionSection(raw, errors) {
28
- const schemaVersion = raw['schema-version'];
29
- if (schemaVersion === undefined || schemaVersion === null) {
30
- errors.push('schema-version is required');
31
- } else if (schemaVersion !== CURRENT_OWNER_SCHEMA_VERSION) {
32
- errors.push(
33
- `schema-version must be ${CURRENT_OWNER_SCHEMA_VERSION}, got: ${JSON.stringify(schemaVersion)}`
34
- );
35
- }
36
- }
37
-
38
- function _validateOwnerSection(raw, errors, ownerOut) {
39
- const owner = raw.owner;
40
- if (!isPlainObject(owner)) {
41
- errors.push('owner must be an object');
42
- return;
43
- }
44
- if (!isNonEmptyString(owner.name, NAME_MAX)) {
45
- errors.push(`owner.name must be a non-empty string (max ${NAME_MAX} chars)`);
46
- } else {
47
- ownerOut.name = owner.name;
48
- }
49
- if (owner['email-hash'] !== undefined && owner['email-hash'] !== null) {
50
- if (typeof owner['email-hash'] !== 'string' || !HEX64_RE.test(owner['email-hash'])) {
51
- errors.push('owner.email-hash must be a 64-char hex string (sha256) or null');
52
- } else {
53
- ownerOut['email-hash'] = owner['email-hash'].toLowerCase();
54
- }
55
- }
56
- if (!isNonEmptyString(owner.language, 10) || !ISO_639_1_RE.test(owner.language)) {
57
- errors.push('owner.language must be an ISO-639-1 code (e.g. "de", "en", "en-US")');
58
- } else {
59
- ownerOut.language = owner.language;
60
- }
61
- }
62
-
63
- function _validateToneSection(raw, errors, toneOut) {
64
- if (raw.tone === undefined) return;
65
- if (!isPlainObject(raw.tone)) {
66
- errors.push('tone must be an object');
67
- return;
68
- }
69
- if (raw.tone.style !== undefined) {
70
- if (!VALID_TONE_STYLES.includes(raw.tone.style)) {
71
- errors.push(`tone.style must be one of ${VALID_TONE_STYLES.join('|')}, got: ${raw.tone.style}`);
72
- } else {
73
- toneOut.style = raw.tone.style;
74
- }
75
- }
76
- if (raw.tone.tonality !== undefined && raw.tone.tonality !== null) {
77
- if (typeof raw.tone.tonality !== 'string' || raw.tone.tonality.length > TONALITY_MAX) {
78
- errors.push(`tone.tonality must be a string (max ${TONALITY_MAX} chars) or null`);
79
- } else {
80
- toneOut.tonality = raw.tone.tonality;
81
- }
82
- }
83
- }
84
-
85
- function _validateEfficiencySection(raw, errors, efficiencyOut) {
86
- if (raw.efficiency === undefined) return;
87
- if (!isPlainObject(raw.efficiency)) {
88
- errors.push('efficiency must be an object');
89
- return;
90
- }
91
- if (raw.efficiency['output-level'] !== undefined) {
92
- if (!VALID_OUTPUT_LEVELS.includes(raw.efficiency['output-level'])) {
93
- errors.push(
94
- `efficiency.output-level must be one of ${VALID_OUTPUT_LEVELS.join('|')}, got: ${raw.efficiency['output-level']}`
95
- );
96
- } else {
97
- efficiencyOut['output-level'] = raw.efficiency['output-level'];
98
- }
99
- }
100
- if (raw.efficiency.preamble !== undefined) {
101
- if (!VALID_PREAMBLE_LEVELS.includes(raw.efficiency.preamble)) {
102
- errors.push(
103
- `efficiency.preamble must be one of ${VALID_PREAMBLE_LEVELS.join('|')}, got: ${raw.efficiency.preamble}`
104
- );
105
- } else {
106
- efficiencyOut.preamble = raw.efficiency.preamble;
107
- }
108
- }
109
- if (raw.efficiency['comments-in-code'] !== undefined) {
110
- if (!VALID_COMMENTS_LEVELS.includes(raw.efficiency['comments-in-code'])) {
111
- errors.push(
112
- `efficiency.comments-in-code must be one of ${VALID_COMMENTS_LEVELS.join('|')}, got: ${raw.efficiency['comments-in-code']}`
113
- );
114
- } else {
115
- efficiencyOut['comments-in-code'] = raw.efficiency['comments-in-code'];
116
- }
117
- }
118
- }
119
-
120
- function _validateHardwareSharingSection(raw, errors, hwOut) {
121
- if (raw['hardware-sharing'] === undefined) return;
122
- const hw = raw['hardware-sharing'];
123
- if (!isPlainObject(hw)) {
124
- errors.push('hardware-sharing must be an object');
125
- return;
126
- }
127
- if (hw.enabled !== undefined) {
128
- if (typeof hw.enabled !== 'boolean') {
129
- errors.push(`hardware-sharing.enabled must be boolean, got: ${typeof hw.enabled}`);
130
- } else {
131
- hwOut.enabled = hw.enabled;
132
- }
133
- }
134
- if (hw['hash-salt'] !== undefined && hw['hash-salt'] !== null) {
135
- if (typeof hw['hash-salt'] !== 'string' || !HEX64_RE.test(hw['hash-salt'])) {
136
- errors.push('hardware-sharing.hash-salt must be a 64-char hex string (32 bytes) or null');
137
- } else {
138
- hwOut['hash-salt'] = hw['hash-salt'].toLowerCase();
139
- }
140
- }
141
- // Privacy contract: enabled=true requires hash-salt to be set.
142
- if (hwOut.enabled && hwOut['hash-salt'] === null) {
143
- errors.push('hardware-sharing.enabled=true requires hash-salt to be set (D4 consent contract)');
144
- }
145
- }
146
-
147
- function _validateDefaultsSection(raw, errors, defaultsOut) {
148
- if (raw.defaults === undefined) return;
149
- if (!isPlainObject(raw.defaults)) {
150
- errors.push('defaults must be an object');
151
- return;
152
- }
153
- const ptc = raw.defaults['preferred-test-command'];
154
- if (ptc !== undefined && ptc !== null) {
155
- if (typeof ptc !== 'string' || ptc.length > TEST_COMMAND_MAX) {
156
- errors.push(`defaults.preferred-test-command must be a string (max ${TEST_COMMAND_MAX} chars) or null`);
157
- } else {
158
- defaultsOut['preferred-test-command'] = ptc;
159
- }
160
- }
161
- const pe = raw.defaults['preferred-editor'];
162
- if (pe !== undefined && pe !== null) {
163
- if (typeof pe !== 'string' || pe.length > EDITOR_MAX) {
164
- errors.push(`defaults.preferred-editor must be a string (max ${EDITOR_MAX} chars) or null`);
165
- } else {
166
- defaultsOut['preferred-editor'] = pe;
167
- }
168
- }
169
- }
170
-
171
- function _validateMetadataSection(raw, errors, metadataOut) {
172
- if (raw.metadata === undefined) return;
173
- if (!isPlainObject(raw.metadata)) {
174
- errors.push('metadata must be an object');
175
- return;
176
- }
177
- if (raw.metadata.created_at !== undefined && raw.metadata.created_at !== null) {
178
- if (typeof raw.metadata.created_at !== 'string' || Number.isNaN(Date.parse(raw.metadata.created_at))) {
179
- errors.push('metadata.created_at must be an ISO 8601 timestamp string or null');
180
- } else {
181
- metadataOut.created_at = raw.metadata.created_at;
182
- }
183
- }
184
- if (raw.metadata.updated_at !== undefined && raw.metadata.updated_at !== null) {
185
- if (typeof raw.metadata.updated_at !== 'string' || Number.isNaN(Date.parse(raw.metadata.updated_at))) {
186
- errors.push('metadata.updated_at must be an ISO 8601 timestamp string or null');
187
- } else {
188
- metadataOut.updated_at = raw.metadata.updated_at;
189
- }
190
- }
191
- }
192
-
193
- // Public API
194
-
195
- /**
196
- * Validate a raw owner config object. Defensive — never throws. Returns
197
- * `{ok, value, errors}` where `value` is the normalized (default-filled)
198
- * config when ok=true. When ok=false, `value` is null and `errors` is a
199
- * non-empty array of human-readable messages.
200
- *
201
- * @param {unknown} raw
202
- * @returns {{ok: boolean, value: object|null, errors: string[]}}
203
- */
204
- export function validate(raw) {
205
- const errors = [];
206
-
207
- if (!isPlainObject(raw)) {
208
- return { ok: false, value: null, errors: ['owner config must be an object'] };
209
- }
210
-
211
- // schema-version (required, must be 1)
212
- _validateSchemaVersionSection(raw, errors);
213
-
214
- // owner (required)
215
- const ownerOut = { name: '', 'email-hash': null, language: '' };
216
- _validateOwnerSection(raw, errors, ownerOut);
217
-
218
- // tone (optional; defaults applied)
219
- const toneOut = { style: 'neutral', tonality: null };
220
- _validateToneSection(raw, errors, toneOut);
221
-
222
- // efficiency (optional; defaults applied)
223
- const efficiencyOut = {
224
- 'output-level': 'full',
225
- preamble: 'minimal',
226
- 'comments-in-code': 'minimal',
227
- };
228
- _validateEfficiencySection(raw, errors, efficiencyOut);
229
-
230
- // hardware-sharing (optional; defaults applied)
231
- const hwOut = { enabled: false, 'hash-salt': null };
232
- _validateHardwareSharingSection(raw, errors, hwOut);
233
-
234
- // defaults (optional)
235
- const defaultsOut = { 'preferred-test-command': null, 'preferred-editor': null };
236
- _validateDefaultsSection(raw, errors, defaultsOut);
237
-
238
- // metadata (optional, auto-managed by writer)
239
- const metadataOut = { created_at: null, updated_at: null };
240
- _validateMetadataSection(raw, errors, metadataOut);
241
-
242
- if (errors.length > 0) {
243
- return { ok: false, value: null, errors };
244
- }
245
-
246
- return {
247
- ok: true,
248
- errors: [],
249
- value: {
250
- 'schema-version': CURRENT_OWNER_SCHEMA_VERSION,
251
- owner: ownerOut,
252
- tone: toneOut,
253
- efficiency: efficiencyOut,
254
- 'hardware-sharing': hwOut,
255
- defaults: defaultsOut,
256
- metadata: metadataOut,
257
- },
258
- };
259
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * owner-config-loader.mjs — File-system loader for owner.yaml (Issue #174).
3
- *
4
- * Companion to the pure schema/validator in `owner-config.mjs`. Resolves the
5
- * canonical config path (XDG-spec aware), reads the YAML, parses it, runs it
6
- * through the validator, and returns a `{ok, value, errors, source, path}`
7
- * envelope.
8
- *
9
- * ── PATH RESOLUTION ──────────────────────────────────────────────────────
10
- *
11
- * Default path (#1223 — delegated to the single resolver
12
- * `config/private-config-dir.mjs`; `SO_CONFIG_HOME` names the private dir
13
- * ITSELF and wins):
14
- * `${SO_CONFIG_HOME ?? ${XDG_CONFIG_HOME ?? ${HOME}/.config}/session-orchestrator}/owner.yaml`
15
- * - macOS / Linux: `~/.config/session-orchestrator/owner.yaml`
16
- * - Windows: `%APPDATA%\session-orchestrator\owner.yaml` when
17
- * APPDATA is set and XDG_CONFIG_HOME is not (Node uses
18
- * USERPROFILE for os.homedir() — falls back gracefully).
19
- *
20
- * Callers may pass an explicit `{path}` override (used by tests + D4
21
- * baseline-propagation when the canonical path is overridden by env).
22
- *
23
- * ── RETURN SHAPE ─────────────────────────────────────────────────────────
24
- *
25
- * {
26
- * ok: boolean, // false on any failure
27
- * value: object|null, // normalized config when ok=true
28
- * errors: string[], // populated on any failure
29
- * source: 'file'|'missing'|'parse-error'|'validation-error',
30
- * path: string, // resolved absolute path
31
- * }
32
- *
33
- * `source: 'missing'` is NOT an error — owner.yaml is opt-in (the bootstrap
34
- * interview in D2 creates it). Callers should treat `source: 'missing'` as
35
- * "no owner persona configured; fall back to plugin defaults".
36
- *
37
- * ── PLATFORM PORTABILITY ─────────────────────────────────────────────────
38
- *
39
- * - Uses `os.homedir()` (Node-stdlib, OS-aware) rather than $HOME so the
40
- * Windows path resolves correctly even when only USERPROFILE is set.
41
- * - File reads are UTF-8 only. Owner.yaml is ASCII-safe by spec; non-UTF-8
42
- * content yields `source: 'parse-error'`.
43
- * - YAML parsing via `js-yaml` (devDep already pulled in by W2 quality-fix).
44
- */
45
-
46
- import { readFile } from 'node:fs/promises';
47
- import { existsSync } from 'node:fs';
48
- import { join } from 'node:path';
49
- import yaml from 'js-yaml';
50
-
51
- import { validate } from './owner-config.mjs';
52
- import { resolvePrivateConfigDir } from './config/private-config-dir.mjs';
53
-
54
- // ---------------------------------------------------------------------------
55
- // Path resolution
56
- // ---------------------------------------------------------------------------
57
-
58
- /**
59
- * Resolve the canonical owner.yaml path on disk: the ONE private-config-dir
60
- * resolver (#1223) plus the file's basename — precedence `SO_CONFIG_HOME` (the
61
- * private dir itself) > `XDG_CONFIG_HOME` (its parent) > `${HOME}/.config`, each
62
- * `.trim()`ed so a whitespace-only value falls through instead of
63
- * short-circuiting (`.claude/rules/development.md` § Error Handling).
64
- *
65
- * Reaches the leaf DIRECTLY rather than through `owner-yaml.mjs`'s
66
- * `resolveOwnerYamlPath()`: that hop exists only to append the same basename and
67
- * drags a `js-yaml`-importing module in with it. The precedence — the part #1223
68
- * consolidated — still lives in exactly one place; only the literal `'owner.yaml'`
69
- * is stated twice.
70
- *
71
- * Before #1223 this function knew only `XDG_CONFIG_HOME` and did not trim, so
72
- * `SO_CONFIG_HOME=<sandbox>` still read the operator's real home.
73
- *
74
- * Returns an absolute path string. Does NOT touch the filesystem.
75
- *
76
- * @returns {string} absolute path to owner.yaml
77
- */
78
- export function resolveOwnerConfigPath() {
79
- return join(resolvePrivateConfigDir(), 'owner.yaml');
80
- }
81
-
82
- // ---------------------------------------------------------------------------
83
- // Loader
84
- // ---------------------------------------------------------------------------
85
-
86
- /**
87
- * Load and validate the owner.yaml config from disk.
88
- *
89
- * Defensive — never throws. Distinguishes four outcomes via the `source`
90
- * field so callers can react differently to "missing file" (opt-in feature
91
- * not yet enabled by the user) vs "malformed YAML" (broken state, surface
92
- * to user).
93
- *
94
- * @param {{path?: string}} [opts]
95
- * @returns {Promise<{ok: boolean, value: object|null, errors: string[], source: string, path: string}>}
96
- */
97
- export async function loadOwnerConfig(opts = {}) {
98
- const path = opts.path ?? resolveOwnerConfigPath();
99
-
100
- if (!existsSync(path)) {
101
- return {
102
- ok: false,
103
- value: null,
104
- errors: [],
105
- source: 'missing',
106
- path,
107
- };
108
- }
109
-
110
- let raw;
111
- try {
112
- raw = await readFile(path, 'utf8');
113
- } catch (err) {
114
- return {
115
- ok: false,
116
- value: null,
117
- errors: [`failed to read owner config: ${err.message}`],
118
- source: 'parse-error',
119
- path,
120
- };
121
- }
122
-
123
- let parsed;
124
- try {
125
- // js-yaml's default schema (CORE_SCHEMA) is safe — it explicitly excludes
126
- // the !!js/function and !!js/regexp tags from the legacy DEFAULT_SCHEMA
127
- // that would let a malicious YAML file execute code or pollute prototypes.
128
- parsed = yaml.load(raw);
129
- } catch (err) {
130
- return {
131
- ok: false,
132
- value: null,
133
- errors: [`YAML parse error: ${err.message}`],
134
- source: 'parse-error',
135
- path,
136
- };
137
- }
138
-
139
- // Empty file or YAML "null" → treat as parse-error so the caller knows
140
- // there's nothing to merge (vs a missing file which opts the user out
141
- // entirely).
142
- if (parsed === null || parsed === undefined) {
143
- return {
144
- ok: false,
145
- value: null,
146
- errors: ['owner config is empty'],
147
- source: 'parse-error',
148
- path,
149
- };
150
- }
151
-
152
- const validation = validate(parsed);
153
- if (!validation.ok) {
154
- return {
155
- ok: false,
156
- value: null,
157
- errors: validation.errors,
158
- source: 'validation-error',
159
- path,
160
- };
161
- }
162
-
163
- return {
164
- ok: true,
165
- value: validation.value,
166
- errors: [],
167
- source: 'file',
168
- path,
169
- };
170
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * owner-config.mjs — Owner Persona schema + validator (Issue #174, Sub-Epic #161).
3
- *
4
- * Thin re-export wrapper. All logic lives in `./owner-config/` submodules:
5
- * constants.mjs — frozen enum arrays + schema version
6
- * error.mjs — OwnerConfigError class
7
- * defaults.mjs — defaults() factory
8
- * validate.mjs — validate() + 7 section helpers
9
- * coerce.mjs — coerce() strict-mode wrapper
10
- * merge.mjs — merge() deep-merge
11
- * index.mjs — re-export hub (aggregates all 10 public symbols)
12
- *
13
- * For the full schema contract and module DAG see `./owner-config/index.mjs`.
14
- * The loader (which does touch the disk) lives in `owner-config-loader.mjs`.
15
- */
16
-
17
- export {
18
- CURRENT_OWNER_SCHEMA_VERSION,
19
- VALID_TONE_STYLES,
20
- VALID_OUTPUT_LEVELS,
21
- VALID_PREAMBLE_LEVELS,
22
- VALID_COMMENTS_LEVELS,
23
- OwnerConfigError,
24
- defaults,
25
- validate,
26
- coerce,
27
- merge,
28
- } from './owner-config/index.mjs';
@@ -1,130 +0,0 @@
1
- /**
2
- * soul-resolve.mjs — Template resolver for soul.md files (Issue #176, D3).
3
- *
4
- * Resolves `{{slot}}` placeholders in soul.md templates using owner persona
5
- * config loaded via `owner-yaml.mjs` (D1). Pure at the `resolveSoul` level;
6
- * `loadAndResolveSoul` performs disk I/O.
7
- *
8
- * ── NO RUNTIME CALLER — read this before assuming a soul.md is resolved ──────
9
- *
10
- * Nothing in `scripts/`, `hooks/`, or any skill body calls either export. Skill
11
- * bodies instruct the coordinator to read soul.md DIRECTLY, so whatever is in
12
- * the file on disk is what the coordinator sees — an unsubstituted `{{slot}}`
13
- * reaches the model verbatim and instructs nothing. `skills/session-start/soul.md`
14
- * is therefore authored pre-resolved: it carries no slots, and the operator's
15
- * `efficiency.output-level` selects one of its literal `### output-level: <value>`
16
- * blocks (see that file's § Output Levels; the skill body performs the lookup).
17
- * Slots that remain in other soul.md files are inert for the same reason.
18
- * Do not add a slot to a soul.md expecting substitution — wire a caller first.
19
- *
20
- * ── Slot syntax ──────────────────────────────────────────────────────────────
21
- *
22
- * {{owner.language}} → 'de' | 'en'
23
- * {{tone.style}} → 'direct' | 'neutral' | 'friendly'
24
- * {{efficiency.output-level}} → 'lite' | 'full' | 'ultra'
25
- * {{efficiency.preamble}} → 'minimal' | 'verbose'
26
- *
27
- * ── Resolution rules ─────────────────────────────────────────────────────────
28
- *
29
- * - Known slot path present in ownerConfig → replaced with the value
30
- * - Known slot path missing in ownerConfig → replaced with default (silent)
31
- * - Unknown slot path → left as-is; warning added to result
32
- *
33
- * ── Exports ───────────────────────────────────────────────────────────────────
34
- *
35
- * resolveSoul(templateContent, ownerConfig) → { resolved: string, warnings: string[] }
36
- * loadAndResolveSoul(soulPath, opts?) → { resolved: string, warnings: string[], source: string }
37
- */
38
-
39
- import { readFileSync } from 'node:fs';
40
- import { loadOwnerConfig, getDefaults } from './owner-yaml.mjs';
41
-
42
- // ---------------------------------------------------------------------------
43
- // Helpers
44
- // ---------------------------------------------------------------------------
45
-
46
- /**
47
- * Look up a dot-notation path such as "efficiency.output-level" in a nested
48
- * object. Returns `undefined` if any segment is missing.
49
- *
50
- * @param {object} obj
51
- * @param {string} dotPath
52
- * @returns {unknown}
53
- */
54
- function getByPath(obj, dotPath) {
55
- const segments = dotPath.split('.');
56
- let cursor = obj;
57
- for (const seg of segments) {
58
- if (cursor === null || cursor === undefined || typeof cursor !== 'object') {
59
- return undefined;
60
- }
61
- cursor = cursor[seg];
62
- }
63
- return cursor;
64
- }
65
-
66
- /**
67
- * Set of slot paths explicitly documented / known by this module.
68
- * Any slot NOT in this set is considered unknown and left in place with a warning.
69
- */
70
- const KNOWN_SLOTS = new Set([
71
- 'owner.language',
72
- 'tone.style',
73
- 'efficiency.output-level',
74
- 'efficiency.preamble',
75
- ]);
76
-
77
- // ---------------------------------------------------------------------------
78
- // Public API
79
- // ---------------------------------------------------------------------------
80
-
81
- /**
82
- * Resolve `{{slot}}` placeholders in `templateContent` using `ownerConfig`.
83
- *
84
- * Pure function — no I/O, no side-effects.
85
- *
86
- * @param {string} templateContent Raw soul.md template text.
87
- * @param {object} ownerConfig Owner persona config (from loadOwnerConfig or getDefaults).
88
- * @returns {{ resolved: string, warnings: string[] }}
89
- */
90
- export function resolveSoul(templateContent, ownerConfig) {
91
- const defaults = getDefaults();
92
- const warnings = [];
93
-
94
- const resolved = templateContent.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_match, path) => {
95
- if (!KNOWN_SLOTS.has(path)) {
96
- warnings.push(`Unknown slot path: {{${path}}} — left in place`);
97
- return `{{${path}}}`;
98
- }
99
-
100
- // Try ownerConfig first, then fall back to defaults
101
- let value = getByPath(ownerConfig, path);
102
- if (value === undefined || value === null || value === '') {
103
- value = getByPath(defaults, path);
104
- }
105
-
106
- return String(value ?? '');
107
- });
108
-
109
- return { resolved, warnings };
110
- }
111
-
112
- /**
113
- * Read a soul.md template from `soulPath`, load owner config (from
114
- * `ownerConfigPath` or the default location), and return the resolved content.
115
- *
116
- * @param {string} soulPath Absolute path to the soul.md template.
117
- * @param {{ ownerConfigPath?: string }} [opts]
118
- * @returns {{ resolved: string, warnings: string[], source: 'file'|'defaults'|'partial' }}
119
- */
120
- export function loadAndResolveSoul(soulPath, opts = {}) {
121
- const templateContent = readFileSync(soulPath, 'utf8');
122
-
123
- const { config, source } = loadOwnerConfig(
124
- opts.ownerConfigPath ? { path: opts.ownerConfigPath } : {},
125
- );
126
-
127
- const { resolved, warnings } = resolveSoul(templateContent, config);
128
-
129
- return { resolved, warnings, source };
130
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * render.mjs — Re-export barrel for vault-mirror render modules (Issue #283 split).
3
- *
4
- * Consumers can import from this file or directly from render-learnings.mjs / render-sessions.mjs.
5
- */
6
-
7
- export { detectLearningSchema, generateLearningNote, generateLearningNoteV2 } from './render-learnings.mjs';
8
- export { detectSessionSchema, generateSessionNote, generateSessionNoteV2, generateSessionNoteV3 } from './render-sessions.mjs';