carthagent 1.0.7 → 1.1.0-preview.12
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 +80 -79
- package/bin/ctg.mjs +14 -732
- 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/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 +314 -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 +177 -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.patch +5137 -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.9.md +130 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -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 +8 -4
- 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 +298 -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/tui/framing.mjs +1 -80
- 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 +197 -0
- package/plugins/carthagent/src/server/delivery.ts +217 -106
- 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 +58 -30
- 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 +39 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +492 -0
- package/plugins/carthagent/src/ui/client.ts +125 -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 +232 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
- package/plugins/carthagent/src/ui/native-review.mjs +154 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -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 +122 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +127 -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 +60 -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 +258 -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/daily-evidence.test.mjs +62 -0
- package/tests/delivery.test.mjs +22 -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 +53 -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 +426 -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-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -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 +267 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +75 -0
- package/tests/native-ship-rpc.test.mjs +71 -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 +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -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-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -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 +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +36 -0
- 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 +43 -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 +876 -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 +100 -18
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +83 -61
- 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>
|
|
@@ -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) {
|
|
@@ -141,9 +152,15 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
141
152
|
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
153
|
name, description, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
|
|
143
154
|
const value = rawValue as DeliveryInput
|
|
144
|
-
const {
|
|
145
|
-
if (!
|
|
146
|
-
|
|
155
|
+
const { active } = await scope(tool.sessionID)
|
|
156
|
+
if (!active || !isShipAgent(tool.agent)) throw Error("Carthagent delivery tools are disabled for this session; select Ship mode to use them.")
|
|
157
|
+
try {
|
|
158
|
+
const result = await execute(value, tool)
|
|
159
|
+
const { data } = await scope(tool.sessionID)
|
|
160
|
+
if (!data.state) return result
|
|
161
|
+
return { ...result, metadata: { deliveryPlan: planSnapshot(data.state) } }
|
|
162
|
+
}
|
|
163
|
+
catch (error) { throw new NativeToolError({ message: error instanceof Error ? error.message : String(error) }) }
|
|
147
164
|
}, options: { namespace: "carthagent", codemode: true },
|
|
148
165
|
})
|
|
149
166
|
|
|
@@ -154,7 +171,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
154
171
|
const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
|
|
155
172
|
validateRevision(data.state, plan)
|
|
156
173
|
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: {} }
|
|
174
|
+
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
175
|
data.discovery = newDiscovery(); const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
159
176
|
mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
|
|
160
177
|
await persist(cwd, tool.sessionID, data)
|
|
@@ -172,23 +189,51 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
172
189
|
await persist(cwd, tool.sessionID, data)
|
|
173
190
|
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
191
|
}))
|
|
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) => {
|
|
192
|
+
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
193
|
if (input.action === "guide") return text(planningGuide())
|
|
177
|
-
return operate(tool.sessionID, async ({ cwd, data }) => {
|
|
194
|
+
return operate(tool.sessionID, async ({ session, cwd, data, options: opts }) => {
|
|
178
195
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
179
196
|
const hash = fingerprint(cwd, ["."]); let result
|
|
180
197
|
if (input.action === "validate") result = validatePlanning(data.state, hash)
|
|
181
198
|
else if (input.action === "inspect") result = inspectPlanning(data.state, hash)
|
|
182
|
-
else if (input.action === "review") result = recordPlanReview(data.state, hash,
|
|
199
|
+
else if (input.action === "review") result = recordPlanReview(data.state, hash, normalizeDesignReview(input))
|
|
183
200
|
else if (input.action === "approve") result = approvePlanning(data.state, hash)
|
|
184
201
|
else throw Error("Unknown delivery_design action")
|
|
202
|
+
let advice
|
|
203
|
+
if (input.action === "inspect") {
|
|
204
|
+
const ship = (session.model ?? data.model)?.providerID === "experiential-labs"
|
|
205
|
+
const enabled = opts.jev === undefined ? ship : opts.jev === true
|
|
206
|
+
const createAdvisor = () => {
|
|
207
|
+
if (opts.jevProvider === "ship" || (ship && !opts.jevProvider)) {
|
|
208
|
+
return { classify: async (plan: unknown) => {
|
|
209
|
+
const active = await ctx.integration.connection.active("experiential-labs")
|
|
210
|
+
if (!active || active.type !== "credential" || active.method !== "oauth") throw Error("Ship OAuth connection required")
|
|
211
|
+
const credential = await ctx.integration.connection.resolve(active)
|
|
212
|
+
if (credential?.type !== "oauth" || credential.methodID !== "carthagent-device" || !credential.access) throw Error("Ship OAuth credential required")
|
|
213
|
+
return createShipJev({ fetchImpl: globalThis.fetch, ...{ accessToken: credential.access, baseUrl: cloudGatewayUrl(), signal: tool.signal } }).classify(plan)
|
|
214
|
+
} }
|
|
215
|
+
}
|
|
216
|
+
return configuredAdvisor({ ...{ enabled, provider: String(opts.jevProvider || "openrouter"), signal: tool.signal } })
|
|
217
|
+
}
|
|
218
|
+
advice = await inspectAdvice(data.state, result, { ...{ enabled, createAdvisor, persist: () => persist(cwd, tool.sessionID, data) } })
|
|
219
|
+
}
|
|
220
|
+
let receipt = designReceipt(input.action, result)
|
|
221
|
+
if (input.action === "inspect" && JSON.stringify(result).length > 16000) {
|
|
222
|
+
try {
|
|
223
|
+
const archive = archiveContext(cwd, runDirectory(cwd, tool.sessionID, data.state.runId), result)
|
|
224
|
+
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." }
|
|
225
|
+
} catch { /* Retain the original capture if archival fails. */ }
|
|
226
|
+
}
|
|
185
227
|
await persist(cwd, tool.sessionID, data)
|
|
186
|
-
return text({ result:
|
|
228
|
+
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
229
|
})
|
|
188
230
|
})
|
|
189
231
|
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
232
|
if (!data.state) return text("No delivery contract.")
|
|
191
|
-
const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
233
|
+
const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
234
|
+
let hash: string
|
|
235
|
+
try { hash = fingerprint(cwd, ["."]) }
|
|
236
|
+
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
237
|
const issues = completionIssues(data.state, cwd, hash); const readyToFinish = Object.values(issues).every(items => !items.length)
|
|
193
238
|
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
239
|
}))
|
|
@@ -212,10 +257,25 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
212
257
|
for (const check of selected) {
|
|
213
258
|
if (tool.signal.aborted) throw Error("Check cancelled before execution")
|
|
214
259
|
const identity = evidenceIdentity(data.state, check); data.state.status = "verifying"; data.state.verificationStarted = true; delete data.state.evidence[check.id]
|
|
215
|
-
|
|
260
|
+
let before: string
|
|
261
|
+
try { before = fingerprint(cwd, ["."]) }
|
|
262
|
+
catch (error) {
|
|
263
|
+
data.state.evidence[check.id] = { passed: false, ...identity, fingerprint: null, code: 1, outputTail: `Evidence scope error: ${error}` }
|
|
264
|
+
await persist(cwd, tool.sessionID, data)
|
|
265
|
+
throw Error(`Evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations rather than retrying.`)
|
|
266
|
+
}
|
|
267
|
+
await persist(cwd, tool.sessionID, data)
|
|
268
|
+
await tool.progress?.({ title: `Running ${check.id}`, description: JSON.stringify(check.argv) })
|
|
216
269
|
const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
|
|
217
270
|
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
|
-
|
|
271
|
+
let after: string
|
|
272
|
+
try { after = fingerprint(cwd, ["."]) }
|
|
273
|
+
catch (error) {
|
|
274
|
+
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)}` }
|
|
275
|
+
await persist(cwd, tool.sessionID, data)
|
|
276
|
+
throw Error(`Post-check evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations.`)
|
|
277
|
+
}
|
|
278
|
+
const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
|
|
219
279
|
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) }
|
|
220
280
|
results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
|
|
221
281
|
await persist(cwd, tool.sessionID, data)
|
|
@@ -223,7 +283,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
223
283
|
const failures = results.filter(r => !r.passed && verification === "required")
|
|
224
284
|
if (failures.length) throw Error(JSON.stringify({ failures, next: "Repair actual failures; do not weaken obligations." }))
|
|
225
285
|
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) })
|
|
286
|
+
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
287
|
}))
|
|
228
288
|
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 }) => {
|
|
229
289
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
@@ -235,17 +295,20 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
235
295
|
if (data.configError) throw Error(data.configError)
|
|
236
296
|
const verification = verificationMode(data.state.plan); let hash: string | null = null
|
|
237
297
|
if (input.status === "verified") {
|
|
238
|
-
hash = fingerprint(cwd, ["."])
|
|
239
298
|
if (verification === "required") {
|
|
299
|
+
hash = fingerprint(cwd, ["."])
|
|
240
300
|
const issues = completionIssues(data.state, cwd, hash)
|
|
241
301
|
if (Object.values(issues).some(items => items.length)) throw Error(`Cannot verify: ${JSON.stringify(issues)}`)
|
|
242
|
-
}
|
|
302
|
+
} else { try { hash = fingerprint(cwd, ["."]) } catch { hash = null } }
|
|
243
303
|
} else {
|
|
244
304
|
if (!input.limitations.length) throw Error("Blocked delivery requires an explicit limitation/reason")
|
|
245
305
|
try { hash = fingerprint(cwd, ["."]) } catch { hash = "unfingerprinted:scope_exceeded" }
|
|
246
306
|
}
|
|
247
|
-
|
|
248
|
-
|
|
307
|
+
const advisory = verification !== "required"
|
|
308
|
+
const advisoryFailures = Object.entries(data.state.evidence).filter(([, value]) => !(value as { passed: boolean }).passed).map(([id]) => id)
|
|
309
|
+
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) }
|
|
310
|
+
await persist(cwd, tool.sessionID, data)
|
|
311
|
+
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
312
|
}))
|
|
250
313
|
}))
|
|
251
314
|
|
|
@@ -256,35 +319,50 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
256
319
|
// share the outer call ID/message ID while still being distinct invocations.
|
|
257
320
|
const callKey = `${event.sessionID}\0${event.messageID}\0${event.id}\0${event.tool}`
|
|
258
321
|
if (admittedCalls.has(callKey)) return
|
|
259
|
-
const { cwd, data, options: opts } = await scope(event.sessionID)
|
|
260
|
-
|
|
322
|
+
const { cwd, data, options: opts, active: selected } = await scope(event.sessionID)
|
|
323
|
+
const active = selected && isShipAgent(event.agent)
|
|
324
|
+
const toolName = localToolName(event.tool)
|
|
325
|
+
if (toolName.startsWith("delivery_") && !active) throw Error("Delivery tools require Ship mode.")
|
|
326
|
+
if (!active && Number(data.budget.limits.tools) === 0 && Number(data.budget.limits.seconds) === 0 && Number(data.budget.limits.repairs) === 0) return
|
|
261
327
|
let persistenceAttempted = false
|
|
262
328
|
try {
|
|
263
329
|
const nativeShell = ["bash", "shell", "powershell"].includes(event.tool)
|
|
264
330
|
const input = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : {}
|
|
265
|
-
if (
|
|
266
|
-
|
|
331
|
+
if (active && toolName === "delivery_status" && data.state) {
|
|
332
|
+
let source
|
|
333
|
+
try { source = fingerprint(cwd, ["."]) } catch (error) { source = String(error) }
|
|
334
|
+
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])
|
|
335
|
+
data.statusPoll = { key, count: data.statusPoll?.key === key ? data.statusPoll.count + 1 : 1 }
|
|
336
|
+
if (data.statusPoll.count > 3) throw Error("Repeated unchanged delivery_status polling is blocked. Execute the next action or finish instead.")
|
|
337
|
+
} else if (active && (toolName.startsWith("delivery_") || ["write", "edit", "patch"].includes(toolName))) data.statusPoll = undefined
|
|
338
|
+
if (active && event.tool !== "execute" && requiresImplementationTool(toolName)) {
|
|
339
|
+
if (!data.state) {
|
|
340
|
+
data.discovery = discoveryEvent(data.discovery, "blocked")
|
|
341
|
+
throw Error(`Implementation locked: call delivery_plan first (tool ${event.tool}). ${discoveryCheckpoint(data.discovery) || ""}`)
|
|
342
|
+
}
|
|
267
343
|
const release = lockWorkspace(cwd)
|
|
268
344
|
try {
|
|
269
345
|
const dir = runDirectory(cwd, event.sessionID, data.state.runId)
|
|
270
346
|
assertActiveReport(cwd, join(dir, "report.json"))
|
|
271
347
|
data.state = reconcileReport(join(dir, "report.json"), data.state)
|
|
272
|
-
enforceToolPolicy(
|
|
348
|
+
enforceToolPolicy(toolName, data.state, cwd)
|
|
273
349
|
await persist(cwd, event.sessionID, data)
|
|
274
350
|
} finally { release() }
|
|
275
351
|
}
|
|
276
|
-
if ((event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
|
|
352
|
+
if (active && (event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
|
|
277
353
|
const path = input.path.replaceAll("\\", "/"), cwdPath = cwd.replaceAll("\\", "/").replace(/^\/+/, ""), name = basename(cwd)
|
|
278
354
|
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
355
|
if (event.tool === "write" && data.initialFiles.includes(resolve(cwd, input.path))) throw Error(`Preserve existing ${input.path}; use focused edits.`)
|
|
280
356
|
}
|
|
281
357
|
const cap = Number(opts.bashCap) || 0
|
|
282
|
-
if (nativeShell) {
|
|
358
|
+
if (active && nativeShell) {
|
|
283
359
|
const command = String(input.command ?? "")
|
|
284
|
-
|
|
285
|
-
if (
|
|
360
|
+
const protection = shellProtection(command, opts.protectExisting ? data.initialFiles : [])
|
|
361
|
+
if (protection) throw Error(protection)
|
|
362
|
+
const capMilliseconds = cap * 1000
|
|
363
|
+
if (cap > 0 && (typeof input.timeout !== "number" || input.timeout === 0 || input.timeout > capMilliseconds)) input.timeout = capMilliseconds
|
|
286
364
|
}
|
|
287
|
-
if (
|
|
365
|
+
if (active && event.tool === "write" && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
|
|
288
366
|
data.writeCounts ||= {}
|
|
289
367
|
const path = String(input.path || "")
|
|
290
368
|
const count = data.writeCounts[path] || 0
|
|
@@ -317,92 +395,115 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
317
395
|
})))
|
|
318
396
|
|
|
319
397
|
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
|
-
}
|
|
398
|
+
const { cwd, data, options: opts, active } = await scope(event.sessionID)
|
|
399
|
+
if (!active || !isShipAgent(event.agent)) return
|
|
400
|
+
const tool = localToolName(event.tool)
|
|
325
401
|
const failed = event.status === "error"
|
|
326
|
-
if (!failed && ["write", "edit"].includes(
|
|
327
|
-
if (data.discovery.active && ["read", "ls", "find", "grep", "code_nav"].includes(
|
|
328
|
-
|
|
402
|
+
if (!failed && ["write", "edit", "patch"].includes(tool)) data.touched = true
|
|
403
|
+
if (data.discovery.active && ["read", "ls", "glob", "find", "grep", "code_nav"].includes(tool)) data.discovery = discoveryEvent(data.discovery, "read")
|
|
404
|
+
const rawContent = event.status === "error" ? event.error.message : event.result?.content
|
|
405
|
+
const content = typeof rawContent === "string" ? [{ type: "text", text: rawContent }] : rawContent || []
|
|
406
|
+
const diagnostic = probeDiagnostics(data.probeState as Parameters<typeof probeDiagnostics>[0], { toolName: tool === "shell" ? "bash" : tool, isError: failed, content })
|
|
407
|
+
data.probeState = diagnostic.state
|
|
408
|
+
const cap = Math.max(0, Number(opts.toolOutputCap) || 0)
|
|
409
|
+
if (cap && event.status === "completed" && ["bash", "shell", "read", "grep", "glob", "find", "ls", "code_nav"].includes(tool)) {
|
|
410
|
+
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) }
|
|
411
|
+
}
|
|
412
|
+
if (diagnostic.message) {
|
|
413
|
+
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 }] }
|
|
414
|
+
else event.error = new NativeToolError({ message: `${event.error.message}\n\n${diagnostic.message}` })
|
|
415
|
+
}
|
|
416
|
+
if (data.state && ["delivery_check", "delivery_progress", "delivery_review", "delivery_finish", "delivery_revise", "delivery_design", "delivery_plan"].includes(tool)) {
|
|
329
417
|
const dir = runDirectory(cwd, event.sessionID, data.state.runId)
|
|
330
|
-
|
|
418
|
+
const release = lockWorkspace(cwd)
|
|
419
|
+
try {
|
|
420
|
+
assertActiveReport(cwd, join(dir, "report.json"))
|
|
421
|
+
data.state = reconcileReport(join(dir, "report.json"), data.state)
|
|
422
|
+
writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
|
|
423
|
+
} finally { release() }
|
|
331
424
|
}
|
|
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 }
|
|
425
|
+
if (event.status === "completed" && data.state && (tool === "execute" || tool.startsWith("delivery_"))) {
|
|
426
|
+
event.result = { ...event.result, metadata: { ...event.result.metadata, deliveryPlan: planSnapshot(data.state) } }
|
|
347
427
|
}
|
|
348
428
|
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
429
|
+
const delay = Math.min(30000, Math.max(0, Number(opts.turnDelayMs) || 0) * Math.max(1, Math.ceil((data.contextBytes || 0) / 4 / 50000)))
|
|
430
|
+
if (delay) await new Promise(resolve => setTimeout(resolve, delay))
|
|
349
431
|
})))
|
|
350
432
|
|
|
351
433
|
registrations.push(await ctx.shell.hook("create.before", async event => {
|
|
352
434
|
// Session-scoped policy, limits, and budget were handled at execute.before.
|
|
353
435
|
// This hook intentionally does not attribute commands: 2.0.32 has no
|
|
354
436
|
// authoritative source.sessionID/callID on ShellCreateBefore.
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
437
|
+
const capMilliseconds = (Number(options.bashCap) || 0) * 1000
|
|
438
|
+
if (capMilliseconds > 0 && (event.timeout === 0 || event.timeout > capMilliseconds)) event.timeout = capMilliseconds
|
|
357
439
|
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
440
|
}))
|
|
359
441
|
|
|
360
|
-
|
|
361
|
-
const { cwd, data } = await scope(event.sessionID)
|
|
362
|
-
if (!
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
442
|
+
registrations.push(await ctx.session.hook("prompt", async event => serialized(event.sessionID, async () => {
|
|
443
|
+
const { cwd, data, active } = await scope(event.sessionID)
|
|
444
|
+
if (!active) return
|
|
445
|
+
const prompt = originalTask(event.prompt.text)
|
|
446
|
+
const routed = routeGuidance(prompt, { cwd, profiles })
|
|
447
|
+
const prior = data.state && !["verified", "blocked"].includes(data.state.status) ? data.guidanceProfiles || data.state.guidanceProfiles || [] : []
|
|
448
|
+
data.guidanceProfiles = [...new Set([...prior, ...routed.map(profile => profile.id)])]
|
|
449
|
+
data.lastPrompt = prompt
|
|
450
|
+
if (!data.state || ["verified", "blocked"].includes(data.state.status)) data.discovery = newDiscovery(!looksInformational(prompt))
|
|
451
|
+
data.touched = false
|
|
452
|
+
data.statusPoll = undefined
|
|
453
|
+
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
454
|
+
})))
|
|
455
|
+
registrations.push(await ctx.session.hook("context", async event => serialized(event.sessionID, async () => {
|
|
456
|
+
const { session, cwd, data, options: opts, active } = await scope(event.sessionID)
|
|
457
|
+
const reason = budgetReason(data.budget)
|
|
458
|
+
if (reason) {
|
|
459
|
+
data.budget.stopped ||= reason
|
|
460
|
+
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
461
|
+
await ctx.session.interrupt({ sessionID: event.sessionID, resume: false })
|
|
462
|
+
throw Error(`Delivery budget exhausted: ${reason}. Do not dispatch another model request until an explicit delivery-budget-reset.`)
|
|
370
463
|
}
|
|
371
|
-
if (!
|
|
372
|
-
const
|
|
373
|
-
if (checkpoint) event.system.push({ type: "text", text: checkpoint })
|
|
464
|
+
if (!active || !isShipAgent(event.agent)) {
|
|
465
|
+
if (event.tools) for (const name of Object.keys(event.tools)) if (localToolName(name).startsWith("delivery_")) delete event.tools[name]
|
|
374
466
|
return
|
|
375
467
|
}
|
|
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
|
-
|
|
468
|
+
data.model = { providerID: event.model.providerID, id: event.model.id }
|
|
469
|
+
let imageInput: boolean | undefined
|
|
470
|
+
if (ctx.model) {
|
|
471
|
+
const models = await ctx.model.list({ location: { directory: session.location.directory } })
|
|
472
|
+
imageInput = models.data.find(model => model.providerID === event.model.providerID && model.id === event.model.id)?.capabilities.input.includes("image")
|
|
473
|
+
}
|
|
474
|
+
const activeProfiles = profiles.filter(profile => (data.guidanceProfiles || data.state?.guidanceProfiles || []).includes(profile.id))
|
|
475
|
+
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 }) })
|
|
476
|
+
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.` })
|
|
477
|
+
const checkpoint = discoveryCheckpoint(data.discovery)
|
|
478
|
+
if (checkpoint) event.system.push({ type: "text", text: checkpoint })
|
|
479
|
+
if (event.tools && !data.state) for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name) && name !== "execute") delete event.tools[name]
|
|
480
|
+
if (data.state && !["verified", "blocked"].includes(data.state.status)) {
|
|
481
|
+
const dir = runDirectory(cwd, event.sessionID, data.state.runId)
|
|
482
|
+
const archived = archiveDeliveryExchanges(event.messages, { cwd, directory: dir, allow: Boolean(data.state.planning?.approval) })
|
|
483
|
+
event.messages = archived.messages
|
|
484
|
+
const summary = deliveryRecovery(data.state, { report: join(dir, "report.json") })
|
|
485
|
+
data.recovery ||= {}
|
|
486
|
+
const records = recoveryMessages([], summary, data.recovery)
|
|
487
|
+
const pressure = (data.contextBytes || 0) > 240000 ? "Context pressure: inspect the current recovery record and targeted source slices; avoid repeating whole captures." : ""
|
|
488
|
+
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)}` })
|
|
489
|
+
try {
|
|
490
|
+
const metrics = contextMetrics({ system: event.system, messages: event.messages })
|
|
491
|
+
data.contextBytes = metrics.totalBytes
|
|
492
|
+
appendFileSync(join(dir, "context-metrics.jsonl"), JSON.stringify(metrics) + "\n")
|
|
493
|
+
} catch { /* Metrics are advisory, never an execution gate. */ }
|
|
494
|
+
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]
|
|
495
|
+
}
|
|
399
496
|
await ctx.storage.set(sessionKey(event.sessionID), data)
|
|
400
497
|
})))
|
|
401
|
-
|
|
498
|
+
registrations.push(await ctx.session.hook("compaction", async event => {
|
|
499
|
+
const { data, active } = await scope(event.sessionID)
|
|
500
|
+
if (!active || !isShipAgent(event.agent) || !data.state || ["verified", "blocked"].includes(data.state.status)) return
|
|
501
|
+
event.system.push({ type: "text", text: JSON.stringify(deliveryRecovery(data.state, { report: join(runDirectory(data.cwd!, event.sessionID, data.state.runId), "report.json") })) })
|
|
502
|
+
}))
|
|
402
503
|
registrations.push(await ctx.command.transform(editor => {
|
|
403
504
|
editor.add({ name: "delivery-budget-status", description: "Show this session's persistent delivery budget", execute: async ({ sessionID }) => {
|
|
404
505
|
const { data } = await scope(sessionID)
|
|
405
|
-
await ctx.session.
|
|
506
|
+
await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget status: ${JSON.stringify(budgetSnapshot(data.budget))}` })
|
|
406
507
|
} })
|
|
407
508
|
editor.add({ name: "delivery-budget-reset", description: "Explicitly authorize a fresh budget for this session", execute: async ({ sessionID }) => {
|
|
408
509
|
const snapshot = await serialized(sessionID, async () => {
|
|
@@ -410,7 +511,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
410
511
|
for (const key of admittedCalls) if (key.startsWith(`${sessionID}\0`)) admittedCalls.delete(key)
|
|
411
512
|
return budgetSnapshot(data.budget)
|
|
412
513
|
})
|
|
413
|
-
await ctx.session.
|
|
514
|
+
await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
|
|
414
515
|
} })
|
|
415
516
|
}))
|
|
416
517
|
|
|
@@ -422,7 +523,8 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
422
523
|
const sessionID = event.data.sessionID
|
|
423
524
|
let followup: string | undefined
|
|
424
525
|
await serialized(sessionID, async () => {
|
|
425
|
-
const { cwd, data, session } = await scope(sessionID)
|
|
526
|
+
const { cwd, data, session, active } = await scope(sessionID)
|
|
527
|
+
if (!active) return
|
|
426
528
|
const reason = budgetReason(data.budget)
|
|
427
529
|
if (reason && !data.budget.stopped) {
|
|
428
530
|
data.budget.stopped = reason
|
|
@@ -432,7 +534,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
432
534
|
}
|
|
433
535
|
if (data.discovery.active) data.discovery = discoveryEvent(data.discovery, "blocked")
|
|
434
536
|
const state = data.state
|
|
435
|
-
if (!state ||
|
|
537
|
+
if (!state || !shouldContinue({ state, touched: data.touched, nudges: data.nudges, stopReason: session.outcome === "succeeded" ? "stop" : "error", pendingMessages: false, fresh: false })) {
|
|
436
538
|
await ctx.storage.set(sessionKey(sessionID), data)
|
|
437
539
|
return
|
|
438
540
|
}
|
|
@@ -459,7 +561,11 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
459
561
|
data.nudges++
|
|
460
562
|
const pending = issues.pendingChecks.join(", ") || "none"
|
|
461
563
|
await ctx.storage.set(sessionKey(sessionID), data)
|
|
462
|
-
|
|
564
|
+
const failures = Object.entries(data.state.evidence || {}).filter(([, e]) => !(e as { passed: boolean }).passed).map(([id, e]) => {
|
|
565
|
+
const evidence = e as { code: number; outputTail?: string }
|
|
566
|
+
return `${id}: exit ${evidence.code}\n${evidence.outputTail || ""}`
|
|
567
|
+
}).join("\n")
|
|
568
|
+
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
569
|
} catch (error) {
|
|
464
570
|
console.error("Carthagent delivery recovery failed closed", error)
|
|
465
571
|
} finally { release() }
|
|
@@ -484,5 +590,10 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
484
590
|
function requiresImplementationTool(name: string) {
|
|
485
591
|
// V2 exposes namespaced tools as `namespace_name` in model context and hooks.
|
|
486
592
|
const toolName = name.startsWith("carthagent_") ? name.slice("carthagent_".length) : name
|
|
487
|
-
return
|
|
593
|
+
return toolName !== "execute" && !["delivery-budget-status", "delivery-budget-reset"].includes(toolName) && requiresPlanApproval(toolName)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function planSnapshot(state: SessionState["state"]) {
|
|
597
|
+
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" }))
|
|
598
|
+
return { runId: state.runId, revision: state.revision, status: state.status, items }
|
|
488
599
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behind-the-scenes phased-delivery framing (the carthagent default flow).
|
|
3
|
+
*
|
|
4
|
+
* Every typed task is a phased delivery run: the raw text is what the user
|
|
5
|
+
* sees echoed in the feed, while the engine receives the expanded delivery template
|
|
6
|
+
* body. Resolving the template here (rather than forwarding "/guide <task>"
|
|
7
|
+
* to the engine) keeps the behavior identical in --isolate and headless -p modes,
|
|
8
|
+
* and keeps the machinery invisible in the feed.
|
|
9
|
+
*
|
|
10
|
+
* This is the default flow for carthagent — the whole product is built around it —
|
|
11
|
+
* so the input pill stays empty and the hint rotates through generic
|
|
12
|
+
* builder one-liners.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, dirname, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = resolve(__dirname, '../../../..');
|
|
20
|
+
const TEMPLATE_PATH = join(ROOT, 'prompts', 'delivery.md');
|
|
21
|
+
|
|
22
|
+
let cached = null;
|
|
23
|
+
|
|
24
|
+
export const FRAME_HINTS = ['Message carthagent…'];
|
|
25
|
+
|
|
26
|
+
export function frameHint() {
|
|
27
|
+
return FRAME_HINTS[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTemplate(path) {
|
|
31
|
+
let body = '';
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(path, 'utf8');
|
|
34
|
+
const fm = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
35
|
+
body = (fm ? raw.slice(fm[0].length) : raw).trim();
|
|
36
|
+
} catch { body = ''; }
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Body of prompts/delivery.md with frontmatter stripped. '' when unreadable. */
|
|
41
|
+
export function framingTemplate(path = TEMPLATE_PATH) {
|
|
42
|
+
if (path === TEMPLATE_PATH) {
|
|
43
|
+
if (cached == null) cached = readTemplate(TEMPLATE_PATH);
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
46
|
+
return readTemplate(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Expand the delivery template for a task. Falls back to the bare task text. */
|
|
50
|
+
export function framePrompt(task, template = framingTemplate()) {
|
|
51
|
+
const t = String(task).trim();
|
|
52
|
+
if (!template) return t;
|
|
53
|
+
return template.includes('$@') ? template.replaceAll('$@', t) : `${template}\n\n${t}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Recover the original task text from a framed prompt, else null. */
|
|
57
|
+
export function unframe(text) {
|
|
58
|
+
// Match the current template suffix first: the task itself may contain
|
|
59
|
+
// blank lines (or even phase headings). Keep the legacy format fallback.
|
|
60
|
+
const value = String(text);
|
|
61
|
+
const template = framingTemplate();
|
|
62
|
+
const parts = template.split('$@');
|
|
63
|
+
if (parts.length === 2 && value.startsWith(parts[0]) && value.endsWith(parts[1])) {
|
|
64
|
+
return value.slice(parts[0].length, parts[1] ? -parts[1].length : undefined).trim();
|
|
65
|
+
}
|
|
66
|
+
const m = value.match(/^Deliver this task in phases(?: with executable evidence)?:\s*([\s\S]*?)\n\n/);
|
|
67
|
+
return m ? m[1].trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when a submitted prompt should get the delivery framing. */
|
|
71
|
+
export function shouldFrame(text, { enabled = true, delivery = true } = {}) {
|
|
72
|
+
if (!enabled || !delivery) return false;
|
|
73
|
+
const v = String(text).trim();
|
|
74
|
+
if (!v) return false;
|
|
75
|
+
if (v.startsWith('/') || v.startsWith('!')) return false;
|
|
76
|
+
// Questions/read-only asks are not delivery runs.
|
|
77
|
+
if (v.endsWith('?') && v.length < 300) return false;
|
|
78
|
+
if (/^(what|why|how|explain|show|list|describe|review|where|is|are|does|do|can)\b/i.test(v) && v.length < 300 && !/\b(build|create|implement|write|add|fix|make|generate)\b/i.test(v)) return false;
|
|
79
|
+
return existsSync(TEMPLATE_PATH);
|
|
80
|
+
}
|