start-command 0.41.0 → 0.43.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.
- package/CHANGELOG.md +23 -0
- package/package.json +3 -3
- package/src/lib/formal-ai-client.js +249 -22
- package/src/lib/formal-ai-files.js +797 -0
- package/src/lib/formal-ai-policy.js +10 -3
- package/src/lib/formal-ai.js +328 -12
- package/src/lib/log-sanitizer.js +4 -0
- package/src/lib/usage.js +3 -1
- package/test/fixtures/formal-ai/receipts-258.json +449 -0
- package/test/fixtures/formal-ai/receipts-261.json +589 -0
- package/test/fixtures/formal-ai/replay.cjs +60 -7
- package/test/regression-255-cli.js +3 -1
- package/test/regression-255-e2e.js +80 -3
- package/test/regression-255-loop.js +6 -1
- package/test/regression-255-policy.js +1 -1
- package/test/regression-257.js +265 -0
- package/test/regression-258-files.js +452 -0
- package/test/regression-258.js +456 -0
- package/test/regression-261.js +856 -0
|
@@ -15,9 +15,9 @@ const path = require('path');
|
|
|
15
15
|
/** Exit statuses of the mode; a finished task keeps its last tool's status. */
|
|
16
16
|
const EXIT = Object.freeze({
|
|
17
17
|
USAGE: 64, // request text or option combination rejected
|
|
18
|
-
UNSUPPORTED: 65, // Formal AI
|
|
18
|
+
UNSUPPORTED: 65, // Formal AI did not understand the task or reported a capability gap
|
|
19
19
|
UNAVAILABLE: 69, // no qualified Formal AI runtime could be resolved
|
|
20
|
-
PROTOCOL: 70, // malformed response, unknown tool or unverified effect
|
|
20
|
+
PROTOCOL: 70, // malformed or 5xx response, unknown tool or unverified effect
|
|
21
21
|
REFUSED: 77, // a tool call was refused by the effect policy
|
|
22
22
|
CONFIG: 78, // invalid START_FORMAL_AI_* bound
|
|
23
23
|
BUDGET: 124, // step, tool, time or retry budget exhausted
|
|
@@ -39,6 +39,11 @@ const BOUND_DEFAULTS = Object.freeze({
|
|
|
39
39
|
value: 64 * 1024,
|
|
40
40
|
max: 16 * 1024 * 1024,
|
|
41
41
|
},
|
|
42
|
+
maxFileBytes: {
|
|
43
|
+
env: 'START_FORMAL_AI_MAX_FILE_BYTES',
|
|
44
|
+
value: 1024 * 1024,
|
|
45
|
+
max: 16 * 1024 * 1024,
|
|
46
|
+
},
|
|
42
47
|
maxRequestChars: { env: 'START_FORMAL_AI_MAX_REQUEST_CHARS', value: 4096 },
|
|
43
48
|
maxMemoryMb: { env: 'START_FORMAL_AI_MAX_MEMORY_MB', value: 1024 },
|
|
44
49
|
maxFingerprintEntries: {
|
|
@@ -265,6 +270,8 @@ function operands(tokens) {
|
|
|
265
270
|
|
|
266
271
|
/**
|
|
267
272
|
* Normalized workspace-relative path of an operand, or null outside it.
|
|
273
|
+
* It uses `/` on every platform, like the fingerprint, so observations,
|
|
274
|
+
* messages and evidence are the same on Windows.
|
|
268
275
|
* @param {string} cwd - Working directory
|
|
269
276
|
* @param {string} operand - Operand text
|
|
270
277
|
* @returns {string|null}
|
|
@@ -275,7 +282,7 @@ function workspacePath(cwd, operand) {
|
|
|
275
282
|
if (!isInside(root, resolved)) {
|
|
276
283
|
return null;
|
|
277
284
|
}
|
|
278
|
-
return path.relative(root, resolved) || '.';
|
|
285
|
+
return path.relative(root, resolved).split(path.sep).join('/') || '.';
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
/**
|
package/src/lib/formal-ai.js
CHANGED
|
@@ -7,10 +7,19 @@
|
|
|
7
7
|
* and keeps evidence of what actually happened. The run ends when Formal AI
|
|
8
8
|
* gives its final answer, says it did not understand, or a bound, refusal,
|
|
9
9
|
* protocol error or cancellation stops it, and the exit status says which.
|
|
10
|
+
* A final answer that is a known capability gap (issue #258) is reported as
|
|
11
|
+
* incomplete with its obligations, never as an answer.
|
|
12
|
+
*
|
|
13
|
+
* A failed change stays unresolved until the same typed tool changes the same
|
|
14
|
+
* target and its readback is verified (issue #261): a later read, a copy over
|
|
15
|
+
* the file or Formal AI's final answer does not resolve it, and the run then
|
|
16
|
+
* ends `incomplete` with the original failure and its exit status.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
const policy = require('./formal-ai-policy');
|
|
13
20
|
const { formatToolResult, runTool } = require('./formal-ai-tool');
|
|
21
|
+
const files = require('./formal-ai-files');
|
|
22
|
+
const { recognizeCapabilityGap } = require('./formal-ai-client');
|
|
14
23
|
|
|
15
24
|
const { EXIT } = policy;
|
|
16
25
|
const PREVIEW_CHARS = 2000;
|
|
@@ -68,13 +77,24 @@ async function runFormalAiTask(options) {
|
|
|
68
77
|
effects: bounds.effects,
|
|
69
78
|
observed: new Set(),
|
|
70
79
|
observedDirs: new Set(),
|
|
80
|
+
contents: new Map(),
|
|
71
81
|
};
|
|
72
|
-
|
|
82
|
+
// Failed calls whose obligation no later verified call met (#261).
|
|
83
|
+
const unresolved = [];
|
|
84
|
+
const finish = (status, exitCode, message, remaining = [], extra = {}) => {
|
|
85
|
+
const open = unresolved.map(failureObligation);
|
|
73
86
|
evidence.outcome = {
|
|
74
87
|
status,
|
|
75
88
|
exitCode,
|
|
76
89
|
message,
|
|
77
|
-
remainingObligations:
|
|
90
|
+
remainingObligations: [
|
|
91
|
+
...remaining,
|
|
92
|
+
...open.filter((item) => !remaining.includes(item)),
|
|
93
|
+
],
|
|
94
|
+
...(unresolved.length
|
|
95
|
+
? { failures: unresolved.map((f) => ({ ...f })) }
|
|
96
|
+
: {}),
|
|
97
|
+
...extra,
|
|
78
98
|
};
|
|
79
99
|
return evidence;
|
|
80
100
|
};
|
|
@@ -119,9 +139,15 @@ async function runFormalAiTask(options) {
|
|
|
119
139
|
: error.exitCode === EXIT.UNAVAILABLE
|
|
120
140
|
? 'unavailable'
|
|
121
141
|
: 'protocol-error';
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
// The cause and attempt count of a runtime failure stay in the
|
|
143
|
+
// evidence (issue #257); they are sanitized and bounded by the client.
|
|
144
|
+
return finish(
|
|
145
|
+
status,
|
|
146
|
+
error.exitCode || EXIT.PROTOCOL,
|
|
147
|
+
error.message,
|
|
148
|
+
['Formal AI did not complete the task'],
|
|
149
|
+
error.details ? { diagnostics: { ...error.details } } : {}
|
|
150
|
+
);
|
|
125
151
|
}
|
|
126
152
|
evidence.steps.push({
|
|
127
153
|
kind: 'response',
|
|
@@ -135,6 +161,34 @@ async function runFormalAiTask(options) {
|
|
|
135
161
|
if (response.content) {
|
|
136
162
|
note(response.content);
|
|
137
163
|
}
|
|
164
|
+
const gap = recognizeCapabilityGap(response.content, messages);
|
|
165
|
+
if (gap) {
|
|
166
|
+
return finish(
|
|
167
|
+
'capability-gap',
|
|
168
|
+
EXIT.UNSUPPORTED,
|
|
169
|
+
describeGap(gap, evidence.effects),
|
|
170
|
+
gapObligations(gap, bounds.effects),
|
|
171
|
+
{ gap }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (unresolved.length) {
|
|
175
|
+
const [first] = unresolved;
|
|
176
|
+
return finish(
|
|
177
|
+
'incomplete',
|
|
178
|
+
first.exitCode || 1,
|
|
179
|
+
`\`${first.command}\` failed (${first.error}) and no later call resolved it; ${describeEffects(evidence.effects)}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const record = unrecognizedGapRecord(response.content, messages);
|
|
183
|
+
if (record) {
|
|
184
|
+
return finish(
|
|
185
|
+
'incomplete',
|
|
186
|
+
EXIT.UNSUPPORTED,
|
|
187
|
+
`Formal AI reported ${record.schema}${record.reason ? ` (${record.reason})` : ''}${record.target ? ` for ${record.target}` : ''}${record.clause ? ` for the clause ${JSON.stringify(record.clause)}` : ''}; nothing was verified as done by Formal AI; ${describeEffects(evidence.effects)}`,
|
|
188
|
+
['Formal AI did not confirm the requested change'],
|
|
189
|
+
{ gapRecord: record }
|
|
190
|
+
);
|
|
191
|
+
}
|
|
138
192
|
if (response.handler === 'unknown' && toolCalls === 0) {
|
|
139
193
|
return finish(
|
|
140
194
|
'unsupported',
|
|
@@ -167,7 +221,10 @@ async function runFormalAiTask(options) {
|
|
|
167
221
|
type: 'function',
|
|
168
222
|
function: {
|
|
169
223
|
name: call.name,
|
|
170
|
-
arguments:
|
|
224
|
+
arguments:
|
|
225
|
+
call.tool === 'bash'
|
|
226
|
+
? JSON.stringify({ command: call.command })
|
|
227
|
+
: call.arguments,
|
|
171
228
|
},
|
|
172
229
|
})),
|
|
173
230
|
});
|
|
@@ -193,6 +250,7 @@ async function runFormalAiTask(options) {
|
|
|
193
250
|
note,
|
|
194
251
|
evidence,
|
|
195
252
|
});
|
|
253
|
+
trackFailures(unresolved, evidence.steps[evidence.steps.length - 1]);
|
|
196
254
|
if (signal && signal.aborted) {
|
|
197
255
|
return cancelled();
|
|
198
256
|
}
|
|
@@ -215,7 +273,269 @@ async function runFormalAiTask(options) {
|
|
|
215
273
|
);
|
|
216
274
|
}
|
|
217
275
|
|
|
218
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Keep the failed calls of a run until a later call meets their obligation
|
|
278
|
+
* (issue #261). A failed edit or write is resolved only by a verified edit or
|
|
279
|
+
* write of the same target (a write also resolves a failed read of the file
|
|
280
|
+
* it created); a failed read by a read of the same target; a failed write
|
|
281
|
+
* command only by the same command succeeding. A copy or move over the target
|
|
282
|
+
* never counts as the edit.
|
|
283
|
+
*/
|
|
284
|
+
function trackFailures(unresolved, step) {
|
|
285
|
+
if (!step || step.kind !== 'tool' || step.decision !== 'allowed') {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const typed = step.tool !== undefined;
|
|
289
|
+
const changed = typed && step.tool !== 'read';
|
|
290
|
+
if (step.exitCode !== 0 && (typed || step.effect === 'write')) {
|
|
291
|
+
unresolved.push({
|
|
292
|
+
command: step.command,
|
|
293
|
+
...(typed ? { tool: step.tool, target: step.target } : {}),
|
|
294
|
+
exitCode: step.exitCode ?? 1,
|
|
295
|
+
error:
|
|
296
|
+
step.error ||
|
|
297
|
+
(step.signal
|
|
298
|
+
? `stopped by ${step.signal}`
|
|
299
|
+
: `exited with ${step.exitCode}`),
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (step.exitCode !== 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const verified = changed && step.verification && step.verification.verified;
|
|
307
|
+
for (let index = unresolved.length - 1; index >= 0; index--) {
|
|
308
|
+
const failure = unresolved[index];
|
|
309
|
+
const resolved = typed
|
|
310
|
+
? failure.target === step.target &&
|
|
311
|
+
(failure.tool === 'read'
|
|
312
|
+
? step.tool === 'read' || verified
|
|
313
|
+
: verified && (failure.tool === step.tool || step.tool === 'edit'))
|
|
314
|
+
: failure.tool === undefined && failure.command === step.command;
|
|
315
|
+
if (resolved) {
|
|
316
|
+
unresolved.splice(index, 1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function failureObligation(failure) {
|
|
322
|
+
return `\`${failure.command}\` failed: ${failure.error}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* A final answer that is exactly one Links Notation record whose record_type
|
|
327
|
+
* names a gap, as Formal AI's task obligations answer (`obligation_gap`, e.g.
|
|
328
|
+
* reason `no_artifact_in_clause`): an id line, then ` key "value"` lines.
|
|
329
|
+
* Returned in the JSON gap shape (schema = record_type, never complete), so
|
|
330
|
+
* a gap Formal AI states in its own notation is never taken as success, even
|
|
331
|
+
* when start verified every change it made (#261).
|
|
332
|
+
* @param {string} text - Trimmed final answer
|
|
333
|
+
* @returns {object|null} {schema, reason?, clause?, complete: false}
|
|
334
|
+
*/
|
|
335
|
+
function linoGapRecord(text) {
|
|
336
|
+
const [id, ...lines] = text.split('\n');
|
|
337
|
+
if (!/^[A-Za-z0-9_]+$/.test(id) || !lines.length) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const fields = {};
|
|
341
|
+
for (const line of lines) {
|
|
342
|
+
const match = /^ {2}([a-z_]+) (?:"([^"]*)"|'([^']*)'|(\S+))$/.exec(line);
|
|
343
|
+
if (!match || Object.hasOwn(fields, match[1])) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
fields[match[1]] = match[2] ?? match[3] ?? match[4];
|
|
347
|
+
}
|
|
348
|
+
if (typeof fields.record_type !== 'string') {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
schema: fields.record_type,
|
|
353
|
+
...(fields.reason ? { reason: fields.reason } : {}),
|
|
354
|
+
...(fields.clause ? { clause: fields.clause } : {}),
|
|
355
|
+
complete: false,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A final answer that is a JSON record of a gap start does not know: it is
|
|
361
|
+
* never taken as success, though its schema grants no recognized gap.
|
|
362
|
+
*/
|
|
363
|
+
function unrecognizedGapRecord(content, messages) {
|
|
364
|
+
let value;
|
|
365
|
+
try {
|
|
366
|
+
value = JSON.parse(String(content || '').trim());
|
|
367
|
+
} catch {
|
|
368
|
+
value = linoGapRecord(String(content || '').trim());
|
|
369
|
+
}
|
|
370
|
+
if (
|
|
371
|
+
!value ||
|
|
372
|
+
typeof value !== 'object' ||
|
|
373
|
+
Array.isArray(value) ||
|
|
374
|
+
typeof value.schema !== 'string' ||
|
|
375
|
+
!/gap/i.test(value.schema) ||
|
|
376
|
+
!(
|
|
377
|
+
value.verified === false ||
|
|
378
|
+
value.complete === false ||
|
|
379
|
+
value.completed === false
|
|
380
|
+
)
|
|
381
|
+
) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
if (files.echoedByTool(String(content).trim(), messages)) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
schema: value.schema,
|
|
389
|
+
...(typeof value.target === 'string' ? { target: value.target } : {}),
|
|
390
|
+
...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
|
|
391
|
+
...(typeof value.clause === 'string' ? { clause: value.clause } : {}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Capability gap, in words. When tools changed the working directory, the
|
|
397
|
+
* message keeps what start observed: Formal AI's gap still means the task is
|
|
398
|
+
* not confirmed, whatever start verified on its own (#261).
|
|
399
|
+
*/
|
|
400
|
+
function describeGap(gap, effects) {
|
|
401
|
+
const missing = gap.missingCapabilities.length
|
|
402
|
+
? `; missing ${gap.missingCapabilities.join(', ')}`
|
|
403
|
+
: '';
|
|
404
|
+
const head = `Formal AI reported a capability gap for ${gap.target} (${gap.reason}${missing})`;
|
|
405
|
+
return effects && policy.hasEffects(effects)
|
|
406
|
+
? `${head}; Formal AI did not confirm the task as done; start observed: ${describeEffects(effects)}`
|
|
407
|
+
: `${head}; nothing was verified as done`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function gapObligations(gap, effects) {
|
|
411
|
+
const obligations = [
|
|
412
|
+
`the requested change to ${gap.target} was not made or not verified`,
|
|
413
|
+
];
|
|
414
|
+
const changes = ['edit', 'write', 'multi_edit'];
|
|
415
|
+
if (
|
|
416
|
+
effects !== 'workspace-write' &&
|
|
417
|
+
gap.missingCapabilities.some((c) => changes.includes(c))
|
|
418
|
+
) {
|
|
419
|
+
obligations.push(
|
|
420
|
+
'set START_FORMAL_AI_EFFECTS=workspace-write to offer the edit and write tools'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return obligations;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function runCall(options) {
|
|
427
|
+
return options.call.tool === 'bash'
|
|
428
|
+
? runBashCall(options)
|
|
429
|
+
: runFileToolCall(options);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Record a call's working-directory diff in its step and the run. */
|
|
433
|
+
function recordEffects(step, diff, evidence) {
|
|
434
|
+
step.effects = diff;
|
|
435
|
+
for (const key of ['added', 'removed', 'changed']) {
|
|
436
|
+
evidence.effects[key].push(...diff[key]);
|
|
437
|
+
}
|
|
438
|
+
evidence.effects.complete = evidence.effects.complete && diff.complete;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** A structured read, edit or write call (issue #258). */
|
|
442
|
+
function runFileToolCall({ call, context, cwd, bounds, io, note, evidence }) {
|
|
443
|
+
const decision = files.evaluateFileCall(call.tool, call.args, context);
|
|
444
|
+
const step = {
|
|
445
|
+
kind: 'tool',
|
|
446
|
+
tool: call.tool,
|
|
447
|
+
command: call.command,
|
|
448
|
+
path: call.args.path,
|
|
449
|
+
};
|
|
450
|
+
evidence.steps.push(step);
|
|
451
|
+
if (!decision.allowed) {
|
|
452
|
+
step.decision = 'refused';
|
|
453
|
+
step.reason = decision.reason;
|
|
454
|
+
note(`refused \`${call.command}\`: ${decision.reason}`);
|
|
455
|
+
return {
|
|
456
|
+
stop: [
|
|
457
|
+
'refused',
|
|
458
|
+
EXIT.REFUSED,
|
|
459
|
+
`refused \`${call.command}\`: ${decision.reason}`,
|
|
460
|
+
['the refused call did not run; nothing after it ran either'],
|
|
461
|
+
],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
Object.assign(step, {
|
|
465
|
+
decision: 'allowed',
|
|
466
|
+
effect: decision.kind,
|
|
467
|
+
target: decision.target,
|
|
468
|
+
});
|
|
469
|
+
note(call.command);
|
|
470
|
+
const started = Date.now();
|
|
471
|
+
const before = policy.fingerprint(cwd, bounds.maxFingerprintEntries);
|
|
472
|
+
const receipt = files.runFileCall(
|
|
473
|
+
call.tool,
|
|
474
|
+
call.args,
|
|
475
|
+
decision,
|
|
476
|
+
context,
|
|
477
|
+
bounds
|
|
478
|
+
);
|
|
479
|
+
const diff = policy.diffFingerprints(
|
|
480
|
+
before,
|
|
481
|
+
policy.fingerprint(cwd, bounds.maxFingerprintEntries)
|
|
482
|
+
);
|
|
483
|
+
Object.assign(step, {
|
|
484
|
+
exitCode: receipt.exitCode,
|
|
485
|
+
durationMs: Date.now() - started,
|
|
486
|
+
...(receipt.error ? { error: receipt.error } : {}),
|
|
487
|
+
...(receipt.bytes !== undefined ? { bytes: receipt.bytes } : {}),
|
|
488
|
+
...(receipt.sha256 ? { sha256: receipt.sha256 } : {}),
|
|
489
|
+
...(receipt.previousSha256
|
|
490
|
+
? { previousSha256: receipt.previousSha256 }
|
|
491
|
+
: {}),
|
|
492
|
+
...(receipt.replacements ? { replacements: receipt.replacements } : {}),
|
|
493
|
+
...(receipt.mutation ? { mutation: receipt.mutation } : {}),
|
|
494
|
+
});
|
|
495
|
+
recordEffects(step, diff, evidence);
|
|
496
|
+
if (receipt.error) {
|
|
497
|
+
note(`${call.tool} failed: ${receipt.error}`);
|
|
498
|
+
} else if (call.tool === 'read') {
|
|
499
|
+
mirrorRead(receipt.text, bounds, io, note);
|
|
500
|
+
if (policy.hasEffects(diff)) {
|
|
501
|
+
step.unexpectedEffects = true;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
step.verification = receipt.verification;
|
|
505
|
+
if (!receipt.verification.verified) {
|
|
506
|
+
return {
|
|
507
|
+
stop: [
|
|
508
|
+
'unverified',
|
|
509
|
+
EXIT.PROTOCOL,
|
|
510
|
+
`\`${call.command}\` ran but ${receipt.verification.detail}`,
|
|
511
|
+
['the change could not be confirmed by readback'],
|
|
512
|
+
],
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
note(receipt.verification.detail);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
exitCode: receipt.exitCode,
|
|
519
|
+
result: receipt.result,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Show what a read returned, like the stdout of `cat`, within the output cap. */
|
|
524
|
+
function mirrorRead(text, bounds, io, note) {
|
|
525
|
+
if (!io.stdout || !text) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
529
|
+
const shown = bytes.subarray(0, bounds.maxOutputBytes).toString('utf8');
|
|
530
|
+
io.stdout.write(shown.endsWith('\n') ? shown : `${shown}\n`);
|
|
531
|
+
if (bytes.length > bounds.maxOutputBytes) {
|
|
532
|
+
note(
|
|
533
|
+
`output truncated at START_FORMAL_AI_MAX_OUTPUT_BYTES=${bounds.maxOutputBytes}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function runBashCall({
|
|
219
539
|
call,
|
|
220
540
|
context,
|
|
221
541
|
cwd,
|
|
@@ -275,12 +595,8 @@ async function runCall({
|
|
|
275
595
|
spawnError: receipt.spawnError,
|
|
276
596
|
stdout: preview(receipt.stdout),
|
|
277
597
|
stderr: preview(receipt.stderr),
|
|
278
|
-
effects: diff,
|
|
279
598
|
});
|
|
280
|
-
|
|
281
|
-
evidence.effects[key].push(...diff[key]);
|
|
282
|
-
}
|
|
283
|
-
evidence.effects.complete = evidence.effects.complete && diff.complete;
|
|
599
|
+
recordEffects(step, diff, evidence);
|
|
284
600
|
if (receipt.truncated) {
|
|
285
601
|
note(
|
|
286
602
|
`output truncated at START_FORMAL_AI_MAX_OUTPUT_BYTES=${bounds.maxOutputBytes}`
|
package/src/lib/log-sanitizer.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* `Authorization: <scheme> <value>` header, so a logged one-line JSON prompt
|
|
13
13
|
* keeps the rest of its line (issue #249, sensitive-data-sanitizer#45); the
|
|
14
14
|
* pre-pass that did this here is gone (issue #253).
|
|
15
|
+
*
|
|
16
|
+
* The package depends on ^0.14.0, the current release flagged by the
|
|
17
|
+
* dependency freshness check; the Rust installer and its version check
|
|
18
|
+
* require the same release (rust/src/lib/log_sanitizer.rs).
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
const fs = require('fs');
|
package/src/lib/usage.js
CHANGED
|
@@ -105,7 +105,9 @@ Examples:
|
|
|
105
105
|
$ -i docker -d --on-kill-resume 3 --recovery-command 'solve --resume' -- solve
|
|
106
106
|
$ -i docker -d --on-kill-resume 3 --on-kill-resume-delay 30-90 -- cargo test
|
|
107
107
|
$ --use-command-stream echo "Hello" # Use command-stream library
|
|
108
|
-
$ --use-formal-ai -- print list of files in current directory
|
|
108
|
+
$ --use-formal-ai -- print list of files in current directory
|
|
109
|
+
$ --use-formal-ai -- show contents of README.md
|
|
110
|
+
START_FORMAL_AI_EFFECTS=workspace-write $ --use-formal-ai -- "In a.js, replace «x» with «y»."`);
|
|
109
111
|
console.log('');
|
|
110
112
|
console.log('Piping with $:');
|
|
111
113
|
console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
|