carthagent 1.0.7 → 1.1.0-beta.2
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/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/cloud-onboarding-acceptance.md +65 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-beta.2.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/cloud/client.mjs +13 -7
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1188 -0
- package/lib/opencode/cli.mjs +253 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/providers/names.mjs +1 -1
- package/lib/tui/framing.mjs +1 -80
- package/lib/tui/models.mjs +1 -1
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +65 -36
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +506 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/cloud-client.test.mjs +2 -2
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +33 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +83 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +248 -20
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/tui.test.mjs +2 -2
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +45 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +882 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +91 -65
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto"
|
|
2
2
|
import type { Context } from "@opencode/plugin/promise/plugin"
|
|
3
|
-
import type
|
|
3
|
+
import { Error as NativeToolError, type ToolContext } from "@opencode/plugin/promise/tool"
|
|
4
4
|
import type { Tool } from "@opencode/schema/tool"
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
6
6
|
import { basename, join, resolve } from "node:path"
|
|
7
|
-
import {
|
|
8
|
-
import { deliveryRecovery, designReceipt, recoveryMessages } from "../../../../lib/delivery-context.mjs"
|
|
7
|
+
import { deliveryPhaseGuidance } from "../../../../lib/delivery-guidance.mjs"
|
|
8
|
+
import { contextMetrics, deliveryRecovery, designReceipt, recoveryMessages } from "../../../../lib/delivery-context.mjs"
|
|
9
9
|
import { validatePlanning, inspectPlanning, recordPlanReview, approvePlanning, planningStatus, startImplementation } from "../../../../lib/planning.mjs"
|
|
10
|
-
import {
|
|
10
|
+
import { planningGuide, planningNext } from "../../../../lib/planning-guide.mjs"
|
|
11
11
|
import { newDiscovery, restoreDiscovery, discoveryCheckpoint, discoveryEvent } from "../../../../lib/planning-discovery.mjs"
|
|
12
12
|
import { budgetLimits, newBudget, budgetReason, budgetSnapshot } from "../../../../lib/budget.mjs"
|
|
13
|
-
import { validatePlan, fingerprint, runCommand, pendingChecks, validateRevision, bindRequiredChecks, loadRequiredChecks, updateStepStatus, verificationMode, looksInformational, revisePlan, mergePlanPatch, completionIssues, planSteps, pendingRequirements, planD2WithProgress, evidenceIdentity } from "../../../../lib/delivery.mjs"
|
|
13
|
+
import { validatePlan, fingerprint, runCommand, pendingChecks, shouldContinue, validateRevision, bindRequiredChecks, loadRequiredChecks, updateStepStatus, verificationMode, looksInformational, revisePlan, mergePlanPatch, completionIssues, planSteps, pendingRequirements, planD2WithProgress, evidenceIdentity } from "../../../../lib/delivery.mjs"
|
|
14
14
|
import { captureDeliveryReview, recordDeliveryReview } from "../../../../lib/delivery-review.mjs"
|
|
15
|
-
import {
|
|
15
|
+
import { loadGuidanceProfiles, routeGuidance } from "../../../../lib/guidance.mjs"
|
|
16
16
|
import { saveReport, reconcileReport } from "../../../../lib/reports.mjs"
|
|
17
17
|
import { lockWorkspace, selectReport, assertActiveReport } from "../../../../lib/workspace.mjs"
|
|
18
|
-
import { enforceToolPolicy,
|
|
18
|
+
import { enforceToolPolicy, requiresPlanApproval } from "./policy.ts"
|
|
19
19
|
import { effectiveSessionOptions } from "../../../../lib/opencode/session-options.mjs"
|
|
20
|
+
import { archiveContext } from "../../../../lib/context-archive.mjs"
|
|
21
|
+
import { probeDiagnostics } from "../../../../lib/probe-diagnostics.mjs"
|
|
22
|
+
import { configuredAdvisor, inspectAdvice } from "../../../../lib/planning-advice.mjs"
|
|
23
|
+
import { createShipJev } from "../../../../lib/ship-jev.mjs"
|
|
24
|
+
import { cloudGatewayUrl } from "../../../../lib/providers/experiential.mjs"
|
|
25
|
+
import { isShipAgent } from "./ship-agents.ts"
|
|
26
|
+
import { originalTask, shipGuidance } from "./ship-guidance.ts"
|
|
27
|
+
import { archiveDeliveryExchanges, localToolName, normalizeDesignReview, shellProtection } from "./ship-context.mjs"
|
|
20
28
|
|
|
21
29
|
/* eslint-disable @typescript-eslint/no-explicit-any -- Existing workflow snapshots mirror evolving JS domain schemas. */
|
|
22
|
-
type SessionState = { state: any; discovery: any; nudges: number; touched: boolean; budget: any; required: any[]; configError?: string; extraGuidance?: string; initialFiles: string[]; lastPrompt?: string; guidance?: string; writeCounts?: Record<string, number>; outcome?: string; cwd?: string }
|
|
30
|
+
type SessionState = { state: any; discovery: any; nudges: number; touched: boolean; budget: any; required: any[]; configError?: string; extraGuidance?: string; initialFiles: string[]; lastPrompt?: string; guidance?: string; writeCounts?: Record<string, number>; outcome?: string; cwd?: string; guidanceProfiles?: string[]; probeState?: { failures: number; family: string | null }; statusPoll?: { key: string; count: number }; contextBytes?: number; recovery?: Record<string, any>; initialFilesCaptured?: boolean; model?: { providerID: string; id: string } }
|
|
23
31
|
type DeliveryOptions = Record<string, unknown>
|
|
24
32
|
type Schema = Record<string, unknown>
|
|
25
33
|
type DeliveryInput = Record<string, any>
|
|
@@ -35,7 +43,7 @@ const short = S()
|
|
|
35
43
|
const steps = A({ anyOf: [short, O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, title: short, kind: { type: "string", enum: ["work", "regression"] }, dependsOn: A(short, 40), checks: A(short, 24) }, ["id", "title"])] }, 40)
|
|
36
44
|
const checks = A(O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, kind: { type: "string", enum: ["test", "runtime", "static"] }, argv: A(short, 80), timeoutSeconds: { type: "integer", minimum: 1, maximum: 300 } }), 24)
|
|
37
45
|
const acceptance = A(O({ requirement: short, checks: A(short, 24) }), 40)
|
|
38
|
-
const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(
|
|
46
|
+
const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(S("Relative file path that must exist at completion, e.g. invoice.mjs. Not a prose deliverable or command result; put test commands in checks. Omit outputs when no files are promised."), 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
|
|
39
47
|
|
|
40
48
|
function sessionKey(sessionID: string) { return `session/${sessionID}` }
|
|
41
49
|
|
|
@@ -97,7 +105,10 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
97
105
|
try { data.extraGuidance = readFileSync(resolve(cwd, opts.context), "utf8") }
|
|
98
106
|
catch (error) { data.configError = `Invalid delivery context: ${error}` }
|
|
99
107
|
}
|
|
100
|
-
if (opts.protectExisting
|
|
108
|
+
if (opts.protectExisting && !data.initialFilesCaptured) {
|
|
109
|
+
data.initialFiles = readdirSync(cwd, { withFileTypes: true }).filter(e => e.isFile()).map(e => resolve(cwd, e.name))
|
|
110
|
+
data.initialFilesCaptured = true
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
if (data.state && data.required.length) {
|
|
103
114
|
for (const check of data.required) {
|
|
@@ -106,7 +117,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
106
117
|
}
|
|
107
118
|
data.state.plan = bindRequiredChecks(data.state.plan, data.required)
|
|
108
119
|
}
|
|
109
|
-
return { session, cwd, data, options: opts }
|
|
120
|
+
return { session, cwd, data, options: opts, active: Boolean(opts.delivery) && isShipAgent(session.agent) }
|
|
110
121
|
}
|
|
111
122
|
function runDirectory(cwd: string, sessionID: string, runId: string) { return join(cwd, ".harness", sessionID, runId) }
|
|
112
123
|
async function persist(cwd: string, sessionID: string, data: SessionState) {
|
|
@@ -138,23 +149,30 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
138
149
|
} finally { release() }
|
|
139
150
|
})
|
|
140
151
|
}
|
|
152
|
+
// The code-mode catalog drops the longest signatures first; pin the plan entry point so it is always listed.
|
|
141
153
|
const add = (editor: Parameters<Parameters<Context["tool"]["transform"]>[0]>[0], name: string, description: string, input: Schema, execute: (input: DeliveryInput, toolContext: RegisteredToolContext) => Promise<{ content: string }>) => editor.add({
|
|
142
|
-
name, description
|
|
154
|
+
name, description: `${description} Returns text; in code mode use JSON.parse(result) for JSON responses before reading fields. Plain-text messages are not JSON.`, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
|
|
143
155
|
const value = rawValue as DeliveryInput
|
|
144
|
-
const {
|
|
145
|
-
if (!
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
const { active } = await scope(tool.sessionID)
|
|
157
|
+
if (!active || !isShipAgent(tool.agent)) throw Error("Carthagent delivery tools are disabled for this session; select Ship mode to use them.")
|
|
158
|
+
try {
|
|
159
|
+
const result = await execute(value, tool)
|
|
160
|
+
const { data } = await scope(tool.sessionID)
|
|
161
|
+
if (!data.state) return result
|
|
162
|
+
return { ...result, metadata: { deliveryPlan: planSnapshot(data.state) } }
|
|
163
|
+
}
|
|
164
|
+
catch (error) { throw new NativeToolError({ message: error instanceof Error ? error.message : String(error) }) }
|
|
165
|
+
}, options: { namespace: "carthagent", codemode: true, ...(name === "delivery_plan" ? { pinned: true } : {}) },
|
|
148
166
|
})
|
|
149
167
|
|
|
150
168
|
registrations.push(await ctx.tool.transform(editor => {
|
|
151
169
|
editor.namespace?.({ name: "carthagent", description: "Evidence-driven CTG delivery workflow tools" })
|
|
152
|
-
add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
|
|
170
|
+
add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Outputs are relative file paths, not prose or test results. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
|
|
153
171
|
if (data.configError) throw Error(data.configError)
|
|
154
172
|
const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
|
|
155
173
|
validateRevision(data.state, plan)
|
|
156
174
|
if (data.state && !["verified", "blocked"].includes(data.state.status)) data.state = revisePlan(data.state, plan, { cwd, hash: fingerprint(cwd, ["."]), reason: "Active plan replaced via delivery_plan" })
|
|
157
|
-
else data.state = { version: 1, workflowMode: opts.workflow === "strict" ? "strict" : "standard", reviewRequired: opts.workflow !== "strict", runId: randomUUID(), revision: (data.state?.revision || 0) + 1, plan, evidence: {}, status: "implementing", createdAt: new Date().toISOString(), stepStatus: {} }
|
|
175
|
+
else data.state = { version: 1, workflowMode: opts.workflow === "strict" ? "strict" : "standard", reviewRequired: opts.workflow !== "strict", runId: randomUUID(), revision: (data.state?.revision || 0) + 1, plan, evidence: {}, status: "implementing", createdAt: new Date().toISOString(), stepStatus: {}, guidanceProfiles: data.guidanceProfiles || [] }
|
|
158
176
|
data.discovery = newDiscovery(); const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
159
177
|
mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
|
|
160
178
|
await persist(cwd, tool.sessionID, data)
|
|
@@ -172,23 +190,51 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
172
190
|
await persist(cwd, tool.sessionID, data)
|
|
173
191
|
return text({ revision: data.state.revision, pendingChecks: pendingChecks(data.state, hash), unverifiedRequirements: pendingRequirements(data.state, hash), next: data.state.workflowMode === "standard" ? "Continue implementation; changed checks require fresh evidence." : "Revision relocks implementation; validate and re-approve." })
|
|
174
192
|
}))
|
|
175
|
-
add(editor, "delivery_design", "Strict workflow design gate. Guide, validate, inspect, adversarially review every scenario, then approve. Review is model-authored, not independent proof.", O({ action: { type: "string", enum: ["guide", "validate", "inspect", "review", "approve"] }, captureId: short, review: { type: "object", additionalProperties: true } }, ["action"]), async (input, tool) => {
|
|
193
|
+
add(editor, "delivery_design", "Strict workflow design gate. Guide, validate, inspect, adversarially review every scenario, then approve. Review is model-authored, not independent proof.", O({ action: { type: "string", enum: ["guide", "validate", "inspect", "review", "approve"] }, captureId: short, review: { type: "object", additionalProperties: true }, challenges: A(short, 40), walkthroughs: A({ type: "object", additionalProperties: true }, 80), findings: A({ type: "object", additionalProperties: true }, 80), limitations: A(short) }, ["action"]), async (input, tool) => {
|
|
176
194
|
if (input.action === "guide") return text(planningGuide())
|
|
177
|
-
return operate(tool.sessionID, async ({ cwd, data }) => {
|
|
195
|
+
return operate(tool.sessionID, async ({ session, cwd, data, options: opts }) => {
|
|
178
196
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
179
197
|
const hash = fingerprint(cwd, ["."]); let result
|
|
180
198
|
if (input.action === "validate") result = validatePlanning(data.state, hash)
|
|
181
199
|
else if (input.action === "inspect") result = inspectPlanning(data.state, hash)
|
|
182
|
-
else if (input.action === "review") result = recordPlanReview(data.state, hash,
|
|
200
|
+
else if (input.action === "review") result = recordPlanReview(data.state, hash, normalizeDesignReview(input))
|
|
183
201
|
else if (input.action === "approve") result = approvePlanning(data.state, hash)
|
|
184
202
|
else throw Error("Unknown delivery_design action")
|
|
203
|
+
let advice
|
|
204
|
+
if (input.action === "inspect") {
|
|
205
|
+
const ship = (session.model ?? data.model)?.providerID === "experiential-labs"
|
|
206
|
+
const enabled = opts.jev === undefined ? ship : opts.jev === true
|
|
207
|
+
const createAdvisor = () => {
|
|
208
|
+
if (opts.jevProvider === "ship" || (ship && !opts.jevProvider)) {
|
|
209
|
+
return { classify: async (plan: unknown) => {
|
|
210
|
+
const active = await ctx.integration.connection.active("experiential-labs")
|
|
211
|
+
if (!active || active.type !== "credential" || active.method !== "oauth") throw Error("Ship OAuth connection required")
|
|
212
|
+
const credential = await ctx.integration.connection.resolve(active)
|
|
213
|
+
if (credential?.type !== "oauth" || credential.methodID !== "carthagent-device" || !credential.access) throw Error("Ship OAuth credential required")
|
|
214
|
+
return createShipJev({ fetchImpl: globalThis.fetch, ...{ accessToken: credential.access, baseUrl: cloudGatewayUrl(), signal: tool.signal } }).classify(plan)
|
|
215
|
+
} }
|
|
216
|
+
}
|
|
217
|
+
return configuredAdvisor({ ...{ enabled, provider: String(opts.jevProvider || "openrouter"), signal: tool.signal } })
|
|
218
|
+
}
|
|
219
|
+
advice = await inspectAdvice(data.state, result, { ...{ enabled, createAdvisor, persist: () => persist(cwd, tool.sessionID, data) } })
|
|
220
|
+
}
|
|
221
|
+
let receipt = designReceipt(input.action, result)
|
|
222
|
+
if (input.action === "inspect" && JSON.stringify(result).length > 16000) {
|
|
223
|
+
try {
|
|
224
|
+
const archive = archiveContext(cwd, runDirectory(cwd, tool.sessionID, data.state.runId), result)
|
|
225
|
+
receipt = { ...result, plan: undefined, capturePath: archive.path, captureDigest: archive.digest, captureBytes: archive.bytes, instruction: "Read the complete capturePath before reviewing. This is not an approval." }
|
|
226
|
+
} catch { /* Retain the original capture if archival fails. */ }
|
|
227
|
+
}
|
|
185
228
|
await persist(cwd, tool.sessionID, data)
|
|
186
|
-
return text({ result:
|
|
229
|
+
return text({ result: receipt, ...(advice ? { advice } : {}), ...(input.action === "inspect" ? { captureId: result.id } : {}), ...(input.action === "validate" ? planningNext(data.state, result) : {}), planningStatus: planningStatus(data.state, hash) })
|
|
187
230
|
})
|
|
188
231
|
})
|
|
189
232
|
add(editor, "delivery_status", "Show delivery state, stale/failed checks and next action. Call finish instead of polling when readyToFinish is true.", empty, async (_input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
190
233
|
if (!data.state) return text("No delivery contract.")
|
|
191
|
-
const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
234
|
+
const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
235
|
+
let hash: string
|
|
236
|
+
try { hash = fingerprint(cwd, ["."]) }
|
|
237
|
+
catch (error) { return text({ ...deliveryRecovery(data.state, { report: join(dir, "report.json") }), pendingChecks: [`[scope error: ${error}]`], readyToFinish: false, next: 'Evidence scope could not be established. Call delivery_finish with status="blocked" and specific limitations.' }) }
|
|
192
238
|
const issues = completionIssues(data.state, cwd, hash); const readyToFinish = Object.values(issues).every(items => !items.length)
|
|
193
239
|
return text({ ...deliveryRecovery(data.state, { report: join(dir, "report.json") }), pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish, next: readyToFinish ? "Call delivery_finish; do not poll again." : "Run declared checks, complete steps/outputs, and resolve outstanding requirements." })
|
|
194
240
|
}))
|
|
@@ -212,40 +258,61 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
212
258
|
for (const check of selected) {
|
|
213
259
|
if (tool.signal.aborted) throw Error("Check cancelled before execution")
|
|
214
260
|
const identity = evidenceIdentity(data.state, check); data.state.status = "verifying"; data.state.verificationStarted = true; delete data.state.evidence[check.id]
|
|
215
|
-
|
|
261
|
+
let before: string
|
|
262
|
+
try { before = fingerprint(cwd, ["."]) }
|
|
263
|
+
catch (error) {
|
|
264
|
+
data.state.evidence[check.id] = { passed: false, ...identity, fingerprint: null, code: 1, outputTail: `Evidence scope error: ${error}` }
|
|
265
|
+
await persist(cwd, tool.sessionID, data)
|
|
266
|
+
throw Error(`Evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations rather than retrying.`)
|
|
267
|
+
}
|
|
268
|
+
await persist(cwd, tool.sessionID, data)
|
|
269
|
+
await tool.progress?.({ title: `Running ${check.id}`, description: JSON.stringify(check.argv) })
|
|
216
270
|
const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
217
271
|
const result = await runCommand(check.argv, { cwd, timeoutSeconds: check.timeoutSeconds, signal: tool.signal, logPath: join(dir, `${check.id}-${randomUUID()}.log`) } as Parameters<typeof runCommand>[1])
|
|
218
|
-
|
|
272
|
+
let after: string
|
|
273
|
+
try { after = fingerprint(cwd, ["."]) }
|
|
274
|
+
catch (error) {
|
|
275
|
+
data.state.evidence[check.id] = { passed: false, ...identity, fingerprint: null, code: result.code, logPath: result.logPath, outputTail: `Post-check evidence scope error: ${error}\n${result.output.slice(-800)}` }
|
|
276
|
+
await persist(cwd, tool.sessionID, data)
|
|
277
|
+
throw Error(`Post-check evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations.`)
|
|
278
|
+
}
|
|
279
|
+
const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
|
|
219
280
|
data.state.evidence[check.id] = { passed, ...identity, executedRevision: data.state.revision, fingerprint: after, code: result.code, timedOut: result.timedOut, cancelled: result.cancelled, logPath: result.logPath, durationMs: result.durationMs, changedDuringCheck: before !== after, outputTail: result.output.slice(-1200) }
|
|
281
|
+
// The first execution in a run is its baseline; later receipts replace evidence only.
|
|
282
|
+
const baselines = (data.state.baselines ??= {}) as Record<string, unknown>
|
|
283
|
+
if (!baselines[check.id]) baselines[check.id] = { passed, code: result.code, checkDigest: identity.checkDigest, fingerprint: after, at: new Date().toISOString() }
|
|
220
284
|
results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
|
|
221
285
|
await persist(cwd, tool.sessionID, data)
|
|
222
286
|
}
|
|
223
287
|
const failures = results.filter(r => !r.passed && verification === "required")
|
|
224
288
|
if (failures.length) throw Error(JSON.stringify({ failures, next: "Repair actual failures; do not weaken obligations." }))
|
|
225
289
|
const hash = fingerprint(cwd, ["."]); const issues = completionIssues(data.state, cwd, hash)
|
|
226
|
-
return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length) })
|
|
290
|
+
return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length), next: "Complete pending steps, review the evidence, then call delivery_finish." })
|
|
227
291
|
}))
|
|
228
|
-
add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
292
|
+
add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", properties: { requirement: { type: "string", description: "Exact acceptance requirement text" }, assertions: { anyOf: [{ type: "string" }, A({ type: "string" }, 32)], description: "Concrete assertions the linked checks make" }, assertion: { type: "string", description: "Alias for a single assertion" }, status: { type: "string", enum: ["demonstrated", "unverified"] } }, required: ["requirement"], additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
229
293
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
230
294
|
const result = input.action === "inspect" ? await captureDeliveryReview(data.state, cwd, runDirectory(cwd, tool.sessionID, data.state.runId), tool.signal) : input.action === "record" ? recordDeliveryReview(data.state, cwd, input) : (() => { throw Error("Review action must be inspect or record") })()
|
|
231
295
|
await persist(cwd, tool.sessionID, data); return text(result)
|
|
232
296
|
}))
|
|
233
|
-
add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
297
|
+
add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, change: { ...short, description: "One-paragraph summary of what changed and why" }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
234
298
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
235
299
|
if (data.configError) throw Error(data.configError)
|
|
236
300
|
const verification = verificationMode(data.state.plan); let hash: string | null = null
|
|
237
301
|
if (input.status === "verified") {
|
|
238
|
-
hash = fingerprint(cwd, ["."])
|
|
239
302
|
if (verification === "required") {
|
|
303
|
+
hash = fingerprint(cwd, ["."])
|
|
240
304
|
const issues = completionIssues(data.state, cwd, hash)
|
|
241
305
|
if (Object.values(issues).some(items => items.length)) throw Error(`Cannot verify: ${JSON.stringify(issues)}`)
|
|
242
|
-
}
|
|
306
|
+
} else { try { hash = fingerprint(cwd, ["."]) } catch { hash = null } }
|
|
243
307
|
} else {
|
|
244
308
|
if (!input.limitations.length) throw Error("Blocked delivery requires an explicit limitation/reason")
|
|
245
309
|
try { hash = fingerprint(cwd, ["."]) } catch { hash = "unfingerprinted:scope_exceeded" }
|
|
246
310
|
}
|
|
247
|
-
|
|
248
|
-
|
|
311
|
+
const advisory = verification !== "required"
|
|
312
|
+
const advisoryFailures = Object.entries(data.state.evidence).filter(([, value]) => !(value as { passed: boolean }).passed).map(([id]) => id)
|
|
313
|
+
data.state.status = input.status; data.state.handoff = { ...input, verification, advisory, advisoryFailures, fingerprint: hash, at: new Date().toISOString(), evidence: structuredClone(data.state.evidence), reviewEvidence: structuredClone(data.state.reviewEvidence || null) }
|
|
314
|
+
await persist(cwd, tool.sessionID, data)
|
|
315
|
+
return text({ status: data.state.status, verification, advisory, advisoryFailures, note: advisory ? "Informational/advisory finish: recorded checks are context, not a delivery guarantee. Distinguish them from unperformed manual/visual review." : "Evidence covers declared checks, not a guarantee of correctness. Distinguish automated evidence from unperformed manual/visual review.", report: join(runDirectory(cwd, tool.sessionID, data.state.runId), "report.json") })
|
|
249
316
|
}))
|
|
250
317
|
}))
|
|
251
318
|
|
|
@@ -256,35 +323,50 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
256
323
|
// share the outer call ID/message ID while still being distinct invocations.
|
|
257
324
|
const callKey = `${event.sessionID}\0${event.messageID}\0${event.id}\0${event.tool}`
|
|
258
325
|
if (admittedCalls.has(callKey)) return
|
|
259
|
-
const { cwd, data, options: opts } = await scope(event.sessionID)
|
|
260
|
-
|
|
326
|
+
const { cwd, data, options: opts, active: selected } = await scope(event.sessionID)
|
|
327
|
+
const active = selected && isShipAgent(event.agent)
|
|
328
|
+
const toolName = localToolName(event.tool)
|
|
329
|
+
if (toolName.startsWith("delivery_") && !active) throw Error("Delivery tools require Ship mode.")
|
|
330
|
+
if (!active && Number(data.budget.limits.tools) === 0 && Number(data.budget.limits.seconds) === 0 && Number(data.budget.limits.repairs) === 0) return
|
|
261
331
|
let persistenceAttempted = false
|
|
262
332
|
try {
|
|
263
333
|
const nativeShell = ["bash", "shell", "powershell"].includes(event.tool)
|
|
264
334
|
const input = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : {}
|
|
265
|
-
if (
|
|
266
|
-
|
|
335
|
+
if (active && toolName === "delivery_status" && data.state) {
|
|
336
|
+
let source
|
|
337
|
+
try { source = fingerprint(cwd, ["."]) } catch (error) { source = String(error) }
|
|
338
|
+
const key = JSON.stringify([data.state.runId, data.state.revision, source, data.state.evidence, data.state.stepStatus, data.state.reviewEvidence, data.state.planning, data.state.status])
|
|
339
|
+
data.statusPoll = { key, count: data.statusPoll?.key === key ? data.statusPoll.count + 1 : 1 }
|
|
340
|
+
if (data.statusPoll.count > 3) throw Error("Repeated unchanged delivery_status polling is blocked. Execute the next action or finish instead.")
|
|
341
|
+
} else if (active && (toolName.startsWith("delivery_") || ["write", "edit", "patch"].includes(toolName))) data.statusPoll = undefined
|
|
342
|
+
if (active && event.tool !== "execute" && requiresImplementationTool(toolName)) {
|
|
343
|
+
if (!data.state) {
|
|
344
|
+
data.discovery = discoveryEvent(data.discovery, "blocked")
|
|
345
|
+
throw Error(`Implementation locked: call delivery_plan first (tool ${event.tool}). ${discoveryCheckpoint(data.discovery) || ""}`)
|
|
346
|
+
}
|
|
267
347
|
const release = lockWorkspace(cwd)
|
|
268
348
|
try {
|
|
269
349
|
const dir = runDirectory(cwd, event.sessionID, data.state.runId)
|
|
270
350
|
assertActiveReport(cwd, join(dir, "report.json"))
|
|
271
351
|
data.state = reconcileReport(join(dir, "report.json"), data.state)
|
|
272
|
-
enforceToolPolicy(
|
|
352
|
+
enforceToolPolicy(toolName, data.state, cwd)
|
|
273
353
|
await persist(cwd, event.sessionID, data)
|
|
274
354
|
} finally { release() }
|
|
275
355
|
}
|
|
276
|
-
if ((event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
|
|
356
|
+
if (active && (event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
|
|
277
357
|
const path = input.path.replaceAll("\\", "/"), cwdPath = cwd.replaceAll("\\", "/").replace(/^\/+/, ""), name = basename(cwd)
|
|
278
358
|
if ([cwdPath, name].some(prefix => path === prefix || path.startsWith(`${prefix}/`))) throw Error("Path recreates working directory inside itself; use a workspace-relative product path.")
|
|
279
359
|
if (event.tool === "write" && data.initialFiles.includes(resolve(cwd, input.path))) throw Error(`Preserve existing ${input.path}; use focused edits.`)
|
|
280
360
|
}
|
|
281
361
|
const cap = Number(opts.bashCap) || 0
|
|
282
|
-
if (nativeShell) {
|
|
362
|
+
if (active && nativeShell) {
|
|
283
363
|
const command = String(input.command ?? "")
|
|
284
|
-
|
|
285
|
-
if (
|
|
364
|
+
const protection = shellProtection(command, opts.protectExisting ? data.initialFiles : [])
|
|
365
|
+
if (protection) throw Error(protection)
|
|
366
|
+
const capMilliseconds = cap * 1000
|
|
367
|
+
if (cap > 0 && (typeof input.timeout !== "number" || input.timeout === 0 || input.timeout > capMilliseconds)) input.timeout = capMilliseconds
|
|
286
368
|
}
|
|
287
|
-
if (
|
|
369
|
+
if (active && event.tool === "write" && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
|
|
288
370
|
data.writeCounts ||= {}
|
|
289
371
|
const path = String(input.path || "")
|
|
290
372
|
const count = data.writeCounts[path] || 0
|
|
@@ -317,92 +399,115 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
317
399
|
})))
|
|
318
400
|
|
|
319
401
|
registrations.push(await ctx.tool.hook("execute.after", async event => serialized(event.sessionID, async () => {
|
|
320
|
-
const { cwd, data } = await scope(event.sessionID)
|
|
321
|
-
if (!
|
|
322
|
-
|
|
323
|
-
return
|
|
324
|
-
}
|
|
402
|
+
const { cwd, data, options: opts, active } = await scope(event.sessionID)
|
|
403
|
+
if (!active || !isShipAgent(event.agent)) return
|
|
404
|
+
const tool = localToolName(event.tool)
|
|
325
405
|
const failed = event.status === "error"
|
|
326
|
-
if (!failed && ["write", "edit"].includes(
|
|
327
|
-
if (data.discovery.active && ["read", "ls", "find", "grep", "code_nav"].includes(
|
|
328
|
-
|
|
406
|
+
if (!failed && ["write", "edit", "patch"].includes(tool)) data.touched = true
|
|
407
|
+
if (data.discovery.active && ["read", "ls", "glob", "find", "grep", "code_nav"].includes(tool)) data.discovery = discoveryEvent(data.discovery, "read")
|
|
408
|
+
const rawContent = event.status === "error" ? event.error.message : event.result?.content
|
|
409
|
+
const content = typeof rawContent === "string" ? [{ type: "text", text: rawContent }] : rawContent || []
|
|
410
|
+
const diagnostic = probeDiagnostics(data.probeState as Parameters<typeof probeDiagnostics>[0], { toolName: tool === "shell" ? "bash" : tool, isError: failed, content })
|
|
411
|
+
data.probeState = diagnostic.state
|
|
412
|
+
const cap = Math.max(0, Number(opts.toolOutputCap) || 0)
|
|
413
|
+
if (cap && event.status === "completed" && ["bash", "shell", "read", "grep", "glob", "find", "ls", "code_nav"].includes(tool)) {
|
|
414
|
+
event.result = { ...event.result, content: (typeof event.result.content === "string" ? [{ type: "text" as const, text: event.result.content }] : event.result?.content || []).map(part => part.type === "text" && part.text.length > cap ? { ...part, text: `${part.text.slice(-cap)}\n[output capped at ${cap} characters; request a targeted slice]` } : part) }
|
|
415
|
+
}
|
|
416
|
+
if (diagnostic.message) {
|
|
417
|
+
if (event.status === "completed") event.result = { ...event.result, content: [...(typeof event.result.content === "string" ? [{ type: "text" as const, text: event.result.content }] : event.result?.content || []), { type: "text", text: diagnostic.message }] }
|
|
418
|
+
else event.error = new NativeToolError({ message: `${event.error.message}\n\n${diagnostic.message}` })
|
|
419
|
+
}
|
|
420
|
+
if (data.state && ["delivery_check", "delivery_progress", "delivery_review", "delivery_finish", "delivery_revise", "delivery_design", "delivery_plan"].includes(tool)) {
|
|
329
421
|
const dir = runDirectory(cwd, event.sessionID, data.state.runId)
|
|
330
|
-
|
|
422
|
+
const release = lockWorkspace(cwd)
|
|
423
|
+
try {
|
|
424
|
+
assertActiveReport(cwd, join(dir, "report.json"))
|
|
425
|
+
data.state = reconcileReport(join(dir, "report.json"), data.state)
|
|
426
|
+
writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
|
|
427
|
+
} finally { release() }
|
|
331
428
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!failed && outputCap > 0 && event.result?.content) {
|
|
335
|
-
if (!Array.isArray(event.result.content)) {
|
|
336
|
-
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
337
|
-
return
|
|
338
|
-
}
|
|
339
|
-
const content = event.result.content.map(item => {
|
|
340
|
-
if (item.type !== "text" || item.text.length <= outputCap) return item
|
|
341
|
-
const marker = `\n...[${item.text.length - outputCap} chars omitted; rerun a narrower command]...\n`
|
|
342
|
-
const head = Math.floor(Math.max(0, outputCap - marker.length) / 3)
|
|
343
|
-
const tail = Math.max(0, outputCap - marker.length - head)
|
|
344
|
-
return { ...item, text: outputCap <= marker.length ? item.text.slice(0, outputCap) : item.text.slice(0, head) + marker + item.text.slice(-tail) }
|
|
345
|
-
})
|
|
346
|
-
event.result = { ...event.result, content }
|
|
429
|
+
if (event.status === "completed" && data.state && (tool === "execute" || tool.startsWith("delivery_"))) {
|
|
430
|
+
event.result = { ...event.result, metadata: { ...event.result.metadata, deliveryPlan: planSnapshot(data.state) } }
|
|
347
431
|
}
|
|
348
432
|
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
433
|
+
const delay = Math.min(30000, Math.max(0, Number(opts.turnDelayMs) || 0) * Math.max(1, Math.ceil((data.contextBytes || 0) / 4 / 50000)))
|
|
434
|
+
if (delay) await new Promise(resolve => setTimeout(resolve, delay))
|
|
349
435
|
})))
|
|
350
436
|
|
|
351
437
|
registrations.push(await ctx.shell.hook("create.before", async event => {
|
|
352
438
|
// Session-scoped policy, limits, and budget were handled at execute.before.
|
|
353
439
|
// This hook intentionally does not attribute commands: 2.0.32 has no
|
|
354
440
|
// authoritative source.sessionID/callID on ShellCreateBefore.
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
441
|
+
const capMilliseconds = (Number(options.bashCap) || 0) * 1000
|
|
442
|
+
if (capMilliseconds > 0 && (event.timeout === 0 || event.timeout > capMilliseconds)) event.timeout = capMilliseconds
|
|
357
443
|
if (/\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)/i.test(event.command)) throw Error("Broad process termination is prohibited; terminate only a captured child PID.")
|
|
358
444
|
}))
|
|
359
445
|
|
|
360
|
-
|
|
361
|
-
const { cwd, data } = await scope(event.sessionID)
|
|
362
|
-
if (!
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
446
|
+
registrations.push(await ctx.session.hook("prompt", async event => serialized(event.sessionID, async () => {
|
|
447
|
+
const { cwd, data, active } = await scope(event.sessionID)
|
|
448
|
+
if (!active) return
|
|
449
|
+
const prompt = originalTask(event.prompt.text)
|
|
450
|
+
const routed = routeGuidance(prompt, { cwd, profiles })
|
|
451
|
+
const prior = data.state && !["verified", "blocked"].includes(data.state.status) ? data.guidanceProfiles || data.state.guidanceProfiles || [] : []
|
|
452
|
+
data.guidanceProfiles = [...new Set([...prior, ...routed.map(profile => profile.id)])]
|
|
453
|
+
data.lastPrompt = prompt
|
|
454
|
+
if (!data.state || ["verified", "blocked"].includes(data.state.status)) data.discovery = newDiscovery(!looksInformational(prompt))
|
|
455
|
+
data.touched = false
|
|
456
|
+
data.statusPoll = undefined
|
|
457
|
+
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
458
|
+
})))
|
|
459
|
+
registrations.push(await ctx.session.hook("context", async event => serialized(event.sessionID, async () => {
|
|
460
|
+
const { session, cwd, data, options: opts, active } = await scope(event.sessionID)
|
|
461
|
+
const reason = budgetReason(data.budget)
|
|
462
|
+
if (reason) {
|
|
463
|
+
data.budget.stopped ||= reason
|
|
464
|
+
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
465
|
+
await ctx.session.interrupt({ sessionID: event.sessionID, resume: false })
|
|
466
|
+
throw Error(`Delivery budget exhausted: ${reason}. Do not dispatch another model request until an explicit delivery-budget-reset.`)
|
|
370
467
|
}
|
|
371
|
-
if (!
|
|
372
|
-
const
|
|
373
|
-
if (checkpoint) event.system.push({ type: "text", text: checkpoint })
|
|
468
|
+
if (!active || !isShipAgent(event.agent)) {
|
|
469
|
+
if (event.tools) for (const name of Object.keys(event.tools)) if (localToolName(name).startsWith("delivery_")) delete event.tools[name]
|
|
374
470
|
return
|
|
375
471
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
472
|
+
data.model = { providerID: event.model.providerID, id: event.model.id }
|
|
473
|
+
let imageInput: boolean | undefined
|
|
474
|
+
if (ctx.model) {
|
|
475
|
+
const models = await ctx.model.list({ location: { directory: session.location.directory } })
|
|
476
|
+
imageInput = models.data.find(model => model.providerID === event.model.providerID && model.id === event.model.id)?.capabilities.input.includes("image")
|
|
477
|
+
}
|
|
478
|
+
const activeProfiles = profiles.filter(profile => (data.guidanceProfiles || data.state?.guidanceProfiles || []).includes(profile.id))
|
|
479
|
+
event.system.push({ type: "text", text: shipGuidance({ state: data.state, workflow: String(opts.workflow || "standard"), prompt: data.lastPrompt, profiles: activeProfiles, discovery: data.discovery, extraGuidance: data.extraGuidance, required: data.required, imageInput }) })
|
|
480
|
+
if (data.budget.limits.tools || data.budget.limits.seconds || data.budget.limits.repairs) event.system.push({ type: "text", text: `Delivery budget: ${JSON.stringify(budgetSnapshot(data.budget))}. Reserve tools for verification/review/handoff; stop at exhaustion. Only the explicit delivery-budget-reset command authorizes a new budget.` })
|
|
481
|
+
const checkpoint = discoveryCheckpoint(data.discovery)
|
|
482
|
+
if (checkpoint) event.system.push({ type: "text", text: checkpoint })
|
|
483
|
+
if (event.tools && !data.state) for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name) && name !== "execute") delete event.tools[name]
|
|
484
|
+
if (data.state && !["verified", "blocked"].includes(data.state.status)) {
|
|
485
|
+
const dir = runDirectory(cwd, event.sessionID, data.state.runId)
|
|
486
|
+
const archived = archiveDeliveryExchanges(event.messages, { cwd, directory: dir, allow: Boolean(data.state.planning?.approval) })
|
|
487
|
+
event.messages = archived.messages
|
|
488
|
+
const summary = deliveryRecovery(data.state, { report: join(dir, "report.json") })
|
|
489
|
+
data.recovery ||= {}
|
|
490
|
+
const records = recoveryMessages([], summary, data.recovery)
|
|
491
|
+
const pressure = (data.contextBytes || 0) > 240000 ? "Context pressure: inspect the current recovery record and targeted source slices; avoid repeating whole captures." : ""
|
|
492
|
+
event.system.push({ type: "text", text: `${records.map((record: { content: string }) => record.content).join("\n")}\nInspect freshness before execution/finish; readyToFinish means finish, not polling.\n${pressure}\n${archived.failed ? "Archive unavailable: affected originals retained." : ""}\n${deliveryPhaseGuidance(data.state)}` })
|
|
493
|
+
try {
|
|
494
|
+
const metrics = contextMetrics({ system: event.system, messages: event.messages })
|
|
495
|
+
data.contextBytes = metrics.totalBytes
|
|
496
|
+
appendFileSync(join(dir, "context-metrics.jsonl"), JSON.stringify(metrics) + "\n")
|
|
497
|
+
} catch { /* Metrics are advisory, never an execution gate. */ }
|
|
498
|
+
if (!data.state.planning?.approval && data.state.workflowMode !== "standard" && event.tools) for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name) && name !== "execute") delete event.tools[name]
|
|
499
|
+
}
|
|
399
500
|
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
400
501
|
})))
|
|
401
|
-
|
|
502
|
+
registrations.push(await ctx.session.hook("compaction", async event => {
|
|
503
|
+
const { data, active } = await scope(event.sessionID)
|
|
504
|
+
if (!active || !isShipAgent(event.agent) || !data.state || ["verified", "blocked"].includes(data.state.status)) return
|
|
505
|
+
event.system.push({ type: "text", text: JSON.stringify(deliveryRecovery(data.state, { report: join(runDirectory(data.cwd!, event.sessionID, data.state.runId), "report.json") })) })
|
|
506
|
+
}))
|
|
402
507
|
registrations.push(await ctx.command.transform(editor => {
|
|
403
508
|
editor.add({ name: "delivery-budget-status", description: "Show this session's persistent delivery budget", execute: async ({ sessionID }) => {
|
|
404
509
|
const { data } = await scope(sessionID)
|
|
405
|
-
await ctx.session.
|
|
510
|
+
await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget status: ${JSON.stringify(budgetSnapshot(data.budget))}` })
|
|
406
511
|
} })
|
|
407
512
|
editor.add({ name: "delivery-budget-reset", description: "Explicitly authorize a fresh budget for this session", execute: async ({ sessionID }) => {
|
|
408
513
|
const snapshot = await serialized(sessionID, async () => {
|
|
@@ -410,7 +515,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
410
515
|
for (const key of admittedCalls) if (key.startsWith(`${sessionID}\0`)) admittedCalls.delete(key)
|
|
411
516
|
return budgetSnapshot(data.budget)
|
|
412
517
|
})
|
|
413
|
-
await ctx.session.
|
|
518
|
+
await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
|
|
414
519
|
} })
|
|
415
520
|
}))
|
|
416
521
|
|
|
@@ -422,7 +527,8 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
422
527
|
const sessionID = event.data.sessionID
|
|
423
528
|
let followup: string | undefined
|
|
424
529
|
await serialized(sessionID, async () => {
|
|
425
|
-
const { cwd, data, session } = await scope(sessionID)
|
|
530
|
+
const { cwd, data, session, active } = await scope(sessionID)
|
|
531
|
+
if (!active) return
|
|
426
532
|
const reason = budgetReason(data.budget)
|
|
427
533
|
if (reason && !data.budget.stopped) {
|
|
428
534
|
data.budget.stopped = reason
|
|
@@ -432,7 +538,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
432
538
|
}
|
|
433
539
|
if (data.discovery.active) data.discovery = discoveryEvent(data.discovery, "blocked")
|
|
434
540
|
const state = data.state
|
|
435
|
-
if (!state ||
|
|
541
|
+
if (!state || !shouldContinue({ state, touched: data.touched, nudges: data.nudges, stopReason: session.outcome === "succeeded" ? "stop" : "error", pendingMessages: false, fresh: false })) {
|
|
436
542
|
await ctx.storage.set(sessionKey(sessionID), data)
|
|
437
543
|
return
|
|
438
544
|
}
|
|
@@ -459,7 +565,11 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
459
565
|
data.nudges++
|
|
460
566
|
const pending = issues.pendingChecks.join(", ") || "none"
|
|
461
567
|
await ctx.storage.set(sessionKey(sessionID), data)
|
|
462
|
-
|
|
568
|
+
const failures = Object.entries(data.state.evidence || {}).filter(([, e]) => !(e as { passed: boolean }).passed).map(([id, e]) => {
|
|
569
|
+
const evidence = e as { code: number; outputTail?: string }
|
|
570
|
+
return `${id}: exit ${evidence.code}\n${evidence.outputTail || ""}`
|
|
571
|
+
}).join("\n")
|
|
572
|
+
followup = `${failures}\nDelivery follow-up ${data.nudges}/2: the required contract has no fresh verified handoff. Pending checks: ${pending}. Inspect current delivery_status, repair the actual cause, rerun checks and finish verified, or record status=blocked with specific limitations. Do not repeat a success claim or weaken obligations.`
|
|
463
573
|
} catch (error) {
|
|
464
574
|
console.error("Carthagent delivery recovery failed closed", error)
|
|
465
575
|
} finally { release() }
|
|
@@ -484,5 +594,10 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
484
594
|
function requiresImplementationTool(name: string) {
|
|
485
595
|
// V2 exposes namespaced tools as `namespace_name` in model context and hooks.
|
|
486
596
|
const toolName = name.startsWith("carthagent_") ? name.slice("carthagent_".length) : name
|
|
487
|
-
return
|
|
597
|
+
return toolName !== "execute" && !["delivery-budget-status", "delivery-budget-reset"].includes(toolName) && requiresPlanApproval(toolName)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function planSnapshot(state: SessionState["state"]) {
|
|
601
|
+
const items = planSteps(state.plan).map((step: { id: string; title: string }, i: number) => ({ id: step.id, content: step.title, status: state.stepStatus[`step${i}`] || "pending" }))
|
|
602
|
+
return { runId: state.runId, revision: state.revision, status: state.status, items }
|
|
488
603
|
}
|