@@ -108,9 +108,55 @@ private function decodeStructuredOutput(string $content): mixed
108108 return $ decodedDocuments ;
109109 }
110110
111+ $ decodedStructuredOutput = $ this ->decodeStructuredOutputAfterTextPreamble ($ content );
112+
113+ if (null !== $ decodedStructuredOutput ) {
114+ return $ decodedStructuredOutput ;
115+ }
116+
111117 return $ content ;
112118 }
113119
120+ /**
121+ * Decodes structured output that is preceded by plain-text warnings or banners.
122+ *
123+ * Some tooling emits advisory text before a valid JSON payload even when a
124+ * machine-readable format is requested. When the suffix starting at the
125+ * first valid JSON token is fully decodable, the textual preamble SHALL be
126+ * ignored so parent command envelopes remain parseable.
127+ *
128+ * @param string $content the buffered output contents
129+ *
130+ * @return mixed the decoded JSON payload when a valid structured suffix exists
131+ */
132+ private function decodeStructuredOutputAfterTextPreamble (string $ content ): mixed
133+ {
134+ $ offset = 0 ;
135+
136+ while (null !== ($ offset = $ this ->findNextJsonDocumentOffset ($ content , $ offset ))) {
137+ $ structuredSuffix = trim (substr ($ content , $ offset ));
138+
139+ if ('' === $ structuredSuffix ) {
140+ return null ;
141+ }
142+
143+ try {
144+ return $ this ->normalizeStructuredPayload (json_decode ($ structuredSuffix , true ));
145+ } catch (JsonException ) {
146+ }
147+
148+ $ decodedDocuments = $ this ->decodeJsonDocumentStream ($ structuredSuffix );
149+
150+ if (null !== $ decodedDocuments ) {
151+ return $ decodedDocuments ;
152+ }
153+
154+ ++$ offset ;
155+ }
156+
157+ return null ;
158+ }
159+
114160 /**
115161 * Decodes a stream that contains multiple JSON documents separated by whitespace.
116162 *
@@ -220,6 +266,27 @@ private function consumeJsonDocument(string $content, int &$offset): ?string
220266 return null ;
221267 }
222268
269+ /**
270+ * Finds the offset of the next possible JSON document opening token.
271+ *
272+ * @param string $content the buffered output contents
273+ * @param int $offset the offset from which scanning SHALL start
274+ *
275+ * @return ?int the offset of the next "{" or "[" token
276+ */
277+ private function findNextJsonDocumentOffset (string $ content , int $ offset ): ?int
278+ {
279+ $ length = \strlen ($ content );
280+
281+ for (; $ offset < $ length ; ++$ offset ) {
282+ if ('{ ' === $ content [$ offset ] || '[ ' === $ content [$ offset ]) {
283+ return $ offset ;
284+ }
285+ }
286+
287+ return null ;
288+ }
289+
223290 /**
224291 * Normalizes decoded structured payloads produced by wrapped tooling.
225292 *
0 commit comments