@opengsd/gsd-core 1.6.0-rc.2 → 1.6.0-rc.3

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.
Files changed (116) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/agents/gsd-planner.md +8 -57
  3. package/agents/gsd-security-auditor.md +37 -18
  4. package/bin/install.js +151 -13
  5. package/gemini-extension.json +1 -1
  6. package/gsd-core/bin/gsd-tools.cjs +35 -3
  7. package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
  8. package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
  9. package/gsd-core/bin/lib/capability-registry.cjs +96 -83
  10. package/gsd-core/bin/lib/capability-validator.cjs +22 -0
  11. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
  12. package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
  13. package/gsd-core/bin/lib/config-schema.cjs +1 -0
  14. package/gsd-core/bin/lib/config.cjs +67 -24
  15. package/gsd-core/bin/lib/coverage.cjs +464 -0
  16. package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
  17. package/gsd-core/bin/lib/init.cjs +8 -4
  18. package/gsd-core/bin/lib/install-profiles.cjs +6 -3
  19. package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
  20. package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
  21. package/gsd-core/bin/lib/profile-output.cjs +18 -6
  22. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +53 -16
  23. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +21 -3
  24. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
  25. package/gsd-core/bin/lib/runtime-name-policy.cjs +47 -2
  26. package/gsd-core/bin/lib/state.cjs +364 -52
  27. package/gsd-core/bin/lib/surface.cjs +42 -10
  28. package/gsd-core/bin/lib/update-context.cjs +2 -2
  29. package/gsd-core/references/planner-guidance.md +66 -0
  30. package/gsd-core/references/planning-config.md +2 -2
  31. package/gsd-core/references/security-asvs-levels.md +27 -0
  32. package/gsd-core/templates/SECURITY.md +6 -4
  33. package/gsd-core/templates/summary-complex.md +4 -0
  34. package/gsd-core/templates/summary-minimal.md +3 -0
  35. package/gsd-core/templates/summary-standard.md +4 -0
  36. package/gsd-core/templates/summary.md +41 -0
  37. package/gsd-core/workflows/execute-plan.md +5 -0
  38. package/gsd-core/workflows/new-project.md +4 -4
  39. package/gsd-core/workflows/plan-phase.md +15 -0
  40. package/gsd-core/workflows/secure-phase.md +13 -7
  41. package/gsd-core/workflows/verify-work.md +30 -1
  42. package/package.json +4 -2
  43. package/scripts/gen-plugin-skills.cjs +117 -0
  44. package/scripts/lint-test-file-count.allowlist.json +2 -1
  45. package/scripts/prompt-injection-scan.sh +4 -0
  46. package/scripts/release-notes/conventional-title.cjs +88 -0
  47. package/scripts/release-notes/format-github-release-notes.cjs +4 -3
  48. package/skills/gsd-add-tests/SKILL.md +38 -0
  49. package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
  50. package/skills/gsd-audit-fix/SKILL.md +33 -0
  51. package/skills/gsd-audit-milestone/SKILL.md +37 -0
  52. package/skills/gsd-audit-uat/SKILL.md +25 -0
  53. package/skills/gsd-autonomous/SKILL.md +51 -0
  54. package/skills/gsd-capture/SKILL.md +67 -0
  55. package/skills/gsd-cleanup/SKILL.md +24 -0
  56. package/skills/gsd-code-review/SKILL.md +59 -0
  57. package/skills/gsd-complete-milestone/SKILL.md +142 -0
  58. package/skills/gsd-config/SKILL.md +56 -0
  59. package/skills/gsd-debug/SKILL.md +53 -0
  60. package/skills/gsd-discuss-phase/SKILL.md +77 -0
  61. package/skills/gsd-docs-update/SKILL.md +49 -0
  62. package/skills/gsd-eval-review/SKILL.md +33 -0
  63. package/skills/gsd-execute-phase/SKILL.md +65 -0
  64. package/skills/gsd-explore/SKILL.md +28 -0
  65. package/skills/gsd-extract-learnings/SKILL.md +22 -0
  66. package/skills/gsd-fast/SKILL.md +31 -0
  67. package/skills/gsd-forensics/SKILL.md +56 -0
  68. package/skills/gsd-graphify/SKILL.md +204 -0
  69. package/skills/gsd-health/SKILL.md +31 -0
  70. package/skills/gsd-help/SKILL.md +29 -0
  71. package/skills/gsd-import/SKILL.md +46 -0
  72. package/skills/gsd-inbox/SKILL.md +39 -0
  73. package/skills/gsd-ingest-docs/SKILL.md +43 -0
  74. package/skills/gsd-manager/SKILL.md +45 -0
  75. package/skills/gsd-map-codebase/SKILL.md +83 -0
  76. package/skills/gsd-mempalace-capture/SKILL.md +71 -0
  77. package/skills/gsd-mempalace-recall/SKILL.md +102 -0
  78. package/skills/gsd-milestone-summary/SKILL.md +51 -0
  79. package/skills/gsd-mvp-phase/SKILL.md +45 -0
  80. package/skills/gsd-new-milestone/SKILL.md +45 -0
  81. package/skills/gsd-new-project/SKILL.md +47 -0
  82. package/skills/gsd-ns-context/SKILL.md +24 -0
  83. package/skills/gsd-ns-ideate/SKILL.md +23 -0
  84. package/skills/gsd-ns-manage/SKILL.md +35 -0
  85. package/skills/gsd-ns-project/SKILL.md +26 -0
  86. package/skills/gsd-ns-review/SKILL.md +28 -0
  87. package/skills/gsd-ns-workflow/SKILL.md +33 -0
  88. package/skills/gsd-pause-work/SKILL.md +43 -0
  89. package/skills/gsd-phase/SKILL.md +57 -0
  90. package/skills/gsd-plan-phase/SKILL.md +63 -0
  91. package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
  92. package/skills/gsd-pr-branch/SKILL.md +26 -0
  93. package/skills/gsd-profile-user/SKILL.md +47 -0
  94. package/skills/gsd-progress/SKILL.md +49 -0
  95. package/skills/gsd-quick/SKILL.md +174 -0
  96. package/skills/gsd-resume-work/SKILL.md +31 -0
  97. package/skills/gsd-review/SKILL.md +42 -0
  98. package/skills/gsd-review-backlog/SKILL.md +63 -0
  99. package/skills/gsd-secure-phase/SKILL.md +36 -0
  100. package/skills/gsd-settings/SKILL.md +29 -0
  101. package/skills/gsd-ship/SKILL.md +24 -0
  102. package/skills/gsd-sketch/SKILL.md +60 -0
  103. package/skills/gsd-spec-phase/SKILL.md +63 -0
  104. package/skills/gsd-spike/SKILL.md +57 -0
  105. package/skills/gsd-stats/SKILL.md +20 -0
  106. package/skills/gsd-surface/SKILL.md +162 -0
  107. package/skills/gsd-thread/SKILL.md +24 -0
  108. package/skills/gsd-ui-phase/SKILL.md +35 -0
  109. package/skills/gsd-ui-review/SKILL.md +33 -0
  110. package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
  111. package/skills/gsd-undo/SKILL.md +35 -0
  112. package/skills/gsd-update/SKILL.md +50 -0
  113. package/skills/gsd-validate-phase/SKILL.md +36 -0
  114. package/skills/gsd-verify-work/SKILL.md +39 -0
  115. package/skills/gsd-workspace/SKILL.md +53 -0
  116. package/skills/gsd-workstreams/SKILL.md +70 -0
