harnery 0.22.0 → 0.22.1

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.
@@ -9,7 +9,16 @@
9
9
  */
10
10
  import type { StageSchema } from "./types.js";
11
11
  export declare function validateAgainstSchema(value: unknown, schema: StageSchema, path?: string): string[];
12
- /** Strip accidental markdown code fences, then strict-parse JSON. */
12
+ /**
13
+ * Parse one schema-gated JSON value.
14
+ *
15
+ * Exact JSON and a whole fenced value remain the normal paths. Some harnesses
16
+ * prepend a short narration despite an explicit JSON-only contract. Accept
17
+ * that drift only when the reply contains exactly one unambiguous top-level
18
+ * object or array, the value reaches the end of the reply, and JSON.parse
19
+ * accepts it. Trailing prose, multiple values, and malformed candidates remain
20
+ * failures.
21
+ */
13
22
  export declare function parseStageOutput(text: string): {
14
23
  value?: unknown;
15
24
  error?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CAmG/F;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAUlF"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CAmG/F;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAYlF"}
@@ -100,7 +100,16 @@ export function validateAgainstSchema(value, schema, path = "$") {
100
100
  return [`${path}: unsupported schema type ${String(schema.type)}`];
101
101
  }
102
102
  }
103
- /** Strip accidental markdown code fences, then strict-parse JSON. */
103
+ /**
104
+ * Parse one schema-gated JSON value.
105
+ *
106
+ * Exact JSON and a whole fenced value remain the normal paths. Some harnesses
107
+ * prepend a short narration despite an explicit JSON-only contract. Accept
108
+ * that drift only when the reply contains exactly one unambiguous top-level
109
+ * object or array, the value reaches the end of the reply, and JSON.parse
110
+ * accepts it. Trailing prose, multiple values, and malformed candidates remain
111
+ * failures.
112
+ */
104
113
  export function parseStageOutput(text) {
105
114
  const stripped = text
106
115
  .trim()
@@ -110,9 +119,74 @@ export function parseStageOutput(text) {
110
119
  return { value: JSON.parse(stripped) };
111
120
  }
112
121
  catch (err) {
122
+ const embedded = singleTrailingJsonContainer(stripped);
123
+ if (embedded !== undefined)
124
+ return { value: embedded };
113
125
  return { error: `not valid JSON: ${err.message}` };
114
126
  }
115
127
  }
128
+ function singleTrailingJsonContainer(text) {
129
+ const candidates = [];
130
+ let start = -1;
131
+ let stack = [];
132
+ let inString = false;
133
+ let escaped = false;
134
+ for (let index = 0; index < text.length; index++) {
135
+ const char = text[index];
136
+ if (start === -1) {
137
+ if (char === "{" || char === "[") {
138
+ start = index;
139
+ stack = [char];
140
+ inString = false;
141
+ escaped = false;
142
+ }
143
+ continue;
144
+ }
145
+ if (inString) {
146
+ if (escaped) {
147
+ escaped = false;
148
+ }
149
+ else if (char === "\\") {
150
+ escaped = true;
151
+ }
152
+ else if (char === '"') {
153
+ inString = false;
154
+ }
155
+ continue;
156
+ }
157
+ if (char === '"') {
158
+ inString = true;
159
+ continue;
160
+ }
161
+ if (char === "{" || char === "[") {
162
+ stack.push(char);
163
+ continue;
164
+ }
165
+ if (char !== "}" && char !== "]")
166
+ continue;
167
+ const expectedOpen = char === "}" ? "{" : "[";
168
+ if (stack.at(-1) !== expectedOpen) {
169
+ start = -1;
170
+ stack = [];
171
+ continue;
172
+ }
173
+ stack.pop();
174
+ if (stack.length > 0)
175
+ continue;
176
+ const end = index + 1;
177
+ try {
178
+ candidates.push({ end, value: JSON.parse(text.slice(start, end)) });
179
+ }
180
+ catch {
181
+ // A balanced brace pair in narration is not a JSON candidate.
182
+ }
183
+ start = -1;
184
+ }
185
+ if (candidates.length !== 1 || text.slice(candidates[0].end).trim() !== "") {
186
+ return undefined;
187
+ }
188
+ return candidates[0].value;
189
+ }
116
190
  function short(value) {
117
191
  const s = JSON.stringify(value);
118
192
  return s === undefined ? String(value) : s.length > 60 ? `${s.slice(0, 60)}…` : s;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -111,7 +111,16 @@ export function validateAgainstSchema(value: unknown, schema: StageSchema, path
111
111
  }
112
112
  }
