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.
- package/.agents/skills/session-plan/SKILL.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +8 -8
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +115 -48
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +1 -0
- package/docs/components.md +2 -2
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +4 -1
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +28 -3
- package/hooks/_lib/vcs-create-matcher.mjs +214 -16
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/pre-bash-issue-budget.mjs +123 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +4 -4
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/file-lock.mjs +47 -5
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +76 -3
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/maintenance-due-banner.mjs +440 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/quality-gate.mjs +13 -6
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +10 -3
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-unwired-features.mjs +8 -7
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +61 -13
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/discovery/probes-ui.md +1 -1
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/evolve/SKILL.md +2 -2
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +23 -65
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
- package/skills/session-end/session-metrics-write.md +31 -12
- package/skills/session-plan/SKILL.md +56 -48
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +18 -2
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/wave-executor/SKILL.md +42 -12
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
- package/skills/wave-executor/references/wave-loop-review.md +1 -1
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- 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';
|