@@ -582,6 +582,27 @@ function validateFeatureBody(cap) {
582
582
  'absolute path, or ".." segment) — it contains unsafe characters: ' + JSON.stringify(h.script),
583
583
  );
584
584
  }
585
+ // #1634: optional tool-scoping `matcher` (a settings.json concept — exact tool name,
586
+ // pipe-separated list, wildcard, or regex; e.g. "Write|Edit"). When present it must be a
587
+ // non-empty string without control characters; absent => match-all (omitted at projection
588
+ // so existing shipped capabilities are unchanged).
589
+ if (h.matcher !== undefined) {
590
+ if (typeof h.matcher !== 'string' || h.matcher.length === 0) {
591
+ errors.push('hooks[' + i + '].matcher must be a non-empty string when present');
592
+ } else {
593
+ // Reject ASCII control characters (0x00-0x1f and 0x7f DEL) via char codes — a literal
594
+ // control-char range regex trips ESLint's no-control-regex rule, and char codes are
595
+ // equally precise.
596
+ let hasControl = false;
597
+ for (let c = 0; c < h.matcher.length; c++) {
598
+ const code = h.matcher.charCodeAt(c);
599
+ if (code < 0x20 || code === 0x7f) { hasControl = true; break; }
600
+ }
601
+ if (hasControl) {
602
+ errors.push('hooks[' + i + '].matcher must not contain control characters');
603
+ }
604
+ }
605
+ }
585
606
  }
