carthagent 0.0.0-stage → 1.0.7
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/LICENSE +21 -0
- package/README.md +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { fixtureDesign, fixtureReview } from './helpers/tested-design.mjs';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { lockWorkspace, selectReport } from '../lib/workspace.mjs';
|
|
10
|
+
import { saveReport, createReport } from '../lib/reports.mjs';
|
|
11
|
+
|
|
12
|
+
// Load the project extension even without a global engine installation.
|
|
13
|
+
const requireEngine = createRequire(import.meta.url);
|
|
14
|
+
const { createJiti } = requireEngine('jiti');
|
|
15
|
+
const jiti = createJiti(import.meta.url, { alias: { '@earendil-works/pi-coding-agent': fileURLToPath(new URL('../vendor/agent/index.js', import.meta.url)) } });
|
|
16
|
+
const factory = await jiti.import(fileURLToPath(new URL('../extensions/delivery.ts', import.meta.url)), { default: true });
|
|
17
|
+
const options = {};
|
|
18
|
+
function fixture(t, { approve = true } = {}) {
|
|
19
|
+
const cwd = mkdtempSync(join(tmpdir(), 'engine extension integration '));
|
|
20
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
21
|
+
writeFileSync(join(cwd, 'app.py'), 'print(1)');
|
|
22
|
+
const hooks = {}, commands = {}, tools = {}, entries = [], messages = [], flags = { 'delivery-strict': true };
|
|
23
|
+
const engine = {
|
|
24
|
+
registerCommand(name, command) { commands[name] = command; },
|
|
25
|
+
registerFlag() {}, getFlag: name => flags[name],
|
|
26
|
+
on(name, fn) { hooks[name] = fn; },
|
|
27
|
+
registerTool(tool) { tools[tool.name] = tool; },
|
|
28
|
+
appendEntry(customType, data) { entries.push({ type: 'custom', customType, data }); },
|
|
29
|
+
sendMessage(message) { messages.push(message); },
|
|
30
|
+
};
|
|
31
|
+
factory(engine);
|
|
32
|
+
const ctx = { cwd, hasUI: false, aborted: 0, abort() { this.aborted++; }, sessionManager: { getEntries: () => entries, getBranch: () => entries, getSessionId: () => 'integration-session' }, hasPendingMessages: () => false };
|
|
33
|
+
const rawCall = (name, params = {}) => tools[name].execute('test-id', params, undefined, undefined, ctx);
|
|
34
|
+
const inspectReview = async () => {
|
|
35
|
+
const inspected = JSON.parse((await rawCall('delivery_design', { action: 'inspect' })).content[0].text);
|
|
36
|
+
return { ...fixtureReview(entries.at(-1).data.plan), captureId: inspected.result.id };
|
|
37
|
+
};
|
|
38
|
+
const call = async (name, params = {}) => {
|
|
39
|
+
if (approve && name === 'delivery_plan' && (params.verification ?? 'required') === 'required') params = { ...params, design: fixtureDesign(params) };
|
|
40
|
+
if (approve && name === 'delivery_revise' && params.patch?.acceptance) params = { ...params, patch: { ...params.patch, design: fixtureDesign({ ...entries.at(-1).data.plan, ...params.patch }) } };
|
|
41
|
+
const result = await rawCall(name, params);
|
|
42
|
+
if (approve && ['delivery_plan', 'delivery_revise'].includes(name)) {
|
|
43
|
+
const current = entries.at(-1)?.data?.plan;
|
|
44
|
+
if (current && (current.verification ?? 'required') === 'required') {
|
|
45
|
+
if (JSON.stringify(current.design) !== JSON.stringify(fixtureDesign(current))) await rawCall('delivery_revise', { reason: 'Bind fixture scenarios to required validators', patch: { design: fixtureDesign(current) } });
|
|
46
|
+
const plan = entries.at(-1).data.plan;
|
|
47
|
+
await rawCall('delivery_design', { action: 'validate' });
|
|
48
|
+
const inspected = JSON.parse((await rawCall('delivery_design', { action: 'inspect' })).content[0].text);
|
|
49
|
+
await rawCall('delivery_design', { action: 'review', review: { ...fixtureReview(plan), captureId: inspected.result.id } });
|
|
50
|
+
await rawCall('delivery_design', { action: 'approve' });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
const plan = { goal: 'Working script', assumptions: [], artifacts: ['app.py'], steps: ['Implement', 'Verify'], acceptance: [{ requirement: 'Runs', checks: ['run'] }], checks: [{ id: 'run', kind: 'runtime', argv: [process.execPath, '-e', 'console.log("passed")'], timeoutSeconds: 5 }] };
|
|
56
|
+
return { cwd, ctx, hooks, commands, entries, messages, flags, call, plan, inspectReview };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('live extension refreshes same-run state and rejects superseded mutations and busy checks', async t => {
|
|
60
|
+
const f = fixture(t);
|
|
61
|
+
const planned = await f.call('delivery_plan', f.plan);
|
|
62
|
+
const path = JSON.parse(planned.content[0].text).report;
|
|
63
|
+
const disk = JSON.parse(readFileSync(path, 'utf8'));
|
|
64
|
+
disk.stepStatus = { step0: 'done' };
|
|
65
|
+
saveReport(path, disk);
|
|
66
|
+
await f.call('delivery_progress', { step: 1, status: 'active' });
|
|
67
|
+
assert.equal(JSON.parse(readFileSync(path, 'utf8')).stepStatus.step0, 'done');
|
|
68
|
+
const release = lockWorkspace(f.cwd);
|
|
69
|
+
await assert.rejects(f.call('delivery_check', { id: 'all' }), /Workspace busy/);
|
|
70
|
+
release();
|
|
71
|
+
const other = createReport(f.cwd, { status: 'implementing', plan: f.plan, evidence: {} });
|
|
72
|
+
selectReport(f.cwd, other.path);
|
|
73
|
+
await assert.rejects(f.call('delivery_progress', { step: 0, status: 'active' }), /superseded/);
|
|
74
|
+
f.hooks.agent_end({ messages: [] }, f.ctx);
|
|
75
|
+
assert.equal(f.messages.length, 0);
|
|
76
|
+
await f.call('delivery_plan', f.plan); // Explicit new work can supersede an idle run.
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('built-in Jev opt-in persists unavailable advice without unlocking implementation', async t => {
|
|
80
|
+
const f = fixture(t, { approve: false });
|
|
81
|
+
f.flags['delivery-jev'] = true;
|
|
82
|
+
f.flags['delivery-jev-provider'] = 'invalid-provider';
|
|
83
|
+
await f.call('delivery_plan', { ...f.plan, design: fixtureDesign(f.plan) });
|
|
84
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
85
|
+
const first = JSON.parse((await f.call('delivery_design', { action: 'inspect' })).content[0].text);
|
|
86
|
+
assert.equal(first.advice.mode, 'unavailable');
|
|
87
|
+
assert.equal(first.planningStatus.locked, true);
|
|
88
|
+
const again = JSON.parse((await f.call('delivery_design', { action: 'inspect' })).content[0].text);
|
|
89
|
+
assert.equal(again.advice.reused, true);
|
|
90
|
+
assert.equal(f.entries.at(-1).data.advisorAttempts.length, 1);
|
|
91
|
+
f.hooks.session_start({}, f.ctx);
|
|
92
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx).block, true);
|
|
93
|
+
assert.match(f.hooks.context({ messages: [] }, f.ctx).messages.at(-1).content, /unavailable/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('design review accepts captureId beside the action or echoed as review.id', async t => {
|
|
97
|
+
const f = fixture(t, { approve: false });
|
|
98
|
+
await f.call('delivery_plan', { ...f.plan, design: fixtureDesign(f.plan) });
|
|
99
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
100
|
+
const first = JSON.parse((await f.call('delivery_design', { action: 'inspect' })).content[0].text);
|
|
101
|
+
assert.equal(first.captureId, first.result.id);
|
|
102
|
+
// Top-level captureId (the delivery_review shape) is normalized into review.
|
|
103
|
+
await f.call('delivery_design', { action: 'review', captureId: first.result.id, review: fixtureReview(entriesPlan(f)) });
|
|
104
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
105
|
+
// A revision relocks: captureId echoed under the inspect field name works too.
|
|
106
|
+
await f.call('delivery_revise', { reason: 'Adjust scope', patch: { assumptions: ['new'] } });
|
|
107
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
108
|
+
const second = JSON.parse((await f.call('delivery_design', { action: 'inspect' })).content[0].text);
|
|
109
|
+
const failed = await f.call('delivery_design', { action: 'review', review: fixtureReview(entriesPlan(f)) }).then(() => null, e => e);
|
|
110
|
+
assert.match(failed.message, new RegExp(second.result.id));
|
|
111
|
+
await f.call('delivery_design', { action: 'review', review: { ...fixtureReview(entriesPlan(f)), id: second.result.id } });
|
|
112
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
113
|
+
// An explicit nested null must not shadow the valid top-level captureId.
|
|
114
|
+
await f.call('delivery_revise', { reason: 'Second adjustment', patch: { assumptions: ['newer'] } });
|
|
115
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
116
|
+
const third = JSON.parse((await f.call('delivery_design', { action: 'inspect' })).content[0].text);
|
|
117
|
+
await f.call('delivery_design', { action: 'review', captureId: third.result.id, review: { ...fixtureReview(entriesPlan(f)), captureId: null } });
|
|
118
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
119
|
+
});
|
|
120
|
+
function entriesPlan(f) { return f.entries.at(-1).data.plan; }
|
|
121
|
+
|
|
122
|
+
test('discovery guide/checkpoint restore, questions, terminal suppression and read permission', async t => {
|
|
123
|
+
const f = fixture(t, { approve: false });
|
|
124
|
+
const guide = JSON.parse((await f.call('delivery_design', { action: 'guide' })).content[0].text);
|
|
125
|
+
assert.equal(guide.authority, 'none'); assert.equal(f.entries.length, 0);
|
|
126
|
+
f.hooks.before_agent_start({ prompt: 'Implement a new feature', systemPrompt: '' }, f.ctx);
|
|
127
|
+
for (let i = 0; i < 8; i++) await f.hooks.tool_result({ toolName: 'read', content: [] }, f.ctx);
|
|
128
|
+
assert.match(f.hooks.context({ messages: [] }, f.ctx).messages[0].content, /8 discovery/);
|
|
129
|
+
assert.equal(f.hooks.tool_call({ toolName: 'read' }, f.ctx), undefined);
|
|
130
|
+
assert.equal(f.hooks.tool_call({ toolName: 'code_nav' }, f.ctx), undefined);
|
|
131
|
+
await f.hooks.tool_result({ toolName: 'code_nav', content: [] }, f.ctx);
|
|
132
|
+
assert.match(f.hooks.context({ messages: [] }, f.ctx).messages[0].content, /9 discovery/);
|
|
133
|
+
f.hooks.session_compact({}, f.ctx); f.hooks.session_start({}, f.ctx);
|
|
134
|
+
assert.match(f.hooks.context({ messages: [] }, f.ctx).messages[0].content, /9 discovery/);
|
|
135
|
+
const branch = f.ctx.sessionManager.getBranch;
|
|
136
|
+
f.ctx.sessionManager.getBranch = () => [];
|
|
137
|
+
f.hooks.session_tree({}, f.ctx);
|
|
138
|
+
assert.equal(f.hooks.context({ messages: [] }, f.ctx), undefined);
|
|
139
|
+
f.ctx.sessionManager.getBranch = branch;
|
|
140
|
+
f.hooks.session_tree({}, f.ctx);
|
|
141
|
+
assert.match(f.hooks.context({ messages: [] }, f.ctx).messages[0].content, /9 discovery/);
|
|
142
|
+
f.hooks.input({ source: 'interactive' });
|
|
143
|
+
f.hooks.before_agent_start({ prompt: 'Explain how this code works', systemPrompt: '' }, f.ctx);
|
|
144
|
+
for (let i = 0; i < 20; i++) await f.hooks.tool_result({ toolName: 'read', content: [] }, f.ctx);
|
|
145
|
+
assert.equal(f.hooks.context({ messages: [] }, f.ctx), undefined);
|
|
146
|
+
f.hooks.input({ source: 'interactive' });
|
|
147
|
+
f.hooks.before_agent_start({ prompt: 'Implement a new feature', systemPrompt: '' }, f.ctx);
|
|
148
|
+
f.hooks.tool_call({ toolName: 'bash' }, f.ctx);
|
|
149
|
+
assert.match(f.hooks.tool_call({ toolName: 'bash' }, f.ctx).reason, /2 blocked/);
|
|
150
|
+
await f.call('delivery_plan', { ...f.plan, design: fixtureDesign(f.plan) });
|
|
151
|
+
await f.call('delivery_finish', { status: 'blocked', review: 'Fixture stop', launch: 'n/a', limitations: ['No implementation'] });
|
|
152
|
+
assert.equal(f.hooks.context({ messages: [] }, f.ctx), undefined);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('schema repair -> Jev -> approved edit -> checks -> final review completes without changing scope', async t => {
|
|
156
|
+
const { createServer } = await import('node:http'); let calls = 0;
|
|
157
|
+
const server = createServer(async (req, res) => {
|
|
158
|
+
let body = ''; for await (const chunk of req) body += chunk;
|
|
159
|
+
const payload = JSON.parse(body); assert.equal(payload.state.goal, 'Working script'); assert.ok(payload.state.checks[0].argv); calls++;
|
|
160
|
+
res.setHeader('content-type', 'application/json');
|
|
161
|
+
res.end(JSON.stringify({ model: 'typesafe/jev-1.13-fixture', answers: { srp: { type: 'noul', noul: .1 }, di: { type: 'noul', noul: .1 }, tests: { type: 'noul', noul: .8 } } }));
|
|
162
|
+
});
|
|
163
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
164
|
+
t.after(() => { server.closeAllConnections(); server.close(); });
|
|
165
|
+
const f = fixture(t, { approve: false });
|
|
166
|
+
f.flags['delivery-jev'] = true; f.flags['delivery-jev-broker'] = `http://127.0.0.1:${server.address().port}/jev`;
|
|
167
|
+
await f.call('delivery_design', { action: 'guide' });
|
|
168
|
+
const plan = { ...f.plan, steps: [{ id: 'build', title: 'Implement and test', checks: ['run'] }] };
|
|
169
|
+
plan.design = fixtureDesign(plan); const correct = structuredClone(plan.design); plan.design.scenarios[0].checks = ['wrong'];
|
|
170
|
+
await f.call('delivery_plan', plan);
|
|
171
|
+
const validation = JSON.parse((await f.call('delivery_design', { action: 'validate' })).content[0].text);
|
|
172
|
+
assert.match(validation.next, /No Jev call/); assert.deepEqual(validation.references.checks, ['run']);
|
|
173
|
+
await assert.rejects(f.call('delivery_design', { action: 'inspect' }), /Fresh passing/); assert.equal(calls, 0);
|
|
174
|
+
assert.equal(f.entries.at(-1).data.advisorAttempts, undefined);
|
|
175
|
+
await f.call('delivery_revise', { reason: 'Fix check reference preserving obligations', patch: { design: correct } });
|
|
176
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
177
|
+
const inspected = JSON.parse((await f.call('delivery_design', { action: 'inspect' })).content[0].text);
|
|
178
|
+
assert.equal(calls, 1); assert.equal(inspected.advice.findings[0].category, 'tests'); assert.equal(inspected.planningStatus.locked, true);
|
|
179
|
+
const current = f.entries.at(-1).data.plan;
|
|
180
|
+
assert.deepEqual(current.acceptance, plan.acceptance); assert.deepEqual(current.checks, plan.checks);
|
|
181
|
+
await f.call('delivery_design', { action: 'review', review: { ...fixtureReview(current), captureId: inspected.result.id, challenges: ['Investigated Jev tests topic; fixture checks plumbing, not semantic completeness'] } });
|
|
182
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
183
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx), undefined);
|
|
184
|
+
writeFileSync(join(f.cwd, 'app.py'), 'print(2)');
|
|
185
|
+
f.hooks.session_compact({}, f.ctx);
|
|
186
|
+
await f.call('delivery_check', { id: 'all' }); await f.call('delivery_progress', { step: 'build', status: 'done' });
|
|
187
|
+
const capture = JSON.parse((await f.call('delivery_review', { action: 'inspect' })).content[0].text);
|
|
188
|
+
await f.call('delivery_review', { action: 'record', captureId: capture.id, coverage: [{ requirement: 'Runs', assertions: 'Fixture subprocess exited zero' }], probes: ['Runtime check exit 0'], findings: [], limitations: ['Plumbing fixture, no Git baseline or semantic proof'] });
|
|
189
|
+
await f.call('delivery_finish', { status: 'verified', review: 'Reviewed fixture', launch: 'python app.py', limitations: ['Plumbing only'] });
|
|
190
|
+
assert.equal(f.entries.at(-1).data.status, 'verified'); assert.equal(calls, 1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('revision tool rejects malformed patches before merging current fields', async t => {
|
|
194
|
+
const f = fixture(t);
|
|
195
|
+
await f.call('delivery_plan', f.plan);
|
|
196
|
+
const before = f.entries.at(-1).data.revision;
|
|
197
|
+
for (const patch of [null, {}, [], 'invalid', { evidence: {} }]) {
|
|
198
|
+
await assert.rejects(f.call('delivery_revise', { reason: 'Invalid patch', patch }), /nonempty|Unknown/);
|
|
199
|
+
}
|
|
200
|
+
assert.equal(f.entries.at(-1).data.revision, before);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('real extension loads, gates writes, executes checks and rejects stale evidence', options, async t => {
|
|
204
|
+
const f = fixture(t);
|
|
205
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }).block, true);
|
|
206
|
+
assert.equal(f.hooks.tool_call({ toolName: 'bash', input: { command: 'mkdir artifacts' } }, f.ctx).block, true);
|
|
207
|
+
assert.equal(f.hooks.tool_call({ toolName: 'bash', input: { command: 'ls -la' } }, f.ctx).block, true);
|
|
208
|
+
await f.call('delivery_plan', f.plan);
|
|
209
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx), undefined);
|
|
210
|
+
const finish = { status: 'verified', review: 'Reviewed executable behavior.', launch: 'python app.py', limitations: [] };
|
|
211
|
+
await assert.rejects(f.call('delivery_finish', finish), /Cannot verify/);
|
|
212
|
+
await f.call('delivery_check', { id: 'all' });
|
|
213
|
+
await f.call('delivery_finish', finish);
|
|
214
|
+
assert.equal(f.entries.at(-1).data.status, 'verified');
|
|
215
|
+
// Undeclared file changes must invalidate evidence, not only artifact roots.
|
|
216
|
+
writeFileSync(join(f.cwd, 'new_config.json'), '{}');
|
|
217
|
+
await assert.rejects(f.call('delivery_finish', finish), /stale checks/);
|
|
218
|
+
});
|
|
219
|
+
test('terminal deliveries stop context reminders and repair turns across restore', options, async t => {
|
|
220
|
+
for (const status of ['verified', 'blocked']) {
|
|
221
|
+
const f = fixture(t);
|
|
222
|
+
await f.call('delivery_plan', f.plan);
|
|
223
|
+
assert.match(f.hooks.context({ messages: [] }).messages.at(-1).content, /Inspect freshness before execution/);
|
|
224
|
+
await f.call('delivery_check', { id: 'all' });
|
|
225
|
+
await f.call('delivery_finish', { status, review: 'Reviewed', launch: 'python app.py', limitations: status === 'blocked' ? ['External prerequisite unavailable'] : [] });
|
|
226
|
+
for (const restore of [false, true]) {
|
|
227
|
+
if (restore) f.hooks.session_start({}, f.ctx);
|
|
228
|
+
assert.equal(f.hooks.context({ messages: [] }), undefined);
|
|
229
|
+
f.hooks.agent_end({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, f.ctx);
|
|
230
|
+
assert.equal(f.messages.length, 0);
|
|
231
|
+
}
|
|
232
|
+
await f.call('delivery_plan', f.plan);
|
|
233
|
+
assert.match(f.hooks.context({ messages: [] }).messages.at(-1).content, /Inspect freshness before execution/);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('session restoration adopts a newer same-run dashboard report without reviving repairs', options, async t => {
|
|
238
|
+
const f = fixture(t);
|
|
239
|
+
await f.call('delivery_plan', f.plan);
|
|
240
|
+
const disk = structuredClone(f.entries.at(-1).data);
|
|
241
|
+
disk.status = 'blocked';
|
|
242
|
+
const path = join(f.cwd, '.harness', 'integration-session', disk.runId, 'report.json');
|
|
243
|
+
saveReport(path, disk);
|
|
244
|
+
f.hooks.session_start({}, f.ctx);
|
|
245
|
+
assert.match((await f.call('delivery_status')).content[0].text, /blocked/);
|
|
246
|
+
f.hooks.agent_end({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, f.ctx);
|
|
247
|
+
assert.equal(f.messages.length, 0);
|
|
248
|
+
await f.call('delivery_finish', { status: 'blocked', review: 'Restored', launch: 'none', limitations: ['fixture'] });
|
|
249
|
+
assert.ok(f.entries.at(-1).data.storageVersion > disk.storageVersion);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('fresh passing evidence advertises delivery_finish and repeated unchanged status polling is blocked', options, async t => {
|
|
253
|
+
const f = fixture(t);
|
|
254
|
+
await f.call('delivery_plan', f.plan);
|
|
255
|
+
const checked = JSON.parse((await f.call('delivery_check', { id: 'all' })).content[0].text);
|
|
256
|
+
assert.equal(checked.readyToFinish, true);
|
|
257
|
+
assert.match(checked.next, /delivery_finish/);
|
|
258
|
+
|
|
259
|
+
const status = JSON.parse((await f.call('delivery_status')).content[0].text);
|
|
260
|
+
assert.equal(status.status, 'verifying', 'explicit finish is still required');
|
|
261
|
+
assert.deepEqual(status.pendingChecks, []);
|
|
262
|
+
assert.equal(status.readyToFinish, true);
|
|
263
|
+
assert.match(status.next, /Do not call delivery_status again/);
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < 3; i++) {
|
|
266
|
+
assert.equal(f.hooks.tool_call({ toolName: 'delivery_status' }, f.ctx), undefined);
|
|
267
|
+
}
|
|
268
|
+
const blocked = f.hooks.tool_call({ toolName: 'delivery_status' }, f.ctx);
|
|
269
|
+
assert.equal(blocked.block, true);
|
|
270
|
+
assert.match(blocked.reason, /Repeated unchanged delivery_status/);
|
|
271
|
+
assert.equal(f.hooks.tool_call({ toolName: 'delivery_status' }, f.ctx).block, true, 'blocked retries stay blocked');
|
|
272
|
+
const workspaceChange = join(f.cwd, 'workspace-change.txt');
|
|
273
|
+
writeFileSync(workspaceChange, 'changed');
|
|
274
|
+
assert.equal(f.hooks.tool_call({ toolName: 'delivery_status' }, f.ctx), undefined, 'workspace changes permit a fresh read');
|
|
275
|
+
rmSync(workspaceChange);
|
|
276
|
+
|
|
277
|
+
// A productive delivery action resets the guard; explicit finish remains the
|
|
278
|
+
// only transition to verified.
|
|
279
|
+
assert.equal(f.hooks.tool_call({ toolName: 'delivery_finish' }, f.ctx), undefined);
|
|
280
|
+
await f.call('delivery_finish', { status: 'verified', review: 'Reviewed fresh evidence.', launch: 'python app.py', limitations: [] });
|
|
281
|
+
assert.equal(f.entries.at(-1).data.status, 'verified');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('real extension serializes sibling checks without tool errors', options, async t => {
|
|
285
|
+
const f = fixture(t);
|
|
286
|
+
f.plan.checks.push({ ...f.plan.checks[0], id: 'second' });
|
|
287
|
+
await f.call('delivery_plan', f.plan);
|
|
288
|
+
await Promise.all([f.call('delivery_check', { id: 'run' }), f.call('delivery_check', { id: 'second' })]);
|
|
289
|
+
assert.ok(f.entries.at(-1).data.evidence.run.passed);
|
|
290
|
+
assert.ok(f.entries.at(-1).data.evidence.second.passed);
|
|
291
|
+
});
|
|
292
|
+
test('compaction context survives restore, branching clears stale contracts', options, async t => {
|
|
293
|
+
const f = fixture(t);
|
|
294
|
+
await f.call('delivery_plan', f.plan);
|
|
295
|
+
f.hooks.session_start({}, f.ctx);
|
|
296
|
+
const context = f.hooks.context({ messages: [] });
|
|
297
|
+
assert.match(context.messages[0].content, /Working script/);
|
|
298
|
+
f.entries.length = 0;
|
|
299
|
+
f.hooks.session_tree({}, f.ctx);
|
|
300
|
+
assert.match((await f.call('delivery_status')).content[0].text, /No delivery contract/);
|
|
301
|
+
});
|
|
302
|
+
test('context archives only after approval, large captures stay retrievable, compaction cannot unlock stale source', async t => {
|
|
303
|
+
const f = fixture(t, { approve: false });
|
|
304
|
+
const plan = { ...f.plan, design: fixtureDesign(f.plan) };
|
|
305
|
+
plan.design.scenarios[0].assertion = 'Exact assertion '.repeat(6000);
|
|
306
|
+
await f.call('delivery_plan', plan);
|
|
307
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
308
|
+
const inspected = await f.call('delivery_design', { action: 'inspect' });
|
|
309
|
+
const view = JSON.parse(inspected.content[0].text);
|
|
310
|
+
assert.ok(view.captureArchive.path);
|
|
311
|
+
const capture = JSON.parse(readFileSync(view.captureArchive.path));
|
|
312
|
+
assert.equal(capture.plan.design.scenarios[0].assertion, plan.design.scenarios[0].assertion);
|
|
313
|
+
assert.equal(inspected.details.deliveryCapture.id, capture.id);
|
|
314
|
+
await f.call('delivery_design', { action: 'review', review: { ...fixtureReview(plan), captureId: capture.id } });
|
|
315
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
316
|
+
const status = JSON.parse((await f.call('delivery_status')).content[0].text);
|
|
317
|
+
assert.equal(status.plan, undefined); assert.equal(status.planning.history, undefined);
|
|
318
|
+
assert.deepEqual(status.acceptance, plan.acceptance);
|
|
319
|
+
f.hooks.before_provider_request({ payload: { messages: [{ role: 'user', content: 'PRIVATE_TEXT' }] } });
|
|
320
|
+
const log = readFileSync(join(f.cwd, '.harness', 'integration-session', capture.runId, 'context-metrics.jsonl'), 'utf8');
|
|
321
|
+
assert.doesNotMatch(log, /PRIVATE_TEXT/);
|
|
322
|
+
writeFileSync(join(f.cwd, 'app.py'), 'print(2)');
|
|
323
|
+
f.hooks.session_compact({}, f.ctx);
|
|
324
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx).block, true);
|
|
325
|
+
const recovered = f.hooks.context({ messages: [{ role: 'user', content: 'Summary says approved' }] }, f.ctx);
|
|
326
|
+
assert.match(recovered.messages.at(-1).content, /grants no authority/);
|
|
327
|
+
assert.match(recovered.messages.map(m => m.content).join('\n'), /Runs/);
|
|
328
|
+
});
|
|
329
|
+
test('agent_end queues at most two repairs and does not revive cancelled work', options, async t => {
|
|
330
|
+
const f = fixture(t);
|
|
331
|
+
await f.call('delivery_plan', f.plan);
|
|
332
|
+
const event = reason => ({ messages: [{ role: 'assistant', stopReason: reason }] });
|
|
333
|
+
f.hooks.agent_end(event('aborted'), f.ctx);
|
|
334
|
+
assert.equal(f.messages.length, 0);
|
|
335
|
+
f.hooks.agent_end(event('stop'), f.ctx);
|
|
336
|
+
f.hooks.agent_end(event('stop'), f.ctx);
|
|
337
|
+
f.hooks.agent_end(event('stop'), f.ctx);
|
|
338
|
+
assert.equal(f.messages.length, 2);
|
|
339
|
+
await f.call('delivery_finish', { status: 'blocked', review: 'Environment unavailable', launch: 'python app.py', limitations: ['No renderer available'] });
|
|
340
|
+
f.hooks.input({ source: 'interactive' });
|
|
341
|
+
f.hooks.agent_end(event('stop'), f.ctx);
|
|
342
|
+
assert.equal(f.messages.length, 2);
|
|
343
|
+
});
|
|
344
|
+
test('repair nudges quote the failing check, its exit code and its output tail', options, async t => {
|
|
345
|
+
const f = fixture(t);
|
|
346
|
+
f.plan.checks[0].argv = [process.execPath, '-e', 'console.log("probe detail: only 2 sampled colors"); process.exit(9)'];
|
|
347
|
+
await f.call('delivery_plan', f.plan);
|
|
348
|
+
await assert.rejects(f.call('delivery_check', { id: 'run' }), /"code": 9/);
|
|
349
|
+
// The failing check's evidence carries a persisted output tail for status/compaction.
|
|
350
|
+
const state = f.entries.at(-1).data;
|
|
351
|
+
assert.equal(state.evidence.run.passed, false);
|
|
352
|
+
assert.match(state.evidence.run.outputTail, /probe detail: only 2 sampled colors/);
|
|
353
|
+
f.hooks.agent_end({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, f.ctx);
|
|
354
|
+
assert.equal(f.messages.length, 1);
|
|
355
|
+
const content = f.messages[0].content;
|
|
356
|
+
assert.match(content, /check run failed \(exit 9, timedOut false\)/);
|
|
357
|
+
assert.match(content, /probe detail: only 2 sampled colors/);
|
|
358
|
+
assert.match(content, /pending checks: run/);
|
|
359
|
+
// Compaction context keeps only a short tail, not the full 1200-character evidence copy.
|
|
360
|
+
const context = f.hooks.context({ messages: [] });
|
|
361
|
+
assert.match(context.messages.at(-1).content, /"passed":false/);
|
|
362
|
+
assert.ok(context.messages.at(-1).content.length < 4000);
|
|
363
|
+
});
|
|
364
|
+
test('bash timeout cap bounds runaway shell commands when configured', options, async t => {
|
|
365
|
+
const f = fixture(t);
|
|
366
|
+
await f.call('delivery_plan', f.plan);
|
|
367
|
+
// Disabled by default: un-timed commands stay un-timed.
|
|
368
|
+
const runaway = { toolName: 'bash', input: { command: 'find / -name verify_ursina.py' } };
|
|
369
|
+
f.hooks.tool_call(runaway, f.ctx);
|
|
370
|
+
assert.equal(runaway.input.timeout, undefined);
|
|
371
|
+
// Enabled: caps missing and oversized timeouts, preserves tighter explicit ones.
|
|
372
|
+
f.flags['delivery-bash-cap'] = 120;
|
|
373
|
+
f.hooks.tool_call(runaway, f.ctx);
|
|
374
|
+
assert.equal(runaway.input.timeout, 120);
|
|
375
|
+
const tight = { toolName: 'bash', input: { command: 'ls', timeout: 10 } };
|
|
376
|
+
f.hooks.tool_call(tight, f.ctx);
|
|
377
|
+
assert.equal(tight.input.timeout, 10);
|
|
378
|
+
const over = { toolName: 'powershell', input: { command: 'x', timeout: 9999 } };
|
|
379
|
+
f.hooks.tool_call(over, f.ctx);
|
|
380
|
+
assert.equal(over.input.timeout, 120);
|
|
381
|
+
// Non-shell tools and blocked-write gating are untouched.
|
|
382
|
+
const read = { toolName: 'read', input: { path: 'app.py' } };
|
|
383
|
+
f.hooks.tool_call(read);
|
|
384
|
+
assert.equal(read.input.timeout, undefined);
|
|
385
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }).block, true);
|
|
386
|
+
});
|
|
387
|
+
test('edit calls normalize a JSON-encoded edits array', options, async t => {
|
|
388
|
+
const f = fixture(t);
|
|
389
|
+
await f.call('delivery_plan', f.plan);
|
|
390
|
+
const event = {
|
|
391
|
+
toolName: 'edit',
|
|
392
|
+
input: {
|
|
393
|
+
path: 'app.py',
|
|
394
|
+
edits: JSON.stringify([{ oldText: 'print(1)', newText: 'print(2)' }]),
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
assert.equal(f.hooks.tool_call(event, f.ctx), undefined);
|
|
398
|
+
assert.deepEqual(event.input.edits, [{ oldText: 'print(1)', newText: 'print(2)' }]);
|
|
399
|
+
});
|
|
400
|
+
test('bash cap blocks process-wide termination but allows targeted child cleanup', options, async t => {
|
|
401
|
+
const f = fixture(t);
|
|
402
|
+
await f.call('delivery_plan', f.plan);
|
|
403
|
+
f.flags['delivery-bash-cap'] = 120;
|
|
404
|
+
for (const command of [
|
|
405
|
+
'pkill -f python',
|
|
406
|
+
'killall node',
|
|
407
|
+
'kill -9 -1',
|
|
408
|
+
'kill 0',
|
|
409
|
+
'taskkill /IM python.exe /F',
|
|
410
|
+
'Stop-Process -Name python',
|
|
411
|
+
'Get-Process | Stop-Process',
|
|
412
|
+
]) {
|
|
413
|
+
const broadKill = { toolName: 'bash', input: { command } };
|
|
414
|
+
assert.match(
|
|
415
|
+
f.hooks.tool_call(broadKill, f.ctx).reason,
|
|
416
|
+
/Broad process termination/
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const targetedKill = {
|
|
421
|
+
toolName: 'bash',
|
|
422
|
+
input: { command: 'kill "$child_pid"' }
|
|
423
|
+
};
|
|
424
|
+
assert.equal(f.hooks.tool_call(targetedKill, f.ctx), undefined);
|
|
425
|
+
assert.equal(targetedKill.input.timeout, 120);
|
|
426
|
+
});
|
|
427
|
+
test('bounded runs cap whole-file rewrites per path and pace tool loops', options, async t => {
|
|
428
|
+
const f = fixture(t);
|
|
429
|
+
f.flags['delivery-strict'] = false;
|
|
430
|
+
f.flags['delivery-rewrite-cap'] = 2;
|
|
431
|
+
await f.call('delivery_plan', f.plan);
|
|
432
|
+
const write = path => f.hooks.tool_call({ toolName: 'write', input: { path } }, f.ctx);
|
|
433
|
+
f.flags['delivery-protect-existing'] = true;
|
|
434
|
+
f.hooks.session_start({}, f.ctx);
|
|
435
|
+
assert.match(write('app.py').reason, /Preserve the existing app.py implementation/);
|
|
436
|
+
assert.equal(f.hooks.tool_call({ toolName: 'edit', input: { path: 'app.py' } }, f.ctx), undefined);
|
|
437
|
+
assert.match(
|
|
438
|
+
f.hooks.tool_call(
|
|
439
|
+
{ toolName: 'bash', input: { command: `cat > "${join(f.cwd, 'app.py')}" <<'EOF'\nprint(2)\nEOF` } },
|
|
440
|
+
f.ctx,
|
|
441
|
+
).reason,
|
|
442
|
+
/Shell redirection cannot replace/,
|
|
443
|
+
);
|
|
444
|
+
assert.match(
|
|
445
|
+
f.hooks.tool_call(
|
|
446
|
+
{ toolName: 'bash', input: { command: "python - <<'PY'\nwith open('app.py', 'w') as f:\n f.write('print(2)')\nPY" } },
|
|
447
|
+
f.ctx,
|
|
448
|
+
).reason,
|
|
449
|
+
/script cannot replace/,
|
|
450
|
+
);
|
|
451
|
+
assert.match(
|
|
452
|
+
f.hooks.tool_call(
|
|
453
|
+
{ toolName: 'bash', input: { command: `mv /tmp/replacement.py "${join(f.cwd, 'app.py')}"` } },
|
|
454
|
+
f.ctx,
|
|
455
|
+
).reason,
|
|
456
|
+
/cannot remove or replace/,
|
|
457
|
+
);
|
|
458
|
+
f.flags['delivery-protect-existing'] = false;
|
|
459
|
+
f.hooks.session_start({}, f.ctx);
|
|
460
|
+
assert.equal(write('app.py'), undefined);
|
|
461
|
+
assert.equal(write('app.py'), undefined);
|
|
462
|
+
assert.match(write('app.py').reason, /Full-file rewrite limit reached/);
|
|
463
|
+
assert.equal(write('README.md'), undefined);
|
|
464
|
+
assert.equal(f.hooks.tool_call({ toolName: 'edit', input: { path: 'app.py' } }, f.ctx), undefined);
|
|
465
|
+
assert.match(
|
|
466
|
+
f.hooks.tool_call(
|
|
467
|
+
{ toolName: 'write', input: { path: f.cwd.replace(/^\/+/, '') + '/nested.py' } },
|
|
468
|
+
f.ctx,
|
|
469
|
+
).reason,
|
|
470
|
+
/recreates the working directory/,
|
|
471
|
+
);
|
|
472
|
+
assert.match(
|
|
473
|
+
f.hooks.tool_call(
|
|
474
|
+
{ toolName: 'write', input: { path: `${f.cwd.split('/').at(-1)}/nested.py` } },
|
|
475
|
+
f.ctx,
|
|
476
|
+
).reason,
|
|
477
|
+
/recreates the working directory/,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
f.flags['delivery-turn-delay-ms'] = 20;
|
|
481
|
+
const started = Date.now();
|
|
482
|
+
await f.hooks.tool_result({ toolName: 'read', isError: false });
|
|
483
|
+
assert.ok(Date.now() - started >= 15);
|
|
484
|
+
|
|
485
|
+
f.flags['delivery-turn-delay-ms'] = 10;
|
|
486
|
+
const scaled = Date.now();
|
|
487
|
+
await f.hooks.tool_result(
|
|
488
|
+
{ toolName: 'read', isError: false },
|
|
489
|
+
{ getContextUsage: () => ({ tokens: 100001 }) },
|
|
490
|
+
);
|
|
491
|
+
assert.ok(Date.now() - scaled >= 25);
|
|
492
|
+
|
|
493
|
+
f.flags['delivery-turn-delay-ms'] = 0;
|
|
494
|
+
f.flags['delivery-tool-output-cap'] = 100;
|
|
495
|
+
const bounded = await f.hooks.tool_result({
|
|
496
|
+
toolName: 'bash',
|
|
497
|
+
isError: false,
|
|
498
|
+
content: [{ type: 'text', text: `${'head'.repeat(30)}${'tail'.repeat(30)}` }],
|
|
499
|
+
});
|
|
500
|
+
assert.equal(bounded.content[0].text.length, 100);
|
|
501
|
+
assert.match(bounded.content[0].text, /characters omitted/);
|
|
502
|
+
assert.match(bounded.content[0].text, /^head/);
|
|
503
|
+
assert.match(bounded.content[0].text, /tail$/);
|
|
504
|
+
});
|
|
505
|
+
test('optional delivery context is explicit and task agnostic', options, t => {
|
|
506
|
+
const f = fixture(t);
|
|
507
|
+
writeFileSync(join(f.cwd, 'quality-context.md'), 'Require evidence from the public user path.');
|
|
508
|
+
f.flags['delivery-context'] = 'quality-context.md';
|
|
509
|
+
f.hooks.session_start({}, f.ctx);
|
|
510
|
+
const context = f.hooks.before_agent_start({ systemPrompt: 'base', prompt: 'Build a voxel game' });
|
|
511
|
+
assert.match(context.systemPrompt, /Require evidence from the public user path/);
|
|
512
|
+
assert.doesNotMatch(context.systemPrompt, /camera.ui_lens.set_film_size/);
|
|
513
|
+
assert.match(context.systemPrompt, /Preserve every user requirement/);
|
|
514
|
+
});
|
|
515
|
+
test('failed infrastructure probes append diagnostics while preserving original output', async t => {
|
|
516
|
+
const f = fixture(t);
|
|
517
|
+
const event = { toolName: 'bash', content: [{ type: 'text', text: 'Cannot find module tsx; exit code: 1' }] };
|
|
518
|
+
assert.equal(await f.hooks.tool_result(event, f.ctx), undefined);
|
|
519
|
+
const result = await f.hooks.tool_result(event, f.ctx);
|
|
520
|
+
assert.equal(result.content[0].text, event.content[0].text);
|
|
521
|
+
assert.match(result.content[1].text, /Diagnostic checkpoint/);
|
|
522
|
+
assert.equal(f.entries.length, 0);
|
|
523
|
+
});
|
|
524
|
+
test('standard mode accepts compact contracts and bounded discovery probe', async t => {
|
|
525
|
+
const f = fixture(t, { approve: false }); f.flags['delivery-workflow'] = 'standard';
|
|
526
|
+
const result = await f.call('delivery_plan', f.plan);
|
|
527
|
+
assert.match(result.content[0].text, /Implement now/);
|
|
528
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write', input: { path: 'new.py' } }, f.ctx), undefined);
|
|
529
|
+
const probe = JSON.parse((await f.call('delivery_probe', { kind: 'node-version' })).content[0].text);
|
|
530
|
+
assert.equal(probe.code, 0); assert.equal(probe.evidence, false);
|
|
531
|
+
await assert.rejects(f.call('delivery_probe', { kind: 'shell' }), /Unknown discovery/);
|
|
532
|
+
await f.call('delivery_check', { id: 'all' });
|
|
533
|
+
await assert.rejects(f.call('delivery_finish', { status: 'verified', review: 'done', launch: 'n/a', limitations: [] }), /Review:/);
|
|
534
|
+
});
|
|
535
|
+
test('resumed runs keep their planned workflow guidance regardless of the launch flag', async t => {
|
|
536
|
+
const f = fixture(t, { approve: false });
|
|
537
|
+
await f.call('delivery_plan', f.plan);
|
|
538
|
+
assert.equal(f.entries.at(-1).data.workflowMode, 'strict');
|
|
539
|
+
f.flags['delivery-workflow'] = 'standard';
|
|
540
|
+
f.hooks.session_start({}, f.ctx);
|
|
541
|
+
const resumed = f.hooks.before_agent_start({ systemPrompt: 'base', prompt: 'continue the implementation' }, f.ctx).systemPrompt;
|
|
542
|
+
assert.doesNotMatch(resumed, /Standard delivery:/);
|
|
543
|
+
assert.match(f.hooks.tool_call({ toolName: 'write', input: { path: 'app.py' } }, f.ctx).reason, /approve/);
|
|
544
|
+
const g = fixture(t, { approve: false }); g.flags['delivery-workflow'] = 'standard';
|
|
545
|
+
await g.call('delivery_plan', g.plan);
|
|
546
|
+
g.flags['delivery-workflow'] = 'strict';
|
|
547
|
+
g.hooks.session_start({}, g.ctx);
|
|
548
|
+
assert.match(g.hooks.before_agent_start({ systemPrompt: 'base', prompt: 'continue the implementation' }, g.ctx).systemPrompt, /Standard delivery:/);
|
|
549
|
+
});
|
|
550
|
+
test('required validators cannot be omitted or replaced by model plans', options, async t => {
|
|
551
|
+
const f = fixture(t);
|
|
552
|
+
const manifest = join(f.cwd, 'validators.json');
|
|
553
|
+
writeFileSync(manifest, JSON.stringify({ version: 1, checks: [{ id: 'oracle', kind: 'test', argv: [process.execPath, '-e', 'process.exit(9)'], timeoutSeconds: 5 }] }));
|
|
554
|
+
f.flags['delivery-validators'] = manifest;
|
|
555
|
+
f.hooks.session_start({}, f.ctx);
|
|
556
|
+
await f.call('delivery_plan', f.plan);
|
|
557
|
+
const state = f.entries.at(-1).data;
|
|
558
|
+
assert.ok(state.plan.checks.some(c => c.id === 'required_oracle'));
|
|
559
|
+
await f.call('delivery_check', { id: 'run' });
|
|
560
|
+
await assert.rejects(f.call('delivery_finish', { status: 'verified', review: 'All good', launch: 'python app.py', limitations: [] }), /required_oracle/);
|
|
561
|
+
await assert.rejects(f.call('delivery_check', { id: 'all' }), /"code": 9/);
|
|
562
|
+
f.plan.checks.push({ id: 'required_oracle', kind: 'runtime', argv: [process.execPath, '-e', 'process.exit(0)'], timeoutSeconds: 5 });
|
|
563
|
+
await f.call('delivery_plan', f.plan);
|
|
564
|
+
assert.match(f.entries.at(-1).data.plan.checks.find(c => c.id === 'required_oracle').argv[2], /exit\(9\)/);
|
|
565
|
+
});
|
|
566
|
+
test('invalid required manifest fails closed', options, async t => {
|
|
567
|
+
const f = fixture(t);
|
|
568
|
+
f.flags['delivery-validators'] = join(f.cwd, 'missing.json');
|
|
569
|
+
f.hooks.session_start({}, f.ctx);
|
|
570
|
+
await assert.rejects(f.call('delivery_plan', f.plan), /Invalid required validators/);
|
|
571
|
+
});
|
|
572
|
+
test('source mutation by a verifier is recorded as failed evidence', options, async t => {
|
|
573
|
+
const f = fixture(t);
|
|
574
|
+
f.plan.checks[0].argv = [process.execPath, '-e', 'require("node:fs").writeFileSync("app.py", "changed")'];
|
|
575
|
+
await f.call('delivery_plan', f.plan);
|
|
576
|
+
await assert.rejects(f.call('delivery_check', { id: 'run' }), /changedDuringCheck/);
|
|
577
|
+
assert.equal(f.entries.at(-1).data.evidence.run.passed, false);
|
|
578
|
+
});
|
|
579
|
+
test('request guidance is injected behind the scenes and survives follow-ups', options, async t => {
|
|
580
|
+
const f = fixture(t);
|
|
581
|
+
writeFileSync(join(f.cwd, 'package.json'), JSON.stringify({ dependencies: { next: '15.1.7' } }));
|
|
582
|
+
const prompt = 'Add authenticated trades and a portfolio time series chart backed by a third-party API';
|
|
583
|
+
const started = f.hooks.before_agent_start({ systemPrompt: 'base', prompt }, f.ctx);
|
|
584
|
+
for (const id of ['web-application', 'authenticated-web', 'transactional-data', 'external-api', 'data-visualization']) assert.match(started.systemPrompt, new RegExp(`\\[${id}\\]`));
|
|
585
|
+
await f.call('delivery_plan', f.plan);
|
|
586
|
+
const state = f.entries.at(-1).data;
|
|
587
|
+
// Persisted only for internal continuity across resumes and follow-up prompts.
|
|
588
|
+
assert.deepEqual(state.guidanceProfiles, ['web-application', 'authenticated-web', 'transactional-data', 'external-api', 'data-visualization']);
|
|
589
|
+
assert.equal(state.plan.guidanceProfiles, undefined);
|
|
590
|
+
assert.doesNotMatch(readFileSync(join(f.cwd, '.harness', 'integration-session', state.runId, 'plan.d2'), 'utf8'), /guidance/i);
|
|
591
|
+
const followUp = f.hooks.before_agent_start({ systemPrompt: 'base', prompt: 'Fix that failure' }, f.ctx);
|
|
592
|
+
assert.match(followUp.systemPrompt, /\[transactional-data\]/);
|
|
593
|
+
assert.match(followUp.systemPrompt, /\[data-visualization\]/);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test('blocked delivery succeeds and finishes cleanly even when evidence scope limit is exceeded', options, async t => {
|
|
597
|
+
const f = fixture(t);
|
|
598
|
+
const oldEnv = process.env.CARTHAGENT_MAX_FILES;
|
|
599
|
+
t.after(() => {
|
|
600
|
+
if (oldEnv === undefined) delete process.env.CARTHAGENT_MAX_FILES;
|
|
601
|
+
else process.env.CARTHAGENT_MAX_FILES = oldEnv;
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Create plan
|
|
605
|
+
await f.call('delivery_plan', f.plan);
|
|
606
|
+
|
|
607
|
+
// Set tight file limit (1 file) and add a second file
|
|
608
|
+
process.env.CARTHAGENT_MAX_FILES = '1';
|
|
609
|
+
writeFileSync(join(f.cwd, 'extra.txt'), 'extra');
|
|
610
|
+
|
|
611
|
+
// delivery_status tolerates scope limit
|
|
612
|
+
const status = await f.call('delivery_status');
|
|
613
|
+
assert.match(status.content[0].text, /scope error/);
|
|
614
|
+
|
|
615
|
+
// delivery_check reports scope error with guidance
|
|
616
|
+
await assert.rejects(
|
|
617
|
+
f.call('delivery_check', { id: 'run' }),
|
|
618
|
+
/Evidence scope exceeds/
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// delivery_finish with status='verified' must still fail
|
|
622
|
+
await assert.rejects(
|
|
623
|
+
f.call('delivery_finish', {
|
|
624
|
+
status: 'verified',
|
|
625
|
+
review: 'Trying to verify despite scope limits',
|
|
626
|
+
launch: 'python app.py',
|
|
627
|
+
limitations: [],
|
|
628
|
+
}),
|
|
629
|
+
/Evidence scope exceeds/
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
// delivery_finish with status='blocked' must SUCCEED without looping or throwing
|
|
633
|
+
const finishBlocked = await f.call('delivery_finish', {
|
|
634
|
+
status: 'blocked',
|
|
635
|
+
review: 'Repository exceeds evidence scope limit; cannot run fingerprint',
|
|
636
|
+
launch: 'python app.py',
|
|
637
|
+
limitations: ['Repository exceeds maximum file count limit for automated checks.'],
|
|
638
|
+
});
|
|
639
|
+
assert.equal(finishBlocked.content[0].type, 'text');
|
|
640
|
+
const lastEntry = f.entries.at(-1).data;
|
|
641
|
+
assert.equal(lastEntry.status, 'blocked');
|
|
642
|
+
assert.equal(lastEntry.handoff.fingerprint, 'unfingerprinted:scope_exceeded');
|
|
643
|
+
|
|
644
|
+
// agent_end should NOT trigger follow-up nudges since status is blocked
|
|
645
|
+
f.messages.length = 0;
|
|
646
|
+
f.hooks.agent_end({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, f.ctx);
|
|
647
|
+
assert.equal(f.messages.length, 0);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('informational receipts get a classification hint, build tasks do not', options, t => {
|
|
651
|
+
const f = fixture(t);
|
|
652
|
+
const question = f.hooks.before_agent_start({ systemPrompt: 'base', prompt: 'what does app.py do?' }, f.ctx);
|
|
653
|
+
assert.match(question.systemPrompt, /Receipt check/);
|
|
654
|
+
assert.match(question.systemPrompt, /verification:"none"/);
|
|
655
|
+
const build = f.hooks.before_agent_start({ systemPrompt: 'base', prompt: 'build a task cli with tests' }, f.ctx);
|
|
656
|
+
assert.doesNotMatch(build.systemPrompt, /Receipt check/);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('verification:none plan finishes cleanly with no checks and no repair nudges', options, async t => {
|
|
660
|
+
const f = fixture(t);
|
|
661
|
+
await f.call('delivery_plan', { ...f.plan, verification: 'none', checks: [], acceptance: [] });
|
|
662
|
+
assert.equal(f.entries.at(-1).data.plan.verification, 'none');
|
|
663
|
+
// delivery_check explains the route to finish instead of erroring into a loop.
|
|
664
|
+
const noChecks = await f.call('delivery_check', { id: 'all' });
|
|
665
|
+
assert.match(noChecks.content[0].text, /no checks to run/);
|
|
666
|
+
const finish = { status: 'verified', review: 'Answered the question from source.', launch: 'n/a', limitations: [] };
|
|
667
|
+
await f.call('delivery_finish', finish);
|
|
668
|
+
const handoff = f.entries.at(-1).data.handoff;
|
|
669
|
+
assert.equal(handoff.status, 'verified');
|
|
670
|
+
assert.equal(handoff.verification, 'none');
|
|
671
|
+
assert.equal(handoff.advisory, true);
|
|
672
|
+
// Unlike a required verified run with a stale fingerprint, informative plans are terminal.
|
|
673
|
+
f.messages.length = 0;
|
|
674
|
+
f.hooks.agent_end({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, f.ctx);
|
|
675
|
+
assert.equal(f.messages.length, 0);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('advisory commands cannot bypass approval; informational finish remains available', options, async t => {
|
|
679
|
+
const f = fixture(t);
|
|
680
|
+
const failing = { ...f.plan, verification: 'advisory' };
|
|
681
|
+
failing.checks = [{ ...f.plan.checks[0], argv: [process.execPath, '-e', 'console.log("advisory context"); process.exit(4)'] }];
|
|
682
|
+
await f.call('delivery_plan', failing);
|
|
683
|
+
await assert.rejects(f.call('delivery_check', { id: 'all' }), /Implementation locked/);
|
|
684
|
+
assert.deepEqual(f.entries.at(-1).data.evidence, {});
|
|
685
|
+
// Advisory classification cannot authorize arbitrary process execution.
|
|
686
|
+
await f.call('delivery_finish', { status: 'verified', review: 'Reported the measured failure as context.', launch: 'n/a', limitations: ['advisory check failed by design'] });
|
|
687
|
+
const handoff = f.entries.at(-1).data.handoff;
|
|
688
|
+
assert.equal(handoff.advisory, true);
|
|
689
|
+
assert.equal(handoff.verification, 'advisory');
|
|
690
|
+
f.messages.length = 0;
|
|
691
|
+
f.hooks.agent_end({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, f.ctx);
|
|
692
|
+
assert.equal(f.messages.length, 0);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test('pre-verify reclassification is refused once evidence exists', options, async t => {
|
|
696
|
+
const f = fixture(t);
|
|
697
|
+
await f.call('delivery_plan', f.plan);
|
|
698
|
+
// No evidence yet: the pre-verify re-check may downgrade to informational.
|
|
699
|
+
await f.call('delivery_plan', { ...f.plan, verification: 'none', checks: [], acceptance: [] });
|
|
700
|
+
assert.equal(f.entries.at(-1).data.plan.verification, 'none');
|
|
701
|
+
await f.call('delivery_plan', f.plan);
|
|
702
|
+
await f.call('delivery_check', { id: 'run' });
|
|
703
|
+
await assert.rejects(
|
|
704
|
+
f.call('delivery_plan', { ...f.plan, verification: 'none', checks: [], acceptance: [] }),
|
|
705
|
+
/Cannot downgrade verification after checks have run/,
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test('required validators force verification:required on an informational plan', options, async t => {
|
|
710
|
+
const f = fixture(t);
|
|
711
|
+
const manifest = join(f.cwd, 'validators.json');
|
|
712
|
+
writeFileSync(manifest, JSON.stringify({ version: 1, checks: [{ id: 'oracle', kind: 'test', argv: [process.execPath, '-e', 'process.exit(0)'], timeoutSeconds: 5 }] }));
|
|
713
|
+
f.flags['delivery-validators'] = manifest;
|
|
714
|
+
f.hooks.session_start({}, f.ctx);
|
|
715
|
+
await f.call('delivery_plan', { ...f.plan, verification: 'none', checks: [], acceptance: [] });
|
|
716
|
+
const state = f.entries.at(-1).data;
|
|
717
|
+
assert.equal(state.plan.verification, 'required');
|
|
718
|
+
assert.ok(state.plan.checks.some(c => c.id === 'required_oracle'));
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
test('session budget survives replan, input and branch restoration; only command resets', options, async t => {
|
|
723
|
+
const f = fixture(t);
|
|
724
|
+
f.flags['delivery-max-tools'] = '2';
|
|
725
|
+
f.hooks.agent_start({}, f.ctx);
|
|
726
|
+
await f.call('delivery_plan', f.plan);
|
|
727
|
+
assert.equal(f.hooks.tool_call({ toolName: 'delivery_status' }, f.ctx), undefined);
|
|
728
|
+
await f.call('delivery_plan', f.plan);
|
|
729
|
+
f.hooks.input({ source: 'user' });
|
|
730
|
+
f.hooks.session_tree({}, f.ctx);
|
|
731
|
+
assert.equal(f.hooks.tool_call({ toolName: 'delivery_status' }, f.ctx), undefined);
|
|
732
|
+
assert.equal(f.hooks.tool_call({ toolName: 'read' }, f.ctx).block, true);
|
|
733
|
+
f.hooks.agent_end({ messages: [] }, f.ctx);
|
|
734
|
+
assert.equal(f.messages.length, 1);
|
|
735
|
+
const stop = JSON.parse(f.messages[0].content);
|
|
736
|
+
assert.equal(stop.reason, 'tool-calls');
|
|
737
|
+
assert.deepEqual(stop.requirements, f.plan.acceptance);
|
|
738
|
+
assert.ok(f.ctx.aborted > 0);
|
|
739
|
+
f.commands['delivery-budget-reset'].handler('', f.ctx);
|
|
740
|
+
assert.equal(f.hooks.tool_call({ toolName: 'read' }, f.ctx), undefined);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test('elapsed budget aborts an active turn without a tool and stays stopped on restore', options, async t => {
|
|
744
|
+
const f = fixture(t);
|
|
745
|
+
f.flags['delivery-max-seconds'] = '1';
|
|
746
|
+
f.hooks.agent_start({}, f.ctx);
|
|
747
|
+
await new Promise(resolve => setTimeout(resolve, 1150));
|
|
748
|
+
assert.equal(f.ctx.aborted, 1);
|
|
749
|
+
assert.equal(JSON.parse(f.messages[0].content).reason, 'elapsed-time');
|
|
750
|
+
f.hooks.session_start({}, f.ctx);
|
|
751
|
+
f.hooks.agent_start({}, f.ctx);
|
|
752
|
+
assert.equal(f.messages.length, 1);
|
|
753
|
+
assert.equal(f.ctx.aborted, 2);
|
|
754
|
+
f.hooks.session_shutdown();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test('repair allowance persists across user follow-ups and stops automatic continuation', options, async t => {
|
|
758
|
+
const f = fixture(t);
|
|
759
|
+
f.flags['delivery-max-repairs'] = '1';
|
|
760
|
+
await f.call('delivery_plan', f.plan);
|
|
761
|
+
const event = { messages: [{ role: 'assistant', stopReason: 'stop' }] };
|
|
762
|
+
f.hooks.agent_end(event, f.ctx);
|
|
763
|
+
assert.equal(f.messages.length, 1);
|
|
764
|
+
f.hooks.input({ source: 'user' });
|
|
765
|
+
f.hooks.session_start({}, f.ctx);
|
|
766
|
+
f.hooks.agent_end(event, f.ctx);
|
|
767
|
+
assert.equal(f.messages.length, 2);
|
|
768
|
+
assert.equal(JSON.parse(f.messages[1].content).reason, 'repair-rounds');
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test('revision tool preserves evidence, progress and discoveries across restore and compaction', async t => {
|
|
772
|
+
const f = fixture(t);
|
|
773
|
+
await f.call('delivery_plan', f.plan);
|
|
774
|
+
await f.call('delivery_progress', { step: 0, status: 'done' });
|
|
775
|
+
await f.call('delivery_check', { id: 'all' });
|
|
776
|
+
const original = structuredClone(f.entries.at(-1).data);
|
|
777
|
+
await f.call('delivery_revise', { reason: 'Discovered a second caller during repository inspection', patch: {
|
|
778
|
+
steps: ['Inspect second caller', ...f.plan.steps], assumptions: ['Keep the old caller compatible'],
|
|
779
|
+
} });
|
|
780
|
+
const revised = f.entries.at(-1).data;
|
|
781
|
+
assert.equal(revised.runId, original.runId);
|
|
782
|
+
assert.equal(revised.revision, original.revision + 1);
|
|
783
|
+
assert.equal(revised.evidence.run.revision, revised.revision);
|
|
784
|
+
assert.equal(revised.evidence.run.logPath, original.evidence.run.logPath);
|
|
785
|
+
assert.equal(revised.stepStatus.step1, 'done');
|
|
786
|
+
f.hooks.session_start({}, f.ctx);
|
|
787
|
+
const context = f.hooks.context({ messages: [] }).messages.map(m => m.content).join('\n');
|
|
788
|
+
assert.match(context, /Keep the old caller compatible/);
|
|
789
|
+
assert.match(context, /Discovered a second caller/);
|
|
790
|
+
assert.match(context, /"step1":"done"/);
|
|
791
|
+
const status = JSON.parse((await f.call('delivery_status')).content[0].text);
|
|
792
|
+
assert.deepEqual(status.pendingChecks, []);
|
|
793
|
+
assert.equal(status.revisions, undefined, 'full snapshots stay in report, not status context');
|
|
794
|
+
assert.equal(status.revisionHistory.length, 1);
|
|
795
|
+
writeFileSync(join(f.cwd, 'app.py'), 'print(2)');
|
|
796
|
+
await f.call('delivery_revise', { reason: 'Implementation changed', patch: { goal: f.plan.goal } });
|
|
797
|
+
assert.deepEqual(JSON.parse((await f.call('delivery_status')).content[0].text).pendingChecks, ['run']);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test('repeated full plans cannot erase verification history to allow downgrades', async t => {
|
|
801
|
+
const f = fixture(t);
|
|
802
|
+
await f.call('delivery_plan', f.plan);
|
|
803
|
+
await f.call('delivery_check', { id: 'all' });
|
|
804
|
+
const changed = structuredClone(f.plan);
|
|
805
|
+
changed.checks[0].timeoutSeconds = 6;
|
|
806
|
+
await f.call('delivery_plan', changed);
|
|
807
|
+
assert.deepEqual(f.entries.at(-1).data.evidence, {});
|
|
808
|
+
await assert.rejects(f.call('delivery_plan', { ...f.plan, verification: 'none', acceptance: [], checks: [] }), /Cannot downgrade/);
|
|
809
|
+
await f.call('delivery_finish', { status: 'blocked', review: 'Need external decision', launch: 'n/a', limitations: ['Missing decision'] });
|
|
810
|
+
await assert.rejects(f.call('delivery_revise', { reason: 'Resume without tests', patch: { verification: 'none', acceptance: [], checks: [] } }), /Cannot downgrade/);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('structured steps and promised outputs are enforced consistently by status and finish', async t => {
|
|
814
|
+
const f = fixture(t);
|
|
815
|
+
f.plan.steps = [
|
|
816
|
+
{ id: 'build', title: 'Implement', checks: ['run'] },
|
|
817
|
+
{ id: 'review', title: 'Review compatibility', dependsOn: ['build'] },
|
|
818
|
+
];
|
|
819
|
+
f.plan.outputs = ['artifacts/report.json'];
|
|
820
|
+
await f.call('delivery_plan', f.plan);
|
|
821
|
+
await assert.rejects(f.call('delivery_progress', { step: 'review', status: 'active' }), /dependencies/);
|
|
822
|
+
const checked = JSON.parse((await f.call('delivery_check', { id: 'all' })).content[0].text);
|
|
823
|
+
assert.equal(checked.readyToFinish, false);
|
|
824
|
+
const finish = { status: 'verified', review: 'Reviewed', launch: 'n/a', limitations: [] };
|
|
825
|
+
await assert.rejects(f.call('delivery_finish', finish), /Incomplete steps/);
|
|
826
|
+
await f.call('delivery_progress', { step: 'build', status: 'done' });
|
|
827
|
+
await f.call('delivery_progress', { step: 'review', status: 'done' });
|
|
828
|
+
await assert.rejects(f.call('delivery_finish', finish), /Missing outputs: artifacts\/report.json/);
|
|
829
|
+
const { mkdirSync } = await import('node:fs');
|
|
830
|
+
mkdirSync(join(f.cwd, 'artifacts'));
|
|
831
|
+
writeFileSync(join(f.cwd, 'artifacts', 'report.json'), '{}');
|
|
832
|
+
await assert.rejects(f.call('delivery_finish', finish), /delivery_review/);
|
|
833
|
+
const capture = JSON.parse((await f.call('delivery_review', { action: 'inspect' })).content[0].text);
|
|
834
|
+
await f.call('delivery_review', { action: 'record', captureId: capture.id, coverage: [{ requirement: 'Runs', assertions: 'runtime command exits zero' }], probes: ['node runtime probe returned passed'], findings: [], limitations: ['No Git baseline in this fixture'] });
|
|
835
|
+
assert.equal(JSON.parse((await f.call('delivery_status')).content[0].text).readyToFinish, true);
|
|
836
|
+
await f.call('delivery_finish', finish);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test('review acceptance gaps persist through session restore and cannot be relabeled as limitations', async t => {
|
|
840
|
+
const f = fixture(t);
|
|
841
|
+
await f.call('delivery_plan', f.plan);
|
|
842
|
+
await f.call('delivery_check', { id: 'all' });
|
|
843
|
+
const capture = JSON.parse((await f.call('delivery_review', { action: 'inspect' })).content[0].text);
|
|
844
|
+
const review = { action: 'record', captureId: capture.id, coverage: [{ requirement: 'Runs', assertions: 'Only component behavior checked', status: 'unverified' }], probes: ['component check exited zero'], findings: [], limitations: [] };
|
|
845
|
+
const result = JSON.parse((await f.call('delivery_review', review)).content[0].text);
|
|
846
|
+
assert.equal(result.recorded, false);
|
|
847
|
+
assert.equal(f.entries.at(-1).data.reviewFindings.length, 1);
|
|
848
|
+
await f.hooks.session_start({}, f.ctx);
|
|
849
|
+
review.coverage[0] = { requirement: 'Runs', assertions: 'component check exited zero' };
|
|
850
|
+
review.limitations = ['No integration test'];
|
|
851
|
+
assert.equal(JSON.parse((await f.call('delivery_review', review)).content[0].text).recorded, false);
|
|
852
|
+
await assert.rejects(f.call('delivery_finish', { status: 'verified', review: 'done', launch: 'n/a', limitations: review.limitations }), /Unresolved review finding/);
|
|
853
|
+
const resolved = await f.call('delivery_review', { ...review, resolutions: [{ id: result.unresolvedFindings[0].id, change: 'Checked actual entrypoint', evidence: 'Entrypoint executes and returns expected output' }] });
|
|
854
|
+
assert.equal(JSON.parse(resolved.content[0].text).recorded, true);
|
|
855
|
+
await f.call('delivery_finish', { status: 'verified', review: 'entrypoint checked', launch: 'n/a', limitations: [] });
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test('all checks collects failures and independent successes rather than stopping at first failure', async t => {
|
|
859
|
+
const f = fixture(t);
|
|
860
|
+
f.plan.checks = [
|
|
861
|
+
{ ...f.plan.checks[0], argv: [process.execPath, '-e', 'console.log("first failure"); process.exit(7)'] },
|
|
862
|
+
{ ...f.plan.checks[0], id: 'second' },
|
|
863
|
+
{ ...f.plan.checks[0], id: 'third', argv: [process.execPath, '-e', 'process.exit(9)'] },
|
|
864
|
+
];
|
|
865
|
+
await f.call('delivery_plan', f.plan);
|
|
866
|
+
await assert.rejects(f.call('delivery_check', { id: 'all' }), /All requested checks ran/);
|
|
867
|
+
const evidence = f.entries.at(-1).data.evidence;
|
|
868
|
+
assert.equal(evidence.run.code, 7);
|
|
869
|
+
assert.equal(evidence.second.passed, true);
|
|
870
|
+
assert.equal(evidence.third.code, 9);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test('revision respects workspace supersession and user-owned validators', async t => {
|
|
874
|
+
const f = fixture(t);
|
|
875
|
+
const manifest = join(f.cwd, 'validators.json');
|
|
876
|
+
writeFileSync(manifest, JSON.stringify({ version: 1, checks: [{ id: 'oracle', kind: 'test', argv: [process.execPath, '-e', 'process.exit(9)'], timeoutSeconds: 5 }] }));
|
|
877
|
+
f.flags['delivery-validators'] = manifest;
|
|
878
|
+
f.hooks.session_start({}, f.ctx);
|
|
879
|
+
await f.call('delivery_plan', f.plan);
|
|
880
|
+
await f.call('delivery_revise', { reason: 'Try overriding validator', patch: { checks: [...f.plan.checks, { ...f.plan.checks[0], id: 'required_oracle' }] } });
|
|
881
|
+
assert.match(f.entries.at(-1).data.plan.checks.find(c => c.id === 'required_oracle').argv[2], /exit\(9\)/);
|
|
882
|
+
const other = createReport(f.cwd, { status: 'implementing', plan: f.plan, evidence: {} });
|
|
883
|
+
selectReport(f.cwd, other.path);
|
|
884
|
+
await assert.rejects(f.call('delivery_revise', { reason: 'Stale writer', patch: { goal: 'Changed' } }), /superseded/);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test('budget status command reports remaining allowance without consuming tools', options, t => {
|
|
888
|
+
const f = fixture(t);
|
|
889
|
+
f.flags['delivery-max-tools'] = '2';
|
|
890
|
+
f.hooks.agent_start({}, f.ctx);
|
|
891
|
+
f.hooks.tool_call({ toolName: 'read' }, f.ctx);
|
|
892
|
+
f.commands['delivery-budget-status'].handler('', f.ctx);
|
|
893
|
+
const status = JSON.parse(f.messages.at(-1).content);
|
|
894
|
+
assert.equal(status.tools, 1);
|
|
895
|
+
assert.equal(status.remaining.tools, 1);
|
|
896
|
+
assert.equal(status.reason, null);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
test('unapproved tasks fail closed for shell, custom tools, tests and disabled legacy strict flag', async t => {
|
|
901
|
+
const f = fixture(t, { approve: false });
|
|
902
|
+
f.flags['delivery-strict'] = false;
|
|
903
|
+
for (const toolName of ['write', 'edit', 'bash', 'powershell', 'custom_runner', 'delivery_check', 'delivery_progress']) {
|
|
904
|
+
assert.equal(f.hooks.tool_call({ toolName, input: { command: 'ls' } }, f.ctx).block, true);
|
|
905
|
+
}
|
|
906
|
+
for (const toolName of ['read', 'ls', 'find', 'grep', 'delivery_plan']) assert.equal(f.hooks.tool_call({ toolName }, f.ctx), undefined);
|
|
907
|
+
await f.call('delivery_plan', { ...f.plan, design: fixtureDesign(f.plan) });
|
|
908
|
+
await assert.rejects(f.call('delivery_check', { id: 'all' }), /Implementation locked/);
|
|
909
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
910
|
+
await f.call('delivery_design', { action: 'review', review: await f.inspectReview() });
|
|
911
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
912
|
+
f.hooks.session_start({}, f.ctx);
|
|
913
|
+
writeFileSync(join(f.cwd, 'app.py'), 'changed before first mutation');
|
|
914
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx).block, true);
|
|
915
|
+
await f.call('delivery_design', { action: 'validate' });
|
|
916
|
+
await f.call('delivery_design', { action: 'review', review: await f.inspectReview() });
|
|
917
|
+
await f.call('delivery_design', { action: 'approve' });
|
|
918
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx), undefined);
|
|
919
|
+
await f.call('delivery_revise', { reason: 'New assumption', patch: { assumptions: ['Retain compatibility'] } });
|
|
920
|
+
assert.equal(f.hooks.tool_call({ toolName: 'write' }, f.ctx).block, true);
|
|
921
|
+
});
|