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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* remote-hosts.mjs — Parser for the `remote-hosts:` Session Config block (#1160).
|
|
5
|
+
*
|
|
6
|
+
* Lets a repo DECLARE ssh-reachable hosts that heavy wave roles (test / ui / perf)
|
|
7
|
+
* may be offloaded to instead of shrinking the wave under local resource pressure.
|
|
8
|
+
* This module only declares — it never probes the network, never dispatches, and
|
|
9
|
+
* never decides. Placement is `scripts/lib/wave-resource-gate.mjs`; the dispatch
|
|
10
|
+
* adapter lives in `scripts/lib/wave-executor/`.
|
|
11
|
+
*
|
|
12
|
+
* Exports:
|
|
13
|
+
* REMOTE_HOST_DEFAULTS — per-host defaults
|
|
14
|
+
* ALLOWED_REMOTE_ROLES — the agent-mapping roles a host may accept
|
|
15
|
+
* _parseRemoteHosts(content) — PURE, no side effects beyond a stderr WARN when a
|
|
16
|
+
* record is dropped. Returns [] when absent/empty.
|
|
17
|
+
*
|
|
18
|
+
* Block shape:
|
|
19
|
+
* remote-hosts:
|
|
20
|
+
* - alias: m5 # required; becomes `-H <alias>` argv
|
|
21
|
+
* roles-allowed: [test, ui, perf] # subset of ALLOWED_REMOTE_ROLES
|
|
22
|
+
* repo-path: ~/Projects/Alice # optional
|
|
23
|
+
* claude-path: ~/.local/bin/claude # optional
|
|
24
|
+
*
|
|
25
|
+
* Modelled on custom-phases.mjs (per-entry SAFE-regex validation, drop-with-warn,
|
|
26
|
+
* column-0 block termination, silent enum fallback). The one shape it adds is the
|
|
27
|
+
* inline `[a, b]` list for `roles-allowed`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Per-host defaults. */
|
|
31
|
+
export const REMOTE_HOST_DEFAULTS = Object.freeze({
|
|
32
|
+
'roles-allowed': ['test', 'ui', 'perf'],
|
|
33
|
+
'repo-path': null,
|
|
34
|
+
'claude-path': null,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The roles a remote host may accept. A deliberate SUBSET of `ALLOWED_ROLES` in
|
|
39
|
+
* scripts/lib/config.mjs — impl / db / security / compliance / docs stay local.
|
|
40
|
+
*
|
|
41
|
+
* NOT the wave-role enum ("Impl-Core", "Quality", …): those are two different
|
|
42
|
+
* enums and conflating them is the documented trap (see resolveApwCap's docstring
|
|
43
|
+
* in wave-resource-gate.mjs). The wave→agent-mapping-role translation is
|
|
44
|
+
* `OFFLOADABLE_WAVE_ROLES` in that same module.
|
|
45
|
+
*
|
|
46
|
+
* @type {readonly string[]}
|
|
47
|
+
*/
|
|
48
|
+
export const ALLOWED_REMOTE_ROLES = Object.freeze(['test', 'ui', 'perf']);
|
|
49
|
+
|
|
50
|
+
// A host `alias` is an ssh destination that reaches argv as `-H <alias>`: letters,
|
|
51
|
+
// digits, hyphen, underscore, dot only. No spaces, no shell metacharacters.
|
|
52
|
+
//
|
|
53
|
+
// The first character is anchored separately from the rest because a charset
|
|
54
|
+
// cannot express a POSITION: an alias of `-H` passes any hyphen-inclusive
|
|
55
|
+
// allowlist and then reaches argv as an OPTION token, where the CLI swallows the
|
|
56
|
+
// operand behind it. Interior hyphens (`m5-box`) stay legal.
|
|
57
|
+
const SAFE_NAME_RE = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
|
|
58
|
+
|
|
59
|
+
// `repo-path` / `claude-path` are filesystem paths handed to a remote shell —
|
|
60
|
+
// same allowlist as custom-phases' `review`, with the same leading-hyphen anchor
|
|
61
|
+
// as SAFE_NAME_RE above (`--x` is a flag, not a path).
|
|
62
|
+
const SAFE_PATH_RE = /^[A-Za-z0-9._~/][A-Za-z0-9._~/-]*$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse the top-level `remote-hosts:` YAML list block from markdown content.
|
|
66
|
+
*
|
|
67
|
+
* A record missing `alias`, or carrying an unsafe `alias` / `repo-path` /
|
|
68
|
+
* `claude-path`, is DROPPED with a stderr WARN. Unknown entries in
|
|
69
|
+
* `roles-allowed` are filtered out with a WARN; a record whose list is empty
|
|
70
|
+
* after filtering is dropped (it could never be selected anyway).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} content — full CLAUDE.md / AGENTS.md file content
|
|
73
|
+
* @returns {Array<{alias: string, 'roles-allowed': string[], 'repo-path': string|null, 'claude-path': string|null}>}
|
|
74
|
+
*/
|
|
75
|
+
export function _parseRemoteHosts(content) {
|
|
76
|
+
const lines = String(content ?? '').split(/\r?\n/);
|
|
77
|
+
let inBlock = false;
|
|
78
|
+
const blockLines = [];
|
|
79
|
+
|
|
80
|
+
for (const rawLine of lines) {
|
|
81
|
+
const line = rawLine.replace(/\r$/, '');
|
|
82
|
+
|
|
83
|
+
if (!inBlock) {
|
|
84
|
+
if (matchBlockHeader(line, 'remote-hosts')) inBlock = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Block terminates at the first non-indented, non-empty line (next top-level key).
|
|
89
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
90
|
+
|
|
91
|
+
blockLines.push(line);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (blockLines.length === 0) return [];
|
|
95
|
+
|
|
96
|
+
const records = [];
|
|
97
|
+
/** @type {Record<string, string>|null} */
|
|
98
|
+
let current = null;
|
|
99
|
+
|
|
100
|
+
const flush = () => {
|
|
101
|
+
if (current === null) return;
|
|
102
|
+
const rec = _validateRecord(current);
|
|
103
|
+
if (rec !== null) records.push(rec);
|
|
104
|
+
current = null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (const rawLine of blockLines) {
|
|
108
|
+
// Strip inline comments + trailing whitespace, preserve leading indent.
|
|
109
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
110
|
+
if (!clean.trim()) continue;
|
|
111
|
+
|
|
112
|
+
const dashMatch = clean.match(/^\s*-\s+(.*)$/);
|
|
113
|
+
if (dashMatch) {
|
|
114
|
+
flush();
|
|
115
|
+
current = {};
|
|
116
|
+
const inlineKv = dashMatch[1].match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
117
|
+
if (inlineKv) _assignKv(current, inlineKv[1], inlineKv[2]);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
122
|
+
if (kvMatch && current !== null) {
|
|
123
|
+
_assignKv(current, kvMatch[1], kvMatch[2]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
flush();
|
|
128
|
+
|
|
129
|
+
return records;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Assign a raw key/value onto a record being built, stripping surrounding quotes.
|
|
134
|
+
* Unknown keys are silently ignored (additive-friendly).
|
|
135
|
+
*
|
|
136
|
+
* @param {Record<string, string>} record
|
|
137
|
+
* @param {string} key
|
|
138
|
+
* @param {string} rawValue
|
|
139
|
+
*/
|
|
140
|
+
function _assignKv(record, key, rawValue) {
|
|
141
|
+
let v = rawValue.trim();
|
|
142
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
143
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
144
|
+
|
|
145
|
+
switch (key) {
|
|
146
|
+
case 'alias':
|
|
147
|
+
case 'roles-allowed':
|
|
148
|
+
case 'repo-path':
|
|
149
|
+
case 'claude-path':
|
|
150
|
+
record[key] = v;
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse an inline `[a, b]` (or bare `a, b`) list into trimmed non-empty strings.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} raw
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
function _parseInlineList(raw) {
|
|
164
|
+
const inner = raw.trim().replace(/^\[/, '').replace(/\]$/, '');
|
|
165
|
+
return inner
|
|
166
|
+
.split(',')
|
|
167
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
168
|
+
.filter((s) => s !== '');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate + normalise a raw record.
|
|
173
|
+
*
|
|
174
|
+
* @param {Record<string, string>} raw
|
|
175
|
+
* @returns {{alias: string, 'roles-allowed': string[], 'repo-path': string|null, 'claude-path': string|null}|null}
|
|
176
|
+
*/
|
|
177
|
+
function _validateRecord(raw) {
|
|
178
|
+
const alias = (raw.alias ?? '').trim();
|
|
179
|
+
if (alias === '') {
|
|
180
|
+
process.stderr.write('remote-hosts: dropped record missing required field: alias\n');
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (!SAFE_NAME_RE.test(alias)) {
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`remote-hosts: dropped record with unsafe alias: ${JSON.stringify(alias)}\n`,
|
|
186
|
+
);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let rolesAllowed = [...REMOTE_HOST_DEFAULTS['roles-allowed']];
|
|
191
|
+
const rawRoles = (raw['roles-allowed'] ?? '').trim();
|
|
192
|
+
if (rawRoles !== '') {
|
|
193
|
+
const declared = _parseInlineList(rawRoles);
|
|
194
|
+
const kept = declared.filter((r) => ALLOWED_REMOTE_ROLES.includes(r));
|
|
195
|
+
const dropped = declared.filter((r) => !ALLOWED_REMOTE_ROLES.includes(r));
|
|
196
|
+
if (dropped.length > 0) {
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
`remote-hosts: host '${alias}' declares unknown role(s) ${dropped.join(', ')} ` +
|
|
199
|
+
`(allowed: ${ALLOWED_REMOTE_ROLES.join(', ')}) — ignored\n`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (kept.length === 0) {
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
`remote-hosts: dropped record '${alias}' — roles-allowed is empty after filtering\n`,
|
|
205
|
+
);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
rolesAllowed = kept;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const paths = {};
|
|
212
|
+
for (const key of ['repo-path', 'claude-path']) {
|
|
213
|
+
let value = REMOTE_HOST_DEFAULTS[key];
|
|
214
|
+
const rawPath = (raw[key] ?? '').trim();
|
|
215
|
+
if (rawPath !== '' && rawPath !== 'null' && rawPath !== 'none') {
|
|
216
|
+
if (!SAFE_PATH_RE.test(rawPath)) {
|
|
217
|
+
process.stderr.write(
|
|
218
|
+
`remote-hosts: dropped record '${alias}' with shell metacharacter in ${key}: ${JSON.stringify(rawPath)}\n`,
|
|
219
|
+
);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
value = rawPath;
|
|
223
|
+
}
|
|
224
|
+
paths[key] = value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
alias,
|
|
229
|
+
'roles-allowed': rolesAllowed,
|
|
230
|
+
'repo-path': paths['repo-path'],
|
|
231
|
+
'claude-path': paths['claude-path'],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -131,7 +131,8 @@ export function findSessionConfigBlock(content, { occurrence = 'first' } = {}) {
|
|
|
131
131
|
/**
|
|
132
132
|
* Extract the raw ## Session Config block lines from markdown content.
|
|
133
133
|
* - CRLF-tolerant
|
|
134
|
-
* - Skips code fence lines (``` alone
|
|
134
|
+
* - Skips code fence lines (``` alone or with an info string)
|
|
135
|
+
* - Skips lines inside a multi-line `<!-- … -->` block (`htmlCommentSkipper`)
|
|
135
136
|
* - Strips trailing whitespace from each line
|
|
136
137
|
* @param {string} content
|
|
137
138
|
* @returns {string[]} lines of the Session Config block
|
|
@@ -140,6 +141,7 @@ export function _extractConfigSection(content) {
|
|
|
140
141
|
const lines = content.split(/\r?\n/);
|
|
141
142
|
const result = [];
|
|
142
143
|
let inSection = false;
|
|
144
|
+
const skipHtmlComment = htmlCommentSkipper();
|
|
143
145
|
|
|
144
146
|
for (const rawLine of lines) {
|
|
145
147
|
const line = rawLine.replace(/\r$/, '');
|
|
@@ -152,8 +154,17 @@ export function _extractConfigSection(content) {
|
|
|
152
154
|
if (inSection) {
|
|
153
155
|
// Next ## header closes the section
|
|
154
156
|
if (/^## /.test(line)) break;
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
+
// Commented-out config is documentation, not config (#1097 review).
|
|
158
|
+
// Shared with `collectUnparsableLines` so the two accept-sets cannot
|
|
159
|
+
// drift — see `htmlCommentSkipper`.
|
|
160
|
+
if (skipHtmlComment(line)) continue;
|
|
161
|
+
// Skip code fences — opener WITH an info string included (#1097).
|
|
162
|
+
// The old predicate was `line.trim() === '```'`, which skips a bare
|
|
163
|
+
// fence but lets ```` ```yaml ```` through as a config line. Harmless
|
|
164
|
+
// while an unmatched line was silently dropped; a false "unparsable"
|
|
165
|
+
// report the moment `collectUnparsableLines` below started naming them —
|
|
166
|
+
// and the fenced form is what one fleet repo actually writes.
|
|
167
|
+
if (isCodeFence(line)) continue;
|
|
157
168
|
// Strip trailing whitespace and collect
|
|
158
169
|
result.push(line.replace(/\s+$/, ''));
|
|
159
170
|
}
|
|
@@ -162,6 +173,135 @@ export function _extractConfigSection(content) {
|
|
|
162
173
|
return result;
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
/**
|
|
177
|
+
* True when `line` is a fenced-code-block delimiter — a bare ``` / ~~~ or an
|
|
178
|
+
* opener carrying an info string (```yaml).
|
|
179
|
+
*
|
|
180
|
+
* @param {string} line
|
|
181
|
+
* @returns {boolean}
|
|
182
|
+
*/
|
|
183
|
+
function isCodeFence(line) {
|
|
184
|
+
return /^\s*(?:```|~~~)/.test(line);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* THE multi-line `<!-- … -->` state machine for the Session Config block —
|
|
189
|
+
* the one place that knows whether a line is commented-out documentation.
|
|
190
|
+
*
|
|
191
|
+
* Exported-by-use (same argument as `_matchKVLine`): `_extractConfigSection`
|
|
192
|
+
* feeds `_parseKV`, and `collectUnparsableLines` classifies — so both call this
|
|
193
|
+
* and their accept-sets are identical BY CONSTRUCTION. They were not, and the
|
|
194
|
+
* divergence ran in exactly the dangerous direction: the collector skipped
|
|
195
|
+
* commented blocks while the extractor read them, so
|
|
196
|
+
*
|
|
197
|
+
* persistence: true
|
|
198
|
+
* <!--
|
|
199
|
+
* enforcement: strict
|
|
200
|
+
* -->
|
|
201
|
+
*
|
|
202
|
+
* yielded a LIVE `enforcement: strict` (measured: `_parseKV` returned it) that
|
|
203
|
+
* `collectUnparsableLines` reported as a clean block — and `parse-config.mjs`
|
|
204
|
+
* branches its own #1097 gate on `config.enforcement`. Commenting a key out is
|
|
205
|
+
* the most ordinary way to disable it; it must not arm the strictest path.
|
|
206
|
+
*
|
|
207
|
+
* Returns a fresh closure per call — it carries per-document state, so a shared
|
|
208
|
+
* instance would leak an unterminated comment from one document into the next.
|
|
209
|
+
*
|
|
210
|
+
* Ceiling (deliberate): the opener must be at the START of a line
|
|
211
|
+
* (`/^\s*<!--/`). A comment opened mid-line (`waves: 5 <!--`) therefore does
|
|
212
|
+
* NOT swallow the following lines — which keeps every single-line trailing
|
|
213
|
+
* `<!-- … -->` on a heading or key line behaving exactly as before, the form
|
|
214
|
+
* this repo's own convention encourages. Revisit if a fleet CLAUDE.md is ever
|
|
215
|
+
* measured opening a multi-line comment after a value.
|
|
216
|
+
*
|
|
217
|
+
* @returns {(line: string) => boolean} true when the line is inside (or opens)
|
|
218
|
+
* a multi-line HTML comment and must be ignored by both consumers.
|
|
219
|
+
*/
|
|
220
|
+
function htmlCommentSkipper() {
|
|
221
|
+
let inComment = false;
|
|
222
|
+
return function skipHtmlComment(line) {
|
|
223
|
+
if (inComment) {
|
|
224
|
+
if (line.includes('-->')) inComment = false;
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (/^\s*<!--/.test(line) && !line.includes('-->')) {
|
|
228
|
+
inComment = true;
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Replace C0/C1 control characters with `?` before a line is handed to a
|
|
237
|
+
* reporter.
|
|
238
|
+
*
|
|
239
|
+
* `collectUnparsableLines` returns text taken verbatim from a file that is
|
|
240
|
+
* itself the thing being reported as malformed, and its only consumer prints
|
|
241
|
+
* it on stderr after a `WARN unparsable Session Config line N:` prefix. A
|
|
242
|
+
* planted `\x1b[2K…\r` erases that prefix and rewrites the operator's terminal
|
|
243
|
+
* line — the report of a defect becoming the vehicle for hiding it. Sanitising
|
|
244
|
+
* at the SOURCE (rather than at the one printer) means every future consumer
|
|
245
|
+
* inherits the safe form.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} text
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
function sanitizeReportText(text) {
|
|
251
|
+
// eslint-disable-next-line no-control-regex -- the control chars ARE the target
|
|
252
|
+
return text.replace(/[\u0000-\u001F\u007F-\u009F]/g, '?');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* THE key/value predicate for a Session Config line — the one place that knows
|
|
257
|
+
* which of the two accepted spellings a line is in.
|
|
258
|
+
*
|
|
259
|
+
* Exported-by-use rather than by name: `_parseKV` builds the map from it and
|
|
260
|
+
* `collectUnparsableLines` decides "this line carries a key" from it, so the
|
|
261
|
+
* accept-set of the two can never drift. A classifier with its own regex would
|
|
262
|
+
* eventually report a line the map DID parse (noise) or stay silent on one it
|
|
263
|
+
* did not (the #1097 defect, unchanged).
|
|
264
|
+
*
|
|
265
|
+
* Format 1: `- **key:** value`
|
|
266
|
+
* Format 2: `key: value`, incl. the YAML list-item form `- key: value` (#497)
|
|
267
|
+
* and any leading indentation (sub-keys of a nested block land here too).
|
|
268
|
+
*
|
|
269
|
+
* @param {string} line
|
|
270
|
+
* @returns {{ key: string, value: string } | null} null when the line carries
|
|
271
|
+
* no key/value pair in either format.
|
|
272
|
+
*/
|
|
273
|
+
function _matchKVLine(line) {
|
|
274
|
+
let key;
|
|
275
|
+
let value;
|
|
276
|
+
|
|
277
|
+
// Format 1: - **key:** value
|
|
278
|
+
const fmt1 = line.match(/^\s*-\s+\*\*([^*:]+):\*\*\s*(.*)/);
|
|
279
|
+
if (fmt1) {
|
|
280
|
+
key = fmt1[1].trim();
|
|
281
|
+
value = fmt1[2].trim();
|
|
282
|
+
} else {
|
|
283
|
+
// Format 2: key: value — supports both plain "key: value" and
|
|
284
|
+
// YAML list-item form "- key: value" (issue #497). Key starts with
|
|
285
|
+
// letter; rest is alphanum/hyphen/underscore.
|
|
286
|
+
const fmt2 = line.match(/^\s*(?:-\s+)?([a-zA-Z][a-zA-Z0-9_-]+):\s+(.*)/);
|
|
287
|
+
if (!fmt2) return null;
|
|
288
|
+
key = fmt2[1].trim();
|
|
289
|
+
value = fmt2[2].trim();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!key) return null;
|
|
293
|
+
|
|
294
|
+
// Strip inline YAML comment (matches block-parser behaviour in _parseVaultSync etc.)
|
|
295
|
+
value = value.replace(/\s+#.*$/, '').trim();
|
|
296
|
+
|
|
297
|
+
// Strip surrounding double quotes from value
|
|
298
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
299
|
+
value = value.slice(1, -1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { key, value };
|
|
303
|
+
}
|
|
304
|
+
|
|
165
305
|
/**
|
|
166
306
|
* Parse the key-value pairs from extracted Session Config lines.
|
|
167
307
|
* Supports Format 1: `- **key:** value`
|
|
@@ -176,39 +316,9 @@ export function _parseKV(lines) {
|
|
|
176
316
|
|
|
177
317
|
for (const line of lines) {
|
|
178
318
|
if (!line.trim()) continue;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Format 1: - **key:** value
|
|
184
|
-
const fmt1 = line.match(/^\s*-\s+\*\*([^*:]+):\*\*\s*(.*)/);
|
|
185
|
-
if (fmt1) {
|
|
186
|
-
key = fmt1[1].trim();
|
|
187
|
-
value = fmt1[2].trim();
|
|
188
|
-
} else {
|
|
189
|
-
// Format 2: key: value — supports both plain "key: value" and
|
|
190
|
-
// YAML list-item form "- key: value" (issue #497). Key starts with
|
|
191
|
-
// letter; rest is alphanum/hyphen/underscore.
|
|
192
|
-
const fmt2 = line.match(/^\s*(?:-\s+)?([a-zA-Z][a-zA-Z0-9_-]+):\s+(.*)/);
|
|
193
|
-
if (fmt2) {
|
|
194
|
-
key = fmt2[1].trim();
|
|
195
|
-
value = fmt2[2].trim();
|
|
196
|
-
} else {
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!key) continue;
|
|
202
|
-
|
|
203
|
-
// Strip inline YAML comment (matches block-parser behaviour in _parseVaultSync etc.)
|
|
204
|
-
value = value.replace(/\s+#.*$/, '').trim();
|
|
205
|
-
|
|
206
|
-
// Strip surrounding double quotes from value
|
|
207
|
-
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
208
|
-
value = value.slice(1, -1);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
allPairs.push([key, value]);
|
|
319
|
+
const match = _matchKVLine(line);
|
|
320
|
+
if (!match) continue;
|
|
321
|
+
allPairs.push([match.key, match.value]);
|
|
212
322
|
}
|
|
213
323
|
|
|
214
324
|
// Last match wins: build the map by iterating in order
|
|
@@ -218,3 +328,92 @@ export function _parseKV(lines) {
|
|
|
218
328
|
}
|
|
219
329
|
return kv;
|
|
220
330
|
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Fail-loud: lines inside the block that carry no meaning (#1097)
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A line that opens a nested block: `vault-integration:` with no value, in any
|
|
338
|
+
* of the four fleet spellings (column-zero, `- key:`, `**key:**`, `- **key:**`)
|
|
339
|
+
* and at any indent. Deliberately NOT imported from block-header.mjs: that
|
|
340
|
+
* matcher answers "does this line open the block for THIS key" for a known key,
|
|
341
|
+
* while here the key is unknown by construction.
|
|
342
|
+
*/
|
|
343
|
+
const NESTED_BLOCK_HEADER_RE = /^\s*(?:-\s+)?(?:\*\*)?[a-zA-Z][a-zA-Z0-9_.-]*:(?:\*\*)?\s*$/;
|
|
344
|
+
|
|
345
|
+
/** A YAML sequence element that is not a `key: value` pair (` - some-value`). */
|
|
346
|
+
const LIST_ITEM_RE = /^\s*-\s+\S/;
|
|
347
|
+
|
|
348
|
+
/** Documentation, not config: `# yaml comment` and `<!-- html -->` / `> quote`. */
|
|
349
|
+
const COMMENT_RE = /^\s*(?:#|<!--|>)/;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Collect the lines inside `## Session Config` that carry no meaning for any
|
|
353
|
+
* parser — the fail-loud half of #1097.
|
|
354
|
+
*
|
|
355
|
+
* The measured defect this exists to end: a key the extractor cannot read is
|
|
356
|
+
* simply absent from the KV map, and every consumer then applies its DEFAULT.
|
|
357
|
+
* For a boolean that default is `false`, so a mis-spelled `vault-integration`
|
|
358
|
+
* block reads exactly like a deliberately disabled one — no error, anywhere, in
|
|
359
|
+
* any log (fleet evidence: a repo whose vault mirror sat at
|
|
360
|
+
* `skipped-vault-disabled` for two months with the key present in its CLAUDE.md).
|
|
361
|
+
*
|
|
362
|
+
* A line is accounted for — and therefore NOT reported — when it is:
|
|
363
|
+
* blank · a code fence · a `#`/`<!--`/`>` documentation line · a key/value
|
|
364
|
+
* pair in either accepted format (`_matchKVLine`) · a nested-block header
|
|
365
|
+
* (`key:` with no value) · a bare YAML list element (`- value`).
|
|
366
|
+
* Everything else is prose sitting where config is expected, which is the only
|
|
367
|
+
* shape a broken key can take once the six forms above are excluded.
|
|
368
|
+
*
|
|
369
|
+
* Line numbers are 1-based and count from the START OF THE DOCUMENT, not from
|
|
370
|
+
* the block — they are for an operator opening the file at that line. The text
|
|
371
|
+
* is run through `sanitizeReportText`, because it is quoted straight into a
|
|
372
|
+
* terminal by the one consumer that prints it.
|
|
373
|
+
*
|
|
374
|
+
* The accept-set is shared with the reader, not merely aligned with it: the
|
|
375
|
+
* `#`/`>` and blank/fence forms are literal here, and the multi-line HTML
|
|
376
|
+
* comment goes through the same `htmlCommentSkipper` call `_extractConfigSection`
|
|
377
|
+
* makes. A form the reader accepts but the classifier does not is noise; a form
|
|
378
|
+
* the READER accepts and the classifier waves through as documentation is the
|
|
379
|
+
* live-commented-out-key defect this shape exists to make unreachable.
|
|
380
|
+
*
|
|
381
|
+
* @param {string} content — full markdown document
|
|
382
|
+
* @returns {Array<{ line: number, text: string }>} empty when the block is
|
|
383
|
+
* well-formed, absent, or the content is not a string.
|
|
384
|
+
*/
|
|
385
|
+
export function collectUnparsableLines(content) {
|
|
386
|
+
if (typeof content !== 'string' || content === '') return [];
|
|
387
|
+
|
|
388
|
+
const lines = content.split(/\r?\n/);
|
|
389
|
+
const unparsable = [];
|
|
390
|
+
let inSection = false;
|
|
391
|
+
const skipHtmlComment = htmlCommentSkipper();
|
|
392
|
+
|
|
393
|
+
for (let i = 0; i < lines.length; i++) {
|
|
394
|
+
const line = lines[i].replace(/\r$/, '');
|
|
395
|
+
|
|
396
|
+
if (isSessionConfigHeading(line)) {
|
|
397
|
+
inSection = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!inSection) continue;
|
|
401
|
+
if (/^## /.test(line)) break;
|
|
402
|
+
|
|
403
|
+
// A multi-line HTML comment: everything up to `-->` is documentation.
|
|
404
|
+
// Same call, same order, as `_extractConfigSection` — that shared call is
|
|
405
|
+
// what makes the two accept-sets identical rather than merely similar.
|
|
406
|
+
if (skipHtmlComment(line)) continue;
|
|
407
|
+
|
|
408
|
+
if (!line.trim()) continue;
|
|
409
|
+
if (isCodeFence(line)) continue;
|
|
410
|
+
if (COMMENT_RE.test(line)) continue;
|
|
411
|
+
if (_matchKVLine(line)) continue;
|
|
412
|
+
if (NESTED_BLOCK_HEADER_RE.test(line)) continue;
|
|
413
|
+
if (LIST_ITEM_RE.test(line)) continue;
|
|
414
|
+
|
|
415
|
+
unparsable.push({ line: i + 1, text: sanitizeReportText(line.trim()) });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return unparsable;
|
|
419
|
+
}
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* Never throws. Never mutates input.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The values `enforcement:` accepts. Exported (#1097) so the gates that BRANCH
|
|
13
|
+
* on enforcement — `scripts/parse-config.mjs`'s unparsable-line gate is the
|
|
14
|
+
* first — read the same Set the validator judges against, instead of re-typing
|
|
15
|
+
* the literal `'strict'` beside a schema that could later add a mode.
|
|
16
|
+
*
|
|
17
|
+
* @type {Set<string>}
|
|
18
|
+
*/
|
|
19
|
+
export const ENFORCEMENT_VALUES = new Set(['strict', 'warn', 'off']);
|
|
12
20
|
// Exported (issue #836) so cross-layer mode-vocabulary parity tests can use
|
|
13
21
|
// this Set as the single source of truth for "the modes a Session Config
|
|
14
22
|
// `*.mode` key accepts" — rather than re-deriving a duplicate literal list
|