586
607
  }
587
608
  }
@@ -665,6 +686,7 @@ const VALID_CONVERTER_NAMES = new Set([
665
686
  'convertClaudeCommandToOpencodeSkill',
666
687
  'convertClaudeCommandToTraeSkill',
667
688
  'convertClaudeCommandToWindsurfSkill',
689
+ 'convertClaudeCommandToWindsurfWorkflow',
668
690
  // agent converters (#1173 — descriptor-driven agent conversion wiring)
669
691
  'convertClaudeAgentToCopilotAgent',
670
692
  'convertClaudeAgentToAntigravityAgent',
@@ -13,6 +13,12 @@
13
13
  // eslint-disable-next-line @typescript-eslint/no-require-imports
14
14
  const commandRoutingHub = require("./command-routing-hub.cjs");
15
15
  const { createHub, ERROR_KINDS } = commandRoutingHub;
16
+ // Phase 2 (#1646): import ERROR_REASON so the UnknownCommand translation can
17
+ // pass `sdk_unknown_command` as the second arg to error(), preserving the
18
+ // JSON-error envelope contract that capability routers' tests assert on.
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const io = require("./io.cjs");
21
+ const { ERROR_REASON } = io;
16
22
  // ─── Implementation ───────────────────────────────────────────────────────────
17
23
  function routeCjsCommandFamily({ args, subcommands, handlers, defaultSubcommand, unsupported = {}, unknownMessage, error, cwd, raw, }) {
18
24
  routeHubCommandFamily({
@@ -44,6 +50,18 @@ function routeHubCommandFamily({ family, args, subcommands, handlers, defaultSub
44
50
  const available = subcommands.filter((s) => !unsupported[s]);
45
51
  const registryHandlers = Object.fromEntries(Object.entries(handlers).map(([name, handler]) => [
46
52
  name,
53
+ // Honestified via amendment #1642 (#1644 Phase 1): the runtime check
54
+ // `'ok' in result` already passes any `{ok:*}` object through, so the
55
+ // historical `{ok:true, data}` return type was a lie whenever the
56
+ // handler returned an err Result. The lying cast below is preserved
57
+ // because the Hub's `export =` syntax doesn't expose `HubResult` for
58
+ // import; the Hub's `_validateErrResult` runtime-validates the actual
59
+ // shape, so structural compatibility is sufficient. The wrapper's
60
+ // 0-arg signature is assignable to the Hub's `(ctx) => HubResult`
61
+ // Handler type via TypeScript parameter bivariance; the Hub's per-call
62
+ // ctx is intentionally ignored (host-router handlers don't use it;
63
+ // capability-router handlers in Phase 2 will return HubResults that
64
+ // already carry context).
47
65
  () => {
48
66
  const result = handler();
49
67
  if (result && typeof result === 'object' && Object.prototype.hasOwnProperty.call(result, 'ok')) {
@@ -66,10 +84,30 @@ function routeHubCommandFamily({ family, args, subcommands, handlers, defaultSub
66
84
  if (result.ok)
67
85
  return;
68
86
  if (result.kind === ERROR_KINDS.UnknownCommand) {
69
- error(unknownMessage(subcommand ?? '', available));
87
+ // Phase 2 (#1646): pass SDK_UNKNOWN_COMMAND as the second arg so the
88
+ // JSON-error envelope (GSD_JSON_ERRORS=1) preserves the typed reason
89
+ // for downstream consumers. Additive for host routers (their existing
90
+ // one-arg `error` callbacks ignore the second arg); required for
91
+ // capability routers whose tests assert on `reason === 'sdk_unknown_command'`.
92
+ error(unknownMessage(subcommand ?? '', available), ERROR_REASON.SDK_UNKNOWN_COMMAND);
93
+ return;
94
+ }
95
+ if (result.kind === ERROR_KINDS.InvalidArgs) {
96
+ // Amendment #1642 (#1644): when the handler provided exitReason, pass it
97
+ // as the second arg to error() so the JSON-error envelope
98
+ // (GSD_JSON_ERRORS=1) preserves the typed ERROR_REASON value for
99
+ // downstream consumers. When exitReason is absent, call error(msg) with
100
+ // exactly one arg — byte-identical with prior behavior.
101
+ const invalidArgs = result;
102
+ if (invalidArgs.exitReason) {
103
+ error(invalidArgs.reason, invalidArgs.exitReason);
104
+ }
105
+ else {
106
+ error(invalidArgs.reason);
107
+ }
70
108
  return;
71
109
  }
72
- if (result.kind === ERROR_KINDS.InvalidArgs || result.kind === ERROR_KINDS.HandlerRefusal) {
110
+ if (result.kind === ERROR_KINDS.HandlerRefusal) {
73
111
  error(result.reason);
74
112
  return;
75
113
  }
@@ -73,8 +73,14 @@ function _safeJson(value) {
73
73
  function makeUnknownCommand(command) {
74
74
  return Object.freeze({ ok: false, kind: ERROR_KINDS.UnknownCommand, command });
75
75
  }
76
- function makeInvalidArgs(arg, reason) {
77
- return Object.freeze({ ok: false, kind: ERROR_KINDS.InvalidArgs, arg, reason });
76
+ function makeInvalidArgs(arg, reason, exitReason) {
77
+ const obj = { ok: false, kind: ERROR_KINDS.InvalidArgs, arg, reason };
78
+ // Conditionally add exitReason only when truthy — preserves strict-keys
79
+ // invariant (2-arg callers must continue to produce a 4-key frozen result).
80
+ if (exitReason) {
81
+ obj.exitReason = exitReason;
82
+ }
83
+ return Object.freeze(obj);
78
84
  }
79
85
  function makeHandlerRefusal(reason) {
80
86
  return Object.freeze({ ok: false, kind: ERROR_KINDS.HandlerRefusal, reason });
@@ -113,7 +119,8 @@ const _VARIANT_SCHEMA = {
113
119
  },
114
120
  InvalidArgs: {
115
121
  required: ['arg', 'reason'],
116
- allowed: new Set(['ok', 'kind', 'arg', 'reason']),
122
+ // Amendment #1642: exitReason? is allowed but not required.
123
+ allowed: new Set(['ok', 'kind', 'arg', 'reason', 'exitReason']),
117
124
  },
118
125
  HandlerRefusal: {
119
126
  required: ['reason'],
@@ -74,4 +74,5 @@ module.exports = {
74
74
  isCapabilityConfigKey,
75
75
  isCentralConfigKey,
76
76
  isValidConfigKey,
77
+ getCapabilityConfigSchema: _capabilityConfigSchema,
77
78
  };
@@ -27,7 +27,7 @@ const modelProfiles = require("./model-profiles.cjs");
27
27
  const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
28
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
29
  const configSchema = require("./config-schema.cjs");
30
- const { VALID_CONFIG_KEYS, isValidConfigKey } = configSchema;
30
+ const { VALID_CONFIG_KEYS, isValidConfigKey, getCapabilityConfigSchema } = configSchema;
31
31
  const secrets_cjs_1 = require("./secrets.cjs");
32
32
  const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
33
33
  const configuration_cjs_1 = require("./configuration.cjs");
@@ -480,6 +480,22 @@ function setConfigValues(cwd, entries) {
480
480
  }
481
481
  });
482
482
  }
483
+ /**
484
+ * Type-safe enum guard for config-set string-enum keys.
485
+ *
486
+ * Rejects any parsedValue that is not a plain string AND a member of `allowed`.
487
+ * This closes the JSON-array coercion bypass: String(["val"]) === "val" satisfies
488
+ * a bare .includes(String(parsedValue)) check, but typeof parsedValue !== 'string'
489
+ * catches the array before the includes test.
490
+ *
491
+ * The `label` parameter is used verbatim in the error message so callers can
492
+ * preserve existing message text byte-for-byte.
493
+ */
494
+ function assertEnumValue(parsedValue, rawVal, allowed, label) {
495
+ if (typeof parsedValue !== 'string' || !allowed.includes(parsedValue)) {
496
+ error(`Invalid ${label} '${rawVal}'. Valid values: ${allowed.join(', ')}`);
497
+ }
498
+ }
483
499
  /**
484
500
  * Command to set a value in the config file, allowing nested values via dot notation (e.g.,
485
501
  * "workflow.research").
@@ -524,14 +540,12 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
524
540
  catch { /* keep as string */ }
525
541
  }
526
542
  const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
527
- if (kp === 'context' && !VALID_CONTEXT_VALUES.includes(String(parsedValue))) {
528
- error(`Invalid context value '${val}'. Valid values: ${VALID_CONTEXT_VALUES.join(', ')}`);
529
- }
543
+ if (kp === 'context')
544
+ assertEnumValue(parsedValue, val, VALID_CONTEXT_VALUES, 'context value');
530
545
  // Codebase drift detector (#2003)
531
546
  const VALID_DRIFT_ACTIONS = ['warn', 'auto-remap'];
532
- if (kp === 'workflow.drift_action' && !VALID_DRIFT_ACTIONS.includes(String(parsedValue))) {
533
- error(`Invalid workflow.drift_action '${val}'. Valid values: ${VALID_DRIFT_ACTIONS.join(', ')}`);
534
- }
547
+ if (kp === 'workflow.drift_action')
548
+ assertEnumValue(parsedValue, val, VALID_DRIFT_ACTIONS, 'workflow.drift_action');
535
549
  if (kp === 'workflow.drift_threshold') {
536
550
  if (typeof parsedValue !== 'number' || !Number.isInteger(parsedValue) || parsedValue < 1) {
537
551
  error(`Invalid workflow.drift_threshold '${val}'. Must be a positive integer.`);
@@ -554,28 +568,23 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
554
568
  }
555
569
  // Human verification checkpoint mode (#3309)
556
570
  const VALID_HUMAN_VERIFY_MODES = ['mid-flight', 'end-of-phase'];
557
- if (kp === 'workflow.human_verify_mode' && !VALID_HUMAN_VERIFY_MODES.includes(String(parsedValue))) {
558
- error(`Invalid workflow.human_verify_mode '${val}'. Valid values: ${VALID_HUMAN_VERIFY_MODES.join(', ')}`);
559
- }
571
+ if (kp === 'workflow.human_verify_mode')
572
+ assertEnumValue(parsedValue, val, VALID_HUMAN_VERIFY_MODES, 'workflow.human_verify_mode');
560
573
  // Context exhaustion guard mode (#1452)
561
574
  const VALID_CONTEXT_GUARD_MODES = ['auto', 'warn', 'off'];
562
- if (kp === 'workflow.context_guard_mode' && !VALID_CONTEXT_GUARD_MODES.includes(String(parsedValue))) {
563
- error(`Invalid workflow.context_guard_mode '${val}'. Valid values: ${VALID_CONTEXT_GUARD_MODES.join(', ')}`);
564
- }
575
+ if (kp === 'workflow.context_guard_mode')
576
+ assertEnumValue(parsedValue, val, VALID_CONTEXT_GUARD_MODES, 'workflow.context_guard_mode');
565
577
  // Context position enum validation (#2937)
566
578
  const VALID_CONTEXT_POSITIONS = ['front', 'end'];
567
- if (kp === 'statusline.context_position' && !VALID_CONTEXT_POSITIONS.includes(String(parsedValue))) {
568
- error(`Invalid statusline.context_position '${val}'. Valid values: ${VALID_CONTEXT_POSITIONS.join(', ')}`);
569
- }
579
+ if (kp === 'statusline.context_position')
580
+ assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position');
570
581
  // Fallow scope + profile enum validation (#3424)
571
582
  const VALID_FALLOW_SCOPES = ['phase', 'repo'];
572
- if (kp === 'code_quality.fallow.scope' && !VALID_FALLOW_SCOPES.includes(String(parsedValue))) {
573
- error(`Invalid code_quality.fallow.scope '${val}'. Valid values: ${VALID_FALLOW_SCOPES.join(', ')}`);
574
- }
583
+ if (kp === 'code_quality.fallow.scope')
584
+ assertEnumValue(parsedValue, val, VALID_FALLOW_SCOPES, 'code_quality.fallow.scope');
575
585
  const VALID_FALLOW_PROFILES = ['minimal', 'standard', 'strict'];
576
- if (kp === 'code_quality.fallow.profile' && !VALID_FALLOW_PROFILES.includes(String(parsedValue))) {
577
- error(`Invalid code_quality.fallow.profile '${val}'. Valid values: ${VALID_FALLOW_PROFILES.join(', ')}`);
578
- }
586
+ if (kp === 'code_quality.fallow.profile')
587
+ assertEnumValue(parsedValue, val, VALID_FALLOW_PROFILES, 'code_quality.fallow.profile');
579
588
  // plan_review.source_grounding (#22) — boolean only
580
589
  if (kp === 'plan_review.source_grounding') {
581
590
  if (typeof parsedValue !== 'boolean') {
@@ -584,8 +593,42 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
584
593
  }
585
594
  // plan_review.source_grounding_authority (#22) — enum
586
595
  const VALID_SOURCE_GROUNDING_AUTHORITIES = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
587
- if (kp === 'plan_review.source_grounding_authority' && !VALID_SOURCE_GROUNDING_AUTHORITIES.includes(String(parsedValue))) {
588
- error(`Invalid plan_review.source_grounding_authority '${val}'. Valid values: ${VALID_SOURCE_GROUNDING_AUTHORITIES.join(', ')}`);
596
+ if (kp === 'plan_review.source_grounding_authority')
597
+ assertEnumValue(parsedValue, val, VALID_SOURCE_GROUNDING_AUTHORITIES, 'plan_review.source_grounding_authority');
598
+ // Generic capability-registry validation (#1628). Capability-owned keys declare
599
+ // their type/values in the registry but most lack a hardcoded guard, so out-of-
600
+ // domain values (including JSON array/object coercion) were stored silently.
601
+ const capDef = getCapabilityConfigSchema(cwd)[kp];
602
+ if (capDef && typeof capDef.type === 'string') {
603
+ switch (capDef.type) {
604
+ case 'enum':
605
+ if (Array.isArray(capDef.values)) {
606
+ assertEnumValue(parsedValue, val, capDef.values.map((v) => String(v)), kp);
607
+ }
608
+ break;
609
+ case 'boolean':
610
+ if (typeof parsedValue !== 'boolean') {
611
+ error(`Invalid ${kp} '${val}'. Must be a boolean (true or false).`);
612
+ }
613
+ break;
614
+ case 'number':
615
+ if (typeof parsedValue !== 'number' || !Number.isFinite(parsedValue)) {
616
+ error(`Invalid ${kp} '${val}'. Must be a number.`);
617
+ }
618
+ break;
619
+ case 'string':
620
+ if (typeof parsedValue !== 'string') {
621
+ error(`Invalid ${kp} '${val}'. Must be a string.`);
622
+ }
623
+ break;
624
+ }
625
+ }
626
+ // Security — ASVS level range (#1628)
627
+ // Must be an integer in {1, 2, 3} (OWASP ASVS levels).
628
+ if (kp === 'workflow.security_asvs_level') {
629
+ if (typeof parsedValue !== 'number' || !Number.isInteger(parsedValue) || parsedValue < 1 || parsedValue > 3) {
630
+ error(`Invalid workflow.security_asvs_level '${val}'. Must be an integer 1, 2, or 3.`);
631
+ }
589
632
  }
590
633
  if (kp === 'review.default_reviewers') {
591
634
  const normalized = (0, review_reviewer_selection_cjs_1.normalizeConfiguredDefaultReviewers)(parsedValue);