opencode-swarm 7.114.9 → 7.116.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.
Files changed (139) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/{config-doctor-jy3mdh9t.js → config-doctor-htzxe394.js} +2 -2
  3. package/dist/cli/{curator-5xt89z05.js → curator-7s5yw6hw.js} +18 -17
  4. package/dist/cli/{curator-llm-factory-pnhvf3s3.js → curator-llm-factory-x96dr0zf.js} +18 -17
  5. package/dist/cli/{dispatch-mczsxy5s.js → dispatch-07jgc3hn.js} +1 -1
  6. package/dist/cli/{evidence-summary-service-fehwj116.js → evidence-summary-service-ef6kgtev.js} +5 -5
  7. package/dist/cli/{gate-evidence-vkpqt300.js → gate-evidence-thqt0beq.js} +4 -4
  8. package/dist/cli/{guardrail-explain-7aztgtyh.js → guardrail-explain-kwqfkwch.js} +19 -18
  9. package/dist/cli/{guardrail-log-84wnx273.js → guardrail-log-dzbqcgz9.js} +3 -3
  10. package/dist/cli/{hive-promoter-q899q2m6.js → hive-promoter-rkkx40qv.js} +18 -17
  11. package/dist/cli/{index-54sevh81.js → index-3b8n3dbd.js} +30 -22
  12. package/dist/cli/{index-8f2270hc.js → index-3yh67t1h.js} +3 -3
  13. package/dist/cli/{index-4sckv7y6.js → index-42cx6g2h.js} +9 -4
  14. package/dist/cli/index-4905hd2m.js +19 -0
  15. package/dist/cli/{index-4h2ea8h5.js → index-81vzywh3.js} +2 -2
  16. package/dist/cli/{index-6f6y2rbp.js → index-8bvh0qfa.js} +3 -3
  17. package/dist/cli/{index-yfedche9.js → index-8j5d3ytd.js} +1 -1
  18. package/dist/cli/{index-80jkseqw.js → index-a6f8b0vn.js} +1 -1
  19. package/dist/cli/{index-0p660a87.js → index-bbsaxnj5.js} +1 -1
  20. package/dist/cli/{index-yrrqs4b0.js → index-cnrhj56z.js} +1160 -47
  21. package/dist/cli/{index-n18yy0z1.js → index-jyetmzcn.js} +3 -3
  22. package/dist/cli/{index-rk6qhyng.js → index-k5ey429j.js} +4 -4
  23. package/dist/cli/{index-7jmrbp68.js → index-m43zgtjn.js} +1 -1
  24. package/dist/cli/{index-9bsmzfk3.js → index-m7hc7nn7.js} +5 -1
  25. package/dist/cli/{index-typ408f4.js → index-me46ffm2.js} +6811 -1947
  26. package/dist/cli/{index-ga4ta3cr.js → index-nprawcc0.js} +7 -1
  27. package/dist/cli/{index-a45jq4b7.js → index-nvc0nsvg.js} +6 -1
  28. package/dist/cli/{index-pa6t33pt.js → index-q2rrv0cy.js} +11 -0
  29. package/dist/cli/{index-g40r5d08.js → index-qgeeyy75.js} +1 -1
  30. package/dist/cli/{index-x12hvfpf.js → index-s385axt1.js} +3 -3
  31. package/dist/cli/{index-m9m7p49w.js → index-tvgvb5pa.js} +2 -2
  32. package/dist/cli/index.js +18 -17
  33. package/dist/cli/{knowledge-escalator-5gwp1ar8.js → knowledge-escalator-0bq62d34.js} +7 -7
  34. package/dist/cli/{knowledge-events-m304swh6.js → knowledge-events-cwwky393.js} +5 -5
  35. package/dist/cli/{knowledge-store-9babt8rd.js → knowledge-store-q8x4h82m.js} +5 -5
  36. package/dist/cli/{knowledge-validator-t9sym59g.js → knowledge-validator-zr0djm02.js} +6 -6
  37. package/dist/cli/{pending-delegations-ddbkbgxy.js → pending-delegations-rr9x47rd.js} +2 -2
  38. package/dist/cli/{pr-subscriptions-vwv7zm2p.js → pr-subscriptions-cny2jz2d.js} +3 -3
  39. package/dist/cli/runtime-ykafcwc6.js +3397 -0
  40. package/dist/cli/{schema-aymfdrsb.js → schema-bqn7g3ez.js} +3 -1
  41. package/dist/cli/{skill-generator-tav44xpm.js → skill-generator-mz267btk.js} +8 -8
  42. package/dist/cli/{telemetry-jny76r83.js → telemetry-qh78ymq4.js} +1 -1
  43. package/dist/commands/archive.d.ts +4 -4
  44. package/dist/commands/command-dispatch.d.ts +2 -0
  45. package/dist/commands/gate-audit.d.ts +28 -0
  46. package/dist/commands/gate-stats.d.ts +1 -0
  47. package/dist/commands/index.d.ts +4 -0
  48. package/dist/commands/registry.d.ts +19 -2
  49. package/dist/config/index.d.ts +2 -2
  50. package/dist/config/plan-schema.d.ts +7 -7
  51. package/dist/config/schema.d.ts +9 -0
  52. package/dist/evaluation/contracts.d.ts +545 -0
  53. package/dist/evaluation/disposable-worktree.d.ts +38 -0
  54. package/dist/evaluation/fixtures.d.ts +8 -0
  55. package/dist/evaluation/gate-audit.d.ts +32 -0
  56. package/dist/evaluation/gate-ground-truth.d.ts +43 -0
  57. package/dist/evaluation/gate-stats.d.ts +47 -0
  58. package/dist/evaluation/hashing.d.ts +34 -0
  59. package/dist/evaluation/index.d.ts +17 -0
  60. package/dist/evaluation/model-dispatcher.d.ts +30 -0
  61. package/dist/evaluation/public-api.d.ts +87 -0
  62. package/dist/evaluation/retention.d.ts +56 -0
  63. package/dist/evaluation/runner.d.ts +48 -0
  64. package/dist/evaluation/statistics.d.ts +9 -0
  65. package/dist/evaluation/store.d.ts +26 -0
  66. package/dist/evidence/manager.d.ts +18 -0
  67. package/dist/index.d.ts +4 -0
  68. package/dist/index.js +497 -450
  69. package/dist/mutation/engine.d.ts +29 -5
  70. package/dist/telemetry.d.ts +11 -1
  71. package/dist/tools/apply-patch.d.ts +21 -0
  72. package/dist/tools/plugin-registration.d.ts +2 -1
  73. package/dist/tools/sast-scan.d.ts +9 -0
  74. package/dist/tools/swarm-command.d.ts +2 -1
  75. package/dist/tools/update-task-status.d.ts +16 -4
  76. package/dist/utils/external-tool-runner.d.ts +2 -1
  77. package/dist/utils/fuzzy-match.d.ts +115 -0
  78. package/dist/utils/sequence-matcher.d.ts +83 -0
  79. package/evaluation-fixtures/tier1/boundary-error/environment/baseline.ts +3 -0
  80. package/evaluation-fixtures/tier1/boundary-error/environment/defect.test.ts +3 -0
  81. package/evaluation-fixtures/tier1/boundary-error/environment/defect.ts +3 -0
  82. package/evaluation-fixtures/tier1/boundary-error/instruction.md +1 -0
  83. package/evaluation-fixtures/tier1/boundary-error/manifest.json +1 -0
  84. package/evaluation-fixtures/tier1/branch-swap/environment/baseline.ts +3 -0
  85. package/evaluation-fixtures/tier1/branch-swap/environment/defect.test.ts +3 -0
  86. package/evaluation-fixtures/tier1/branch-swap/environment/defect.ts +3 -0
  87. package/evaluation-fixtures/tier1/branch-swap/instruction.md +1 -0
  88. package/evaluation-fixtures/tier1/branch-swap/manifest.json +1 -0
  89. package/evaluation-fixtures/tier1/curated-off-by-one/environment/baseline.ts +3 -0
  90. package/evaluation-fixtures/tier1/curated-off-by-one/environment/defect.test.ts +3 -0
  91. package/evaluation-fixtures/tier1/curated-off-by-one/environment/defect.ts +3 -0
  92. package/evaluation-fixtures/tier1/curated-off-by-one/instruction.md +1 -0
  93. package/evaluation-fixtures/tier1/curated-off-by-one/manifest.json +1 -0
  94. package/evaluation-fixtures/tier1/guard-removal/environment/baseline.ts +3 -0
  95. package/evaluation-fixtures/tier1/guard-removal/environment/defect.test.ts +3 -0
  96. package/evaluation-fixtures/tier1/guard-removal/environment/defect.ts +3 -0
  97. package/evaluation-fixtures/tier1/guard-removal/instruction.md +1 -0
  98. package/evaluation-fixtures/tier1/guard-removal/manifest.json +1 -0
  99. package/evaluation-fixtures/tier1/injection-prone-string/environment/baseline.ts +3 -0
  100. package/evaluation-fixtures/tier1/injection-prone-string/environment/defect.test.ts +3 -0
  101. package/evaluation-fixtures/tier1/injection-prone-string/environment/defect.ts +3 -0
  102. package/evaluation-fixtures/tier1/injection-prone-string/instruction.md +1 -0
  103. package/evaluation-fixtures/tier1/injection-prone-string/manifest.json +1 -0
  104. package/evaluation-fixtures/tier1/missing-auth-check/environment/baseline.ts +4 -0
  105. package/evaluation-fixtures/tier1/missing-auth-check/environment/defect.test.ts +3 -0
  106. package/evaluation-fixtures/tier1/missing-auth-check/environment/defect.ts +3 -0
  107. package/evaluation-fixtures/tier1/missing-auth-check/instruction.md +1 -0
  108. package/evaluation-fixtures/tier1/missing-auth-check/manifest.json +1 -0
  109. package/evaluation-fixtures/tier1/missing-await/environment/baseline.ts +7 -0
  110. package/evaluation-fixtures/tier1/missing-await/environment/defect.test.ts +3 -0
  111. package/evaluation-fixtures/tier1/missing-await/environment/defect.ts +4 -0
  112. package/evaluation-fixtures/tier1/missing-await/instruction.md +1 -0
  113. package/evaluation-fixtures/tier1/missing-await/manifest.json +1 -0
  114. package/evaluation-fixtures/tier1/mutation-off-by-one/environment/baseline.ts +3 -0
  115. package/evaluation-fixtures/tier1/mutation-off-by-one/environment/defect.test.ts +3 -0
  116. package/evaluation-fixtures/tier1/mutation-off-by-one/environment/defect.ts +3 -0
  117. package/evaluation-fixtures/tier1/mutation-off-by-one/instruction.md +1 -0
  118. package/evaluation-fixtures/tier1/mutation-off-by-one/manifest.json +1 -0
  119. package/evaluation-fixtures/tier1/null-substitution/environment/baseline.ts +3 -0
  120. package/evaluation-fixtures/tier1/null-substitution/environment/defect.test.ts +3 -0
  121. package/evaluation-fixtures/tier1/null-substitution/environment/defect.ts +3 -0
  122. package/evaluation-fixtures/tier1/null-substitution/instruction.md +1 -0
  123. package/evaluation-fixtures/tier1/null-substitution/manifest.json +1 -0
  124. package/evaluation-fixtures/tier1/operator-swap/environment/baseline.ts +3 -0
  125. package/evaluation-fixtures/tier1/operator-swap/environment/defect.test.ts +3 -0
  126. package/evaluation-fixtures/tier1/operator-swap/environment/defect.ts +3 -0
  127. package/evaluation-fixtures/tier1/operator-swap/instruction.md +1 -0
  128. package/evaluation-fixtures/tier1/operator-swap/manifest.json +1 -0
  129. package/evaluation-fixtures/tier1/side-effect-deletion/environment/baseline.ts +4 -0
  130. package/evaluation-fixtures/tier1/side-effect-deletion/environment/defect.test.ts +3 -0
  131. package/evaluation-fixtures/tier1/side-effect-deletion/environment/defect.ts +3 -0
  132. package/evaluation-fixtures/tier1/side-effect-deletion/instruction.md +1 -0
  133. package/evaluation-fixtures/tier1/side-effect-deletion/manifest.json +1 -0
  134. package/evaluation-fixtures/tier1/swallowed-error/environment/baseline.ts +3 -0
  135. package/evaluation-fixtures/tier1/swallowed-error/environment/defect.test.ts +3 -0
  136. package/evaluation-fixtures/tier1/swallowed-error/environment/defect.ts +3 -0
  137. package/evaluation-fixtures/tier1/swallowed-error/instruction.md +1 -0
  138. package/evaluation-fixtures/tier1/swallowed-error/manifest.json +1 -0
  139. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- type SpawnSyncFn = typeof spawnSync;
