@yagni-app/code-staging 1.1.6-staging.1454.1 → 1.1.7-staging.1457.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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash exit-code semantics — a port of Claude Code's
|
|
3
|
+
* `src/tools/BashTool/commandSemantics.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Many commands use exit codes to convey information other than success or
|
|
6
|
+
* failure: grep returns 1 when no matches are found, test returns 1 when the
|
|
7
|
+
* condition is false — neither is an error. pi's bash tool throws on ANY
|
|
8
|
+
* non-zero exit (`Command exited with code N`), which marks the result as an
|
|
9
|
+
* error for both the model and the renderer. This module classifies those
|
|
10
|
+
* throws so the condensed bash wrapper can demote the benign ones back to
|
|
11
|
+
* normal results (Claude Code's behavior) while real failures stay errors.
|
|
12
|
+
*
|
|
13
|
+
* The classification is a presentation/UX heuristic, NOT a security gate
|
|
14
|
+
* (Claude Code's own splitter carries the same caveat). It fails CLOSED:
|
|
15
|
+
* anything it cannot parse confidently — heredocs, negations, env-var
|
|
16
|
+
* prefixes, `timeout` wrappers — classifies as an error, because a
|
|
17
|
+
* misclassification must err toward red, never green.
|
|
18
|
+
*/
|
|
19
|
+
/** Exit-code interpretation for one command family. */
|
|
20
|
+
export interface ExitSemantics {
|
|
21
|
+
/** Whether this exit code is a real error (vs. a benign signal). */
|
|
22
|
+
isError: boolean;
|
|
23
|
+
/** Human-readable meaning for a non-error exit code, if any. */
|
|
24
|
+
message?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The base command name of the LAST segment (the one that determines the
|
|
28
|
+
* shell's exit status), mirroring Claude Code's
|
|
29
|
+
* `heuristicallyExtractBaseCommand`. Fails closed: null for heredocs and
|
|
30
|
+
* anything unparseable, so the caller applies default semantics.
|
|
31
|
+
*/
|
|
32
|
+
export declare function lastSegmentBaseCommand(command: string): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Interpret a command + exit code (Claude Code's `interpretCommandResult`).
|
|
35
|
+
* Unknown, unparsed, or empty commands use the default semantic (fail closed).
|
|
36
|
+
*/
|
|
37
|
+
export declare function interpretCommandResult(command: string, exitCode: number): ExitSemantics;
|
|
38
|
+
/** The exit-status line pi appends to thrown bash errors (its `appendStatus`). */
|
|
39
|
+
export declare const EXIT_STATUS_LINE_RE: RegExp;
|
|
40
|
+
/** A thrown message parsed as an exit-status error (pi's bash throw format). */
|
|
41
|
+
export interface ExitStatusError {
|
|
42
|
+
/** The command output above the status line. */
|
|
43
|
+
output: string;
|
|
44
|
+
/** The parsed exit code. */
|
|
45
|
+
exitCode: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse a thrown error message as pi's `Command exited with code N` throw
|
|
49
|
+
* (output + `\n\n` + status line, or the bare status line when output was
|
|
50
|
+
* empty). Returns undefined for every other throw shape (`Command aborted`,
|
|
51
|
+
* `Command timed out after N seconds`, arbitrary errors) — callers must
|
|
52
|
+
* rethrow those untouched.
|
|
53
|
+
*/
|
|
54
|
+
export declare function splitExitStatusError(message: string): ExitStatusError | undefined;
|
|
55
|
+
//# sourceMappingURL=bashSemantics.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash exit-code semantics — a port of Claude Code's
|
|
3
|
+
* `src/tools/BashTool/commandSemantics.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Many commands use exit codes to convey information other than success or
|
|
6
|
+
* failure: grep returns 1 when no matches are found, test returns 1 when the
|
|
7
|
+
* condition is false — neither is an error. pi's bash tool throws on ANY
|
|
8
|
+
* non-zero exit (`Command exited with code N`), which marks the result as an
|
|
9
|
+
* error for both the model and the renderer. This module classifies those
|
|
10
|
+
* throws so the condensed bash wrapper can demote the benign ones back to
|
|
11
|
+
* normal results (Claude Code's behavior) while real failures stay errors.
|
|
12
|
+
*
|
|
13
|
+
* The classification is a presentation/UX heuristic, NOT a security gate
|
|
14
|
+
* (Claude Code's own splitter carries the same caveat). It fails CLOSED:
|
|
15
|
+
* anything it cannot parse confidently — heredocs, negations, env-var
|
|
16
|
+
* prefixes, `timeout` wrappers — classifies as an error, because a
|
|
17
|
+
* misclassification must err toward red, never green.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Per-command semantics, mirroring Claude Code's COMMAND_SEMANTICS table.
|
|
21
|
+
* POSIX exit-code conventions:
|
|
22
|
+
* grep/rg: 0 = matches, 1 = no matches, >=2 = error
|
|
23
|
+
* find: 0 = success, 1 = partial (inaccessible dirs), >=2 = error
|
|
24
|
+
* diff: 0 = same, 1 = differ, >=2 = error
|
|
25
|
+
* test/[: 0 = true, 1 = false, >=2 = error
|
|
26
|
+
*/
|
|
27
|
+
const COMMAND_SEMANTICS = new Map([
|
|
28
|
+
[
|
|
29
|
+
"grep",
|
|
30
|
+
(code) => ({
|
|
31
|
+
isError: code >= 2,
|
|
32
|
+
message: code === 1 ? "No matches found" : undefined,
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"rg",
|
|
37
|
+
(code) => ({
|
|
38
|
+
isError: code >= 2,
|
|
39
|
+
message: code === 1 ? "No matches found" : undefined,
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"find",
|
|
44
|
+
(code) => ({
|
|
45
|
+
isError: code >= 2,
|
|
46
|
+
message: code === 1 ? "Some directories were inaccessible" : undefined,
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
"diff",
|
|
51
|
+
(code) => ({
|
|
52
|
+
isError: code >= 2,
|
|
53
|
+
message: code === 1 ? "Files differ" : undefined,
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"test",
|
|
58
|
+
(code) => ({
|
|
59
|
+
isError: code >= 2,
|
|
60
|
+
message: code === 1 ? "Condition is false" : undefined,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"[",
|
|
65
|
+
(code) => ({
|
|
66
|
+
isError: code >= 2,
|
|
67
|
+
message: code === 1 ? "Condition is false" : undefined,
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
]);
|
|
71
|
+
/** Default semantic: only 0 is success, everything else is an error. */
|
|
72
|
+
const DEFAULT_SEMANTIC = (code) => ({
|
|
73
|
+
isError: code !== 0,
|
|
74
|
+
message: undefined,
|
|
75
|
+
});
|
|
76
|
+
/** Operators that separate command segments (the exit status belongs to the LAST). */
|
|
77
|
+
const SEGMENT_OPERATORS = /(?:&&|\|\||[;|&\n])/;
|
|
78
|
+
/**
|
|
79
|
+
* Split a command into its top-level segments on `;`, `&&`, `||`, `|`, `&`,
|
|
80
|
+
* and newlines — quote-aware (`grep 'a;b' x` does not split).
|
|
81
|
+
* Returns null when the command cannot be split confidently (heredocs,
|
|
82
|
+
* unbalanced quotes): callers must fail closed on null.
|
|
83
|
+
*/
|
|
84
|
+
function splitSegments(command) {
|
|
85
|
+
const segments = [];
|
|
86
|
+
let current = "";
|
|
87
|
+
let quote = null;
|
|
88
|
+
for (let i = 0; i < command.length; i++) {
|
|
89
|
+
const ch = command[i];
|
|
90
|
+
if (quote) {
|
|
91
|
+
current += ch;
|
|
92
|
+
if (ch === quote)
|
|
93
|
+
quote = null;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === '"' || ch === "'") {
|
|
97
|
+
quote = ch;
|
|
98
|
+
current += ch;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === "\\") {
|
|
102
|
+
// Escaped next char: copy both, never treat the next char as an operator.
|
|
103
|
+
current += ch;
|
|
104
|
+
if (i + 1 < command.length)
|
|
105
|
+
current += command[++i];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// FD-merge redirects (`2>&1`, `>&1`, `>>&2`, `2>&`): the `&` here is part
|
|
109
|
+
// of a redirection, NOT a background separator — treating it as one
|
|
110
|
+
// splits `grep foo bar 2>&1` into a last segment of "1" and the benign
|
|
111
|
+
// exit 1 stays red (the exact false-error this module exists to demote).
|
|
112
|
+
// A `&` preceded by `>` (the redirect form) is never a segment boundary.
|
|
113
|
+
if (ch === "&" && command[i - 1] === ">") {
|
|
114
|
+
current += ch;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (SEGMENT_OPERATORS.test(ch)) {
|
|
118
|
+
segments.push(current);
|
|
119
|
+
current = "";
|
|
120
|
+
// Consume the second char of `||` / `&&` so it cannot start a segment.
|
|
121
|
+
if ((ch === "|" || ch === "&") && command[i + 1] === ch)
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
current += ch;
|
|
126
|
+
}
|
|
127
|
+
if (quote)
|
|
128
|
+
return null; // Unbalanced quote: fail closed.
|
|
129
|
+
segments.push(current);
|
|
130
|
+
return segments;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The base command name of the LAST segment (the one that determines the
|
|
134
|
+
* shell's exit status), mirroring Claude Code's
|
|
135
|
+
* `heuristicallyExtractBaseCommand`. Fails closed: null for heredocs and
|
|
136
|
+
* anything unparseable, so the caller applies default semantics.
|
|
137
|
+
*/
|
|
138
|
+
export function lastSegmentBaseCommand(command) {
|
|
139
|
+
if (!command.trim())
|
|
140
|
+
return null;
|
|
141
|
+
// Heredoc bodies are arbitrary text; a heuristic misread there must err
|
|
142
|
+
// toward error, never demote. (Claude Code strips heredocs before parsing;
|
|
143
|
+
// failing closed is the safe cheaper equivalent.)
|
|
144
|
+
if (/<<\s*-?\s*['"]?\w/.test(command))
|
|
145
|
+
return null;
|
|
146
|
+
const segments = splitSegments(command);
|
|
147
|
+
if (!segments)
|
|
148
|
+
return null;
|
|
149
|
+
const last = segments[segments.length - 1].trim();
|
|
150
|
+
if (!last)
|
|
151
|
+
return null;
|
|
152
|
+
const firstWord = last.split(/\s+/)[0];
|
|
153
|
+
if (!firstWord)
|
|
154
|
+
return null;
|
|
155
|
+
// Env-var assignments (`FOO=1 psql …`) and any prefix we do not recognize
|
|
156
|
+
// classify via the default semantic (the first word IS the assignment).
|
|
157
|
+
return firstWord;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Interpret a command + exit code (Claude Code's `interpretCommandResult`).
|
|
161
|
+
* Unknown, unparsed, or empty commands use the default semantic (fail closed).
|
|
162
|
+
*/
|
|
163
|
+
export function interpretCommandResult(command, exitCode) {
|
|
164
|
+
const base = lastSegmentBaseCommand(command);
|
|
165
|
+
if (!base)
|
|
166
|
+
return DEFAULT_SEMANTIC(exitCode);
|
|
167
|
+
const semantic = COMMAND_SEMANTICS.get(base);
|
|
168
|
+
return semantic ? semantic(exitCode) : DEFAULT_SEMANTIC(exitCode);
|
|
169
|
+
}
|
|
170
|
+
/** The exit-status line pi appends to thrown bash errors (its `appendStatus`). */
|
|
171
|
+
export const EXIT_STATUS_LINE_RE = /\n*(Command exited with code \d+)\s*$/;
|
|
172
|
+
/**
|
|
173
|
+
* Parse a thrown error message as pi's `Command exited with code N` throw
|
|
174
|
+
* (output + `\n\n` + status line, or the bare status line when output was
|
|
175
|
+
* empty). Returns undefined for every other throw shape (`Command aborted`,
|
|
176
|
+
* `Command timed out after N seconds`, arbitrary errors) — callers must
|
|
177
|
+
* rethrow those untouched.
|
|
178
|
+
*/
|
|
179
|
+
export function splitExitStatusError(message) {
|
|
180
|
+
const match = message.match(EXIT_STATUS_LINE_RE);
|
|
181
|
+
if (!match)
|
|
182
|
+
return undefined;
|
|
183
|
+
const exitCode = Number(match[1].match(/\d+/)[0]);
|
|
184
|
+
const output = message.slice(0, match.index).trimEnd();
|
|
185
|
+
return { output, exitCode };
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=bashSemantics.js.map
|
|
@@ -50,7 +50,9 @@ export declare function isScratchpadPath(rawPath: unknown, scratchpadDir: string
|
|
|
50
50
|
export declare function primaryArg(name: BuiltinName, args: Record<string, unknown> | undefined, cwd?: string): string;
|
|
51
51
|
/** `● Tool(arg)` — the visible-row (and live-row) title line. */
|
|
52
52
|
export declare function formatRowTitle(name: BuiltinName, args: Record<string, unknown> | undefined, status: RowStatus, theme: RenderTheme, cwd?: string): string;
|
|
53
|
-
/** Extract "Command exited with code N" from a bash error body, if present.
|
|
53
|
+
/** Extract "Command exited with code N" from a bash error body, if present.
|
|
54
|
+
* The anchor regex lives in bashSemantics (the wrapper's exit-classifier uses
|
|
55
|
+
* the same format), so parsing stays in one place. */
|
|
54
56
|
export declare function splitBashError(outputText: string): {
|
|
55
57
|
output: string;
|
|
56
58
|
exitLine: string | undefined;
|
|
@@ -29,6 +29,8 @@ import * as path from "node:path";
|
|
|
29
29
|
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getAgentDir, getLanguageFromPath, highlightCode, renderDiff, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
31
31
|
import { logEvent } from "./errorSink.js";
|
|
32
|
+
import { EXIT_STATUS_LINE_RE, interpretCommandResult, lastSegmentBaseCommand, splitExitStatusError } from "./bashSemantics.js";
|
|
33
|
+
import { isSandboxDenialOutput } from "./sandbox/bash.js";
|
|
32
34
|
import { ReadStateTracker, READ_FIRST_ERROR, readIsPartial, readTextOf, STALE_ERROR } from "./fileGuards.js";
|
|
33
35
|
import { isQuiet, kindForTool, ToolRunTracker } from "./toolRuns.js";
|
|
34
36
|
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
@@ -146,9 +148,11 @@ export function formatRowTitle(name, args, status, theme, cwd) {
|
|
|
146
148
|
const title = theme.bold(theme.fg("toolTitle", TITLE_BY_NAME[name]));
|
|
147
149
|
return `${statusDot(status, theme)} ${title}(${theme.fg("accent", primaryArg(name, args, cwd))})`;
|
|
148
150
|
}
|
|
149
|
-
/** Extract "Command exited with code N" from a bash error body, if present.
|
|
151
|
+
/** Extract "Command exited with code N" from a bash error body, if present.
|
|
152
|
+
* The anchor regex lives in bashSemantics (the wrapper's exit-classifier uses
|
|
153
|
+
* the same format), so parsing stays in one place. */
|
|
150
154
|
export function splitBashError(outputText) {
|
|
151
|
-
const match = outputText.match(
|
|
155
|
+
const match = outputText.match(EXIT_STATUS_LINE_RE);
|
|
152
156
|
if (!match)
|
|
153
157
|
return { output: outputText.trimEnd(), exitLine: undefined };
|
|
154
158
|
return { output: outputText.slice(0, match.index).trimEnd(), exitLine: match[1] };
|
|
@@ -285,6 +289,54 @@ function textComponent(lines) {
|
|
|
285
289
|
function defaultLoadSettings(cwd) {
|
|
286
290
|
return SettingsManager.create(cwd, getAgentDir());
|
|
287
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Exit-code semantics (Claude Code's commandSemantics port): pi throws on
|
|
294
|
+
* ANY non-zero exit, but for some commands a non-zero exit is a benign
|
|
295
|
+
* signal, not an error (grep 1 = no matches, test 1 = false). Classify the
|
|
296
|
+
* throw: a benign signal becomes a normal result (green row, folds into the
|
|
297
|
+
* run summary, and the model sees clean output with the interpretation
|
|
298
|
+
* note); everything else — real failures, aborts, timeouts, sandbox
|
|
299
|
+
* denials, unknown shapes — returns undefined and the caller rethrows
|
|
300
|
+
* untouched (fail closed).
|
|
301
|
+
*
|
|
302
|
+
* Sandbox denials are detected with the canonical sandbox/bash.ts predicate
|
|
303
|
+
* (data-vs-diagnostic aware), so a benign grep whose MATCHED OUTPUT happens
|
|
304
|
+
* to quote a denial line still demotes; a real denial never does.
|
|
305
|
+
*/
|
|
306
|
+
function demoteBashExit(name, args, err) {
|
|
307
|
+
if (name !== "bash" || !(err instanceof Error))
|
|
308
|
+
return undefined;
|
|
309
|
+
const command = typeof args?.command === "string" ? args.command : "";
|
|
310
|
+
const exit = splitExitStatusError(err.message);
|
|
311
|
+
if (!exit || isSandboxDenialOutput(err.message))
|
|
312
|
+
return undefined;
|
|
313
|
+
const interpretation = interpretCommandResult(command, exit.exitCode);
|
|
314
|
+
if (interpretation.isError)
|
|
315
|
+
return undefined;
|
|
316
|
+
// Sink contract: NEVER raw content on a non-debug line — the base command
|
|
317
|
+
// is a small closed vocabulary plus args, so log it, not the raw command.
|
|
318
|
+
logEvent({
|
|
319
|
+
source: "condensedTools",
|
|
320
|
+
level: "info",
|
|
321
|
+
event: "bash_exit_demoted",
|
|
322
|
+
fields: { baseCommand: lastSegmentBaseCommand(command) ?? "", exitCode: exit.exitCode },
|
|
323
|
+
});
|
|
324
|
+
// The model-facing note: pi substitutes "(no output)" for empty output,
|
|
325
|
+
// which carries no signal — append the interpretation so the model sees
|
|
326
|
+
// WHY the command exited 1. Non-empty output already speaks for itself.
|
|
327
|
+
// The match is on TRIMMED equality (not byte-exact) so a drift in pi's
|
|
328
|
+
// placeholder padding cannot silently drop the note; wording drift fails
|
|
329
|
+
// the empty-output demote test loudly.
|
|
330
|
+
const piEmptyPlaceholder = "(no output)";
|
|
331
|
+
const outputIsPlaceholder = exit.output.trim() === piEmptyPlaceholder;
|
|
332
|
+
const text = outputIsPlaceholder && interpretation.message
|
|
333
|
+
? `${exit.output}\n${interpretation.message}`
|
|
334
|
+
: exit.output;
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text", text }],
|
|
337
|
+
details: { exitCode: exit.exitCode, returnCodeInterpretation: interpretation.message },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
288
340
|
/**
|
|
289
341
|
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
290
342
|
* tracker so callers (and tests) can feed message boundaries into it.
|
|
@@ -379,7 +431,16 @@ export function registerCondensedTools(pi, deps = {}) {
|
|
|
379
431
|
// Not a refusal — delegate and let pi's execute handle it.
|
|
380
432
|
}
|
|
381
433
|
}
|
|
382
|
-
|
|
434
|
+
let result;
|
|
435
|
+
try {
|
|
436
|
+
result = await definitionsFor(cwd)[name].execute(toolCallId, params, signal, onUpdate, ctx);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
const demoted = demoteBashExit(name, args, err);
|
|
440
|
+
if (!demoted)
|
|
441
|
+
throw err;
|
|
442
|
+
result = demoted;
|
|
443
|
+
}
|
|
383
444
|
// read: record what the model saw. write/edit: the just-written
|
|
384
445
|
// state is the fresh truth. AWAITED, not fire-and-forget — a follow-up
|
|
385
446
|
// write/edit in the same burst must not race a pending refresh and
|
|
@@ -451,7 +512,12 @@ export function registerCondensedTools(pi, deps = {}) {
|
|
|
451
512
|
return textComponent(formatEditBody(args.path, result.details, true, theme, slice.cwd));
|
|
452
513
|
}
|
|
453
514
|
const output = textOf(result);
|
|
454
|
-
|
|
515
|
+
if (output)
|
|
516
|
+
return textComponent(formatExpandedOutput(output, theme));
|
|
517
|
+
// A demoted benign exit (grep no-match) with empty output: Claude
|
|
518
|
+
// Code shows the interpretation as the dim row body — mirror it.
|
|
519
|
+
const interpretation = result.details?.returnCodeInterpretation;
|
|
520
|
+
return name === "bash" && interpretation ? textComponent([` ${theme.fg("muted", "⎿")} ${theme.fg("dim", interpretation)}`]) : empty();
|
|
455
521
|
}
|
|
456
522
|
// Backgrounded bash: the launch line (id + output path) plus a dim
|
|
457
523
|
// manage hint — BEFORE the quiet-run collapse, so a backgrounded call
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7-staging.1457.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "84e42e8d01be3c12114488a3f5961df75774a9b2"
|
|
62
62
|
}
|