[object Object]

← back to Cli Printing Press

fix(cli): score composed auth and shell continuations (#636)

2035b276df2ea411c39d2b36382a2a3c5153d4b8 · 2026-05-05 15:07:50 -0700 · Trevin Chow

Files touched

Diff

commit 2035b276df2ea411c39d2b36382a2a3c5153d4b8
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue May 5 15:07:50 2026 -0700

    fix(cli): score composed auth and shell continuations (#636)
---
 ...er-scorecard-scoring-architecture-2026-03-27.md |   9 +-
 ...er-auth-and-example-continuations-2026-05-05.md | 102 +++++++
 internal/pipeline/scorecard.go                     | 159 +++++++++-
 internal/pipeline/scorecard_tier2_test.go          | 324 +++++++++++++++++++++
 internal/shellargs/shellargs.go                    |   9 +
 internal/shellargs/shellargs_test.go               |   5 +
 6 files changed, 596 insertions(+), 12 deletions(-)

diff --git a/docs/solutions/best-practices/steinberger-scorecard-scoring-architecture-2026-03-27.md b/docs/solutions/best-practices/steinberger-scorecard-scoring-architecture-2026-03-27.md
index 27441544..018bf4d3 100644
--- a/docs/solutions/best-practices/steinberger-scorecard-scoring-architecture-2026-03-27.md
+++ b/docs/solutions/best-practices/steinberger-scorecard-scoring-architecture-2026-03-27.md
@@ -1,7 +1,7 @@
 ---
 title: "Steinberger scorecard scoring system: architecture, conventions, and modification rules"
 date: 2026-03-27
-last_updated: 2026-03-28
+last_updated: 2026-05-05
 category: best-practices
 module: internal/pipeline
 problem_type: best_practice
@@ -209,15 +209,18 @@ Six prefixes appear in both workflow and insight lists: `stale`, `conflicts`, `s
 
 9. **Unknown evidence must become `N/A`, not a midpoint.** If the spec or other authority lacks the data needed to evaluate a dimension, mark it unscored, expose it in `unscored_dimensions`, skip it in gap reports, and remove its max points from the tier denominator.
 
-10. **Update tier constants when adding dimensions.** Tier 1 constant is `120` (12 x 10). Tier 2 constant is `50`. Both live in the `RunScorecard` function. If dimensions can become unscored, adjust the runtime denominator too.
+10. **For AuthProtocol, score runtime emission after using the spec to identify the contract.** OpenAPI `securitySchemes` can model one composed header protocol as multiple same-prefix `apiKey` headers. Expand only signing-style companions, preserve explicit OR alternatives, and verify each required header is assigned in the generated client.
 
-11. **Test every scoring function independently.** Each `score*()` function should have fixture-based tests covering: high score, low score, dimension-specific edge cases, and unscored/unknown states for evidence-dependent dimensions.
+11. **Update tier constants when adding dimensions.** Tier 1 constant is `120` (12 x 10). Tier 2 constant is `50`. Both live in the `RunScorecard` function. If dimensions can become unscored, adjust the runtime denominator too.
+
+12. **Test every scoring function independently.** Each `score*()` function should have fixture-based tests covering: high score, low score, dimension-specific edge cases, and unscored/unknown states for evidence-dependent dimensions.
 
 For detailed examples of bugs caused by violating these rules, see `docs/solutions/logic-errors/scorecard-accuracy-broadened-pattern-matching-2026-03-27.md`.
 
 ## Related
 
 - `docs/solutions/logic-errors/scorecard-accuracy-broadened-pattern-matching-2026-03-27.md` -- bug-fix doc with before/after code for 10 specific scoring bugs
+- `docs/solutions/logic-errors/scorer-dogfood-composed-header-auth-and-example-continuations-2026-05-05.md` -- composed header-auth scoring and shell-style example-tokenizer bug fixes
 - `docs/plans/2026-03-25-feat-visionary-research-phase-plan.md` -- defines the Steinberger vision scoring and workflow/insight semantics
 - `docs/plans/2026-03-25-fix-scorecard-too-easy-real-quality-plan.md` -- predecessor plan that redesigned scoring from presence-only to quality-aware
 - `skills/printing-press/references/scorecard-patterns.md` -- **STALE**: documents only 9 of 18 dimensions, wrong total range, pre-broadening file assumptions. Needs full rewrite.
diff --git a/docs/solutions/logic-errors/scorer-dogfood-composed-header-auth-and-example-continuations-2026-05-05.md b/docs/solutions/logic-errors/scorer-dogfood-composed-header-auth-and-example-continuations-2026-05-05.md
new file mode 100644
index 00000000..4dcc1452
--- /dev/null
+++ b/docs/solutions/logic-errors/scorer-dogfood-composed-header-auth-and-example-continuations-2026-05-05.md
@@ -0,0 +1,102 @@
+---
+title: "Scorecard and dogfood parsing: composed header auth and example continuations"
+date: 2026-05-05
+category: logic-errors
+module: internal/pipeline
+problem_type: logic_error
+component: tooling
+related_components:
+  - authentication
+  - testing_framework
+symptoms:
+  - "Auth protocol scoring under-scored or over-scored composed header authentication when multiple security schemes represented one runtime auth contract."
+  - "Kalshi-style KALSHI-ACCESS-KEY, KALSHI-ACCESS-SIGNATURE, and KALSHI-ACCESS-TIMESTAMP headers were not scored as one composed protocol."
+  - "Dogfood matrix parsing split shell line continuations in Cobra Example fields into incorrect tokens."
+  - "Escaped backslashes that were not shell line continuations still needed to remain intact."
+root_cause: logic_error
+resolution_type: code_fix
+severity: medium
+tags:
+  - auth-protocol
+  - scorecard
+  - securityschemes
+  - dogfood
+  - shellargs
+  - cobra-examples
+  - tokenizer
+  - line-continuation
+---
+
+# Scorecard and dogfood parsing: composed header auth and example continuations
+
+## Problem
+
+Two independent retro findings had the same shape: a static checker was judging generated CLI behavior from a narrower representation than the runtime actually uses.
+
+For `auth_protocol`, the scorecard looked at OpenAPI security declarations too literally. Kalshi-style auth is one runtime protocol made from multiple headers: an access key, a signature, and a timestamp. The OpenAPI security requirement may name only the key scheme, while the generated client still emits all required headers at runtime. Scoring declaration counts missed that composed contract. Session history from the Kalshi reprint also confirmed that the printed CLI's RSA-PSS header emission was correct; the misleading signal was the scorecard, not the generated auth implementation.
+
+For dogfood matrix parsing, Cobra `Example:` blocks used shell backslash-newline continuations. The shared `internal/shellargs` tokenizer treated those physical line breaks as token separators instead of removing the continuation before tokenization.
+
+## Symptoms
+
+- `auth_protocol` could score a composed header-auth CLI poorly even when runtime code set every required header.
+- A missing sibling header in a composed protocol could hide behind the referenced key scheme and score too generously.
+- Independent same-prefix headers could be accidentally grouped if the scorer used prefix matching alone.
+- Example commands using `\` followed by LF or CRLF split into the wrong argv.
+- Mid-token continuations like `foo\` followed by `bar` needed to become `foobar`, while ordinary escaped backslashes needed to stay unchanged.
+
+## What Didn't Work
+
+1. Treating every unreferenced OpenAPI header security scheme as unused. Some specs model composed auth by referencing the key scheme while leaving signature and timestamp siblings as separate security schemes.
+2. Merging every same-prefix header scheme. `X-API-KEY` and `X-API-TOKEN` may be independent headers, not one protocol.
+3. Merging schemes that are already listed as separate OpenAPI security alternatives. If the spec says `X-API-KEY` OR `X-API-SIGNATURE`, the scorer must not reinterpret that as KEY AND SIGNATURE.
+4. Awarding broad auth points from unrelated signals like query auth plus env config. That can mask missing runtime header emission for required schemes.
+5. Normalizing line continuations by inserting whitespace. In shell syntax, backslash-newline is removed, so mid-token continuations concatenate.
+
+## Solution
+
+For `auth_protocol`, score the runtime behavior of the generated client:
+
+- Start from each OpenAPI security alternative.
+- Expand a referenced `apiKey` header scheme only to unreferenced sibling header schemes with the same prefix when that prefix has signing-style companions such as `SIGNATURE`, `TIMESTAMP`, `DATE`, `NONCE`, or `DIGEST`.
+- Do not pull in sibling schemes that are already referenced by any OpenAPI security requirement.
+- For composed header groups, inspect `internal/client/client.go` for each required header assignment through `Header.Set` or `Header.Add`.
+- If a required multi-scheme alternative has any zero-score member, keep the alternative below the passing band instead of letting the average hide the failure.
+
+For `internal/shellargs`, remove shell line continuations before tokenization:
+
+- Delete `\\\n` and `\\\r\n`.
+- Do this before quote, escape, and whitespace processing.
+- Leave all other backslash sequences alone.
+
+The fix is intentionally systemic: it changes the scorer and shared tokenizer that every future printed CLI uses, rather than patching one printed Kalshi CLI or one matrix fixture.
+
+## Why This Works
+
+OpenAPI `securitySchemes` describe names and carriers, but composed request-signing auth is enforced by the concrete headers emitted at runtime. The scorecard should therefore use the spec to identify the expected contract, then use generated client code to verify that contract is actually implemented.
+
+The signing-style suffix gate keeps the expansion narrow. It recognizes common multi-header auth families without converting arbitrary same-prefix API keys into one required group. Excluding schemes already referenced in `security` preserves explicit OpenAPI OR semantics.
+
+The shellargs change follows POSIX shell behavior for line continuations: the escaped newline is removed before lexical splitting. That handles LF, CRLF, quoted strings, and mid-token examples with one rule while preserving unrelated escaped backslashes.
+
+## Prevention
+
+1. **Prefer runtime evidence when the scored dimension is runtime behavior.** For auth protocol scoring, declarations identify candidates; generated header emission proves implementation.
+2. **Gate heuristic grouping with semantic shape, not just shared text.** Same-prefix matching is too broad unless paired with known composed-auth suffixes and security-requirement checks.
+3. **Test both false negatives and false positives.** The composed-auth scorer needs tests for full Kalshi-style success, missing sibling failure, same-prefix independent headers, and explicit OR alternatives.
+4. **Keep command-example parsing centralized.** Cobra examples, dogfood matrices, and future validators should all use `internal/shellargs` so shell-compatibility fixes land once.
+5. **Model shell continuations before tokenization.** Backslash-newline removal changes the byte stream the lexer sees; treating it as a post-token cleanup misses mid-token cases.
+6. **Make exact-score tests when calibrating a scorer.** A loose `>= 8` assertion can pass while hiding whether the intended protocol path scored full credit.
+
+## Related Issues
+
+- `#596` -- Auth_protocol scorer evaluates runtime header emission, not declaration counts.
+- `#601` -- Matrix tokenizer joins line-continuation in Cobra Example fields.
+- `#594` -- Kalshi retro that surfaced the composed header-auth scoring gap.
+- `#597` -- Dogfood retro wave that surfaced the example-tokenizer gap.
+
+## Related Docs
+
+- `docs/solutions/best-practices/steinberger-scorecard-scoring-architecture-2026-03-27.md` -- architecture and invariants for scorecard dimensions.
+- `docs/solutions/logic-errors/scorecard-accuracy-broadened-pattern-matching-2026-03-27.md` -- earlier scorecard false-positive and false-negative fixes.
+- `docs/solutions/design-patterns/dry-run-default-for-mutator-probes-in-test-harnesses-2026-05-05.md` -- adjacent dogfood guidance for safe test-harness behavior.
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index 40d8b0fb..43d7dbfc 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -1501,6 +1501,14 @@ type openAPISecurityScheme struct {
 	HeaderName string
 }
 
+const (
+	composedHeaderSuffixSignature = "SIGNATURE"
+	composedHeaderSuffixTimestamp = "TIMESTAMP"
+	composedHeaderSuffixDate      = "DATE"
+	composedHeaderSuffixNonce     = "NONCE"
+	composedHeaderSuffixDigest    = "DIGEST"
+)
+
 type securityRequirementSet struct {
 	Alternatives    [][]string
 	AllowsAnonymous bool
@@ -1746,6 +1754,7 @@ func evaluateAuthProtocol(dir string, spec *openAPISpecInfo) dimensionScore {
 		return dimensionScore{scored: true}
 	}
 
+	referencedSchemes := referencedSecuritySchemes(spec.SecurityRequirements)
 	totalScore := 0
 	scoredSets := 0
 	for _, requirementSet := range spec.SecurityRequirements {
@@ -1756,7 +1765,7 @@ func evaluateAuthProtocol(dir string, spec *openAPISpecInfo) dimensionScore {
 		bestScore := -1
 		scoreable := false
 		for _, alternative := range requirementSet.Alternatives {
-			score, ok := scoreAuthAlternative(clientContent, configContent, authContent, spec.SecuritySchemes, alternative)
+			score, ok := scoreAuthAlternative(clientContent, configContent, authContent, spec.SecuritySchemes, alternative, referencedSchemes)
 			if !ok {
 				continue
 			}
@@ -1827,29 +1836,57 @@ func parseSecurityRequirementSet(value any) (securityRequirementSet, bool) {
 	return set, true
 }
 
-func scoreAuthAlternative(clientContent, configContent, authContent string, schemes map[string]openAPISecurityScheme, alternative []string) (int, bool) {
+func referencedSecuritySchemes(requirementSets []securityRequirementSet) map[string]bool {
+	referenced := make(map[string]bool)
+	for _, requirementSet := range requirementSets {
+		for _, alternative := range requirementSet.Alternatives {
+			for _, key := range alternative {
+				referenced[key] = true
+			}
+		}
+	}
+	return referenced
+}
+
+func scoreAuthAlternative(clientContent, configContent, authContent string, schemes map[string]openAPISecurityScheme, alternative []string, referencedSchemes map[string]bool) (int, bool) {
 	if len(alternative) == 0 {
 		return 0, false
 	}
 
+	expandedAlternative := expandComposedHeaderAlternative(schemes, alternative, referencedSchemes)
+	composedHeaders := isComposedHeaderAlternative(schemes, expandedAlternative)
 	total := 0
 	scoreableSchemes := 0
-	for _, key := range alternative {
+	hasZeroScore := false
+	for _, key := range expandedAlternative {
 		scheme, ok := schemes[key]
 		if !ok {
 			continue
 		}
-		score, scoreable := scoreAuthScheme(clientContent, configContent, authContent, scheme)
+		var score int
+		var scoreable bool
+		if composedHeaders && isAPIKeyHeaderScheme(scheme) {
+			score, scoreable = scoreComposedHeaderScheme(clientContent, scheme)
+		} else {
+			score, scoreable = scoreAuthScheme(clientContent, configContent, authContent, scheme)
+		}
 		if !scoreable {
 			continue
 		}
 		total += score
 		scoreableSchemes++
+		if score == 0 {
+			hasZeroScore = true
+		}
 	}
 	if scoreableSchemes == 0 {
 		return 0, false
 	}
-	return total / scoreableSchemes, true
+	score := total / scoreableSchemes
+	if hasZeroScore && scoreableSchemes > 1 && score >= 5 {
+		score = 4
+	}
+	return score, true
 }
 
 func scoreAuthScheme(clientContent, configContent, authContent string, scheme openAPISecurityScheme) (int, bool) {
@@ -1887,8 +1924,7 @@ func scoreAuthScheme(clientContent, configContent, authContent string, scheme op
 		// Credit authHeaderMatched if the client sets the correct header name,
 		// since that proves the auth plumbing is wired correctly regardless of format.
 		if scheme.In == "header" && headerName != "" {
-			if strings.Contains(clientContent, `Header.Set("`+headerName+`"`) ||
-				strings.Contains(clientContent, `Header.Add("`+headerName+`"`) {
+			if headerAssignmentPresent(clientContent, headerName) {
 				authHeaderMatched = true
 			}
 		}
@@ -1902,8 +1938,7 @@ func scoreAuthScheme(clientContent, configContent, authContent string, scheme op
 		return 0, false
 	}
 
-	if strings.Contains(clientContent, `Header.Set("`+headerName+`"`) ||
-		strings.Contains(clientContent, `Header.Add("`+headerName+`"`) {
+	if headerAssignmentPresent(clientContent, headerName) {
 		headerNameMatched = true
 	}
 
@@ -1940,9 +1975,115 @@ func scoreAuthScheme(clientContent, configContent, authContent string, scheme op
 	if score > 10 {
 		score = 10
 	}
+	if headerNameMatched && authHeaderMatched && envMatched {
+		score = 10
+	} else if headerNameMatched && authHeaderMatched && score < 8 {
+		score = 8
+	}
 	return score, true
 }
 
+func expandComposedHeaderAlternative(schemes map[string]openAPISecurityScheme, alternative []string, referencedSchemes map[string]bool) []string {
+	expanded := append([]string(nil), alternative...)
+	seen := make(map[string]bool, len(alternative))
+	prefixes := make(map[string]bool)
+	for _, key := range alternative {
+		seen[key] = true
+		if prefix := composedHeaderPrefix(schemes[key]); prefix != "" {
+			prefixes[prefix] = composedHeaderPrefixHasCompanion(schemes, prefix)
+		}
+	}
+	if len(prefixes) == 0 {
+		return expanded
+	}
+	for key, scheme := range schemes {
+		if seen[key] || !isAPIKeyHeaderScheme(scheme) {
+			continue
+		}
+		if referencedSchemes[key] {
+			continue
+		}
+		if prefixes[composedHeaderPrefix(scheme)] {
+			expanded = append(expanded, key)
+		}
+	}
+	slices.Sort(expanded)
+	return expanded
+}
+
+func composedHeaderPrefixHasCompanion(schemes map[string]openAPISecurityScheme, prefix string) bool {
+	for _, scheme := range schemes {
+		if composedHeaderPrefix(scheme) == prefix && composedHeaderCompanionSuffix(scheme) {
+			return true
+		}
+	}
+	return false
+}
+
+func composedHeaderCompanionSuffix(scheme openAPISecurityScheme) bool {
+	if !isAPIKeyHeaderScheme(scheme) {
+		return false
+	}
+	suffix := strings.TrimPrefix(strings.ToUpper(strings.TrimSpace(scheme.HeaderName)), composedHeaderPrefix(scheme))
+	switch suffix {
+	case composedHeaderSuffixSignature, composedHeaderSuffixTimestamp, composedHeaderSuffixDate, composedHeaderSuffixNonce, composedHeaderSuffixDigest:
+		return true
+	default:
+		return false
+	}
+}
+
+func isComposedHeaderAlternative(schemes map[string]openAPISecurityScheme, alternative []string) bool {
+	counts := make(map[string]int)
+	for _, key := range alternative {
+		prefix := composedHeaderPrefix(schemes[key])
+		if prefix != "" {
+			counts[prefix]++
+		}
+	}
+	for _, count := range counts {
+		if count > 1 {
+			return true
+		}
+	}
+	return false
+}
+
+func composedHeaderPrefix(scheme openAPISecurityScheme) string {
+	if !isAPIKeyHeaderScheme(scheme) {
+		return ""
+	}
+	headerName := strings.ToUpper(strings.TrimSpace(scheme.HeaderName))
+	idx := strings.LastIndex(headerName, "-")
+	if idx < 0 {
+		return ""
+	}
+	prefix := headerName[:idx+1]
+	if strings.Count(prefix, "-") < 2 {
+		return ""
+	}
+	return prefix
+}
+
+func isAPIKeyHeaderScheme(scheme openAPISecurityScheme) bool {
+	return strings.EqualFold(scheme.Type, "apikey") && scheme.In == "header" && strings.TrimSpace(scheme.HeaderName) != ""
+}
+
+func scoreComposedHeaderScheme(clientContent string, scheme openAPISecurityScheme) (int, bool) {
+	if !isAPIKeyHeaderScheme(scheme) {
+		return 0, false
+	}
+	if headerAssignmentPresent(clientContent, scheme.HeaderName) {
+		return 10, true
+	}
+	return 0, true
+}
+
+func headerAssignmentPresent(clientContent, headerName string) bool {
+	return strings.Contains(clientContent, `Header.Set("`+headerName+`"`) ||
+		strings.Contains(clientContent, `Header.Add("`+headerName+`"`)
+}
+
 func authPrefixLiteralPresent(prefix string, contents ...string) bool {
 	doubleQuoted := `"` + prefix + ` "`
 	rawQuoted := "`" + prefix + " `"
diff --git a/internal/pipeline/scorecard_tier2_test.go b/internal/pipeline/scorecard_tier2_test.go
index 30b0358d..6b03b6a0 100644
--- a/internal/pipeline/scorecard_tier2_test.go
+++ b/internal/pipeline/scorecard_tier2_test.go
@@ -824,6 +824,330 @@ func (c Config) AuthHeader() string {
 		assert.GreaterOrEqual(t, sc.Steinberger.AuthProtocol, 7)
 	})
 
+	t.Run("single header auth scheme scores full runtime protocol", func(t *testing.T) {
+		dir := t.TempDir()
+		writeScorecardFixture(t, dir, "internal/client/client.go", `
+package client
+
+import "net/http"
+
+func setAuth(req *http.Request, token string) {
+	req.Header.Set("Authorization", "Bearer "+token)
+}
+`)
+		writeScorecardFixture(t, dir, "internal/config/config.go", `
+package config
+
+import "os"
+
+func LoadToken() string {
+	return os.Getenv("TOKEN")
+}
+`)
+
+		specPath := filepath.Join(dir, "spec-single-bearer.json")
+		writeScorecardFixture(t, dir, "spec-single-bearer.json", `{
+  "security": [
+    {
+      "TOKEN": []
+    }
+  ],
+  "paths": {
+    "/portfolio": {
+      "get": {
+        "responses": {
+          "200": { "description": "ok" }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "TOKEN": {
+        "type": "http",
+        "scheme": "bearer"
+      }
+    }
+  }
+}`)
+
+		pipelineDir := t.TempDir()
+		sc, err := RunScorecard(dir, pipelineDir, specPath, nil)
+		assert.NoError(t, err)
+		assert.NotContains(t, sc.UnscoredDimensions, "auth_protocol")
+		assert.Equal(t, 10, sc.Steinberger.AuthProtocol)
+	})
+
+	t.Run("composed header auth scores unreferenced sibling emissions", func(t *testing.T) {
+		dir := t.TempDir()
+		writeScorecardFixture(t, dir, "internal/client/client.go", `
+package client
+
+import "net/http"
+
+func signKalshiRequest(req *http.Request, key string, signature string, timestamp string) {
+	req.Header.Set("KALSHI-ACCESS-KEY", key)
+	req.Header.Set("KALSHI-ACCESS-SIGNATURE", signature)
+	req.Header.Set("KALSHI-ACCESS-TIMESTAMP", timestamp)
+}
+`)
+
+		specPath := filepath.Join(dir, "spec-kalshi-composed.json")
+		writeScorecardFixture(t, dir, "spec-kalshi-composed.json", `{
+  "security": [
+    {
+      "kalshiAccessKey": []
+    }
+  ],
+  "paths": {
+    "/portfolio/balance": {
+      "get": {
+        "responses": {
+          "200": { "description": "ok" }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "kalshiAccessKey": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "KALSHI-ACCESS-KEY"
+      },
+      "kalshiAccessSignature": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "KALSHI-ACCESS-SIGNATURE"
+      },
+      "kalshiAccessTimestamp": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "KALSHI-ACCESS-TIMESTAMP"
+      }
+    }
+  }
+}`)
+
+		pipelineDir := t.TempDir()
+		sc, err := RunScorecard(dir, pipelineDir, specPath, nil)
+		assert.NoError(t, err)
+		assert.NotContains(t, sc.UnscoredDimensions, "auth_protocol")
+		assert.Equal(t, 10, sc.Steinberger.AuthProtocol)
+	})
+
+	t.Run("composed header auth penalizes missing sibling emissions", func(t *testing.T) {
+		dir := t.TempDir()
+		writeScorecardFixture(t, dir, "internal/client/client.go", `
+package client
+
+import "net/http"
+
+func signKalshiRequest(req *http.Request, key string, signature string) {
+	req.Header.Set("KALSHI-ACCESS-KEY", key)
+	req.Header.Set("KALSHI-ACCESS-SIGNATURE", signature)
+}
+`)
+
+		specPath := filepath.Join(dir, "spec-kalshi-composed-missing.json")
+		writeScorecardFixture(t, dir, "spec-kalshi-composed-missing.json", `{
+  "security": [
+    {
+      "kalshiAccessKey": []
+    }
+  ],
+  "paths": {
+    "/portfolio/balance": {
+      "get": {
+        "responses": {
+          "200": { "description": "ok" }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "kalshiAccessKey": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "KALSHI-ACCESS-KEY"
+      },
+      "kalshiAccessSignature": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "KALSHI-ACCESS-SIGNATURE"
+      },
+      "kalshiAccessTimestamp": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "KALSHI-ACCESS-TIMESTAMP"
+      }
+    }
+  }
+}`)
+
+		pipelineDir := t.TempDir()
+		sc, err := RunScorecard(dir, pipelineDir, specPath, nil)
+		assert.NoError(t, err)
+		assert.NotContains(t, sc.UnscoredDimensions, "auth_protocol")
+		assert.Less(t, sc.Steinberger.AuthProtocol, 5)
+	})
+
+	t.Run("same-prefix standalone header scheme is not pulled into composed auth", func(t *testing.T) {
+		dir := t.TempDir()
+		writeScorecardFixture(t, dir, "internal/client/client.go", `
+package client
+
+import "net/http"
+
+func setAuth(req *http.Request, key string) {
+	req.Header.Set("X-API-KEY", key)
+}
+`)
+
+		specPath := filepath.Join(dir, "spec-same-prefix-standalone.json")
+		writeScorecardFixture(t, dir, "spec-same-prefix-standalone.json", `{
+  "security": [
+    {
+      "apiKey": []
+    }
+  ],
+  "paths": {
+    "/links": {
+      "get": {
+        "responses": {
+          "200": { "description": "ok" }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "apiKey": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "X-API-KEY"
+      },
+      "apiToken": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "X-API-TOKEN"
+      }
+    }
+  }
+}`)
+
+		pipelineDir := t.TempDir()
+		sc, err := RunScorecard(dir, pipelineDir, specPath, nil)
+		assert.NoError(t, err)
+		assert.NotContains(t, sc.UnscoredDimensions, "auth_protocol")
+		assert.GreaterOrEqual(t, sc.Steinberger.AuthProtocol, 8)
+	})
+
+	t.Run("same-prefix security alternatives are not merged into composed auth", func(t *testing.T) {
+		dir := t.TempDir()
+		writeScorecardFixture(t, dir, "internal/client/client.go", `
+package client
+
+import "net/http"
+
+func setAuth(req *http.Request, key string) {
+	req.Header.Set("X-API-KEY", key)
+}
+`)
+
+		specPath := filepath.Join(dir, "spec-same-prefix-alternatives.json")
+		writeScorecardFixture(t, dir, "spec-same-prefix-alternatives.json", `{
+  "security": [
+    {
+      "apiKey": []
+    },
+    {
+      "apiSignature": []
+    }
+  ],
+  "paths": {
+    "/links": {
+      "get": {
+        "responses": {
+          "200": { "description": "ok" }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "apiKey": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "X-API-KEY"
+      },
+      "apiSignature": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "X-API-SIGNATURE"
+      }
+    }
+  }
+}`)
+
+		pipelineDir := t.TempDir()
+		sc, err := RunScorecard(dir, pipelineDir, specPath, nil)
+		assert.NoError(t, err)
+		assert.NotContains(t, sc.UnscoredDimensions, "auth_protocol")
+		assert.GreaterOrEqual(t, sc.Steinberger.AuthProtocol, 8)
+	})
+
+	t.Run("required auth alternative penalizes an unimplemented scheme", func(t *testing.T) {
+		dir := t.TempDir()
+		writeScorecardFixture(t, dir, "internal/client/client.go", `
+package client
+
+import "net/http"
+
+func setAuth(req *http.Request, key string) {
+	req.Header.Set("X-API-Key", key)
+}
+`)
+
+		specPath := filepath.Join(dir, "spec-required-oauth-and-key.json")
+		writeScorecardFixture(t, dir, "spec-required-oauth-and-key.json", `{
+  "paths": {
+    "/links": {
+      "get": {
+        "security": [
+          {
+            "oauth": [],
+            "apiKey": []
+          }
+        ],
+        "responses": {
+          "200": { "description": "ok" }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "oauth": {
+        "type": "oauth2"
+      },
+      "apiKey": {
+        "type": "apiKey",
+        "in": "header",
+        "name": "X-API-Key"
+      }
+    }
+  }
+}`)
+
+		pipelineDir := t.TempDir()
+		sc, err := RunScorecard(dir, pipelineDir, specPath, nil)
+		assert.NoError(t, err)
+		assert.NotContains(t, sc.UnscoredDimensions, "auth_protocol")
+		assert.Less(t, sc.Steinberger.AuthProtocol, 5)
+	})
+
 	t.Run("anonymous alternative leaves auth unscored", func(t *testing.T) {
 		dir := t.TempDir()
 		writeScorecardFixture(t, dir, "internal/client/client.go", `
diff --git a/internal/shellargs/shellargs.go b/internal/shellargs/shellargs.go
index 28664507..ea8bd9af 100644
--- a/internal/shellargs/shellargs.go
+++ b/internal/shellargs/shellargs.go
@@ -9,6 +9,8 @@ import (
 // README/SKILL narrative. It preserves double-quoted tokens and backslash
 // escapes, but intentionally does not perform shell expansion.
 func Split(s string) ([]string, error) {
+	s = joinLineContinuations(s)
+
 	var tokens []string
 	var current strings.Builder
 	inQuote := false
@@ -68,6 +70,13 @@ func Split(s string) ([]string, error) {
 	return tokens, nil
 }
 
+func joinLineContinuations(s string) string {
+	for _, newline := range []string{"\\\r\n", "\\\n"} {
+		s = strings.ReplaceAll(s, newline, "")
+	}
+	return s
+}
+
 // ArgsAfterBinary returns every token after the leading binary name.
 func ArgsAfterBinary(example string) ([]string, error) {
 	tokens, err := Split(example)
diff --git a/internal/shellargs/shellargs_test.go b/internal/shellargs/shellargs_test.go
index a938488d..37878aba 100644
--- a/internal/shellargs/shellargs_test.go
+++ b/internal/shellargs/shellargs_test.go
@@ -14,6 +14,11 @@ func TestSplit(t *testing.T) {
 		{`cli goat "chicken tikka masala" --limit 5`, []string{"cli", "goat", "chicken tikka masala", "--limit", "5"}},
 		{`cli  multiple   spaces`, []string{"cli", "multiple", "spaces"}},
 		{`cli query \"literal\"`, []string{"cli", "query", `"literal"`}},
+		{"cli slots find \\\n  --event-type-id 123 \\\n  --start \"2026-01-01T00:00:00Z\"", []string{"cli", "slots", "find", "--event-type-id", "123", "--start", "2026-01-01T00:00:00Z"}},
+		{"cli slots find \\\r\n  --event-type-id 123", []string{"cli", "slots", "find", "--event-type-id", "123"}},
+		{"cli --name foo\\\nbar", []string{"cli", "--name", "foobar"}},
+		{"cli --name \"foo\\\nbar\"", []string{"cli", "--name", "foobar"}},
+		{`cli regex \\d+\\s+goat`, []string{"cli", "regex", `\d+\s+goat`}},
 	}
 	for _, tc := range cases {
 		got, err := Split(tc.in)

← 0310d3fb feat(skills): data-driven Phase 6 menu and hold-path support  ·  back to Cli Printing Press  ·  docs(cli): rewrite install steps and collapse secondary sect 5776553b →