3
- export type MutationOutcome = 'killed' | 'survived' | 'timeout' | 'error' | 'equivalent' | 'skipped';
2
+ import { resolveExecutableFromPath, runExternalTool } from '../utils/external-tool-runner.js';
3
+ export type MutationOutcome = 'killed' | 'survived' | 'timeout' | 'cancelled' | 'error' | 'equivalent' | 'skipped';
4
4
  /**
5
5
  * Known test runner executables permitted as the first element of testCommand.
6
6
  * Validated as the basename (without extension) to support platform-specific
@@ -35,6 +35,7 @@ export interface MutationReport {
35
35
  killed: number;
36
36
  survived: number;
37
37
  timeout: number;
38
+ cancelled: number;
38
39
  equivalent: number;
39
40
  skipped: number;
40
41
  errors: number;
@@ -55,13 +56,36 @@ export interface MutationReport {
55
56
  timestamp: string;
56
57
  }
57
58
  export declare const MAX_MUTATIONS_PER_FUNCTION = 10;
59
+ type LegacySpawnSyncFn = typeof spawnSync;
60
+ export type MutationCommandResult = {
61
+ status: 'completed' | 'timeout' | 'cancelled' | 'spawn-error';
62
+ exitCode: number | null;
63
+ stdout: string;
64
+ stderr: string;
65
+ message?: string;
66
+ };
67
+ export type MutationCommandRunner = (args: {
68
+ executable: string;
69
+ args: string[];
70
+ cwd: string;
71
+ timeoutMs: number;
72
+ abortSignal?: AbortSignal;
73
+ }) => Promise<MutationCommandResult>;
74
+ export type MutationExecutionOptions = {
75
+ abortSignal?: AbortSignal;
76
+ runner?: MutationCommandRunner;
77
+ };
78
+ export declare const runMutationCommand: MutationCommandRunner;
58
79
  export declare const _internals: {
59
80
  executeMutation: typeof executeMutation;
60
81
  computeReport: typeof computeReport;
61
82
  executeMutationSuite: typeof executeMutationSuite;
62
- spawnSync: SpawnSyncFn;
83
+ spawnSync: LegacySpawnSyncFn;
84
+ runCommand: MutationCommandRunner;
85
+ runExternalTool: typeof runExternalTool;
86
+ resolveExecutableFromPath: typeof resolveExecutableFromPath;
63
87
  };
64
- export declare function executeMutation(patch: MutationPatch, testCommand: string[], testFiles: string[], workingDir: string): Promise<MutationResult>;
88
+ export declare function executeMutation(patch: MutationPatch, testCommand: string[], testFiles: string[], workingDir: string, options?: MutationExecutionOptions): Promise<MutationResult>;
65
89
  export declare function computeReport(results: MutationResult[], durationMs: number, budgetMs?: number): MutationReport;
66
- export declare function executeMutationSuite(patches: MutationPatch[], testCommand: string[], testFiles: string[], workingDir: string, budgetMs?: number, onProgress?: (completed: number, total: number, result: MutationResult) => void, sourceFiles?: Map<string, string>): Promise<MutationReport>;
90
+ export declare function executeMutationSuite(patches: MutationPatch[], testCommand: string[], testFiles: string[], workingDir: string, budgetMs?: number, onProgress?: (completed: number, total: number, result: MutationResult) => void, sourceFiles?: Map<string, string>, options?: MutationExecutionOptions): Promise<MutationReport>;
67
91
  export {};
@@ -1,5 +1,14 @@
1
1
  import type { DelegationCostFields } from './services/cost-accounting.js';
2
- export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'gate_parse_error' | 'phase_changed' | 'budget_updated' | 'model_fallback' | 'hard_limit_hit' | 'revision_limit_hit' | 'loop_detected' | 'scope_violation' | 'qa_skip_violation' | 'heartbeat' | 'turbo_mode_changed' | 'auto_oversight_escalation' | 'environment_detected' | 'evidence_lock_acquired' | 'evidence_lock_contended' | 'evidence_lock_stale_recovered' | 'plan_ledger_cas_retry' | 'plan_md_write_failed' | 'snapshot_failed' | 'prm_pattern_detected' | 'prm_course_correction_injected' | 'prm_escalation_triggered' | 'prm_hard_stop';
2
+ export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'gate_parse_error' | 'reviewer_gate_decision' | 'phase_changed' | 'budget_updated' | 'model_fallback' | 'hard_limit_hit' | 'revision_limit_hit' | 'loop_detected' | 'scope_violation' | 'qa_skip_violation' | 'heartbeat' | 'turbo_mode_changed' | 'auto_oversight_escalation' | 'environment_detected' | 'evidence_lock_acquired' | 'evidence_lock_contended' | 'evidence_lock_stale_recovered' | 'plan_ledger_cas_retry' | 'plan_md_write_failed' | 'snapshot_failed' | 'prm_pattern_detected' | 'prm_course_correction_injected' | 'prm_escalation_triggered' | 'prm_hard_stop';
3
+ /** Stable classification for how a reviewer-gate decision was established. */
4
+ export type ReviewerGateEvidenceKind = 'genuine' | 'fallback' | 'data_quality' | 'block';
5
+ /**
6
+ * Stable reason codes for reviewer-gate decision telemetry.
7
+ *
8
+ * These values are persisted in telemetry.jsonl and consumed by offline gate
9
+ * analytics, so additions are allowed but existing values must not be renamed.
10
+ */
11
+ export type ReviewerGateReasonCode = 'lean_turbo_completed_lane' | 'standard_turbo_non_tier3' | 'durable_evidence_complete' | 'workflow_state_complete' | 'stage_b_parallel_complete' | 'no_active_sessions' | 'zero_valid_sessions' | 'restart_recovery_complete' | 'scoped_delegation_complete' | 'unscoped_delegation_complete' | 'corrupt_evidence' | 'required_gates_missing' | 'inspection_error';
3
12
  export type TelemetryListener = (event: TelemetryEvent, data: Record<string, unknown>) => void;
