session-orchestrator 3.22.0 → 3.24.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Exports:
|
|
20
20
|
* GENERATOR_MARKER — frontmatter sentinel identifying generator-owned files
|
|
21
|
+
* NARRATIVE_EVENT — canonical telemetry event name for a mirror attempt
|
|
21
22
|
* extractNarrative — pure: STATE.md contents → { waveHistory, deviations, whatNotToRetry, missionStatus }
|
|
22
23
|
* renderNarrative — pure: narrative + repo + now → full markdown (frontmatter + body)
|
|
23
24
|
* writeNarrative — idempotent write with skip-handwritten / skip-noop / dry-run + _overview refusal
|
|
@@ -28,8 +29,8 @@
|
|
|
28
29
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
29
30
|
import { readFile } from 'node:fs/promises';
|
|
30
31
|
import path from 'node:path';
|
|
31
|
-
import os from 'node:os';
|
|
32
32
|
|
|
33
|
+
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
33
34
|
import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
|
|
34
35
|
import {
|
|
35
36
|
parseFrontmatter,
|
|
@@ -41,6 +42,7 @@ import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
|
|
|
41
42
|
import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
42
43
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
43
44
|
import { createSecretValueMasker } from '../secret-masker.mjs';
|
|
45
|
+
import { expandTilde } from '../common.mjs';
|
|
44
46
|
|
|
45
47
|
/** Frontmatter sentinel that identifies generator-owned narrative files. */
|
|
46
48
|
export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
|
|
@@ -61,23 +63,6 @@ const SECTION_TITLES = {
|
|
|
61
63
|
|
|
62
64
|
// ── Raw section extraction ──────────────────────────────────────────────────────
|
|
63
65
|
|
|
64
|
-
/**
|
|
65
|
-
* Expand a leading `~` to the current user's home directory.
|
|
66
|
-
*
|
|
67
|
-
* NOTE: deferred shared-helper extraction. The same `expandHome` pattern lives in
|
|
68
|
-
* other vault-status modules; W2 forbids introducing a shared new file, so this is
|
|
69
|
-
* inlined here. Consolidate into a shared util in a follow-up wave.
|
|
70
|
-
*
|
|
71
|
-
* @param {string} p
|
|
72
|
-
* @returns {string}
|
|
73
|
-
*/
|
|
74
|
-
function expandHome(p) {
|
|
75
|
-
if (typeof p !== 'string' || p.length === 0) return p;
|
|
76
|
-
if (p === '~') return os.homedir();
|
|
77
|
-
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
78
|
-
return p;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
66
|
/**
|
|
82
67
|
* Match an ATX markdown heading line. Returns `{ level, text }` or null.
|
|
83
68
|
*
|
|
@@ -557,6 +542,128 @@ function resolveLooseSlug(vaultDir, candidateSlug, fsSeam = {}) {
|
|
|
557
542
|
return matches.length === 1 ? matches[0] : candidateSlug;
|
|
558
543
|
}
|
|
559
544
|
|
|
545
|
+
// ── Telemetry ────────────────────────────────────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Canonical event name for a narrative-mirror attempt (issue #1129).
|
|
549
|
+
*
|
|
550
|
+
* ONE event per {@link mirrorNarrative} call — the REJECTION and NO-OP paths
|
|
551
|
+
* included, because those are the point. Before this existed, a vault-disabled
|
|
552
|
+
* config, a missing STATE.md, a hand-authored target file and a healthy write
|
|
553
|
+
* were all indistinguishable from the ledger's side: measured 2026-08-23 over
|
|
554
|
+
* 28 387 records in `.orchestrator/metrics/events.jsonl`, ZERO board/mirror
|
|
555
|
+
* events, because this module did not import {@link emitEvent} at all. Its only
|
|
556
|
+
* caller is shell prose in `skills/session-end/session-metrics-write.md`, whose
|
|
557
|
+
* `mode: warn` degradation prints a WARNING and closes the session anyway — so
|
|
558
|
+
* an outage of this writer left no durable trace whatsoever.
|
|
559
|
+
*
|
|
560
|
+
* Name deliberately NOT minted here: it is the one the wave assigned, so the
|
|
561
|
+
* fact "a narrative mirror ran" has exactly one address in the ledger.
|
|
562
|
+
*/
|
|
563
|
+
export const NARRATIVE_EVENT = 'orchestrator.vault.narrative_mirrored';
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Emit the narrative-mirror telemetry record. Best-effort: never throws, never
|
|
567
|
+
* alters the mirror result.
|
|
568
|
+
*
|
|
569
|
+
* ABSENT IS NOT ZERO (`docs/events-schema.md`): every optional field is spread
|
|
570
|
+
* conditionally, so an UNMEASURED field is MISSING from the record rather than
|
|
571
|
+
* written as `0`. `chars: 0` would then honestly mean "the narrative rendered to
|
|
572
|
+
* an empty document"; an absent `chars` means "no render was reached on this
|
|
573
|
+
* path" — reading the missing key as `0` conflates the two in both directions.
|
|
574
|
+
*
|
|
575
|
+
* NAMED CEILING (build-value BV-004): when `repoRoot` is absent or empty this
|
|
576
|
+
* emits NOTHING. `emitEvent` without `opts.repoRoot` falls back to
|
|
577
|
+
* `SO_PROJECT_DIR` (#941), so the only destination left for a rootless call is
|
|
578
|
+
* whichever repo the process happens to sit in — i.e. a caller that named no
|
|
579
|
+
* repo would silently append to a FOREIGN ledger, and the two call shapes that
|
|
580
|
+
* reach this branch today are the `repoRoot: ''` / omitted-`repoRoot` unit
|
|
581
|
+
* tests, which would then write synthetic records into this repo's real fleet
|
|
582
|
+
* telemetry on every suite run. REVISIT TRIGGER: if a production caller ever
|
|
583
|
+
* legitimately invokes `mirrorNarrative` without a `repoRoot`, that call needs a
|
|
584
|
+
* destination decision of its own — not this silent skip.
|
|
585
|
+
*
|
|
586
|
+
* @param {object} opts
|
|
587
|
+
* @param {string} [opts.repoRoot] — pins the ledger to THIS repo's
|
|
588
|
+
* `.orchestrator/metrics/events.jsonl` (#941), and supplies session attribution.
|
|
589
|
+
* @param {string} opts.action — the `action` the mirror returned, or `'error'`
|
|
590
|
+
* when the writer threw. ALWAYS present.
|
|
591
|
+
* @param {string} [opts.path] — resolved narrative path, when one was resolved.
|
|
592
|
+
* @param {number} [opts.chars] — length of the rendered narrative document this
|
|
593
|
+
* call produced. Present whenever the render was reached (so also on
|
|
594
|
+
* `skipped-noop` / `dry-run`, where the document was built but not written —
|
|
595
|
+
* `action` is what says whether it landed); absent on the earlier no-op paths,
|
|
596
|
+
* which return before anything is rendered.
|
|
597
|
+
* @param {string} [opts.errorCode] — `err.code` on the throw path (a bounded
|
|
598
|
+
* token such as `EACCES`). The error MESSAGE is deliberately NOT recorded: it
|
|
599
|
+
* can quote a path or STATE.md content, and this module's whole reason for
|
|
600
|
+
* masking (#1025) is that such prose carries secrets.
|
|
601
|
+
* @returns {Promise<void>}
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* Reduce an absolute vault path to its LAST TWO segments for telemetry.
|
|
605
|
+
*
|
|
606
|
+
* The full path is the module's public return contract and stays untouched.
|
|
607
|
+
* What must not travel is the path in the EMITTED payload: on a real host it
|
|
608
|
+
* reads `/Users/<name>/Projects/<vault>/01-projects/<private-slug>/…`, i.e. an
|
|
609
|
+
* OS username plus a private project slug. Those are exactly the two shapes
|
|
610
|
+
* `scripts/lib/validate/check-owner-leakage.mjs` blocks as CP1 and CP6 — and
|
|
611
|
+
* that scanner structurally cannot see this one, because it walks `git ls-files`
|
|
612
|
+
* and `.orchestrator/metrics/*.jsonl` is gitignored (`.gitignore:40`).
|
|
613
|
+
* The record is invisible to the pre-commit guard and visible to the optional
|
|
614
|
+
* Clank webhook (`scripts/lib/events.mjs`, `CLANK_EVENT_URL`), which posts the
|
|
615
|
+
* payload verbatim with no redaction.
|
|
616
|
+
*
|
|
617
|
+
* The BASENAME is the deliberate ceiling — one segment, not two. Two segments
|
|
618
|
+
* would keep the parent directory, and under `01-projects/` that directory IS
|
|
619
|
+
* the private project slug, i.e. exactly the CP6 shape this is meant to drop.
|
|
620
|
+
* The diagnostic value lives in the filename alone: it says WHICH writer ran
|
|
621
|
+
* (`_session-narrative.md` vs `_active-sessions.md`), which is the question the
|
|
622
|
+
* event exists to answer. Which project it was is already answerable from the
|
|
623
|
+
* record's own `session_id` / repo-scoped ledger location.
|
|
624
|
+
* Revisit trigger: a consumer that needs more than the filename — then it
|
|
625
|
+
* belongs in the RETURN value, which already carries the absolute path, never
|
|
626
|
+
* in the event.
|
|
627
|
+
*
|
|
628
|
+
* @param {unknown} outputPath
|
|
629
|
+
* @returns {string|undefined} `undefined` when there is nothing measured to report.
|
|
630
|
+
*/
|
|
631
|
+
function telemetrySafePath(outputPath) {
|
|
632
|
+
if (typeof outputPath !== 'string' || outputPath.length === 0) return undefined;
|
|
633
|
+
const base = path.basename(outputPath);
|
|
634
|
+
return base.length > 0 ? base : undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function emitNarrativeEvent({ repoRoot, action, path: outputPath, chars, errorCode }) {
|
|
638
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
|
|
639
|
+
try {
|
|
640
|
+
await emitEvent(
|
|
641
|
+
NARRATIVE_EVENT,
|
|
642
|
+
{
|
|
643
|
+
action,
|
|
644
|
+
...(telemetrySafePath(outputPath) !== undefined ? { path_tail: telemetrySafePath(outputPath) } : {}),
|
|
645
|
+
// `typeof … === 'number'` rather than `!= null`: the repo's eslint
|
|
646
|
+
// `eqeqeq: always` forbids the loose form, and this shape additionally
|
|
647
|
+
// refuses a non-numeric `chars` outright. A measured `0` still lands.
|
|
648
|
+
...(typeof chars === 'number' ? { chars } : {}),
|
|
649
|
+
// `typeof === 'string'`, not truthiness: an `err.code` of '' is a measured
|
|
650
|
+
// empty code, and the sibling `chars` field four lines up already states
|
|
651
|
+
// why this file rejects the loose form.
|
|
652
|
+
...(typeof errorCode === 'string' && errorCode.length > 0 ? { error_code: errorCode } : {}),
|
|
653
|
+
// session_id / semantic_session_id — omitted entirely when no session
|
|
654
|
+
// lock is readable (CI, ad-hoc runs). See sessionAttribution's contract:
|
|
655
|
+
// a fabricated id would read as a real session.
|
|
656
|
+
...sessionAttribution(repoRoot),
|
|
657
|
+
},
|
|
658
|
+
{ repoRoot },
|
|
659
|
+
);
|
|
660
|
+
} catch {
|
|
661
|
+
/* Best-effort telemetry. emitEvent does real file I/O (mkdir + append), so a
|
|
662
|
+
read-only or occupied ledger path WILL throw — and a broken ledger must
|
|
663
|
+
never fail a narrative write. The mirror result is authoritative. */
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
560
667
|
// ── Convenience orchestration ────────────────────────────────────────────────────
|
|
561
668
|
|
|
562
669
|
/**
|
|
@@ -594,10 +701,50 @@ function resolveLooseSlug(vaultDir, candidateSlug, fsSeam = {}) {
|
|
|
594
701
|
* @returns {Promise<{ action: string, path?: string }>}
|
|
595
702
|
*/
|
|
596
703
|
export async function mirrorNarrative(opts) {
|
|
704
|
+
let outcome;
|
|
705
|
+
try {
|
|
706
|
+
outcome = await runNarrativeMirror(opts);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
// The harshest silent-failure case: `session-metrics-write.md` catches this
|
|
709
|
+
// throw, prints a WARNING under `mode: warn` and closes the session anyway.
|
|
710
|
+
// Record it, then re-throw — telemetry observes, it never swallows.
|
|
711
|
+
await emitNarrativeEvent({
|
|
712
|
+
repoRoot: opts?.repoRoot,
|
|
713
|
+
action: 'error',
|
|
714
|
+
errorCode: typeof err?.code === 'string' ? err.code : undefined,
|
|
715
|
+
});
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
await emitNarrativeEvent({
|
|
720
|
+
repoRoot: opts?.repoRoot,
|
|
721
|
+
action: outcome.result.action,
|
|
722
|
+
path: outcome.result.path,
|
|
723
|
+
chars: outcome.chars,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
return outcome.result;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* The mirror itself — every early return of {@link mirrorNarrative} lives here.
|
|
731
|
+
*
|
|
732
|
+
* Split out so that exactly ONE emit site covers EVERY outcome: a future early
|
|
733
|
+
* return added inside this function is telemetered by construction, whereas
|
|
734
|
+
* hand-placing an emit beside each of the seven `return`s makes "forgot the new
|
|
735
|
+
* one" the default failure. The `chars` companion travels beside the result
|
|
736
|
+
* rather than inside it because the returned object is a PUBLIC shape that
|
|
737
|
+
* callers (and tests) compare with `toEqual` — adding a key there would be an
|
|
738
|
+
* observable contract change for a purely internal measurement.
|
|
739
|
+
*
|
|
740
|
+
* @param {Parameters<typeof mirrorNarrative>[0]} opts
|
|
741
|
+
* @returns {Promise<{ result: { action: string, path?: string }, chars?: number }>}
|
|
742
|
+
*/
|
|
743
|
+
async function runNarrativeMirror(opts) {
|
|
597
744
|
const { repoRoot, repo, now = new Date(), dryRun = false, fs: injectedFs, hostPaths } = opts;
|
|
598
745
|
|
|
599
746
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
600
|
-
return { action: 'skipped-vault-disabled' };
|
|
747
|
+
return { result: { action: 'skipped-vault-disabled' } };
|
|
601
748
|
}
|
|
602
749
|
|
|
603
750
|
// Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
|
|
@@ -606,12 +753,12 @@ export async function mirrorNarrative(opts) {
|
|
|
606
753
|
const configText = await readConfigFile(repoRoot);
|
|
607
754
|
config = parseSessionConfig(configText, { hostPaths });
|
|
608
755
|
} catch {
|
|
609
|
-
return { action: 'skipped-vault-disabled' };
|
|
756
|
+
return { result: { action: 'skipped-vault-disabled' } };
|
|
610
757
|
}
|
|
611
758
|
|
|
612
759
|
const vaultIntegration = config?.['vault-integration'];
|
|
613
760
|
if (!vaultIntegration || vaultIntegration.enabled !== true) {
|
|
614
|
-
return { action: 'skipped-vault-disabled' };
|
|
761
|
+
return { result: { action: 'skipped-vault-disabled' } };
|
|
615
762
|
}
|
|
616
763
|
|
|
617
764
|
// Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
|
|
@@ -628,10 +775,10 @@ export async function mirrorNarrative(opts) {
|
|
|
628
775
|
|
|
629
776
|
const rawVaultDir = vaultIntegration['vault-dir'];
|
|
630
777
|
if (!rawVaultDir || typeof rawVaultDir !== 'string') {
|
|
631
|
-
return { action: 'skipped-vault-disabled' };
|
|
778
|
+
return { result: { action: 'skipped-vault-disabled' } };
|
|
632
779
|
}
|
|
633
780
|
|
|
634
|
-
const vaultDir = path.resolve(
|
|
781
|
+
const vaultDir = path.resolve(expandTilde(rawVaultDir));
|
|
635
782
|
const candidateSlug = subjectToSlug(repoName) || 'unknown';
|
|
636
783
|
// Loose-match against existing 01-projects/ folders before minting a new
|
|
637
784
|
// slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
|
|
@@ -640,9 +787,19 @@ export async function mirrorNarrative(opts) {
|
|
|
640
787
|
const outputPath = resolveNarrativePath(vaultDir, repoSlug);
|
|
641
788
|
|
|
642
789
|
// Defense-in-depth: ensure the resolved file stays inside the vault root.
|
|
643
|
-
|
|
790
|
+
//
|
|
791
|
+
// `canonicalizeRoot: true` (#1033) is load-bearing, not hygiene. The guard's
|
|
792
|
+
// realpath phase is skipped while the target is ABSENT (ENOENT) and fires once
|
|
793
|
+
// it EXISTS — so on a vault root reached through a symlink, run 1 writes and
|
|
794
|
+
// run 2 resolves the file to the canonical path, reads it as outside the
|
|
795
|
+
// LEXICAL root, and returns `skipped-invalid-path` forever after. Canonicalizing
|
|
796
|
+
// the root makes both sides of the comparison the same kind of path. Same call
|
|
797
|
+
// shape as scripts/lib/reconcile/writer.mjs and scripts/lib/memory-proposals/store.mjs.
|
|
798
|
+
const inside = validatePathInsideProject(path.relative(vaultDir, outputPath), vaultDir, {
|
|
799
|
+
canonicalizeRoot: true,
|
|
800
|
+
});
|
|
644
801
|
if (!inside.ok) {
|
|
645
|
-
return { action: 'skipped-invalid-path', path: outputPath };
|
|
802
|
+
return { result: { action: 'skipped-invalid-path', path: outputPath } };
|
|
646
803
|
}
|
|
647
804
|
|
|
648
805
|
// Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
|
|
@@ -651,7 +808,7 @@ export async function mirrorNarrative(opts) {
|
|
|
651
808
|
try {
|
|
652
809
|
stateContents = await readFile(stateMdPath, 'utf8');
|
|
653
810
|
} catch {
|
|
654
|
-
return { action: 'skipped-no-statemd', path: outputPath };
|
|
811
|
+
return { result: { action: 'skipped-no-statemd', path: outputPath } };
|
|
655
812
|
}
|
|
656
813
|
|
|
657
814
|
// #1025: the ONE masking site for the narrative mirror — after the frontmatter
|
|
@@ -660,5 +817,11 @@ export async function mirrorNarrative(opts) {
|
|
|
660
817
|
const narrative = maskNarrative(extractNarrative(stateContents));
|
|
661
818
|
const content = renderNarrative({ repo: repoName, narrative, now });
|
|
662
819
|
|
|
663
|
-
|
|
820
|
+
// `chars` measures the document THIS call rendered — so it is present on
|
|
821
|
+
// `skipped-noop` and `dry-run` too, where the render happened but nothing
|
|
822
|
+
// landed. `action` is the field that says whether it landed.
|
|
823
|
+
return {
|
|
824
|
+
result: writeNarrative({ outputPath, content, dryRun, fs: injectedFs }),
|
|
825
|
+
chars: content.length,
|
|
826
|
+
};
|
|
664
827
|
}
|