@tea-agent/loop-agent 0.16.26 → 0.17.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 +38 -1
- package/README.md +14 -3
- package/dist/cli/command-definitions.js +43 -0
- package/dist/cli/program.js +26 -0
- package/dist/commands/dag-approve.js +4 -0
- package/dist/commands/dag-resume.js +1 -0
- package/dist/commands/dag-validate.js +6 -0
- package/dist/commands/operator.js +44 -0
- package/dist/commands/task-contract.js +271 -0
- package/dist/executors/dag-pi-executor.js +55 -7
- package/dist/executors/pi-executor.js +206 -13
- package/dist/executors/pi-sdk-executor.js +21 -6
- package/dist/executors/shell-executor.js +55 -8
- package/dist/executors/shell-presets.js +16 -3
- package/dist/shared/operator/capabilities.js +255 -0
- package/dist/shared/operator/envelope.js +59 -0
- package/dist/shared/operator/index.js +4 -0
- package/dist/shared/operator/registry.js +38 -0
- package/dist/shared/operator/types.js +5 -0
- package/dist/task/contract/adopt.js +166 -0
- package/dist/task/contract/apply.js +326 -0
- package/dist/task/contract/canonicalize.js +60 -0
- package/dist/task/contract/constants.js +29 -0
- package/dist/task/contract/diff.js +177 -0
- package/dist/task/contract/hash.js +42 -0
- package/dist/task/contract/import-revision.js +96 -0
- package/dist/task/contract/index.js +17 -0
- package/dist/task/contract/journal.js +155 -0
- package/dist/task/contract/lock.js +153 -0
- package/dist/task/contract/observe.js +296 -0
- package/dist/task/contract/paths.js +19 -0
- package/dist/task/contract/project.js +170 -0
- package/dist/task/contract/recover.js +312 -0
- package/dist/task/contract/request-ledger.js +37 -0
- package/dist/task/contract/schema.js +151 -0
- package/dist/task/contract/transaction.js +160 -0
- package/dist/task/contract/types.js +1 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/index.js +3 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/runtime.js +5 -1
- package/dist/task/source-references.js +7 -0
- package/dist/worker/cli.js +150 -32
- package/dist/worker/console/app-data.js +185 -0
- package/dist/worker/console/dag-confirmation.js +313 -0
- package/dist/worker/console/doctor.js +169 -0
- package/dist/worker/console/draft-store.js +80 -0
- package/dist/worker/console/index.js +15 -0
- package/dist/worker/console/interview/assessment.js +67 -0
- package/dist/worker/console/interview/session.js +100 -0
- package/dist/worker/console/interview/tools.js +109 -0
- package/dist/worker/console/loopback.js +16 -0
- package/dist/worker/console/observe-health-match.js +174 -0
- package/dist/worker/console/observe-link.js +33 -0
- package/dist/worker/console/operation-runner.js +166 -0
- package/dist/worker/console/operation-sse.js +158 -0
- package/dist/worker/console/operation-store.js +147 -0
- package/dist/worker/console/operator-actions.js +769 -0
- package/dist/worker/console/pi-readiness.js +94 -0
- package/dist/worker/console/recovery-cta.js +133 -0
- package/dist/worker/console/repo-fingerprint.js +29 -0
- package/dist/worker/console/resource-loader.js +95 -0
- package/dist/worker/console/routes.js +368 -0
- package/dist/worker/console/security.js +126 -0
- package/dist/worker/console/server.js +149 -0
- package/dist/worker/console/sibling-controller.js +28 -0
- package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
- package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
- package/dist/worker/console/static/index.html +13 -0
- package/dist/worker/console/vite.config.js +27 -0
- package/dist/worker/observe/health.js +57 -0
- package/dist/worker/observe/routes.js +16 -9
- package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
- package/dist/workflows/dag/frontend-repair.js +1 -10
- package/dist/workflows/dag/init-hybrid.js +269 -116
- package/dist/workflows/dag/node-execution.js +25 -1
- package/dist/workflows/dag/retry-policy.js +16 -1
- package/dist/workflows/dag/runner.js +9 -0
- package/dist/workflows/dag/task-contract-binding.js +138 -0
- package/dist/workflows/dag/types.js +74 -10
- package/dist/workflows/dag/validate.js +25 -7
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +2 -0
- package/docs/architecture/system-overview.md +6 -0
- package/docs/architecture/worker-and-feature.md +7 -0
- package/docs/templates/agent-dag.schema.json +49 -2
- package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
- package/docs/templates/backend-test-dag.json +26 -154
- package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
- package/package.json +8 -2
- package/skills/agent-worker/SKILL.md +1 -0
- package/skills/agent-worker/references/agent-worker-operator.md +3 -2
- package/skills/frontend-design-review/SKILL.md +25 -16
- package/skills/frontend-implementation/references/node-contracts.md +5 -5
- package/skills/loop-agent/references/command-reference.md +48 -1
- package/skills/loop-agent/references/hybrid-dag.md +4 -4
|
@@ -50,6 +50,10 @@ export function extractSubagentStats(stdout) {
|
|
|
50
50
|
export const DEFAULT_TIMEOUT_MS = 1_800_000;
|
|
51
51
|
export const MAX_ALLOWED_TIMEOUT_MS = 1_800_000;
|
|
52
52
|
const PI_BACKEND_ENV = 'CODE_AGENT_PI_BACKEND';
|
|
53
|
+
const OUTPUT_PREVIEW_HEAD_CHARS = 64_000;
|
|
54
|
+
const OUTPUT_PREVIEW_TAIL_CHARS = 64_000;
|
|
55
|
+
const MAX_STREAM_JSONL_LINE_CHARS = 16_000_000;
|
|
56
|
+
const MAX_ASSISTANT_TEXT_CHARS = 16_000_000;
|
|
53
57
|
const PI_CLI_PACKAGE_NAMES = [
|
|
54
58
|
'@earendil-works/pi-coding-agent',
|
|
55
59
|
'@mariozechner/pi-coding-agent',
|
|
@@ -186,6 +190,10 @@ function resolvePiCliPath() {
|
|
|
186
190
|
path.join(nvmBase, nodeVersion, 'lib', 'node_modules'),
|
|
187
191
|
].flatMap((nodeModulesRoot) => PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(nodeModulesRoot, packageName)));
|
|
188
192
|
const otherCandidates = [
|
|
193
|
+
// Prefer the project-local Pi dependency. `npm run` adds node_modules/.bin
|
|
194
|
+
// to PATH, but direct invocations from the DAG runner may not, so resolve
|
|
195
|
+
// the package from the current workspace explicitly.
|
|
196
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.resolve(process.cwd(), 'node_modules'), packageName)),
|
|
189
197
|
// Global npm installation (non-nvm)
|
|
190
198
|
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot('/usr/local/lib/node_modules', packageName)),
|
|
191
199
|
// Bun installation
|
|
@@ -327,8 +335,167 @@ function extractAssistantTextFromMessage(message) {
|
|
|
327
335
|
function isRecord(value) {
|
|
328
336
|
return typeof value === 'object' && value !== null;
|
|
329
337
|
}
|
|
338
|
+
export class BoundedTextPreview {
|
|
339
|
+
label;
|
|
340
|
+
headMax;
|
|
341
|
+
tailMax;
|
|
342
|
+
parts = [];
|
|
343
|
+
head = '';
|
|
344
|
+
tail = '';
|
|
345
|
+
bufferedChars = 0;
|
|
346
|
+
totalChars = 0;
|
|
347
|
+
truncated = false;
|
|
348
|
+
constructor(label, headMax = OUTPUT_PREVIEW_HEAD_CHARS, tailMax = OUTPUT_PREVIEW_TAIL_CHARS) {
|
|
349
|
+
this.label = label;
|
|
350
|
+
this.headMax = headMax;
|
|
351
|
+
this.tailMax = tailMax;
|
|
352
|
+
}
|
|
353
|
+
append(chunk) {
|
|
354
|
+
if (!chunk)
|
|
355
|
+
return;
|
|
356
|
+
this.totalChars += chunk.length;
|
|
357
|
+
if (!this.truncated) {
|
|
358
|
+
if (this.bufferedChars + chunk.length <= this.headMax + this.tailMax) {
|
|
359
|
+
this.parts.push(chunk);
|
|
360
|
+
this.bufferedChars += chunk.length;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const combined = this.parts.join('') + chunk;
|
|
364
|
+
this.head = combined.slice(0, this.headMax);
|
|
365
|
+
this.tail = combined.slice(-this.tailMax);
|
|
366
|
+
this.parts = [];
|
|
367
|
+
this.bufferedChars = 0;
|
|
368
|
+
this.truncated = true;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.tail = (this.tail + chunk).slice(-this.tailMax);
|
|
372
|
+
}
|
|
373
|
+
text() {
|
|
374
|
+
if (!this.truncated)
|
|
375
|
+
return this.parts.join('');
|
|
376
|
+
const omitted = Math.max(0, this.totalChars - this.head.length - this.tail.length);
|
|
377
|
+
return [
|
|
378
|
+
this.head,
|
|
379
|
+
`[${this.label} truncated: omitted ${omitted} chars; kept first ${this.head.length} and last ${this.tail.length}]`,
|
|
380
|
+
this.tail,
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export class PiJsonlStreamCollector {
|
|
385
|
+
lineBuffer = '';
|
|
386
|
+
discardUntilNewline = false;
|
|
387
|
+
assistantText = '';
|
|
388
|
+
parsedEvents = 0;
|
|
389
|
+
tokensUsed = 0;
|
|
390
|
+
outputTooLarge = false;
|
|
391
|
+
subagentTotalCalls = 0;
|
|
392
|
+
subagentFailedCalls = 0;
|
|
393
|
+
subagentModes = new Set();
|
|
394
|
+
subagentAgents = new Set();
|
|
395
|
+
append(chunk) {
|
|
396
|
+
if (!chunk)
|
|
397
|
+
return;
|
|
398
|
+
const lines = chunk.split(/\r?\n/);
|
|
399
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
400
|
+
const part = lines[index] ?? '';
|
|
401
|
+
const isLast = index === lines.length - 1;
|
|
402
|
+
if (this.discardUntilNewline) {
|
|
403
|
+
if (!isLast)
|
|
404
|
+
this.discardUntilNewline = false;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (this.lineBuffer.length + part.length > MAX_STREAM_JSONL_LINE_CHARS) {
|
|
408
|
+
this.outputTooLarge = true;
|
|
409
|
+
this.lineBuffer = '';
|
|
410
|
+
if (isLast)
|
|
411
|
+
this.discardUntilNewline = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
this.lineBuffer += part;
|
|
415
|
+
if (!isLast) {
|
|
416
|
+
this.consumeLine(this.lineBuffer);
|
|
417
|
+
this.lineBuffer = '';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
finish() {
|
|
422
|
+
if (!this.discardUntilNewline && this.lineBuffer.trim()) {
|
|
423
|
+
this.consumeLine(this.lineBuffer);
|
|
424
|
+
}
|
|
425
|
+
this.lineBuffer = '';
|
|
426
|
+
return {
|
|
427
|
+
assistantText: this.assistantText.trim(),
|
|
428
|
+
outputTooLarge: this.outputTooLarge,
|
|
429
|
+
parsedEvents: this.parsedEvents,
|
|
430
|
+
subagentStats: this.formatSubagentStats(),
|
|
431
|
+
tokensUsed: this.tokensUsed,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
consumeLine(line) {
|
|
435
|
+
const trimmed = line.trim();
|
|
436
|
+
if (!trimmed)
|
|
437
|
+
return;
|
|
438
|
+
try {
|
|
439
|
+
const event = JSON.parse(trimmed);
|
|
440
|
+
this.parsedEvents += 1;
|
|
441
|
+
this.consumeEvent(event);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// tolerate non-json noise
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
consumeEvent(event) {
|
|
448
|
+
const candidate = extractAssistantTextFromEvent(event);
|
|
449
|
+
if (candidate) {
|
|
450
|
+
if (candidate.length > MAX_ASSISTANT_TEXT_CHARS) {
|
|
451
|
+
this.outputTooLarge = true;
|
|
452
|
+
this.assistantText = candidate.slice(0, MAX_ASSISTANT_TEXT_CHARS);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
this.assistantText = candidate;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const usage = extractUsageFromEvent(event);
|
|
459
|
+
if (usage !== null)
|
|
460
|
+
this.tokensUsed += usage;
|
|
461
|
+
if (event.type === 'tool_execution_start' && event.toolName === 'subagent') {
|
|
462
|
+
this.subagentTotalCalls += 1;
|
|
463
|
+
if (isRecord(event.input)) {
|
|
464
|
+
if (event.input.agent)
|
|
465
|
+
this.subagentAgents.add(String(event.input.agent));
|
|
466
|
+
if (event.input.tasks)
|
|
467
|
+
this.subagentModes.add('parallel');
|
|
468
|
+
if (event.input.chain)
|
|
469
|
+
this.subagentModes.add('chain');
|
|
470
|
+
if (event.input.agent && !event.input.tasks && !event.input.chain) {
|
|
471
|
+
this.subagentModes.add('single');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (event.type === 'tool_execution_end' && event.toolName === 'subagent') {
|
|
476
|
+
if (event.isError === true)
|
|
477
|
+
this.subagentFailedCalls += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
formatSubagentStats() {
|
|
481
|
+
if (this.subagentTotalCalls === 0)
|
|
482
|
+
return 'calls=0';
|
|
483
|
+
const parts = [`calls=${this.subagentTotalCalls}`];
|
|
484
|
+
if (this.subagentFailedCalls > 0)
|
|
485
|
+
parts.push(`${this.subagentFailedCalls} failed`);
|
|
486
|
+
if (this.subagentModes.size > 0)
|
|
487
|
+
parts.push(Array.from(this.subagentModes).sort().join('+'));
|
|
488
|
+
if (this.subagentAgents.size > 0) {
|
|
489
|
+
parts.push(`agents=[${Array.from(this.subagentAgents).sort().join(',')}]`);
|
|
490
|
+
}
|
|
491
|
+
return parts.join(' | ');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
export function createPiJsonlStreamCollector() {
|
|
495
|
+
return new PiJsonlStreamCollector();
|
|
496
|
+
}
|
|
330
497
|
function withSubagentStats(result) {
|
|
331
|
-
return { ...result, subagentStats: extractSubagentStats(result.stdout) };
|
|
498
|
+
return { ...result, subagentStats: result.subagentStats ?? extractSubagentStats(result.stdout) };
|
|
332
499
|
}
|
|
333
500
|
function withCliBackend(result) {
|
|
334
501
|
return { ...result, backend: 'cli' };
|
|
@@ -354,7 +521,10 @@ async function runCliOrchestration(options, meta) {
|
|
|
354
521
|
const fallbackResult = await executeSingleCliAttempt(options, primary.fallback);
|
|
355
522
|
attemptedModels.push(fallbackResult.modelDisplay);
|
|
356
523
|
const combinedStderr = [primaryResult.stderr, fallbackResult.stderr].filter(Boolean).join('\n--- fallback ---\n');
|
|
357
|
-
const combinedStdout = [primaryResult.stdout, fallbackResult.stdout].filter(Boolean).join('\n');
|
|
524
|
+
const combinedStdout = [primaryResult.stdout, fallbackResult.stdout].filter(Boolean).join('\n--- fallback ---\n');
|
|
525
|
+
const combinedStdoutTruncated = primaryResult.stdoutTruncated || fallbackResult.stdoutTruncated;
|
|
526
|
+
const combinedStderrTruncated = primaryResult.stderrTruncated || fallbackResult.stderrTruncated;
|
|
527
|
+
const combinedOutputTooLarge = primaryResult.outputTooLarge || fallbackResult.outputTooLarge;
|
|
358
528
|
let fallbackIssues = [];
|
|
359
529
|
if (fallbackResult.ok && options.validateOutput) {
|
|
360
530
|
fallbackIssues = options.validateOutput(fallbackResult.assistantText);
|
|
@@ -367,6 +537,9 @@ async function runCliOrchestration(options, meta) {
|
|
|
367
537
|
sdkAttempted: meta?.sdkAttempted,
|
|
368
538
|
stderr: combinedStderr,
|
|
369
539
|
stdout: combinedStdout,
|
|
540
|
+
stdoutTruncated: combinedStdoutTruncated,
|
|
541
|
+
stderrTruncated: combinedStderrTruncated,
|
|
542
|
+
outputTooLarge: combinedOutputTooLarge,
|
|
370
543
|
}));
|
|
371
544
|
}
|
|
372
545
|
if (fallbackResult.ok && fallbackIssues.length > 0) {
|
|
@@ -379,6 +552,9 @@ async function runCliOrchestration(options, meta) {
|
|
|
379
552
|
sdkAttempted: meta?.sdkAttempted,
|
|
380
553
|
stderr: combinedStderr + `\n[invalid-output] ${fallbackIssues.join('; ')}`,
|
|
381
554
|
stdout: combinedStdout,
|
|
555
|
+
stdoutTruncated: combinedStdoutTruncated,
|
|
556
|
+
stderrTruncated: combinedStderrTruncated,
|
|
557
|
+
outputTooLarge: combinedOutputTooLarge,
|
|
382
558
|
}));
|
|
383
559
|
}
|
|
384
560
|
return withSubagentStats(withCliBackend({
|
|
@@ -388,6 +564,9 @@ async function runCliOrchestration(options, meta) {
|
|
|
388
564
|
sdkAttempted: meta?.sdkAttempted,
|
|
389
565
|
stderr: combinedStderr,
|
|
390
566
|
stdout: combinedStdout,
|
|
567
|
+
stdoutTruncated: combinedStdoutTruncated,
|
|
568
|
+
stderrTruncated: combinedStderrTruncated,
|
|
569
|
+
outputTooLarge: combinedOutputTooLarge,
|
|
391
570
|
}));
|
|
392
571
|
}
|
|
393
572
|
if (primaryInvalidOutput) {
|
|
@@ -516,12 +695,18 @@ async function executeSingleCliAttempt(options, modelConfig) {
|
|
|
516
695
|
},
|
|
517
696
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
518
697
|
});
|
|
519
|
-
|
|
520
|
-
|
|
698
|
+
const stdoutPreview = new BoundedTextPreview('stdout');
|
|
699
|
+
const stderrPreview = new BoundedTextPreview('stderr', 16_000, 16_000);
|
|
700
|
+
const stdoutCollector = new PiJsonlStreamCollector();
|
|
521
701
|
child.stdout.setEncoding('utf-8');
|
|
522
702
|
child.stderr.setEncoding('utf-8');
|
|
523
|
-
child.stdout.on('data', (chunk) => {
|
|
524
|
-
|
|
703
|
+
child.stdout.on('data', (chunk) => {
|
|
704
|
+
stdoutPreview.append(chunk);
|
|
705
|
+
stdoutCollector.append(chunk);
|
|
706
|
+
});
|
|
707
|
+
child.stderr.on('data', (chunk) => {
|
|
708
|
+
stderrPreview.append(chunk);
|
|
709
|
+
});
|
|
525
710
|
child.on('error', (err) => {
|
|
526
711
|
if (timeoutHandle)
|
|
527
712
|
clearTimeout(timeoutHandle);
|
|
@@ -535,28 +720,34 @@ async function executeSingleCliAttempt(options, modelConfig) {
|
|
|
535
720
|
if (sigkillHandle)
|
|
536
721
|
clearTimeout(sigkillHandle);
|
|
537
722
|
const durationMs = Date.now() - startedAt;
|
|
538
|
-
const
|
|
539
|
-
const
|
|
723
|
+
const stdout = stdoutPreview.text();
|
|
724
|
+
const stderr = stderrPreview.text();
|
|
725
|
+
const collected = stdoutCollector.finish();
|
|
540
726
|
const failureCategory = classifyPiFailure({
|
|
541
|
-
assistantText,
|
|
727
|
+
assistantText: collected.assistantText,
|
|
542
728
|
exitCode: code,
|
|
543
729
|
stderr,
|
|
544
730
|
stdout,
|
|
545
731
|
timedOut,
|
|
732
|
+
outputTooLarge: collected.outputTooLarge,
|
|
546
733
|
});
|
|
547
734
|
resolve({
|
|
548
|
-
assistantText,
|
|
735
|
+
assistantText: collected.assistantText,
|
|
549
736
|
command: ['pi', ...piArgs],
|
|
550
737
|
durationMs,
|
|
551
738
|
exitCode: code,
|
|
552
739
|
failureCategory,
|
|
553
740
|
modelDisplay,
|
|
554
|
-
ok: code === 0 && assistantText.length > 0,
|
|
555
|
-
parsedEvents,
|
|
741
|
+
ok: code === 0 && collected.assistantText.length > 0 && !collected.outputTooLarge,
|
|
742
|
+
parsedEvents: collected.parsedEvents,
|
|
556
743
|
stderr,
|
|
557
744
|
stdout,
|
|
745
|
+
stdoutTruncated: stdoutPreview.truncated,
|
|
746
|
+
stderrTruncated: stderrPreview.truncated,
|
|
747
|
+
outputTooLarge: collected.outputTooLarge,
|
|
558
748
|
timedOut,
|
|
559
|
-
tokensUsed,
|
|
749
|
+
tokensUsed: collected.tokensUsed,
|
|
750
|
+
subagentStats: collected.subagentStats,
|
|
560
751
|
});
|
|
561
752
|
});
|
|
562
753
|
if (timeoutMs > 0) {
|
|
@@ -585,6 +776,8 @@ export function shouldRetryWithFallback(stderr, stdout) {
|
|
|
585
776
|
export function classifyPiFailure(input) {
|
|
586
777
|
if (input.timedOut)
|
|
587
778
|
return 'timeout';
|
|
779
|
+
if (input.outputTooLarge)
|
|
780
|
+
return 'output-too-large';
|
|
588
781
|
if (input.exitCode === 0 && input.assistantText.trim())
|
|
589
782
|
return 'success';
|
|
590
783
|
const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { classifyPiFailure, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from './pi-executor.js';
|
|
3
|
+
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from './pi-executor.js';
|
|
4
4
|
import { serializeSessionEvent } from './pi-event-serializer.js';
|
|
5
5
|
let sdkSessionFactoryOverride;
|
|
6
6
|
let sdkImportOverrideForTests;
|
|
@@ -283,7 +283,8 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
283
283
|
const startedAt = Date.now();
|
|
284
284
|
let timedOut = false;
|
|
285
285
|
let stderr = '';
|
|
286
|
-
const
|
|
286
|
+
const stdoutPreview = new BoundedTextPreview('stdout');
|
|
287
|
+
const stdoutCollector = createPiJsonlStreamCollector();
|
|
287
288
|
const usageSamples = [];
|
|
288
289
|
let session;
|
|
289
290
|
let timeoutHandle;
|
|
@@ -307,7 +308,8 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
307
308
|
if (!shouldPersistSessionEvent(event))
|
|
308
309
|
return;
|
|
309
310
|
const line = serializeSessionEvent(event);
|
|
310
|
-
|
|
311
|
+
stdoutPreview.append(`${line}\n`);
|
|
312
|
+
stdoutCollector.append(`${line}\n`);
|
|
311
313
|
sessionEventAppender?.append(line, event);
|
|
312
314
|
});
|
|
313
315
|
const filePrefix = options.attachedFiles.map((file) => `@${file}`).join(' ');
|
|
@@ -363,13 +365,23 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
}
|
|
366
|
-
const stdout =
|
|
368
|
+
const stdout = stdoutPreview.text();
|
|
367
369
|
const durationMs = Date.now() - startedAt;
|
|
368
|
-
const
|
|
370
|
+
const collected = stdoutCollector.finish();
|
|
371
|
+
const fallbackParsed = collected.parsedEvents > 0
|
|
372
|
+
? undefined
|
|
373
|
+
: extractAssistantTextFromPiJson(stdout);
|
|
374
|
+
const assistantText = collected.parsedEvents > 0
|
|
375
|
+
? collected.assistantText
|
|
376
|
+
: fallbackParsed?.assistantText ?? '';
|
|
377
|
+
const parsedEvents = collected.parsedEvents > 0
|
|
378
|
+
? collected.parsedEvents
|
|
379
|
+
: fallbackParsed?.parsedEvents ?? 0;
|
|
369
380
|
const tokensUsed = aggregateSdkTokenUsage(usageSamples);
|
|
370
381
|
const failureCategory = classifyPiFailure({
|
|
371
382
|
assistantText,
|
|
372
383
|
exitCode: timedOut ? 1 : stderr ? 1 : 0,
|
|
384
|
+
outputTooLarge: collected.outputTooLarge,
|
|
373
385
|
stderr,
|
|
374
386
|
stdout,
|
|
375
387
|
timedOut,
|
|
@@ -382,11 +394,14 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
382
394
|
exitCode: timedOut || stderr ? 1 : 0,
|
|
383
395
|
failureCategory,
|
|
384
396
|
modelDisplay,
|
|
385
|
-
ok: !timedOut && !stderr && assistantText.length > 0,
|
|
397
|
+
ok: !timedOut && !stderr && assistantText.length > 0 && !collected.outputTooLarge,
|
|
386
398
|
parsedEvents,
|
|
387
399
|
stderr,
|
|
388
400
|
stdout,
|
|
401
|
+
stdoutTruncated: stdoutPreview.truncated,
|
|
402
|
+
outputTooLarge: collected.outputTooLarge,
|
|
389
403
|
timedOut,
|
|
390
404
|
tokensUsed,
|
|
405
|
+
subagentStats: collected.subagentStats,
|
|
391
406
|
};
|
|
392
407
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { truncateOutput } from "../shared/output-truncation.js";
|
|
@@ -54,6 +54,28 @@ export function resolveShellCwd(root, requestedCwd) {
|
|
|
54
54
|
}
|
|
55
55
|
return resolved;
|
|
56
56
|
}
|
|
57
|
+
async function resolveJsonArtifactSourceNodeId(input) {
|
|
58
|
+
const candidates = [
|
|
59
|
+
input.fromNodeId,
|
|
60
|
+
...new Set((input.fallbackFromNodeIds ?? []).filter((nodeId) => nodeId !== input.fromNodeId)),
|
|
61
|
+
];
|
|
62
|
+
for (const nodeId of candidates) {
|
|
63
|
+
try {
|
|
64
|
+
await stat(path.join(input.runDir, `${nodeId}.json`));
|
|
65
|
+
return nodeId;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error &&
|
|
69
|
+
typeof error === "object" &&
|
|
70
|
+
"code" in error &&
|
|
71
|
+
error.code === "ENOENT") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`missing jsonArtifactGate node output (tried: ${candidates.join(", ")})`);
|
|
78
|
+
}
|
|
57
79
|
/** Preset commands first, then explicit shell.commands, then verdictGate (deterministic ordering). */
|
|
58
80
|
export function resolveShellCommands(shell) {
|
|
59
81
|
const fromPreset = shell.preset ? expandShellPreset(shell.preset) : [];
|
|
@@ -421,12 +443,30 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
421
443
|
const selected = await executePipelineCommands(input, meta, [buildBackendTestCanonicalResultFromInitialShellSnippet()]);
|
|
422
444
|
if (!selected.every((result) => result.ok))
|
|
423
445
|
throw new Error(selected.find((result) => !result.ok)?.stderr || "canonical result materialization failed");
|
|
446
|
+
const readNodeOutput = async (nodeId) => {
|
|
447
|
+
const record = JSON.parse(await readFile(path.join(meta.runDir, `${nodeId}.json`), "utf8"));
|
|
448
|
+
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
449
|
+
if (!text)
|
|
450
|
+
throw new Error(`missing auditable node output: ${nodeId}`);
|
|
451
|
+
return text;
|
|
452
|
+
};
|
|
453
|
+
const caseReview = await readNodeOutput("review-backend-cases-pi");
|
|
454
|
+
const reviewVerdicts = caseReview.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("VERDICT:"));
|
|
455
|
+
if (reviewVerdicts.length !== 1 || !["VERDICT: pass", "VERDICT: request-revision"].includes(reviewVerdicts[0])) {
|
|
456
|
+
throw new Error("invalid advisory case review evidence: expected exactly one auditable VERDICT line");
|
|
457
|
+
}
|
|
458
|
+
const traceability = await readNodeOutput("backend-test-traceability-gate-shell");
|
|
459
|
+
if (!traceability.includes("Traceability gate: pass") || !/Matched symbols:\s*\d+/.test(traceability)) {
|
|
460
|
+
throw new Error("invalid traceability evidence: deterministic pass summary is missing");
|
|
461
|
+
}
|
|
424
462
|
const contracts = path.join(meta.runDir, "contracts");
|
|
425
463
|
const context = {
|
|
426
464
|
schemaVersion: 1,
|
|
427
465
|
result: JSON.parse(await readFile(path.join(contracts, "backend-test-result.json"), "utf8")),
|
|
428
466
|
manifest: JSON.parse(await readFile(path.join(contracts, "backend-test-case-manifest.json"), "utf8")),
|
|
429
467
|
classification: JSON.parse(await readFile(path.join(contracts, "backend-test-classification.json"), "utf8")),
|
|
468
|
+
caseReview: { verdict: reviewVerdicts[0].slice("VERDICT: ".length), evidence: caseReview },
|
|
469
|
+
traceability: { status: "pass", evidence: traceability },
|
|
430
470
|
};
|
|
431
471
|
outputs.push(`classification=${classification.path}`, ...selected.map((result) => result.stdout), JSON.stringify(context, null, 2));
|
|
432
472
|
}
|
|
@@ -510,13 +550,20 @@ export async function executeDagShellNode(input, meta) {
|
|
|
510
550
|
const started = Date.now();
|
|
511
551
|
try {
|
|
512
552
|
const gate = shell.jsonArtifactGate;
|
|
553
|
+
const sourceNodeId = gate.fallbackFromNodeIds?.length
|
|
554
|
+
? await resolveJsonArtifactSourceNodeId({
|
|
555
|
+
runDir: meta.runDir,
|
|
556
|
+
fromNodeId: gate.fromNodeId,
|
|
557
|
+
fallbackFromNodeIds: gate.fallbackFromNodeIds,
|
|
558
|
+
})
|
|
559
|
+
: gate.fromNodeId;
|
|
513
560
|
let artifact;
|
|
514
561
|
switch (gate.schemaId) {
|
|
515
562
|
case "backend-test-analysis-v1":
|
|
516
563
|
case "backend-test-analysis-v2":
|
|
517
564
|
artifact = await materializeBackendTestAnalysisContract({
|
|
518
565
|
runDir: meta.runDir,
|
|
519
|
-
fromNodeId:
|
|
566
|
+
fromNodeId: sourceNodeId,
|
|
520
567
|
artifactName: gate.artifactName,
|
|
521
568
|
outputDir: gate.outputDir,
|
|
522
569
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -525,7 +572,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
525
572
|
case "backend-test-execution-v1":
|
|
526
573
|
artifact = await materializeBackendTestExecutionContract({
|
|
527
574
|
runDir: meta.runDir,
|
|
528
|
-
fromNodeId:
|
|
575
|
+
fromNodeId: sourceNodeId,
|
|
529
576
|
artifactName: gate.artifactName,
|
|
530
577
|
outputDir: gate.outputDir,
|
|
531
578
|
});
|
|
@@ -533,7 +580,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
533
580
|
case "backend-test-result-v1":
|
|
534
581
|
artifact = await materializeBackendTestResultFromRunDir({
|
|
535
582
|
runDir: meta.runDir,
|
|
536
|
-
fromNodeId:
|
|
583
|
+
fromNodeId: sourceNodeId,
|
|
537
584
|
artifactName: gate.artifactName,
|
|
538
585
|
outputDir: gate.outputDir,
|
|
539
586
|
junitRelativePath: gate.junitRelativePath,
|
|
@@ -542,7 +589,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
542
589
|
case "backend-test-classification-v1":
|
|
543
590
|
artifact = await materializeBackendTestClassification({
|
|
544
591
|
runDir: meta.runDir,
|
|
545
|
-
fromNodeId:
|
|
592
|
+
fromNodeId: sourceNodeId,
|
|
546
593
|
artifactName: gate.artifactName,
|
|
547
594
|
outputDir: gate.outputDir,
|
|
548
595
|
});
|
|
@@ -550,7 +597,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
550
597
|
case "backend-test-semantic-review-v1":
|
|
551
598
|
artifact = await materializeBackendTestSemanticReview({
|
|
552
599
|
runDir: meta.runDir,
|
|
553
|
-
fromNodeId:
|
|
600
|
+
fromNodeId: sourceNodeId,
|
|
554
601
|
artifactName: gate.artifactName,
|
|
555
602
|
outputDir: gate.outputDir,
|
|
556
603
|
});
|
|
@@ -558,7 +605,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
558
605
|
case "backend-test-case-manifest-v1":
|
|
559
606
|
artifact = await materializeBackendTestCaseManifest({
|
|
560
607
|
runDir: meta.runDir,
|
|
561
|
-
fromNodeId:
|
|
608
|
+
fromNodeId: sourceNodeId,
|
|
562
609
|
artifactName: gate.artifactName,
|
|
563
610
|
outputDir: gate.outputDir,
|
|
564
611
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -567,7 +614,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
567
614
|
case "frontend-implementation-contract-v1":
|
|
568
615
|
artifact = await materializeFrontendImplementationContract({
|
|
569
616
|
runDir: meta.runDir,
|
|
570
|
-
fromNodeId:
|
|
617
|
+
fromNodeId: sourceNodeId,
|
|
571
618
|
artifactName: gate.artifactName,
|
|
572
619
|
outputDir: gate.outputDir,
|
|
573
620
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -60,12 +60,25 @@ export function buildVerdictGateShellCommand(gate) {
|
|
|
60
60
|
export function buildRequirementCoverageGateShellCommand(gate) {
|
|
61
61
|
const label = gate.label ?? "requirement coverage";
|
|
62
62
|
const requiredJson = escapeShellSingleQuoted(JSON.stringify(gate.requiredIds.map((id) => id.toUpperCase())));
|
|
63
|
-
const
|
|
63
|
+
const sourceConfig = {
|
|
64
|
+
primary: gate.fromNodeIds,
|
|
65
|
+
fallback: [
|
|
66
|
+
...new Set((gate.fallbackFromNodeIds ?? []).filter((nodeId) => !gate.fromNodeIds.includes(nodeId))),
|
|
67
|
+
],
|
|
68
|
+
};
|
|
64
69
|
const program = [
|
|
65
70
|
'const fs=require("fs");',
|
|
71
|
+
'const path=require("path");',
|
|
66
72
|
"const required=JSON.parse(process.argv[1]);",
|
|
73
|
+
`const sources=${JSON.stringify(sourceConfig)};`,
|
|
74
|
+
"let files=sources.primary.map((id)=>path.join(process.env.HARNESS_DAG_RUN_DIR,id+'.json'));",
|
|
75
|
+
"if(sources.fallback.length){",
|
|
76
|
+
" files=[]; const candidates=[sources.primary[0],...sources.fallback];",
|
|
77
|
+
" for(const id of candidates){const file=path.join(process.env.HARNESS_DAG_RUN_DIR,id+'.json'); try{fs.statSync(file);files=[file];break;}catch(error){if(error&&error.code==='ENOENT')continue;throw error;}}",
|
|
78
|
+
` if(!files.length){console.error(${JSON.stringify(`${label} gate blocked: missing node output (tried: `)}+[sources.primary[0],...sources.fallback].join(', ')+')');process.exit(1);}`,
|
|
79
|
+
"}",
|
|
67
80
|
"let text='';",
|
|
68
|
-
"for(const file of
|
|
81
|
+
"for(const file of files){",
|
|
69
82
|
" if(!fs.existsSync(file)){console.error('missing requirement coverage node output: '+file);process.exit(1);}",
|
|
70
83
|
" const raw=JSON.parse(fs.readFileSync(file,'utf8')); text+='\\n'+String(raw.assistantText ?? raw.stdout ?? '');",
|
|
71
84
|
"}",
|
|
@@ -74,5 +87,5 @@ export function buildRequirementCoverageGateShellCommand(gate) {
|
|
|
74
87
|
`if(missing.length){console.error(${JSON.stringify(`${label} gate blocked: missing `)}+missing.join(', '));process.exit(1);}`,
|
|
75
88
|
`console.log(${JSON.stringify(`${label} gate passed: `)}+required.join(', '));`,
|
|
76
89
|
].join("");
|
|
77
|
-
return `test -n "\${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for ${label} gate" >&2; exit 1; }; node -e '${escapeShellSingleQuoted(program)}' '${requiredJson}'
|
|
90
|
+
return `test -n "\${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for ${label} gate" >&2; exit 1; }; node -e '${escapeShellSingleQuoted(program)}' '${requiredJson}'`;
|
|
78
91
|
}
|