4
13
  /**
5
14
  * Number of emits between rotation checks. 50 keeps the overhead at one
@@ -48,6 +57,7 @@ export declare const telemetry: {
48
57
  gatePassed(sessionId: string, gate: string, taskId: string): void;
49
58
  gateParseError(taskId: string, error: Error): void;
50
59
  gateFailed(sessionId: string, gate: string, taskId: string, reason: string): void;
60
+ reviewerGateDecision(sessionId: string, taskId: string, blocked: boolean, reasonCode: ReviewerGateReasonCode, evidenceKind: ReviewerGateEvidenceKind): void;
51
61
  phaseChanged(sessionId: string, oldPhase: number, newPhase: number): void;
52
62
  budgetUpdated(sessionId: string, budgetPct: number, agentName: string): void;
53
63
  modelFallback(sessionId: string, agentName: string, fromModel: string, toModel: string, reason: string): void;
@@ -7,6 +7,17 @@
7
7
  * Pure TypeScript, no shell/git/external binaries, standard node:fs sync I/O.
8
8
  */
9
9
  import type { ToolDefinition } from '@opencode-ai/plugin/tool';
10
+ import { loadPluginConfigWithMeta } from '../config';
11
+ /**
12
+ * DI seam for hermetic config-load substitution in tests (AGENTS.md invariant 7).
13
+ * Mirrors the pattern at `src/tools/context-status.ts:81-88`. Tests override
14
+ * `_internals.loadPluginConfigWithMeta` and restore it in `afterEach` instead
15
+ * of writing real config files or using `mock.module` (which leaks across
16
+ * test files in Bun's shared runner).
17
+ */
18
+ export declare const _internals: {
19
+ loadPluginConfigWithMeta: typeof loadPluginConfigWithMeta;
20
+ };
10
21
  /** Per-file error detail in the structured output. */
