session-orchestrator 4.1.0 → 5.0.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/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- 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/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- 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 +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- 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 +16 -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 +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- 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/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- 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/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- 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-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- 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/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- 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/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -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 +60 -209
- 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/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- 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/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- 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/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- 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
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -40,9 +40,9 @@ import path from 'node:path';
|
|
|
40
40
|
import crypto from 'node:crypto';
|
|
41
41
|
|
|
42
42
|
import { _parseStateMdLock } from '../config/state-md-lock.mjs';
|
|
43
|
-
import { tryAcquireFileLock } from '../file-lock.mjs';
|
|
44
|
-
import {
|
|
45
|
-
import { nowIso, delay
|
|
43
|
+
import { releaseFileLock, tryAcquireFileLock } from '../file-lock.mjs';
|
|
44
|
+
import { stableHostname } from '../host-identity.mjs';
|
|
45
|
+
import { nowIso, delay } from './lock-body.mjs';
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
// Constants
|
|
@@ -198,6 +198,9 @@ export async function acquireStateLock({
|
|
|
198
198
|
* { ok: true } — lock unlinked
|
|
199
199
|
* { ok: false, reason: 'not-found' } — no lock file exists
|
|
200
200
|
* { ok: false, reason: 'not-owner' } — lock held by different holder/PID
|
|
201
|
+
* { ok: false, reason: 'busy' } — the shared `.acquire` guard
|
|
202
|
+
* stayed taken past its budget;
|
|
203
|
+
* the lock is left untouched
|
|
201
204
|
* { ok: false, reason: 'fs-error', error } — filesystem failure
|
|
202
205
|
*
|
|
203
206
|
* Never throws.
|
|
@@ -210,42 +213,13 @@ export async function acquireStateLock({
|
|
|
210
213
|
export function releaseStateLock({ repoRoot, sessionId, holder } = {}) {
|
|
211
214
|
const lockFile = stateLockPathFor(repoRoot);
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return { ok: false, reason: 'fs-error', error: err.message };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const lock = parseLockBody(raw);
|
|
224
|
-
if (lock === null) {
|
|
225
|
-
// Unparseable — refuse to delete; some other process may be writing now.
|
|
226
|
-
return { ok: false, reason: 'not-owner' };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const expectedHolder = holder ?? sessionId ?? null;
|
|
230
|
-
const ownerMatch = expectedHolder !== null
|
|
231
|
-
? lock.holder === expectedHolder
|
|
232
|
-
// #1072: alias-aware host identity — a raw comparison strands the lock as
|
|
233
|
-
// 'not-owner' once os.hostname() flips spelling mid-session.
|
|
234
|
-
: lock.pid === process.pid && hostnamesMatch(lockHostCandidate(lock), os.hostname());
|
|
235
|
-
|
|
236
|
-
if (!ownerMatch) {
|
|
237
|
-
return { ok: false, reason: 'not-owner' };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
fs.unlinkSync(lockFile);
|
|
242
|
-
return { ok: true };
|
|
243
|
-
} catch (err) {
|
|
244
|
-
if (err.code === 'ENOENT') {
|
|
245
|
-
return { ok: false, reason: 'not-found' };
|
|
246
|
-
}
|
|
247
|
-
return { ok: false, reason: 'fs-error', error: err.message };
|
|
248
|
-
}
|
|
216
|
+
// Consolidated onto releaseFileLock (#1349); the `pid` staleCheck makes the
|
|
217
|
+
// takeover race unreachable today — if this lock ever moves to a lease mode,
|
|
218
|
+
// the shared guard is what keeps release safe.
|
|
219
|
+
// `sessionId` is the legacy alias for `holder` (see the docblock); an empty
|
|
220
|
+
// expected holder falls through to releaseFileLock's PID + host owner check,
|
|
221
|
+
// exactly as the inline sequence did.
|
|
222
|
+
return releaseFileLock(lockFile, { holder: holder ?? sessionId ?? undefined });
|
|
249
223
|
}
|
|
250
224
|
|
|
251
225
|
/**
|
|
@@ -345,11 +319,15 @@ export async function withStateMdLock(repoRoot, fn, opts = {}) {
|
|
|
345
319
|
caughtError = err;
|
|
346
320
|
} finally {
|
|
347
321
|
// Always release — even on fn() throw — so the lock does not leak.
|
|
348
|
-
//
|
|
349
|
-
//
|
|
322
|
+
// WARN only where OUR lock is left behind: 'fs-error' and 'busy' (the
|
|
323
|
+
// shared guard stayed taken — #1349). 'not-found' and 'not-owner' are
|
|
324
|
+
// recoverable race conditions (someone else cleaned up our lock — already
|
|
325
|
+
// safe to proceed) and stay silent, as before.
|
|
350
326
|
const releaseResult = releaseStateLock({ repoRoot, holder });
|
|
351
327
|
if (!releaseResult.ok && releaseResult.reason === 'fs-error') {
|
|
352
328
|
console.warn(`withStateMdLock: release failed (fs-error: ${releaseResult.error ?? 'unknown'})`);
|
|
329
|
+
} else if (!releaseResult.ok && releaseResult.reason === 'busy') {
|
|
330
|
+
console.warn('withStateMdLock: release failed (busy: acquire guard held) — state.lock left for its stale policy');
|
|
353
331
|
}
|
|
354
332
|
}
|
|
355
333
|
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maintenance-due-banner.mjs — ONE session-start probe for the whole
|
|
3
|
+
* maintenance loop.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this module exists
|
|
6
|
+
*
|
|
7
|
+
* Measured 2026-09-09 across consumer repos: `orchestrator.evolve.completed`
|
|
8
|
+
* fired ZERO times, all 628 learnings were still active, the session-end
|
|
9
|
+
* auto-dialectic nudge recorded `decided: true` while nobody ever ran it, and
|
|
10
|
+
* memory-cleanup had run in 1 of 3 repos. The maintenance loop was not
|
|
11
|
+
* missing — it was ADVERTISED IN THREE PLACES nobody reads at a moment nobody
|
|
12
|
+
* acts: two session-end nudges (3.6.5 auto-dream, 3.6.7 auto-dialectic) fire
|
|
13
|
+
* while the operator is closing down, and the session-start `reconcile-nudge`
|
|
14
|
+
* probe spoke for one signal out of several.
|
|
15
|
+
*
|
|
16
|
+
* This probe replaces all three with a single reading at the ONE moment the
|
|
17
|
+
* operator can act on it — session start — recommending the one verb that
|
|
18
|
+
* runs the whole loop: `/session housekeeping`.
|
|
19
|
+
*
|
|
20
|
+
* ## Design constraints it is written against
|
|
21
|
+
*
|
|
22
|
+
* - **HR-101 (a signal may only warn if it is rare).** Nothing is due ⇒ the
|
|
23
|
+
* probe is SILENT. Six independent signals are ANDed with a cooldown, not
|
|
24
|
+
* ORed into a permanent warning: a repo that ran `/session housekeeping`
|
|
25
|
+
* within the last {@link HOUSEKEEPING_COOLDOWN_DAYS} days says nothing at
|
|
26
|
+
* all, because the operator already did the thing the banner would ask for.
|
|
27
|
+
* - **HR-106 (the banner reports what the rule judged).** Every DUE signal
|
|
28
|
+
* carries the number or the date that made it due — never a bare count.
|
|
29
|
+
* - **Three-state, never two.** A signal whose inputs cannot be read goes to
|
|
30
|
+
* `undeterminable`; it is never folded into "clean". Copied wholesale from
|
|
31
|
+
* `reconcile-nudge-banner.mjs`'s `never` vs `undeterminable` discipline.
|
|
32
|
+
* - **NO new Session Config key.** Every threshold below is an EXISTING key or
|
|
33
|
+
* an existing module constant. Same posture as `reconcile-nudge-banner.mjs`
|
|
34
|
+
* and `loop-readiness-banner.mjs`: an advisory banner needs no switch.
|
|
35
|
+
*
|
|
36
|
+
* ## The six signals
|
|
37
|
+
*
|
|
38
|
+
* | id | due when | source |
|
|
39
|
+
* |-------------------|-----------------------------------------------------------------------|--------|
|
|
40
|
+
* | `evolve` | no `orchestrator.evolve.completed` on record AND ≥20 active learnings | `events.jsonl` + `computeReconcileNudge` |
|
|
41
|
+
* | `sweep` | the dry-run expiry sweep would archive ≥1 entry | `learnings/expiry-sweep.mjs` |
|
|
42
|
+
* | `reconcile` | `computeReconcileNudge().nudge === true` | `reconcile-nudge-banner.mjs` (reused whole) |
|
|
43
|
+
* | `dialectic` | `shouldDispatchAutoDialectic().trigger === true` | `auto-dialectic.mjs` |
|
|
44
|
+
* | `memory-cleanup` | `shouldDispatchAutoDream().trigger === true` | `auto-dream.mjs` |
|
|
45
|
+
* | `pending-sidecar` | a pending dream/dialectic proposal younger than 14 days is unapplied | `.orchestrator/*-pending*.md` |
|
|
46
|
+
*
|
|
47
|
+
* Only SIDE-EFFECT-FREE signal functions are called — never a variant that
|
|
48
|
+
* advances `.orchestrator/dialectic-last-run`, because a probe that writes the
|
|
49
|
+
* stamp would consume the very signal it reports. (The former recording wrapper
|
|
50
|
+
* around this signal was removed in #1288; only the pure decision function
|
|
51
|
+
* remains.)
|
|
52
|
+
*
|
|
53
|
+
* Never throws. `computeMaintenanceDue` always returns the full shape;
|
|
54
|
+
* `checkMaintenanceDue` returns the banner object or `null`.
|
|
55
|
+
*
|
|
56
|
+
* @module scripts/lib/maintenance-due-banner
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { closeSync, existsSync, fstatSync, openSync, readSync, statSync } from 'node:fs';
|
|
60
|
+
import path from 'node:path';
|
|
61
|
+
|
|
62
|
+
import { computeReconcileNudge } from './reconcile-nudge-banner.mjs';
|
|
63
|
+
import { sweepExpiredLearnings } from './learnings/expiry-sweep.mjs';
|
|
64
|
+
import { shouldDispatchAutoDialectic } from './auto-dialectic.mjs';
|
|
65
|
+
import { shouldDispatchAutoDream } from './auto-dream.mjs';
|
|
66
|
+
import { resolveMemoryDir } from './memory-paths.mjs';
|
|
67
|
+
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
68
|
+
import { filterRealSessions } from './session-schema.mjs';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How many signals this probe knows about. The banner denominator is this
|
|
72
|
+
* number MINUS the signals a kill-switch skipped on this host — see the
|
|
73
|
+
* `skipped` array in {@link computeMaintenanceDue}'s result.
|
|
74
|
+
*/
|
|
75
|
+
export const MAINTENANCE_TOTAL_SIGNALS = 6;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Active-learning floor for the `evolve` signal. Deliberately the SAME number
|
|
79
|
+
* as `NUDGE_MIN_LEARNINGS` in `reconcile-nudge-banner.mjs` (20) — both answer
|
|
80
|
+
* "is the corpus large enough that never having processed it is a finding?".
|
|
81
|
+
* Defined locally rather than imported so the two can diverge if the reconcile
|
|
82
|
+
* threshold is ever retuned for reconcile-specific reasons.
|
|
83
|
+
*/
|
|
84
|
+
export const MAINTENANCE_MIN_LEARNINGS = 20;
|
|
85
|
+
|
|
86
|
+
/** Suppress the banner when a housekeeping session completed this recently. */
|
|
87
|
+
export const HOUSEKEEPING_COOLDOWN_DAYS = 7;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A pending sidecar older than this is not a nudge, it is archaeology — the
|
|
91
|
+
* operator has demonstrably moved on, and re-raising it every session start is
|
|
92
|
+
* the HR-101 failure mode.
|
|
93
|
+
*/
|
|
94
|
+
export const SIDECAR_MAX_AGE_DAYS = 14;
|
|
95
|
+
|
|
96
|
+
const MS_PER_DAY = 86_400_000;
|
|
97
|
+
|
|
98
|
+
/** The event name whose ABSENCE is the `evolve` signal. */
|
|
99
|
+
const EVOLVE_EVENT = 'orchestrator.evolve.completed';
|
|
100
|
+
|
|
101
|
+
/** Pending-proposal sidecars, repo-relative (owned by auto-dream / auto-dialectic). */
|
|
102
|
+
const PENDING_SIDECARS = ['.orchestrator/pending-dream.md', '.orchestrator/dialectic-pending.md'];
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Small readers — every one of them is never-throw and three-state
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/** @param {unknown} now @returns {number} */
|
|
109
|
+
function nowMs(now) {
|
|
110
|
+
if (now instanceof Date) return now.getTime();
|
|
111
|
+
if (typeof now === 'number' && Number.isFinite(now)) return now;
|
|
112
|
+
const parsed = typeof now === 'string' ? Date.parse(now) : NaN;
|
|
113
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** ISO timestamp → `YYYY-MM-DD`, or null. */
|
|
117
|
+
function isoDay(ts) {
|
|
118
|
+
return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Backwards-scan chunk size (#1290 item 2).
|
|
123
|
+
*
|
|
124
|
+
* NAMED CEILING: 256 KiB is ~800 records in this repo's ledger, so the common
|
|
125
|
+
* case — a repo that ran /evolve within its recent history — answers after a
|
|
126
|
+
* handful of reads instead of loading the whole 7.9 MB file. The scan is
|
|
127
|
+
* UNBOUNDED in the worst case ON PURPOSE: "never ran" is a claim about every
|
|
128
|
+
* line and cannot be made from a tail, so a repo with no `evolve.completed`
|
|
129
|
+
* record still walks the file to its start — just in chunks, never all at once
|
|
130
|
+
* in one string.
|
|
131
|
+
*
|
|
132
|
+
* REVISIT TRIGGER: the maintenance probe's median passes 1000 ms (half
|
|
133
|
+
* `PROBE_BUDGET_MS`), or one repo's `events.jsonl` passes 50 MB. Either means
|
|
134
|
+
* the "never ran" walk has become the cost that matters and the answer needs an
|
|
135
|
+
* index rather than a scan.
|
|
136
|
+
*/
|
|
137
|
+
export const TAIL_CHUNK_BYTES = 256 * 1024;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Scan a buffer of COMPLETE lines backwards for the newest evolve record.
|
|
141
|
+
*
|
|
142
|
+
* @param {Buffer} buf
|
|
143
|
+
* @returns {{lastAt: string|null}|null} null ⇒ no record in this buffer
|
|
144
|
+
*/
|
|
145
|
+
function scanEvolveLines(buf) {
|
|
146
|
+
if (buf.length === 0) return null;
|
|
147
|
+
const lines = buf.toString('utf8').split('\n');
|
|
148
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
149
|
+
const line = lines[i];
|
|
150
|
+
if (!line || !line.includes(EVOLVE_EVENT)) continue; // cheap pre-filter before JSON.parse
|
|
151
|
+
try {
|
|
152
|
+
const rec = JSON.parse(line);
|
|
153
|
+
if (rec?.event !== EVOLVE_EVENT) continue;
|
|
154
|
+
return { lastAt: typeof rec.timestamp === 'string' ? rec.timestamp : null };
|
|
155
|
+
} catch {
|
|
156
|
+
continue; // a malformed line is not evidence either way — keep scanning
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find the most recent `orchestrator.evolve.completed` record.
|
|
164
|
+
*
|
|
165
|
+
* Reads the ledger BACKWARDS in {@link TAIL_CHUNK_BYTES} chunks and stops at
|
|
166
|
+
* the first hit, because the interesting answer is the LAST occurrence. The
|
|
167
|
+
* former implementation `readFileSync`-ed the whole file (7.9 MB here, 30–44 ms)
|
|
168
|
+
* to answer a question the last few kilobytes usually settle.
|
|
169
|
+
*
|
|
170
|
+
* The one bug a naive chunked scan introduces is a record SPLIT across a chunk
|
|
171
|
+
* boundary: the bytes before the first newline of a chunk are the tail of a line
|
|
172
|
+
* whose head is in the chunk not read yet, so they are CARRIED, never parsed
|
|
173
|
+
* here. Splitting on the 0x0A byte is safe on UTF-8 — no continuation byte can
|
|
174
|
+
* equal a newline — so a multibyte character never splits a line either.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} repoRoot
|
|
177
|
+
* @returns {{ok: boolean, lastAt: string|null}} `ok: false` ⇒ the ledger exists
|
|
178
|
+
* but could not be read — the caller must record `undeterminable`, never clean.
|
|
179
|
+
*/
|
|
180
|
+
function readLastEvolveRun(repoRoot) {
|
|
181
|
+
const file = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
|
|
182
|
+
if (!existsSync(file)) return { ok: true, lastAt: null }; // fresh repo: genuinely never
|
|
183
|
+
let fd;
|
|
184
|
+
try {
|
|
185
|
+
fd = openSync(file, 'r');
|
|
186
|
+
let pos = fstatSync(fd).size;
|
|
187
|
+
/** Partial line at the FRONT of everything read so far. */
|
|
188
|
+
let carry = Buffer.alloc(0);
|
|
189
|
+
|
|
190
|
+
while (pos > 0) {
|
|
191
|
+
const length = Math.min(TAIL_CHUNK_BYTES, pos);
|
|
192
|
+
pos -= length;
|
|
193
|
+
const buf = Buffer.alloc(length);
|
|
194
|
+
readSync(fd, buf, 0, length, pos);
|
|
195
|
+
const block = carry.length > 0 ? Buffer.concat([buf, carry]) : buf;
|
|
196
|
+
const firstNewline = block.indexOf(0x0a);
|
|
197
|
+
if (firstNewline === -1) {
|
|
198
|
+
carry = block; // no complete line yet — a line longer than one chunk
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const hit = scanEvolveLines(block.subarray(firstNewline + 1));
|
|
202
|
+
if (hit) return { ok: true, lastAt: hit.lastAt };
|
|
203
|
+
carry = block.subarray(0, firstNewline);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// pos === 0: the carry is the file's FIRST line, complete by construction.
|
|
207
|
+
const hit = scanEvolveLines(carry);
|
|
208
|
+
return { ok: true, lastAt: hit ? hit.lastAt : null };
|
|
209
|
+
} catch {
|
|
210
|
+
return { ok: false, lastAt: null };
|
|
211
|
+
} finally {
|
|
212
|
+
if (fd !== undefined) {
|
|
213
|
+
try {
|
|
214
|
+
closeSync(fd);
|
|
215
|
+
} catch {
|
|
216
|
+
/* best-effort */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Most recent COMPLETED housekeeping session, or null.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} repoRoot
|
|
226
|
+
* @returns {string|null} ISO timestamp
|
|
227
|
+
*/
|
|
228
|
+
function readLastHousekeeping(repoRoot) {
|
|
229
|
+
try {
|
|
230
|
+
const entries = filterRealSessions(readCanonicalSessions({ repoRoot }));
|
|
231
|
+
let max = null;
|
|
232
|
+
for (const e of entries) {
|
|
233
|
+
if (e?.session_type !== 'housekeeping') continue;
|
|
234
|
+
const ts = e.completed_at;
|
|
235
|
+
if (typeof ts !== 'string' || ts.length === 0) continue;
|
|
236
|
+
if (max === null || ts > max) max = ts;
|
|
237
|
+
}
|
|
238
|
+
return max;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Claude-Code-only memory dir gate — mirrors `session-end/phase-skip.mjs`. */
|
|
245
|
+
function isClaudePlatform(platform) {
|
|
246
|
+
if (platform === undefined || platform === null || platform === '') return true;
|
|
247
|
+
const p = String(platform).toLowerCase();
|
|
248
|
+
return p === 'claude' || p === 'claude-code' || p === 'claudecode';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Computation
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compute the maintenance-due reading. Pure with respect to the repo: reads
|
|
257
|
+
* only, writes nothing, advances no last-run stamp. Never throws.
|
|
258
|
+
*
|
|
259
|
+
* @param {object} [opts]
|
|
260
|
+
* @param {string} [opts.repoRoot] — defaults to `process.cwd()`.
|
|
261
|
+
* @param {object} [opts.config] — parsed Session Config (thresholds only).
|
|
262
|
+
* @param {Date|number} [opts.now] — injectable clock.
|
|
263
|
+
* @param {string} [opts.platform] — harness platform; non-Claude skips `memory-cleanup`.
|
|
264
|
+
* @returns {Promise<{
|
|
265
|
+
* due: Array<{id: string, detail: string}>,
|
|
266
|
+
* total: number,
|
|
267
|
+
* skipped: string[],
|
|
268
|
+
* undeterminable: string[],
|
|
269
|
+
* lastHousekeeping: string|null,
|
|
270
|
+
* }>}
|
|
271
|
+
*/
|
|
272
|
+
export async function computeMaintenanceDue(opts = {}) {
|
|
273
|
+
const repoRoot =
|
|
274
|
+
typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0 ? opts.repoRoot : process.cwd();
|
|
275
|
+
const config = opts.config ?? {};
|
|
276
|
+
const now = nowMs(opts.now);
|
|
277
|
+
|
|
278
|
+
/** @type {Array<{id: string, detail: string}>} */
|
|
279
|
+
const due = [];
|
|
280
|
+
/** @type {string[]} */
|
|
281
|
+
const undeterminable = [];
|
|
282
|
+
/**
|
|
283
|
+
* Signals a kill-switch turned OFF for this host. They are neither due nor
|
|
284
|
+
* undeterminable — they were never judged, so they must not appear in the
|
|
285
|
+
* denominator either (HR-106: the banner reports what the rule judged).
|
|
286
|
+
* @type {string[]}
|
|
287
|
+
*/
|
|
288
|
+
const skipped = [];
|
|
289
|
+
const markDue = (id, detail) => due.push({ id, detail });
|
|
290
|
+
|
|
291
|
+
// --- reconcile (S3) + the active-learning count S1 needs ------------------
|
|
292
|
+
// ONE call serves both signals: re-deriving the active-learning filter here
|
|
293
|
+
// would be a second, drifting copy of the reconcile thresholds (BV-001.2).
|
|
294
|
+
let nudge;
|
|
295
|
+
try {
|
|
296
|
+
nudge = await computeReconcileNudge({ repoRoot, now: opts.now });
|
|
297
|
+
} catch {
|
|
298
|
+
/* left undefined — the two signals below become `undeterminable`, not clean */
|
|
299
|
+
}
|
|
300
|
+
if (!nudge) {
|
|
301
|
+
undeterminable.push('evolve', 'reconcile');
|
|
302
|
+
} else {
|
|
303
|
+
// --- evolve (S1) -------------------------------------------------------
|
|
304
|
+
const evolve = readLastEvolveRun(repoRoot);
|
|
305
|
+
if (!evolve.ok) {
|
|
306
|
+
undeterminable.push('evolve');
|
|
307
|
+
} else if (evolve.lastAt === null && nudge.activeLearnings >= MAINTENANCE_MIN_LEARNINGS) {
|
|
308
|
+
markDue('evolve', `never, ${nudge.activeLearnings} active learnings`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- reconcile (S3) ----------------------------------------------------
|
|
312
|
+
if (nudge.nudge === true) {
|
|
313
|
+
// HR-106: report what the rule JUDGED. `computeReconcileNudge` nudges on
|
|
314
|
+
// a BACKLOG (eligible-unmaterialized learnings), never on a date — so
|
|
315
|
+
// printing `lastRunAt` here put today's date next to the word "due" and
|
|
316
|
+
// read as "last run today and already due again". Measured 2026-09-09
|
|
317
|
+
// (learning 013a45ba): with 108 learnings capped under
|
|
318
|
+
// `max-proposals-per-run: 10`, the row stayed due after every run while
|
|
319
|
+
// showing that run's own date. `reasons` is the judgment itself.
|
|
320
|
+
const why = Array.isArray(nudge.reasons) && nudge.reasons.length > 0
|
|
321
|
+
? nudge.reasons.join(', ')
|
|
322
|
+
: `last run ${isoDay(nudge.lastRunAt) ?? 'never'}`;
|
|
323
|
+
markDue('reconcile', why);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- sweep (S2) ----------------------------------------------------------
|
|
328
|
+
try {
|
|
329
|
+
const metrics = path.join(repoRoot, '.orchestrator', 'metrics');
|
|
330
|
+
const res = await sweepExpiredLearnings({
|
|
331
|
+
filePath: path.join(metrics, 'learnings.jsonl'),
|
|
332
|
+
archivePath: path.join(metrics, 'learnings-archive.jsonl'),
|
|
333
|
+
now: opts.now,
|
|
334
|
+
dryRun: true,
|
|
335
|
+
});
|
|
336
|
+
if (Number(res?.archived) > 0) markDue('sweep', `${res.archived} expired`);
|
|
337
|
+
} catch {
|
|
338
|
+
undeterminable.push('sweep');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- dialectic (S4) ------------------------------------------------------
|
|
342
|
+
const cadence = config?.dialectic?.cadence ?? 5;
|
|
343
|
+
if (cadence === 0) {
|
|
344
|
+
skipped.push('dialectic');
|
|
345
|
+
} else {
|
|
346
|
+
try {
|
|
347
|
+
const dec = await shouldDispatchAutoDialectic({ repoRoot, cadence });
|
|
348
|
+
if (dec?.trigger === true) {
|
|
349
|
+
markDue('dialectic', isoDay(dec.signals?.lastRunAt) ?? 'never');
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
undeterminable.push('dialectic');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- memory-cleanup (S5) -------------------------------------------------
|
|
357
|
+
const threshold = config?.['memory-cleanup-threshold'] ?? 5;
|
|
358
|
+
if (threshold === 0 || !isClaudePlatform(opts.platform)) {
|
|
359
|
+
skipped.push('memory-cleanup');
|
|
360
|
+
} else {
|
|
361
|
+
try {
|
|
362
|
+
const dec = await shouldDispatchAutoDream({
|
|
363
|
+
repoRoot,
|
|
364
|
+
memoryDir: resolveMemoryDir(repoRoot),
|
|
365
|
+
threshold,
|
|
366
|
+
softLimit: config?.['memory-cleanup-soft-limit'] ?? 180,
|
|
367
|
+
});
|
|
368
|
+
if (dec?.trigger === true) {
|
|
369
|
+
const s = dec.signals ?? {};
|
|
370
|
+
markDue(
|
|
371
|
+
'memory-cleanup',
|
|
372
|
+
isoDay(s.lastCleanupAt) ?? `never, ${s.sessionsSinceCleanup ?? 0} sessions`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
undeterminable.push('memory-cleanup');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// --- pending sidecar (S6) ------------------------------------------------
|
|
381
|
+
try {
|
|
382
|
+
const pending = [];
|
|
383
|
+
for (const rel of PENDING_SIDECARS) {
|
|
384
|
+
const file = path.join(repoRoot, rel);
|
|
385
|
+
if (!existsSync(file)) continue;
|
|
386
|
+
const ageDays = (now - statSync(file).mtimeMs) / MS_PER_DAY;
|
|
387
|
+
if (ageDays <= SIDECAR_MAX_AGE_DAYS) pending.push(path.basename(rel));
|
|
388
|
+
}
|
|
389
|
+
if (pending.length > 0) markDue('pending-sidecar', pending.join(', '));
|
|
390
|
+
} catch {
|
|
391
|
+
undeterminable.push('pending-sidecar');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
due,
|
|
396
|
+
// HR-106: the denominator counts the signals this host actually EVALUATED.
|
|
397
|
+
// A signal a kill-switch turned off was never judged, so reporting it in
|
|
398
|
+
// "3 of 6" would quote a denominator the rule never used.
|
|
399
|
+
total: MAINTENANCE_TOTAL_SIGNALS - skipped.length,
|
|
400
|
+
skipped,
|
|
401
|
+
undeterminable,
|
|
402
|
+
lastHousekeeping: readLastHousekeeping(repoRoot),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Session-start probe entry point.
|
|
408
|
+
*
|
|
409
|
+
* Silent (`null`) when nothing is due and everything was readable, and ALSO
|
|
410
|
+
* when a housekeeping session completed within {@link HOUSEKEEPING_COOLDOWN_DAYS}
|
|
411
|
+
* days — the cooldown is what keeps this instrument rare (HR-101). The
|
|
412
|
+
* computation still runs and is still available via {@link computeMaintenanceDue};
|
|
413
|
+
* only the banner is suppressed.
|
|
414
|
+
*
|
|
415
|
+
* @param {{repoRoot?: string, config?: object, now?: Date|number, platform?: string}} [opts]
|
|
416
|
+
* @returns {Promise<null | {severity: 'warn', message: string}>}
|
|
417
|
+
*/
|
|
418
|
+
export async function checkMaintenanceDue(opts = {}) {
|
|
419
|
+
try {
|
|
420
|
+
const computed = await computeMaintenanceDue(opts);
|
|
421
|
+
if (computed.due.length === 0 && computed.undeterminable.length === 0) return null;
|
|
422
|
+
|
|
423
|
+
// Cooldown — no new config key: the housekeeping session's own record IS
|
|
424
|
+
// the "last maintenance run" stamp.
|
|
425
|
+
if (computed.lastHousekeeping !== null) {
|
|
426
|
+
const ageMs = nowMs(opts.now) - Date.parse(computed.lastHousekeeping);
|
|
427
|
+
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < HOUSEKEEPING_COOLDOWN_DAYS * MS_PER_DAY) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// HR-106: every number in the banner is a number the verdict was computed
|
|
433
|
+
// from — no aggregate stands alone without the signal that produced it.
|
|
434
|
+
const detail = computed.due.map((d) => `${d.id}: ${d.detail}`).join(' · ');
|
|
435
|
+
const unknown =
|
|
436
|
+
computed.undeterminable.length > 0
|
|
437
|
+
? ` · undeterminable: ${computed.undeterminable.join(', ')}`
|
|
438
|
+
: '';
|
|
439
|
+
const message =
|
|
440
|
+
`⚠ maintenance due: ${computed.due.length} of ${computed.total}` +
|
|
441
|
+
(detail ? ` (${detail})` : '') +
|
|
442
|
+
unknown +
|
|
443
|
+
' — run /session housekeeping.';
|
|
444
|
+
|
|
445
|
+
return { severity: 'warn', message };
|
|
446
|
+
} catch {
|
|
447
|
+
// Defensive catch-all — a session-start banner must never throw.
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
@@ -4,69 +4,52 @@
|
|
|
4
4
|
# ~/.config/session-orchestrator/owner.yaml (or $XDG_CONFIG_HOME/...) and
|
|
5
5
|
# fill in your values. NEVER commit this file to a project repo.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# docs/owner-config-schema.md.
|
|
9
|
-
|
|
10
|
-
# Schema version. MUST be 1. The plugin refuses to load mismatched versions.
|
|
11
|
-
schema-version: 1
|
|
7
|
+
# SSOT is code, not this file: scripts/lib/owner-yaml.mjs (schema, validator,
|
|
8
|
+
# loader, writer). Doc cross-check: docs/owner-config-schema.md.
|
|
12
9
|
|
|
13
10
|
owner:
|
|
14
|
-
# Required. Display name.
|
|
11
|
+
# Required. Display name.
|
|
15
12
|
name: Your Name
|
|
16
13
|
|
|
17
|
-
#
|
|
18
|
-
# identity correlation when sharing learnings (D4/C4). 64 hex chars exactly,
|
|
19
|
-
# or null to opt out. Plugin can compute this for you on first bootstrap.
|
|
20
|
-
email-hash: null
|
|
21
|
-
|
|
22
|
-
# Required. ISO-639-1 language code (e.g. "de", "en"). Optionally with a
|
|
23
|
-
# region tag like "en-US". Drives default response language in soul.md.
|
|
14
|
+
# Required. ISO-639-1 language code — only "de" or "en" are valid.
|
|
24
15
|
language: en
|
|
25
16
|
|
|
26
17
|
tone:
|
|
27
|
-
#
|
|
28
|
-
# Default: neutral.
|
|
18
|
+
# Required. One of: direct | neutral | friendly.
|
|
29
19
|
style: direct
|
|
30
20
|
|
|
31
|
-
# Optional free-form descriptor.
|
|
32
|
-
# "austrian-pragmatic", "british-dry", "minimal-comments".
|
|
21
|
+
# Optional free-form descriptor, e.g. "austrian-pragmatic", "british-dry".
|
|
33
22
|
tonality: austrian-pragmatic
|
|
34
23
|
|
|
35
24
|
efficiency:
|
|
36
|
-
# Caveman-inspired output dial. One of: lite | full | ultra.
|
|
37
|
-
# lite — minimum tokens, telegram style
|
|
25
|
+
# Required. Caveman-inspired output dial. One of: lite | full | ultra.
|
|
26
|
+
# lite — minimum tokens, telegram style.
|
|
38
27
|
# full — default, sentence-style narration.
|
|
39
|
-
# ultra — full + extra context, narrative tone
|
|
28
|
+
# ultra — full + extra context, narrative tone.
|
|
40
29
|
output-level: full
|
|
41
30
|
|
|
42
|
-
# Pre-tool-call narration intensity. One of: minimal | verbose.
|
|
31
|
+
# Required. Pre-tool-call narration intensity. One of: minimal | verbose.
|
|
43
32
|
preamble: minimal
|
|
44
33
|
|
|
45
|
-
# Inline code comments. One of: minimal | full.
|
|
46
|
-
comments-in-code: minimal
|
|
47
|
-
|
|
48
34
|
hardware-sharing:
|
|
49
|
-
# Consent gate for sharing hardware-pattern learnings
|
|
50
|
-
#
|
|
51
|
-
#
|
|
35
|
+
# Required. Consent gate for sharing hardware-pattern learnings. When true,
|
|
36
|
+
# the plugin may export anonymized hardware patterns (e.g. "Mac M1 16GB
|
|
37
|
+
# freezes at 8 parallel agents"). Default: false (off).
|
|
52
38
|
enabled: false
|
|
53
39
|
|
|
54
|
-
#
|
|
55
|
-
# anonymous host-class identifier.
|
|
56
|
-
#
|
|
57
|
-
hash-salt:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
created_at: null
|
|
71
|
-
# Auto-bumped on every successful save.
|
|
72
|
-
updated_at: null
|
|
40
|
+
# Required (non-empty) only when enabled: true above. Per-host salt used to
|
|
41
|
+
# derive a stable but anonymous host-class identifier. Leave empty while
|
|
42
|
+
# enabled: false — the plugin can generate one for you on opt-in.
|
|
43
|
+
hash-salt: ''
|
|
44
|
+
|
|
45
|
+
# Optional. Host-local path overrides (#653). Uncomment and set any of these
|
|
46
|
+
# to override the committed repo-level defaults on THIS host only.
|
|
47
|
+
# paths:
|
|
48
|
+
# vault-dir: '' # '' = no override
|
|
49
|
+
# baseline-path: '' # '' = no override
|
|
50
|
+
# namespace-map-path: '' # '' = no override (#725 D5)
|
|
51
|
+
# confidential-names-file: '' # '' = no override (#728a)
|
|
52
|
+
|
|
53
|
+
# Optional. Host-local cross-repo dispatcher autonomy override (#679).
|
|
54
|
+
# dispatcher:
|
|
55
|
+
# autonomy: '' # '' = no override; one of off | advisory | autonomous-gated
|