113
113
 
114
- /** Strip accidental markdown code fences, then strict-parse JSON. */
114
+ /**
115
+ * Parse one schema-gated JSON value.
116
+ *
117
+ * Exact JSON and a whole fenced value remain the normal paths. Some harnesses
118
+ * prepend a short narration despite an explicit JSON-only contract. Accept
119
+ * that drift only when the reply contains exactly one unambiguous top-level
120
+ * object or array, the value reaches the end of the reply, and JSON.parse
121
+ * accepts it. Trailing prose, multiple values, and malformed candidates remain
122
+ * failures.
123
+ */
115
124
  export function parseStageOutput(text: string): { value?: unknown; error?: string } {
116
125
  const stripped = text
117
126
  .trim()
@@ -120,10 +129,75 @@ export function parseStageOutput(text: string): { value?: unknown; error?: strin
120
129
  try {
121
130
  return { value: JSON.parse(stripped) };
122
131
  } catch (err) {
132
+ const embedded = singleTrailingJsonContainer(stripped);
133
+ if (embedded !== undefined) return { value: embedded };
123
134
  return { error: `not valid JSON: ${(err as Error).message}` };
124
135
  }
125
136
  }
126
137
 
138
+ function singleTrailingJsonContainer(text: string): unknown | undefined {
139
+ const candidates: Array<{ end: number; value: unknown }> = [];
140
+ let start = -1;
141
+ let stack: string[] = [];
142
+ let inString = false;
143
+ let escaped = false;
144
+
145
+ for (let index = 0; index < text.length; index++) {
146
+ const char = text[index]!;
147
+ if (start === -1) {
148
+ if (char === "{" || char === "[") {
149
+ start = index;
150
+ stack = [char];
151
+ inString = false;
152
+ escaped = false;
153
+ }
154
+ continue;
155
+ }
156
+
157
+ if (inString) {
158
+ if (escaped) {
159
+ escaped = false;
160
+ } else if (char === "\\") {
161
+ escaped = true;
162
+ } else if (char === '"') {
163
+ inString = false;
164
+ }
165
+ continue;
166
+ }
167
+ if (char === '"') {
168
+ inString = true;
169
+ continue;
170
+ }
171
+ if (char === "{" || char === "[") {
172
+ stack.push(char);
173
+ continue;
174
+ }
175
+ if (char !== "}" && char !== "]") continue;
176
+
177
+ const expectedOpen = char === "}" ? "{" : "[";
178
+ if (stack.at(-1) !== expectedOpen) {
179
+ start = -1;
180
+ stack = [];
181
+ continue;
182
+ }
183
+ stack.pop();
184
+ if (stack.length > 0) continue;
185
+
186
+ const end = index + 1;
187
+ try {
188
+ candidates.push({ end, value: JSON.parse(text.slice(start, end)) });
189
+ } catch {
190
+ // A balanced brace pair in narration is not a JSON candidate.
191
+ }
192
+ start = -1;
193
+ }
194
+
195
+ if (candidates.length !== 1 || text.slice(candidates[0]!.end).trim() !== "") {
196
+ return undefined;
197
+ }
198
+ return candidates[0]!.value;
199
+ }
200
+
127
201
  function short(value: unknown): string {
128
202
  const s = JSON.stringify(value);
129
203
  return s === undefined ? String(value) : s.length > 60 ? `${s.slice(0, 60)}…` : s;