session-orchestrator 3.22.0 → 3.23.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/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 +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- 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 +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- 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 +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -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 +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- 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/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 +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- 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/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- 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/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/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 +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- 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 +281 -13
- 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 +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- 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 +159 -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-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/sync.mjs +46 -8
- 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 +16 -32
- 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-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- 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-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- 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/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- 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 +11 -3
- 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 +30 -16
- 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 +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
* - Strip all absolute paths (macOS, Linux system paths, Windows)
|
|
28
28
|
* - Redact IPv4 addresses
|
|
29
29
|
* - Redact GitHub/GitLab URLs containing org/repo paths
|
|
30
|
-
* - Replace hostname references with `host_class` label
|
|
30
|
+
* - Replace hostname references with `host_class` label — form-based for
|
|
31
|
+
* suffixed names, PLUS value-based for this host's own suffix-stripped
|
|
32
|
+
* `host_id` spellings (#1072 follow-up; see `redactLocalHostNames`)
|
|
31
33
|
* - Redact email, git author, token patterns
|
|
32
34
|
* - No free-form text from user — only structured fields
|
|
33
35
|
* - Round ram/cpu to 1 GB / 10% buckets
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
|
|
46
48
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
47
49
|
import { readFileSync } from 'node:fs';
|
|
50
|
+
import os from 'node:os';
|
|
48
51
|
import path from 'node:path';
|
|
49
52
|
import {
|
|
50
53
|
readLearnings,
|
|
@@ -58,6 +61,7 @@ import {
|
|
|
58
61
|
import { findProjectRoot, resolveInstructionFile, expandTilde } from './lib/common.mjs';
|
|
59
62
|
import { parseSessionConfig } from './lib/config.mjs';
|
|
60
63
|
import { createSecretValueMasker } from './lib/secret-masker.mjs';
|
|
64
|
+
import { stableHostname, readHostAliases } from './lib/host-identity.mjs';
|
|
61
65
|
|
|
62
66
|
// Vault-relative default write target (Epic #774 — docs Public-Split removed
|
|
63
67
|
// the prior in-repo generated telemetry doc in favor of the private Meta-Vault).
|
|
@@ -138,6 +142,137 @@ const SIGNED_OFF_RE = /Signed-off-by:[^\n]+/g;
|
|
|
138
142
|
// those never appear with a .local/.lan/.home suffix.
|
|
139
143
|
const HOSTNAME_RE = /\b[A-Za-z0-9]+(?:[-.][A-Za-z0-9]+)*\.(?:local|lan|home|internal|corp)\b/g;
|
|
140
144
|
|
|
145
|
+
/** Marker for every host-identifying redaction — form-based AND value-based. */
|
|
146
|
+
const HOSTNAME_MARKER = '<redacted-hostname>';
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Value-based host-identity redaction (#1072 follow-up)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
//
|
|
152
|
+
// HOSTNAME_RE above is FORM-based and fires only on a name that still carries a
|
|
153
|
+
// `.local` / `.lan` / `.home` / `.internal` / `.corp` suffix. The session-lock
|
|
154
|
+
// body's `host_id` field is exactly the suffix-STRIPPED lowercase form that
|
|
155
|
+
// `stableHostname()` produces, so a `host_id` value reaching a learning passes
|
|
156
|
+
// through un-redacted. Measured on the reference host 2026-08-24, before this
|
|
157
|
+
// block existed:
|
|
158
|
+
//
|
|
159
|
+
// anonymizeString('ferdinands-macbook-pro') → 'ferdinands-macbook-pro' (no match)
|
|
160
|
+
// anonymizeString('mac') → 'mac' (no match)
|
|
161
|
+
//
|
|
162
|
+
// Widening HOSTNAME_RE to bare labels is NOT the fix: once the suffix is gone a
|
|
163
|
+
// machine name is shape-identical to any other word, so a form rule broad
|
|
164
|
+
// enough to catch it would redact ordinary prose. The discriminating
|
|
165
|
+
// information is not the shape but the VALUE — and the values are known at
|
|
166
|
+
// runtime: `stableHostname(os.hostname())` plus every spelling this machine has
|
|
167
|
+
// recorded about ITSELF in the self-alias ledger (`readHostAliases()`, the
|
|
168
|
+
// #1072 lock-identity ledger). Exact strings, case-insensitive, word-boundary
|
|
169
|
+
// anchored: this host's identifiers precisely, which is the privacy contract.
|
|
170
|
+
//
|
|
171
|
+
// WHERE IT RUNS, AND WHY NOT BESIDE `maskSecretValues` — measured, because the
|
|
172
|
+
// obvious "all value-based rules go first" placement LEAKS. A secret is an
|
|
173
|
+
// opaque span nothing else owns; a hostname is routinely a SUBSTRING of a
|
|
174
|
+
// larger PII span that another rule owns. Two reachable cases, measured
|
|
175
|
+
// 2026-08-24 on the two orderings:
|
|
176
|
+
//
|
|
177
|
+
// 'ferdinand@Ferdinands-MacBook-Pro.local'
|
|
178
|
+
// value-first → 'ferdinand@<redacted-hostname>' ← email local-part leaks
|
|
179
|
+
// host-slot → '<redacted-email>'
|
|
180
|
+
// '/Users/ferdinands-macbook-pro/Projects/secret-client/app.js'
|
|
181
|
+
// value-first → '<redacted-path><redacted-hostname>/Projects/secret-client/app.js'
|
|
182
|
+
// host-slot → '<redacted-path>'
|
|
183
|
+
//
|
|
184
|
+
// The second is the sharper one: UNIX_PATH_RE's tail class is `[^\s"'<>]*`, so
|
|
185
|
+
// the ANGLE BRACKETS of an early marker truncate the path match and strand the
|
|
186
|
+
// rest of the path in the output. (`[REDACTED]`, the #1025 marker, is
|
|
187
|
+
// bracket-free — which is why that one is safe to run first.) Running LAST
|
|
188
|
+
// among the span rules can never lose a hostname in exchange: every rule here
|
|
189
|
+
// only ever replaces a span WITH the hostname inside it, so the name is gone
|
|
190
|
+
// either way.
|
|
191
|
+
//
|
|
192
|
+
// CEILING (BV-004): on a host whose stable name is an ordinary word — a Mac
|
|
193
|
+
// literally named `mac`, a Raspberry Pi named `pi` — every standalone
|
|
194
|
+
// occurrence of that word in an insight is redacted too. Accepted deliberately,
|
|
195
|
+
// and without a minimum-length floor: over-redaction in a PUBLIC export is the
|
|
196
|
+
// cheap direction, and a length floor would be a silent hole for exactly the
|
|
197
|
+
// short names that are most common. Revisit trigger: an exported document whose
|
|
198
|
+
// readability is measurably degraded by this collateral.
|
|
199
|
+
|
|
200
|
+
/** Optional trailing local-network suffix, so `mac.local` is consumed whole. */
|
|
201
|
+
const LOCAL_SUFFIX_GROUP = '(?:\\.(?:local|lan|home|localdomain|internal|corp))?';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Escape regex metacharacters so a hostname is matched as a literal.
|
|
205
|
+
* @param {string} s
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function escapeRegExp(s) {
|
|
209
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Compile one alternation over the given names, or `null` when nothing is left
|
|
214
|
+
* to match. Longest name first, because regex alternation is leftmost-first:
|
|
215
|
+
* with `mac` ahead of `mac-pro`, the shorter name would win and strand `-pro`.
|
|
216
|
+
*
|
|
217
|
+
* @param {string[]} names — raw or normalised hostnames; normalised here.
|
|
218
|
+
* @returns {RegExp|null}
|
|
219
|
+
*/
|
|
220
|
+
function compileHostNameRe(names) {
|
|
221
|
+
const uniq = [
|
|
222
|
+
...new Set(names.map((n) => stableHostname(typeof n === 'string' ? n : '')).filter(Boolean)),
|
|
223
|
+
].sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
224
|
+
if (uniq.length === 0) return null;
|
|
225
|
+
return new RegExp(`\\b(?:${uniq.map(escapeRegExp).join('|')})${LOCAL_SUFFIX_GROUP}\\b`, 'gi');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Lazily-built, process-wide alternation over THIS host's own names — the same
|
|
230
|
+
* once-per-process shape as `_secretMasker` below, for the same reason:
|
|
231
|
+
* `readHostAliases()` is a synchronous file read, and rebuilding per record
|
|
232
|
+
* would put one read on EVERY insight and EVERY evidence string.
|
|
233
|
+
*
|
|
234
|
+
* Tests either pass `names` to {@link redactLocalHostNames} directly, or
|
|
235
|
+
* re-import the module after `vi.resetModules()` to rebuild it against a
|
|
236
|
+
* stubbed `SO_HOST_ALIASES_FILE` (the shape the #1025 block in
|
|
237
|
+
* `tests/scripts/export-hw-learnings.test.mjs` already uses).
|
|
238
|
+
*
|
|
239
|
+
* @type {RegExp|null|undefined} `undefined` = not built yet; `null` = nothing to redact.
|
|
240
|
+
*/
|
|
241
|
+
let _localHostRe;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Redact THIS machine's own hostnames — any recorded spelling, with or without
|
|
245
|
+
* a local-network suffix — from a free-form string.
|
|
246
|
+
*
|
|
247
|
+
* Fail-soft: a non-string passes through by reference, and an unresolvable host
|
|
248
|
+
* identity makes this the identity function. Redaction must never be the reason
|
|
249
|
+
* an export dies.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} s
|
|
252
|
+
* @param {string[]} [names] — override the name set (tests). Omitted → this
|
|
253
|
+
* host's `os.hostname()` plus the self-alias ledger, resolved once.
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
export function redactLocalHostNames(s, names) {
|
|
257
|
+
if (typeof s !== 'string' || s === '') return s;
|
|
258
|
+
|
|
259
|
+
let re;
|
|
260
|
+
if (Array.isArray(names)) {
|
|
261
|
+
re = compileHostNameRe(names);
|
|
262
|
+
} else {
|
|
263
|
+
if (_localHostRe === undefined) {
|
|
264
|
+
try {
|
|
265
|
+
_localHostRe = compileHostNameRe([stableHostname(os.hostname()), ...readHostAliases()]);
|
|
266
|
+
} catch {
|
|
267
|
+
_localHostRe = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
re = _localHostRe;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return re ? s.replace(re, HOSTNAME_MARKER) : s;
|
|
274
|
+
}
|
|
275
|
+
|
|
141
276
|
// ---------------------------------------------------------------------------
|
|
142
277
|
// Value-based secret masking (#1025) — the SECOND half of the pipeline
|
|
143
278
|
// ---------------------------------------------------------------------------
|
|
@@ -207,7 +342,13 @@ export function anonymizeString(s) {
|
|
|
207
342
|
out = out.replace(VCS_URL_RE, '<VCS-URL>');
|
|
208
343
|
for (const re of ABS_PATH_RES) out = out.replace(re, '<redacted-path>');
|
|
209
344
|
out = out.replace(IPV4_RE, '<IP>');
|
|
210
|
-
|
|
345
|
+
// Both host rules occupy the SAME slot — after every rule that owns a span a
|
|
346
|
+
// hostname can sit inside (email domain, home directory, VCS URL), before
|
|
347
|
+
// TOKEN_RE. The value rule runs first of the two so it consumes name+suffix
|
|
348
|
+
// whole; see the "WHERE IT RUNS" measurement above for why neither may move
|
|
349
|
+
// to the front of the chain.
|
|
350
|
+
out = redactLocalHostNames(out);
|
|
351
|
+
out = out.replace(HOSTNAME_RE, HOSTNAME_MARKER);
|
|
211
352
|
out = out.replace(TOKEN_RE, '<redacted-token>');
|
|
212
353
|
return out;
|
|
213
354
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* express-path.mjs — the mechanical caller for the Express Path decision (#214, #1119, #1146).
|
|
4
|
+
*
|
|
5
|
+
* ## Why a CLI, and why this is the only shape that works
|
|
6
|
+
*
|
|
7
|
+
* `scripts/lib/express-path.mjs` has held the decision AND its telemetry since
|
|
8
|
+
* #1119. Measured at HEAD 01eb35d, it had ZERO production callers:
|
|
9
|
+
*
|
|
10
|
+
* rg -n "evaluateExpressPath" scripts hooks → the definition, and nothing else
|
|
11
|
+
*
|
|
12
|
+
* Its only "caller" was a fenced ```js block in
|
|
13
|
+
* `skills/session-start/phase-8-5-express-path.md` — prose that no process runs.
|
|
14
|
+
* That is the repo's standing disease (built, not wired): a decision module whose
|
|
15
|
+
* whole point is to record whether the fast path fired, reachable only if a
|
|
16
|
+
* coordinator reads the right paragraph and hand-writes an import.
|
|
17
|
+
*
|
|
18
|
+
* A hook cannot close the gap. Both `sessionType` and `taskCount` exist only
|
|
19
|
+
* AFTER the Phase 8 Q&A resolves, and no hook event fires at that moment. The
|
|
20
|
+
* caller therefore has to be something the coordinator RUNS, in Phase 8.5, with
|
|
21
|
+
* the answers it just received — i.e. this file.
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* node scripts/express-path.mjs --repo-root <path> --session-type <type> \
|
|
26
|
+
* --task-count <n> [--parallel-agents true|false] [--config-file <path>]
|
|
27
|
+
*
|
|
28
|
+
* Options:
|
|
29
|
+
* --repo-root <path> REQUIRED. Repo whose `.orchestrator/metrics/events.jsonl`
|
|
30
|
+
* receives the record. Never defaulted: see below.
|
|
31
|
+
* --session-type <type> REQUIRED. The type confirmed in Phase 8 (housekeeping|feature|deep).
|
|
32
|
+
* --task-count <n> REQUIRED. Agreed issue/task scope, a non-negative integer.
|
|
33
|
+
* --parallel-agents <bool> Optional `true`/`false`. Omitting it ASSERTS NOTHING —
|
|
34
|
+
* the field is left out of the record entirely.
|
|
35
|
+
* --config-file <path> Optional. Defaults to CLAUDE.md / AGENTS.md under
|
|
36
|
+
* --repo-root (SO_CONFIG_FILE honoured first, as in
|
|
37
|
+
* parse-config.mjs).
|
|
38
|
+
* --help, -h Print this usage block.
|
|
39
|
+
*
|
|
40
|
+
* Output: ONE JSON line on stdout — the `evaluateExpressPath` verdict verbatim,
|
|
41
|
+
* `{"activated":<bool>,"reasons":[…]}`. Always JSON, so there is no `--json`
|
|
42
|
+
* flag, matching its two siblings `parse-config.mjs` and `emit-session.mjs`.
|
|
43
|
+
* Diagnostics — including the activation banner — go to stderr.
|
|
44
|
+
*
|
|
45
|
+
* Exit codes:
|
|
46
|
+
* 0 — the evaluation COMPLETED. `activated:false` is a completed evaluation and
|
|
47
|
+
* exits 0: a refusal is an answer, not a failure. Callers branch on the
|
|
48
|
+
* stdout `activated` field, never on the exit code.
|
|
49
|
+
* 1 — user/input error: missing or invalid --repo-root / --session-type /
|
|
50
|
+
* --task-count / --parallel-agents, or an unknown flag.
|
|
51
|
+
* 2 — config I/O error: an unreadable or unparseable config file.
|
|
52
|
+
*
|
|
53
|
+
* ## Two deliberate asymmetries
|
|
54
|
+
*
|
|
55
|
+
* `--repo-root` is REQUIRED and is never filled from `SO_PROJECT_DIR`. The
|
|
56
|
+
* library refuses that fallback for a measured reason (#941: an ambient
|
|
57
|
+
* destination put a synthetic record into the operator's real fleet ledger), and
|
|
58
|
+
* a CLI that quietly supplied one would reinstate exactly what the library
|
|
59
|
+
* refuses. A missing repo-root is an input error here, not a default.
|
|
60
|
+
*
|
|
61
|
+
* A config file that EXISTS but cannot be read or parsed exits 2 — it may carry
|
|
62
|
+
* `express-path.enabled: false`, and guessing past an unreadable opt-out would
|
|
63
|
+
* activate a path that skips every inter-wave quality gate. A config file that
|
|
64
|
+
* is simply ABSENT is not an error: there is no opt-out to lose, the documented
|
|
65
|
+
* default (`enabled: true`) applies to the decision, and `enabled` is omitted
|
|
66
|
+
* from the record so "nobody looked" stays distinguishable from "operator said
|
|
67
|
+
* true".
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
71
|
+
import { join, resolve } from 'node:path';
|
|
72
|
+
import { parseArgs } from 'node:util';
|
|
73
|
+
import { resolveInstructionFile } from './lib/common.mjs';
|
|
74
|
+
import { parseSessionConfig } from './lib/config.mjs';
|
|
75
|
+
import { evaluateExpressPath } from './lib/express-path.mjs';
|
|
76
|
+
|
|
77
|
+
const USAGE = [
|
|
78
|
+
'Usage: node scripts/express-path.mjs --repo-root <path> --session-type <type> --task-count <n>',
|
|
79
|
+
' [--parallel-agents true|false] [--config-file <path>]',
|
|
80
|
+
'',
|
|
81
|
+
' --repo-root REQUIRED repo receiving the .orchestrator/metrics/events.jsonl record',
|
|
82
|
+
' --session-type REQUIRED session type confirmed in session-start Phase 8',
|
|
83
|
+
' --task-count REQUIRED agreed issue/task scope (non-negative integer)',
|
|
84
|
+
' --parallel-agents optional true|false; omitted means "not asserted", never false',
|
|
85
|
+
' --config-file optional defaults to CLAUDE.md / AGENTS.md under --repo-root',
|
|
86
|
+
'',
|
|
87
|
+
'stdout: one JSON line — {"activated":<bool>,"reasons":[…]}',
|
|
88
|
+
'Exit codes: 0 evaluation completed (activated true OR false), 1 input error, 2 config I/O error',
|
|
89
|
+
].join('\n');
|
|
90
|
+
|
|
91
|
+
/** User/input error — usage class. */
|
|
92
|
+
const EXIT_INPUT = 1;
|
|
93
|
+
/** System error — config could not be read or parsed. */
|
|
94
|
+
const EXIT_CONFIG_IO = 2;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write a diagnostic line to stderr. stdout carries the verdict and nothing
|
|
98
|
+
* else, so every human-facing line goes here (`cli-design.md` § JSON-First).
|
|
99
|
+
*
|
|
100
|
+
* @param {string} message
|
|
101
|
+
* @returns {void}
|
|
102
|
+
*/
|
|
103
|
+
function warn(message) {
|
|
104
|
+
process.stderr.write(`express-path: ${message}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fail with a usage-class diagnostic and the given exit code.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} message
|
|
111
|
+
* @param {number} code
|
|
112
|
+
* @returns {never}
|
|
113
|
+
*/
|
|
114
|
+
function fail(message, code) {
|
|
115
|
+
warn(message);
|
|
116
|
+
process.exit(code);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the config file to read, anchored at `--repo-root`.
|
|
121
|
+
*
|
|
122
|
+
* The CLAUDE.md → AGENTS.md half is NOT re-implemented here: that walk is
|
|
123
|
+
* `resolveInstructionFile()` in `scripts/lib/common.mjs`, the SSOT for the
|
|
124
|
+
* instruction-file alias rule (`skills/_shared/instruction-file-resolution.md`)
|
|
125
|
+
* that 8 other modules already share. Reusing it also inherits its size > 0
|
|
126
|
+
* guard, which this function lacked: a zero-byte CLAUDE.md used to be accepted
|
|
127
|
+
* and parsed, publishing `enabled` from an empty document as if it had been
|
|
128
|
+
* measured. Only the candidate the shared walk does not know about is layered
|
|
129
|
+
* on top — the `SO_CONFIG_FILE` override `scripts/parse-config.mjs` honours,
|
|
130
|
+
* plus an explicit `--config-file`, which wins outright.
|
|
131
|
+
*
|
|
132
|
+
* Anchoring at `repoRoot` instead of walking up from cwd is deliberate: the
|
|
133
|
+
* repo is already known here, and a walk-up could reach a DIFFERENT repo's
|
|
134
|
+
* config than the one being recorded.
|
|
135
|
+
*
|
|
136
|
+
* Named `resolveRepoConfigPath`, NOT `resolveConfigFile`: that name is already
|
|
137
|
+
* taken twice with two other meanings — `scripts/lib/platform.mjs` maps a
|
|
138
|
+
* PLATFORM to a bare filename, `scripts/lib/ecosystem-wizard/config-writer.mjs`
|
|
139
|
+
* resolves the wizard's own target. A third meaning under one name is a trap
|
|
140
|
+
* for the next person who greps it.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} repoRoot — absolute repo root
|
|
143
|
+
* @param {string|undefined} explicit — value of `--config-file`, if given
|
|
144
|
+
* @returns {string|null} absolute path, or null when no config file exists
|
|
145
|
+
*/
|
|
146
|
+
function resolveRepoConfigPath(repoRoot, explicit) {
|
|
147
|
+
if (explicit) {
|
|
148
|
+
const abs = resolve(explicit);
|
|
149
|
+
if (!existsSync(abs)) fail(`config file not found: ${explicit}`, EXIT_CONFIG_IO);
|
|
150
|
+
return abs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (process.env.SO_CONFIG_FILE) {
|
|
154
|
+
const override = join(repoRoot, process.env.SO_CONFIG_FILE);
|
|
155
|
+
if (existsSync(override)) return override;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resolveInstructionFile(repoRoot)?.path ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Read and parse the Session Config.
|
|
163
|
+
*
|
|
164
|
+
* Returns `undefined` — not `{}` — when no config file exists, so
|
|
165
|
+
* `evaluateExpressPath` can tell "not measured" from "measured as absent" and
|
|
166
|
+
* keep `enabled` out of the record. See the module docstring's second asymmetry.
|
|
167
|
+
*
|
|
168
|
+
* @param {string|null} configFile
|
|
169
|
+
* @returns {object|undefined}
|
|
170
|
+
*/
|
|
171
|
+
function loadConfig(configFile) {
|
|
172
|
+
if (configFile === null) {
|
|
173
|
+
warn('no CLAUDE.md / AGENTS.md under --repo-root; applying the documented default enabled=true');
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let content;
|
|
178
|
+
try {
|
|
179
|
+
content = readFileSync(configFile, 'utf8');
|
|
180
|
+
} catch (err) {
|
|
181
|
+
fail(`failed to read ${configFile}: ${err.message}`, EXIT_CONFIG_IO);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return parseSessionConfig(content);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
fail(`failed to parse ${configFile}: ${err.message}`, EXIT_CONFIG_IO);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse `--parallel-agents`. Tri-state on purpose: `undefined` means the
|
|
193
|
+
* coordinator asserted nothing, which the library records differently from an
|
|
194
|
+
* explicit `false`.
|
|
195
|
+
*
|
|
196
|
+
* @param {string|undefined} raw
|
|
197
|
+
* @returns {boolean|undefined}
|
|
198
|
+
*/
|
|
199
|
+
function parseParallelAgents(raw) {
|
|
200
|
+
if (raw === undefined) return undefined;
|
|
201
|
+
const value = String(raw).trim().toLowerCase();
|
|
202
|
+
if (value === 'true') return true;
|
|
203
|
+
if (value === 'false') return false;
|
|
204
|
+
return fail(`--parallel-agents must be "true" or "false" (got: ${raw})`, EXIT_INPUT);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function main() {
|
|
208
|
+
/** @type {{values: Record<string, string|boolean>}} */
|
|
209
|
+
let parsed;
|
|
210
|
+
try {
|
|
211
|
+
parsed = parseArgs({
|
|
212
|
+
args: process.argv.slice(2),
|
|
213
|
+
options: {
|
|
214
|
+
'repo-root': { type: 'string' },
|
|
215
|
+
'session-type': { type: 'string' },
|
|
216
|
+
'task-count': { type: 'string' },
|
|
217
|
+
'parallel-agents': { type: 'string' },
|
|
218
|
+
'config-file': { type: 'string' },
|
|
219
|
+
help: { type: 'boolean', short: 'h' },
|
|
220
|
+
},
|
|
221
|
+
allowPositionals: false,
|
|
222
|
+
});
|
|
223
|
+
} catch (err) {
|
|
224
|
+
fail(`${err.message}\n\n${USAGE}`, EXIT_INPUT);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { values } = parsed;
|
|
228
|
+
|
|
229
|
+
if (values.help) {
|
|
230
|
+
process.stdout.write(`${USAGE}\n`);
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --repo-root — required, never defaulted from the ambient env (#941).
|
|
235
|
+
const repoRootArg = typeof values['repo-root'] === 'string' ? values['repo-root'].trim() : '';
|
|
236
|
+
if (repoRootArg === '') {
|
|
237
|
+
fail(`--repo-root is required (never defaulted from SO_PROJECT_DIR)\n\n${USAGE}`, EXIT_INPUT);
|
|
238
|
+
}
|
|
239
|
+
const repoRoot = resolve(repoRootArg);
|
|
240
|
+
if (!existsSync(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
241
|
+
// A typo'd root would otherwise be CREATED by the emitter's mkdir, leaving
|
|
242
|
+
// an orphan `.orchestrator/metrics/` tree that answers no question.
|
|
243
|
+
fail(`--repo-root is not an existing directory: ${repoRootArg}`, EXIT_INPUT);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --session-type — required. The library fails CLOSED on an unknown type; the
|
|
247
|
+
// CLI refuses it outright so the miss is reported rather than recorded.
|
|
248
|
+
const sessionType =
|
|
249
|
+
typeof values['session-type'] === 'string' ? values['session-type'].trim() : '';
|
|
250
|
+
if (sessionType === '') {
|
|
251
|
+
fail(`--session-type is required\n\n${USAGE}`, EXIT_INPUT);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --task-count — required, non-negative integer.
|
|
255
|
+
const taskCountRaw = typeof values['task-count'] === 'string' ? values['task-count'].trim() : '';
|
|
256
|
+
if (taskCountRaw === '') {
|
|
257
|
+
fail(`--task-count is required\n\n${USAGE}`, EXIT_INPUT);
|
|
258
|
+
}
|
|
259
|
+
const taskCount = Number(taskCountRaw);
|
|
260
|
+
if (!Number.isInteger(taskCount) || taskCount < 0) {
|
|
261
|
+
fail(`--task-count must be a non-negative integer (got: ${taskCountRaw})`, EXIT_INPUT);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const parallelAgentsRequired = parseParallelAgents(
|
|
265
|
+
typeof values['parallel-agents'] === 'string' ? values['parallel-agents'] : undefined,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const configFile = resolveRepoConfigPath(
|
|
269
|
+
repoRoot,
|
|
270
|
+
typeof values['config-file'] === 'string' ? values['config-file'] : undefined,
|
|
271
|
+
);
|
|
272
|
+
const config = loadConfig(configFile);
|
|
273
|
+
|
|
274
|
+
const verdict = await evaluateExpressPath({
|
|
275
|
+
repoRoot,
|
|
276
|
+
config,
|
|
277
|
+
sessionType,
|
|
278
|
+
taskCount,
|
|
279
|
+
parallelAgentsRequired,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// The banner the coordinator echoes into the transcript — `/go` and
|
|
283
|
+
// session-plan's Express Path Short-Circuit both key off this EXACT line
|
|
284
|
+
// (`commands/go.md` § Express Path Detection), so it is written verbatim,
|
|
285
|
+
// without the `express-path:` diagnostic prefix. Still stderr: stdout belongs
|
|
286
|
+
// to the verdict.
|
|
287
|
+
if (verdict.activated) {
|
|
288
|
+
process.stderr.write(
|
|
289
|
+
`Express path activated — ${taskCount} tasks, coordinator-direct, no inter-wave checks.\n`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
process.stdout.write(`${JSON.stringify(verdict)}\n`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
main().catch((err) => {
|
|
297
|
+
process.stderr.write(`express-path: unexpected error: ${err?.stack ?? err}\n`);
|
|
298
|
+
process.exit(EXIT_CONFIG_IO);
|
|
299
|
+
});
|