11
22
  export interface ApplyPatchFileError {
12
23
  hunkIndex: number;
@@ -45,6 +56,16 @@ export interface ApplyPatchArgs {
45
56
  allowCreates?: boolean;
46
57
  allowDeletes?: boolean;
47
58
  }
59
+ /**
60
+ * Opt-in fuzzy-matching options threaded from `execute` → `processFileDiff`
61
+ * → `applyHunks`. Both flags default false (exact-match-only, B3 decision).
62
+ */
63
+ export interface ApplyHunksOptions {
64
+ /** Enable fuzzy fallback (strategies 1-8) on exact-match failure. */
65
+ fuzzyMatch: boolean;
66
+ /** Additionally enable strategy 9 (context_aware). Only effective with fuzzyMatch. */
67
+ fuzzyMatchContextAware: boolean;
68
+ }
48
69
  /**
49
70
  * Swarm unified-diff patch tool (formerly registered as apply_patch).
50
71
  * Renamed to swarm_apply_patch so it no longer shadows the native opencode
@@ -11,6 +11,7 @@
11
11
  import type { ToolDefinition } from '@opencode-ai/plugin/tool';
12
12
  import type { AgentDefinition } from '../agents/index.js';
13
13
  import type { PluginConfig } from '../config/index.js';
14
+ import type { EvaluationModelDispatcher } from '../evaluation/model-dispatcher.js';
14
15
  /**
15
16
  * Construct the plugin tool object: one handler per manifest entry, with
16
17
  * `swarm_command` overridden by its dependency-injected instance.
@@ -21,4 +22,4 @@ import type { PluginConfig } from '../config/index.js';
21
22
  *
22
23
  * Knowledge tools are conditionally excluded when config.knowledge.enabled = false.
23
24
  */
24
- export declare function buildPluginToolObject(agents: Record<string, AgentDefinition>, config?: PluginConfig): Record<string, ToolDefinition>;
25
+ export declare function buildPluginToolObject(agents: Record<string, AgentDefinition>, config?: PluginConfig, evaluationModelDispatcher?: EvaluationModelDispatcher): Record<string, ToolDefinition>;
@@ -5,9 +5,16 @@
5
5
  import type { ToolDefinition } from '@opencode-ai/plugin/tool';
6
6
  import type { PluginConfig } from '../config';
7
7
  import type { EvidenceVerdict } from '../config/evidence-schema';
8
+ import { isSemgrepAvailable, runSemgrep } from '../sast/semgrep';
8
9
  export interface SastScanInput {
9
10
  /** List of files to scan */
10
11
  changed_files: string[];
12
+ /**
13
+ * Restrict execution to built-in Tier A rules. This is used by isolated,
14
+ * reproducible evaluation runs where PATH discovery and Semgrep execution
15
+ * (including remote `--config=auto`) are forbidden.
16
+ */
17
+ offline_only?: boolean;
11
18
  /** Minimum severity that causes failure (default: 'medium') */
12
19
  severity_threshold?: 'low' | 'medium' | 'high' | 'critical';
13
20
  /**
@@ -94,4 +101,6 @@ export declare const sast_scan: ToolDefinition;
94
101
  export declare const _internals: {
95
102
  sastScan: typeof sastScan;
96
103
  sast_scan: typeof sast_scan;
104
+ isSemgrepAvailable: typeof isSemgrepAvailable;
105
+ runSemgrep: typeof runSemgrep;
97
106
  };
@@ -1,4 +1,5 @@
1
1
  import type { AgentDefinition } from '../agents/index.js';
2
+ import type { EvaluationModelDispatcher } from '../evaluation/model-dispatcher.js';
2
3
  import { createSwarmTool } from './create-tool.js';
3
- export declare function createSwarmCommandTool(agents: Record<string, AgentDefinition>): ReturnType<typeof createSwarmTool>;
4
+ export declare function createSwarmCommandTool(agents: Record<string, AgentDefinition>, evaluationModelDispatcher?: EvaluationModelDispatcher): ReturnType<typeof createSwarmTool>;
4
5
  export declare const swarm_command: ReturnType<typeof createSwarmTool>;
@@ -3,8 +3,12 @@
3
3
  * Allows agents to mark tasks as pending, in_progress, completed, or blocked.
4
4
  */
5
5
  import type { ToolContext, ToolDefinition } from '@opencode-ai/plugin/tool';
6
+ import { readTaskEvidenceRaw } from '../gate-evidence.js';
6
7
  import { tryAcquireLock } from '../parallel/file-locks.js';
7
8
  import { loadPlan, updateTaskStatus } from '../plan/manager';
9
+ import { hasActiveLeanTurbo, hasActiveTurboMode } from '../state';
10
+ import { type ReviewerGateEvidenceKind, type ReviewerGateReasonCode } from '../telemetry.js';
11
+ import { verifyLeanTurboTaskCompletion } from '../turbo/lean/task-completion';
8
12
  import { resolveWorkingDirectory } from './resolve-working-directory';
9
13
  /**
10
14
  * Internal seams for test injection.
@@ -12,10 +16,16 @@ import { resolveWorkingDirectory } from './resolve-working-directory';
12
16
  * module-scope vi.mock, which leaks across files in Bun's shared test runner.
13
17
  */
14
18
  export declare const _internals: {
15
- readonly tryAcquireLock: typeof tryAcquireLock;
16
- readonly updateTaskStatus: typeof updateTaskStatus;
17
- readonly resolveWorkingDirectory: typeof resolveWorkingDirectory;
18
- readonly loadPlan: typeof loadPlan;
19
+ tryAcquireLock: typeof tryAcquireLock;
20
+ updateTaskStatus: typeof updateTaskStatus;
21
+ resolveWorkingDirectory: typeof resolveWorkingDirectory;
22
+ loadPlan: typeof loadPlan;
23
+ readTaskEvidenceRaw: typeof readTaskEvidenceRaw;
24
+ hasActiveLeanTurbo: typeof hasActiveLeanTurbo;
25
+ hasActiveTurboMode: typeof hasActiveTurboMode;
26
+ verifyLeanTurboTaskCompletion: typeof verifyLeanTurboTaskCompletion;
27
+ hasPassedDurableGateEvidence: typeof hasPassedDurableGateEvidence;
28
+ emitReviewerGateDecision: (sessionId: string, taskId: string, blocked: boolean, reasonCode: ReviewerGateReasonCode, evidenceKind: ReviewerGateEvidenceKind) => void;
19
29
  };
20
30
  /**
21
31
  * Arguments for the update_task_status tool
@@ -58,6 +68,7 @@ export interface ReviewerGateResult {
58
68
  blocked: boolean;
59
69
  reason: string;
60
70
  }
71
+ declare function hasPassedDurableGateEvidence(workingDirectory: string, taskId: string): boolean;
61
72
  /**
62
73
  * Check if a task has passed required QA gates using the state machine.
63
74
  * Requires the task to be in 'tests_run' or 'complete' state, which means
@@ -138,3 +149,4 @@ export declare function executeUpdateTaskStatus(args: UpdateTaskStatusArgs, fall
138
149
  * Tool definition for update_task_status
139
150
  */
140
151
  export declare const update_task_status: ToolDefinition;
152
+ export {};
@@ -7,9 +7,10 @@ export interface ExternalToolRunOptions {
7
7
  maxStdoutBytes: number;
8
8
  maxStderrBytes: number;
9
9
  env?: Record<string, string | undefined>;
10
+ abortSignal?: AbortSignal;
10
11
  }
11
12
  export interface ExternalToolRunResult {
12
- status: 'completed' | 'timeout' | 'spawn-error';
13
+ status: 'completed' | 'timeout' | 'cancelled' | 'spawn-error';
13
14
  exitCode: number | null;
14
15
  stdout: string;
15
16
  stderr: string;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Fuzzy text-matching engine — faithful TypeScript port of hermes-agent's
3
+ * `tools/fuzzy_match.py` (issue #1718).
4
+ *
5
+ * Implements a 9-strategy matching chain that finds and replaces text,
6
+ * tolerating the whitespace, indentation, escape-sequence, and Unicode
7
+ * drift common in LLM-generated patches. Integrated into `apply-patch`
8
+ * as an **opt-in fallback** (default off) gated by the `apply_patch.fuzzy_match`
9
+ * and `apply_patch.fuzzy_match_context_aware` config flags.
10
+ *
11
+ * Porting conventions:
12
+ * - Operates on **UTF-16 code units** consistently (`s[i]`, `s.length`).
13
+ * Matches `src/utils/sequence-matcher.ts`. Round-trip safe.
14
+ * - Per-line stripping uses JS `String.prototype.trim()`, which strips the
15
+ * Unicode WhiteSpace + LineTerminator set. Coincides with Python
16
+ * `str.strip()` for the ASCII/BMP content in the test suite; minor
17
+ * divergence on exotic whitespace categories is acceptable.
18
+ * - Strategy 9 (`contextAware`) is opt-in via the `includeContextAware`
19
+ * option to `fuzzyFindAndReplace`. It is the loosest, most-false-positive-
20
+ * prone strategy and is separately gated in apply-patch by
21
+ * `apply_patch.fuzzy_match_context_aware`.
22
+ */
23
+ /** A `[start, end)` character span in the content string. */
24
+ type Span = [number, number];
25
+ /** Result of {@link fuzzyFindAndReplace}. */
26
+ export interface FuzzyResult {
27
+ /** Modified content on success; original content on failure. */
28
+ content: string;
29
+ /** Number of replacements made (0 on failure). */
30
+ matchCount: number;
31
+ /** Name of the strategy that matched, or `null` on failure. */
32
+ strategy: string | null;
33
+ /** `null` on success; an error description on failure. */
34
+ error: string | null;
35
+ }
36
+ /** Options for {@link fuzzyFindAndReplace}. */
37
+ export interface FuzzyOptions {
38
+ /**
39
+ * When true (default at the utility level), the `context_aware` strategy
40
+ * (9) is included in the chain. Strategy 9 is the loosest/most-false-
41
+ * positive-prone strategy (requires 50% of lines to reach 0.80 per-line
42
+ * similarity) and has quadratic cost, so it is bounded by an internal
43
+ * cell-count cap.
44
+ *
45
+ * **Default divergence (intentional):** the utility defaults this to
46
+ * `true` so the byte-faithful ported hermes test suite passes unmodified
47
+ * (strategy 9 is a legitimate part of the matching chain and some test
48
+ * cases depend on it). The `apply-patch` integration — the only consumer
49
+ * today — explicitly passes `false` unless the separate
50
+ * `apply_patch.fuzzy_match_context_aware` config flag is set, honoring
51
+ * issue #1718's Non-Goal "do not port strategy 9 as default-on" at the
52
+ * integration layer. New callers should pass `false` unless they
53
+ * specifically want strategy 9.
54
+ */
55
+ includeContextAware?: boolean;
56
+ }
57
+ /**
58
+ * Maps Unicode typographic characters to their ASCII equivalents.
59
+ * Some replacements EXPAND a single code point into multiple ASCII chars
60
+ * (em-dash → "--", ellipsis → "..."); the position-remap helpers handle the
61
+ * resulting offset divergence.
62
+ */
63
+ export declare const UNICODE_MAP: Record<string, string>;
64
+ /** Normalize Unicode typographic characters to ASCII equivalents. */
65
+ export declare function unicodeNormalize(text: string): string;
66
+ /**
67
+ * Find and replace text using a chain of increasingly fuzzy strategies.
68
+ *
69
+ * Strategies are tried in order; the first that yields matches wins. On a
70
+ * unique match (or when `replaceAll` is true), the replacement is applied.
71
+ * On failure, returns the original content with an error description.
72
+ *
73
+ * Guards (ported verbatim from hermes):
74
+ * - Ambiguity: `>1 match && !replaceAll` → fail with a helpful message.
75
+ * - Escape-drift: `\'`/`\"` present in both old+new but absent from the
76
+ * matched file region → block (transport serialization artifact).
77
+ * - Selective unescape: `\t`/`\r` in new_string → real bytes only when the
78
+ * matched file region contains the corresponding control char. `\n` excluded.
79
+ * - Unicode preservation: under strategy 7, unchanged spans keep the file's
80
+ * original Unicode characters rather than the ASCII-normalized equivalents.
81
+ */
82
+ export declare function fuzzyFindAndReplace(content: string, oldString: string, newString: string, replaceAll?: boolean, options?: FuzzyOptions): FuzzyResult;
83
+ /** Strategy 1: exact string match, non-overlapping. */
84
+ export declare function strategyExact(content: string, pattern: string): Span[];
85
+ /** Strategy 2: per-line `.trim()` + block equality. */
86
+ export declare function strategyLineTrimmed(content: string, pattern: string): Span[];
87
+ /** Strategy 3: collapse `[ \t]+` → single space, preserve newlines. */
88
+ export declare function strategyWhitespaceNormalized(content: string, pattern: string): Span[];
89
+ /** Strategy 4: strip all leading whitespace per line (lstrip). */
90
+ export declare function strategyIndentationFlexible(content: string, pattern: string): Span[];
91
+ /** Strategy 5: unescape `\n`/`\t`/`\r` literals → bytes, then exact match. */
92
+ export declare function strategyEscapeNormalized(content: string, pattern: string): Span[];
93
+ /** Strategy 6: trim only first and last lines, sliding window. */
94
+ export declare function strategyTrimmedBoundary(content: string, pattern: string): Span[];
95
+ /** Strategy 7: Unicode normalization (smart quotes, em/en dash, ellipsis, NBSP). */
96
+ export declare function strategyUnicodeNormalized(content: string, pattern: string): Span[];
97
+ /** Strategy 8: anchor on first+last lines, similarity for the middle. */
98
+ export declare function strategyBlockAnchor(content: string, pattern: string): Span[];
99
+ export declare function strategyContextAware(content: string, pattern: string): Span[];
100
+ /**
101
+ * Find lines in `content` most similar to `oldString` for "did you mean?" feedback.
102
+ *
103
+ * Returns a formatted string showing the closest matching lines with context
104
+ * and line numbers, or `''` if no useful match is found.
105
+ */
106
+ export declare function findClosestLines(oldString: string, content: string, contextLines?: number, maxResults?: number): string;
107
+ /**
108
+ * Return a "Did you mean..." snippet for plain no-match errors.
109
+ *
110
+ * Gated so the hint only fires for actual "old_string not found" failures.
111
+ * Ambiguous-match, escape-drift, and identical-strings errors all have
112
+ * `matchCount === 0` but a did-you-mean snippet would be misleading.
113
+ */
114
+ export declare function formatNoMatchHint(error: string | null, matchCount: number, oldString: string, content: string): string;
115
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Faithful TypeScript port of CPython's `difflib.SequenceMatcher`.
3
+ *
4
+ * Implements the Ratcliff/Obershelp pattern-matching algorithm with the
5
+ * "popular element" (autojunk) guard, so that `.ratio()` produces values
6
+ * identical to CPython 3.x for the BMP character content used by the
7
+ * fuzzy-match strategies. The thresholds in `src/utils/fuzzy-match.ts`
8
+ * (0.50 / 0.70 / 0.80) were tuned against CPython's exact `2*M/T` ratio
9
+ * formula, so substituting a Levenshtein-based library would silently
10
+ * break those thresholds.
11
+ *
12
+ * Porting notes:
13
+ * - Operates on **UTF-16 code units** (`s[i]`, `s.length`, `s.charCodeAt(i)`)
14
+ * consistently throughout. CPython operates on Unicode code points; for
15
+ * ASCII/BMP content the two coincide. Astral-plane characters (emoji)
16
+ * consume two UTF-16 units, so ratios for emoji-heavy input may differ
17
+ * marginally from CPython. This is acceptable for the fuzzy-match use
18
+ * case (source-code patches) and is round-trip-safe — no index mixing,
19
+ * no file corruption.
20
+ * - `autojunk` defaults to `true`, matching CPython. When `b.length >= 200`
21
+ * and an element appears more than 1% of the time in `b`, it is treated
22
+ * as "popular" and excluded as an anchor candidate in `find_longest_match`.
23
+ * This is essential for ratio fidelity on real file sections.
24
+ */
25
+ export interface Match {
26
+ /** Start index in sequence `a`. */
27
+ a: number;
28
+ /** Start index in sequence `b`. */
29
+ b: number;
30
+ /** Length of the matching block. */
31
+ size: number;
32
+ }
33
+ export type OpcodeTag = 'equal' | 'replace' | 'delete' | 'insert';
34
+ export interface Opcode {
35
+ tag: OpcodeTag;
36
+ i1: number;
37
+ i2: number;
38
+ j1: number;
39
+ j2: number;
40
+ }
41
+ /** A predicate that marks a character as "junk" (never a sync point). */
42
+ export type IsJunk = ((ch: string) => boolean) | null;
43
+ /**
44
+ * Difflib-compatible sequence matcher. Construct with two strings, then
45
+ * call `.ratio()`, `.get_matching_blocks()`, or `.get_opcodes()`.
46
+ */
47
+ export declare class SequenceMatcher {
48
+ private a;
49
+ private b;
50
+ private isJunk;
51
+ private autoJunk;
52
+ private b2j;
53
+ private popularSet;
54
+ private matchingBlocks;
55
+ constructor(isJunk: IsJunk, a: string, b: string, autoJunk?: boolean);
56
+ /** Allow changing the input sequences (mirrors CPython `set_seqs`/`set_seq2`). */
57
+ setSeqs(a: string, b: string): void;
58
+ /** Precompute the `b` index map and the popular-element set. */
59
+ private chainB;
60
+ /**
61
+ * Find the longest matching block in `a[alo:ahi]` and `b[blo:bhi]`.
62
+ *
63
+ * Returns `{ a, b, size }` where `size` is maximal; ties are broken by
64
+ * smallest `a`, then smallest `b` (CPython canonical tiebreak).
65
+ *
66
+ * A "match" means `a[i] === b[j]` and the run extends as far as possible.
67
+ * Elements in the popular set are only used as anchor candidates when no
68
+ * non-popular match exists (CPython behavior: popular elements are
69
+ * excluded from `b2j`, so they only match opportunistically via the
70
+ * extending run after a non-popular anchor).
71
+ */
72
+ findLongestMatch(alo: number, ahi: number, blo: number, bhi: number): Match;
73
+ /** Recursively compute the list of matching blocks (descending order). */
74
+ getMatchingBlocks(): Match[];
75
+ /**
76
+ * Return a float in [0, 1]: `2*M / T`, where `M` is the total matched
77
+ * characters and `T` is the sum of the two sequence lengths.
78
+ * Returns `1.0` when both sequences are empty (matches CPython).
79
+ */
80
+ ratio(): number;
81
+ /** Compute the opcodes describing how to turn `a` into `b`. */
82
+ getOpcodes(): Opcode[];
83
+ }
@@ -0,0 +1,3 @@
1
+ export function validPort(port: number): boolean {
2
+ return port >= 1 && port <= 65_535;
3
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { validPort } from './defect';
3
+ test('rejects the first invalid port', () => expect(validPort(65_536)).toBe(false));
@@ -0,0 +1,3 @@
1
+ export function validPort(port: number): boolean {
2
+ return port >= 1 && port <= 65_536;
3
+ }
@@ -0,0 +1 @@
1
+ Inspect the upper-bound validation in `environment/defect.ts` and identify the invalid value it permits.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"boundary-error","source":"curated","split":"train","category":"boundary-error","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"ac9cd9f603d86640900fe3af16e7373a322eb5deff009b72bdf4239df13661b8"}
@@ -0,0 +1,3 @@
1
+ export function accessLabel(admin: boolean): string {
2
+ return admin ? 'admin' : 'user';
3
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { accessLabel } from './defect';
3
+ test('labels administrators', () => expect(accessLabel(true)).toBe('admin'));
@@ -0,0 +1,3 @@
1
+ export function accessLabel(admin: boolean): string {
2
+ return admin ? 'user' : 'admin';
3
+ }
@@ -0,0 +1 @@
1
+ Inspect `environment/defect.ts` for swapped conditional branches and report the incorrect results.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"branch-swap","source":"curated","split":"train","category":"branch-swap","protected":false,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"f9b09fb7fa980fd94e1603a0e26e1ef43cd30a1d91211a195933913847943f23"}
@@ -0,0 +1,3 @@
1
+ export function pageStart(page: number, size: number): number {
2
+ return page * size;
3
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { pageStart } from './defect';
3
+ test('first page begins at zero', () => expect(pageStart(0, 20)).toBe(0));
@@ -0,0 +1,3 @@
1
+ export function pageStart(page: number, size: number): number {
2
+ return page * size + 1;
3
+ }
@@ -0,0 +1 @@
1
+ Review the pagination boundary in `environment/defect.ts` and identify the page that is skipped.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"curated-off-by-one","source":"curated","split":"train","category":"curated-off-by-one","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"2894129b530e0624dc1739ee7910839d261728a8655474b0103b8c98a7ea9f81"}
@@ -0,0 +1,3 @@
1
+ export function normalized(value: string | undefined): string {
2
+ return value?.trim().toLowerCase() ?? '';
3
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { normalized } from './defect';
3
+ test('handles missing input', () => expect(normalized(undefined)).toBe(''));
@@ -0,0 +1,3 @@
1
+ export function normalized(value: string | undefined): string {
2
+ return value!.trim().toLowerCase();
3
+ }
@@ -0,0 +1 @@
1
+ Find the removed input guard in `environment/defect.ts` and state which input now fails.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"guard-removal","source":"curated","split":"train","category":"guard-removal","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"7943c549ab0c9ce6503ca8df45a9430ae0a431094b4e358be43da1cfb63a1f7d"}
@@ -0,0 +1,3 @@
1
+ export function shellCommand(filename: string): string {
2
+ return `cat -- ${filename.replace(/[^A-Za-z0-9._-]/g, '')}`;
3
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { shellCommand } from './defect';
3
+ test('does not interpolate shell syntax', () => expect(shellCommand('a; rm -rf x')).not.toContain(';'));
@@ -0,0 +1,3 @@
1
+ export function shellCommand(filename: string): string {
2
+ return `cat ${filename}`;
3
+ }
@@ -0,0 +1 @@
1
+ Find the injection-prone command construction in `environment/defect.ts` and identify the unsafe input boundary.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"injection-prone-string","source":"curated","split":"train","category":"injection-prone-string","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"0f45604dbdd251aaec2af3f82afeec10b2242b16a88d86087994918796a657f4"}
@@ -0,0 +1,4 @@
1
+ export function deleteAccount(actorRole: string, accountId: string): string {
2
+ if (actorRole !== 'admin') throw new Error('forbidden');
3
+ return `deleted:${accountId}`;
4
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { deleteAccount } from './defect';
3
+ test('blocks ordinary users', () => expect(() => deleteAccount('user', '42')).toThrow());
@@ -0,0 +1,3 @@
1
+ export function deleteAccount(_actorRole: string, accountId: string): string {
2
+ return `deleted:${accountId}`;
3
+ }
@@ -0,0 +1 @@
1
+ Review `environment/defect.ts` for a missing authorization check and state who can perform the protected action.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"missing-auth-check","source":"curated","split":"train","category":"missing-auth-check","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"9b6d8ff362df8aad25d3c579b10e0fa202e923a0565cf5d18d4c4d1da75af482"}
@@ -0,0 +1,7 @@
1
+ export async function saveThenRead(
2
+ save: () => Promise<void>,
3
+ read: () => string,
4
+ ): Promise<string> {
5
+ await save();
6
+ return read();
7
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { saveThenRead } from './defect';
3
+ test('waits for save', async () => { let value = ''; expect(await saveThenRead(async () => { await Promise.resolve(); value = 'saved'; }, () => value)).toBe('saved'); });
@@ -0,0 +1,4 @@
1
+ export async function saveThenRead(save: () => Promise<void>, read: () => string): Promise<string> {
2
+ save();
3
+ return read();
4
+ }
@@ -0,0 +1 @@
1
+ Find the missing await in `environment/defect.ts` and explain the ordering failure.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"missing-await","source":"curated","split":"train","category":"missing-await","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"d444592ce7d955882de61601bfe414be54c616f6c441ea735450e6716bc837fd"}
@@ -0,0 +1,3 @@
1
+ export function last<T>(items: T[]): T | undefined {
2
+ return items[items.length - 1];
3
+ }
@@ -0,0 +1,3 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { last } from './defect';
3
+ test('returns the final item', () => expect(last([1, 2])).toBe(2));
@@ -0,0 +1,3 @@
1
+ export function last<T>(items: T[]): T | undefined {
2
+ return items[items.length];
3
+ }
@@ -0,0 +1 @@
1
+ Find the boundary defect in `environment/defect.ts`. Report whether the implementation is safe and explain the exact failing case.
@@ -0,0 +1 @@
1
+ {"v":1,"id":"mutation-off-by-one","source":"curated","split":"train","category":"mutation-off-by-one","protected":true,"instructionPath":"instruction.md","environment":{"kind":"fixture","path":"environment"},"scorer":{"kind":"builtin","argv":["tier1-defect"],"timeoutMs":30000,"scoreRange":[0,1]},"provenance":{"origin":"opencode-swarm#1820","license":"MIT"},"contentHash":"9cec6a89ff87b1b85691313a2430916e3ebf393d532644b3f82faa10fe0203cd"}
@@ -0,0 +1,3 @@
1
+ export function displayName(name: string | null): string {
2
+ return name === null ? '' : name;
3
+ }