carthagent 0.0.0-stage → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function terminalTitleSequence(cwd) {
|
|
4
|
+
// Folder names must not inject terminal controls into the OSC title.
|
|
5
|
+
const folder = basename(cwd).replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
6
|
+
return `\x1b]0;Carthagent${folder ? ` - ${folder}` : ''}\x07`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export const UPDATE_CHANNELS = ['stable', 'beta', 'experimental'];
|
|
7
|
+
|
|
8
|
+
export function validateUpdateChannel(channel) {
|
|
9
|
+
if (!UPDATE_CHANNELS.includes(channel)) throw Error(`Invalid update channel: ${channel}. Choose stable, beta, or experimental.`);
|
|
10
|
+
return channel;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function updateChannelPath(env = process.env) {
|
|
14
|
+
return env.CARTHAGENT_UPDATE_CHANNEL_FILE || join(env.HOME || env.USERPROFILE || homedir(), '.carthagent', 'update-channel.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readUpdateChannel(env = process.env) {
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(readFileSync(updateChannelPath(env), 'utf8'));
|
|
20
|
+
if (value.version !== 1) throw Error('Unsupported update channel file version');
|
|
21
|
+
return validateUpdateChannel(value.channel);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT') return 'stable';
|
|
24
|
+
throw Error(`Cannot read update channel; use ctg update --channel stable to reset it: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function writeUpdateChannel(channel, env = process.env) {
|
|
29
|
+
validateUpdateChannel(channel);
|
|
30
|
+
const path = updateChannelPath(env);
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
33
|
+
try {
|
|
34
|
+
writeFileSync(temp, JSON.stringify({ version: 1, channel }) + '\n', { mode: 0o600, flag: 'wx' });
|
|
35
|
+
renameSync(temp, path);
|
|
36
|
+
} finally { rmSync(temp, { force: true }); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseUpdateOptions(args) {
|
|
40
|
+
const options = { checkOnly: false, force: false, channel: null };
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (arg === '--check') options.checkOnly = true;
|
|
44
|
+
else if (arg === '--force') options.force = true;
|
|
45
|
+
else if (arg === '--channel' || arg.startsWith('--channel=')) {
|
|
46
|
+
if (options.channel !== null) throw Error('Specify --channel only once.');
|
|
47
|
+
options.channel = validateUpdateChannel(arg === '--channel' ? args[++i] : arg.slice('--channel='.length));
|
|
48
|
+
} else throw Error(`Unknown update option: ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
return options;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function releaseAllowedOnChannel(release, channel) {
|
|
54
|
+
validateUpdateChannel(channel);
|
|
55
|
+
if (!release || release.draft || typeof release.version !== 'string') return false;
|
|
56
|
+
const version = release.version.split('+')[0];
|
|
57
|
+
const label = version.includes('-') ? version.slice(version.indexOf('-') + 1) : '';
|
|
58
|
+
if (!label && !release.prerelease) return true;
|
|
59
|
+
if (channel === 'stable') return false;
|
|
60
|
+
if (channel === 'experimental') return true;
|
|
61
|
+
return /^(beta|rc)(\.|$)/.test(label || '');
|
|
62
|
+
}
|
package/lib/update.mjs
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update support for `ctg update` and the passive once-a-day console notice.
|
|
3
|
+
*
|
|
4
|
+
* Release resolution order:
|
|
5
|
+
* 1. Control plane GET <CARTHAGENT_CLOUD_URL|api.carthagent.xyz>/v1/cli/releases/latest
|
|
6
|
+
* (operator-controlled "latest" pointer — staged rollouts, pullbacks)
|
|
7
|
+
* 2. GitHub releases API for B-icy/Carthagent — fallback while the endpoint is
|
|
8
|
+
* unconfigured (503/404) or unreachable.
|
|
9
|
+
*
|
|
10
|
+
* Install methods:
|
|
11
|
+
* - clone : the package root contains .git (installed via `npm install -g .` from a
|
|
12
|
+
* clone) → checkout the tag in place, `npm ci`, re-link.
|
|
13
|
+
* - npm : global `github:` install → `npm install -g --install-links=true <spec>`.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import { cloudControlUrl } from './cloud/client.mjs';
|
|
20
|
+
import { shipAgentDir } from './ship-auth.mjs';
|
|
21
|
+
import { installPingHeaders } from './ship-telemetry.mjs';
|
|
22
|
+
import { readUpdateChannel, releaseAllowedOnChannel, validateUpdateChannel } from './update-channel.mjs';
|
|
23
|
+
|
|
24
|
+
export const GITHUB_REPO = 'B-icy/Carthagent';
|
|
25
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
27
|
+
|
|
28
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
|
|
30
|
+
/** The installed package version (package.json next to lib/). */
|
|
31
|
+
export function packageVersion() {
|
|
32
|
+
try { return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version; }
|
|
33
|
+
catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Semver-ish ordering: numeric triple, then a release outranks its prereleases. */
|
|
37
|
+
export function compareVersions(a, b) {
|
|
38
|
+
const parse = value => {
|
|
39
|
+
const m = String(value || '').trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] || '' };
|
|
42
|
+
};
|
|
43
|
+
const left = parse(a), right = parse(b);
|
|
44
|
+
if (!left || !right) throw new Error(`cannot compare versions '${a}' and '${b}'`);
|
|
45
|
+
for (let i = 0; i < 3; i++) if (left.nums[i] !== right.nums[i]) return left.nums[i] - right.nums[i];
|
|
46
|
+
if (left.pre === right.pre) return 0;
|
|
47
|
+
if (!left.pre) return 1;
|
|
48
|
+
if (!right.pre) return -1;
|
|
49
|
+
const lp = left.pre.split('.'), rp = right.pre.split('.');
|
|
50
|
+
for (let i = 0; i < Math.max(lp.length, rp.length); i++) {
|
|
51
|
+
if (lp[i] === undefined) return -1;
|
|
52
|
+
if (rp[i] === undefined) return 1;
|
|
53
|
+
if (lp[i] === rp[i]) continue;
|
|
54
|
+
const ln = /^\d+$/.test(lp[i]), rn = /^\d+$/.test(rp[i]);
|
|
55
|
+
if (ln && rn) return Number(lp[i]) - Number(rp[i]);
|
|
56
|
+
if (ln !== rn) return ln ? -1 : 1; // numeric identifiers rank below alphanumeric
|
|
57
|
+
return lp[i] < rp[i] ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Normalize a control-plane or GitHub releases payload into the manifest shape. */
|
|
63
|
+
export function normalizeRelease(raw, { githubRepo = GITHUB_REPO } = {}) {
|
|
64
|
+
if (!raw || typeof raw !== 'object' || raw.draft === true) return null;
|
|
65
|
+
if (['draft', 'prerelease'].some(key => raw[key] !== undefined && typeof raw[key] !== 'boolean')) return null;
|
|
66
|
+
// GitHub releases/latest shape: { tag_name, name, published_at, body }
|
|
67
|
+
const suppliedTag = raw.tag ?? raw.tag_name;
|
|
68
|
+
if (suppliedTag !== undefined && (typeof suppliedTag !== 'string' || suppliedTag.length === 0)) return null;
|
|
69
|
+
const tag = suppliedTag ?? '';
|
|
70
|
+
const suppliedVersion = raw.version;
|
|
71
|
+
if (suppliedVersion !== undefined && (typeof suppliedVersion !== 'string' || suppliedVersion.length === 0)) return null;
|
|
72
|
+
const version = suppliedVersion ?? tag.replace(/^v/, '');
|
|
73
|
+
if (!SEMVER_RE.test(version)) return null;
|
|
74
|
+
// Never let a valid version select a different mutable ref (for example
|
|
75
|
+
// `main`, another release, or a whitespace/URL-encoded ref). The only
|
|
76
|
+
// accepted release tag is the canonical tag for this exact version.
|
|
77
|
+
const expectedTag = `v${version}`;
|
|
78
|
+
if (tag && tag !== expectedTag) return null;
|
|
79
|
+
// The manifest is an install instruction: `ctg update` runs
|
|
80
|
+
// `npm install -g <installSpec>` verbatim. Pin it to this release's tag on
|
|
81
|
+
// the canonical repo so a leaked publish token cannot redirect installs.
|
|
82
|
+
const canonicalTag = expectedTag;
|
|
83
|
+
const installSpec = `github:${githubRepo}#${canonicalTag}`;
|
|
84
|
+
if (raw.installSpec !== undefined && (typeof raw.installSpec !== 'string' || raw.installSpec !== installSpec)) return null;
|
|
85
|
+
return {
|
|
86
|
+
version,
|
|
87
|
+
prerelease: raw.prerelease === true || version.split('+')[0].includes('-'),
|
|
88
|
+
tag: canonicalTag,
|
|
89
|
+
installSpec,
|
|
90
|
+
minNode: String(raw.minNode || '22.19.0').trim(),
|
|
91
|
+
note: typeof raw.note === 'string' && raw.note.trim() ? raw.note.trim()
|
|
92
|
+
: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : undefined,
|
|
93
|
+
publishedAt: raw.publishedAt || raw.published_at || undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Latest release manifest: control plane first, GitHub releases as fallback. */
|
|
98
|
+
export async function fetchLatestRelease({ cloudUrl = cloudControlUrl(), githubRepo = GITHUB_REPO, fetchImpl = globalThis.fetch, timeoutMs = 8000, env = process.env, agentDir = shipAgentDir(env), signal, channel = readUpdateChannel(env) } = {}) {
|
|
99
|
+
validateUpdateChannel(channel);
|
|
100
|
+
const getJson = async (url, headers = {}) => {
|
|
101
|
+
signal?.throwIfAborted();
|
|
102
|
+
const res = await fetchImpl(url, {
|
|
103
|
+
headers: { accept: 'application/json', 'user-agent': `carthagent-cli`, ...headers },
|
|
104
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
|
|
105
|
+
});
|
|
106
|
+
const body = await res.json().catch(() => null);
|
|
107
|
+
return { status: res.status, body };
|
|
108
|
+
};
|
|
109
|
+
if (channel !== 'stable') {
|
|
110
|
+
const candidates = [];
|
|
111
|
+
for (let page = 1; page <= 10; page++) {
|
|
112
|
+
const { status, body } = await getJson(`https://api.github.com/repos/${githubRepo}/releases?per_page=100&page=${page}`);
|
|
113
|
+
if (status !== 200 || !Array.isArray(body)) throw Error(`GitHub ${channel} release listing failed (${status}).`);
|
|
114
|
+
for (const item of body) {
|
|
115
|
+
const release = normalizeRelease(item, { githubRepo });
|
|
116
|
+
if (releaseAllowedOnChannel(release, channel)) candidates.push(release);
|
|
117
|
+
}
|
|
118
|
+
if (body.length < 100) {
|
|
119
|
+
candidates.sort((a, b) => compareVersions(b.version, a.version));
|
|
120
|
+
if (!candidates.length) throw Error(`No published releases are available on the ${channel} channel.`);
|
|
121
|
+
return { release: candidates[0], source: 'github' };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw Error('GitHub release listing exceeded 1,000 entries; refusing an incomplete channel selection.');
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
// The release check doubles as the anonymous install ping; GitHub never
|
|
128
|
+
// sees these headers.
|
|
129
|
+
const { status, body } = await getJson(`${String(cloudUrl).replace(/\/+$/, '')}/v1/cli/releases/latest`, installPingHeaders({ env, agentDir, version: packageVersion() }));
|
|
130
|
+
if (status === 200) {
|
|
131
|
+
const release = normalizeRelease(body, { githubRepo });
|
|
132
|
+
if (releaseAllowedOnChannel(release, channel)) return { release, source: 'control-plane' };
|
|
133
|
+
// A 200 manifest that fails validation (e.g. a mismatched installSpec)
|
|
134
|
+
// must not dead-end the update — resolve the GitHub release instead.
|
|
135
|
+
} else if (![404, 503].includes(status)) throw new Error(`control plane returned ${status}`);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
signal?.throwIfAborted();
|
|
138
|
+
if (!/control plane|fetch|network|abort|timed? ?out|connect|socket/i.test(error?.message || String(error))) throw error;
|
|
139
|
+
// unreachable/unconfigured control plane → fall through to GitHub
|
|
140
|
+
}
|
|
141
|
+
signal?.throwIfAborted();
|
|
142
|
+
const { status, body } = await getJson(`https://api.github.com/repos/${githubRepo}/releases/latest`);
|
|
143
|
+
if (status !== 200) throw new Error(status === 404 ? 'no tagged GitHub release exists yet' : `GitHub releases returned ${status}`);
|
|
144
|
+
const release = normalizeRelease(body, { githubRepo });
|
|
145
|
+
if (!releaseAllowedOnChannel(release, channel)) throw new Error('GitHub latest release is not a valid stable release.');
|
|
146
|
+
return { release, source: 'github' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 'clone' when the running package root is a git checkout, else 'npm'. */
|
|
150
|
+
export function detectInstallMethod(root) {
|
|
151
|
+
return existsSync(join(root, '.git')) ? 'clone' : 'npm';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Entries under a global node_modules dir that break `npm install -g` with
|
|
156
|
+
* ENOTDIR/EEXIST: a non-directory `carthagent` entry (stray file or dangling
|
|
157
|
+
* symlink from an earlier `npm link`) and `.carthagent-*` stash paths npm
|
|
158
|
+
* abandons when an install is interrupted mid-swap.
|
|
159
|
+
*/
|
|
160
|
+
export function staleGlobalInstallPaths(npmRoot) {
|
|
161
|
+
const stale = [];
|
|
162
|
+
const entry = join(npmRoot, 'carthagent');
|
|
163
|
+
try {
|
|
164
|
+
if (!lstatSync(entry).isDirectory()) stale.push(entry);
|
|
165
|
+
} catch { /* absent */ }
|
|
166
|
+
let names = [];
|
|
167
|
+
try { names = readdirSync(npmRoot); } catch { /* unreadable */ }
|
|
168
|
+
for (const name of names) if (/^\.carthagent-.+/.test(name)) stale.push(join(npmRoot, name));
|
|
169
|
+
return stale;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Remove stale install leftovers; returns the paths it deleted. */
|
|
173
|
+
export function cleanStaleGlobalInstall(npmRoot) {
|
|
174
|
+
const removed = [];
|
|
175
|
+
for (const path of staleGlobalInstallPaths(npmRoot)) {
|
|
176
|
+
try { rmSync(path, { recursive: true, force: true }); removed.push(path); }
|
|
177
|
+
catch { /* best-effort */ }
|
|
178
|
+
}
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Passive-check cache (~/.carthagent/update-check.json). */
|
|
183
|
+
export function updateCheckPath(env = process.env) {
|
|
184
|
+
return env.CARTHAGENT_UPDATE_CHECK_FILE || join(os.homedir(), '.carthagent', 'update-check.json');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function updateCheckEnabled(env = process.env) {
|
|
188
|
+
const off = env.CARTHAGENT_NO_UPDATE_CHECK || env.CARTHAGENT_UPDATE_CHECK === '0' || env.CARTHAGENT_UPDATE_CHECK === 'false';
|
|
189
|
+
return !off;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function readUpdateCache(path = updateCheckPath()) {
|
|
193
|
+
try {
|
|
194
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
195
|
+
return data && typeof data === 'object' ? data : null;
|
|
196
|
+
} catch { return null; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function writeUpdateCache(entry, path = updateCheckPath()) {
|
|
200
|
+
try {
|
|
201
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
202
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
203
|
+
writeFileSync(tmp, JSON.stringify(entry));
|
|
204
|
+
renameSync(tmp, path);
|
|
205
|
+
} catch { /* cache is best-effort */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Latest release for the passive notice, throttled to one fetch per day.
|
|
210
|
+
* Cache writes happen on every attempt (success or failure) so a failing
|
|
211
|
+
* network doesn't get retried every launch.
|
|
212
|
+
*/
|
|
213
|
+
export async function latestReleaseCached({ cachePath = updateCheckPath(), now = Date.now(), intervalMs = UPDATE_CHECK_INTERVAL_MS, env = process.env, channel = readUpdateChannel(env), ...fetchOpts } = {}) {
|
|
214
|
+
validateUpdateChannel(channel);
|
|
215
|
+
const cached = readUpdateCache(cachePath);
|
|
216
|
+
const sameChannel = (cached?.channel ?? 'stable') === channel;
|
|
217
|
+
const normalized = normalizeRelease(cached?.release);
|
|
218
|
+
const staleRelease = sameChannel && releaseAllowedOnChannel(normalized, channel) ? normalized : null;
|
|
219
|
+
if (sameChannel && (!cached?.release || staleRelease) && cached && Number.isFinite(cached.checkedAt) && now - cached.checkedAt < intervalMs) {
|
|
220
|
+
return { release: staleRelease, fresh: true };
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const { release, source } = await fetchLatestRelease({ ...fetchOpts, env, channel });
|
|
224
|
+
writeUpdateCache({ checkedAt: now, release, source, channel }, cachePath);
|
|
225
|
+
return { release, fresh: false };
|
|
226
|
+
} catch {
|
|
227
|
+
writeUpdateCache({ checkedAt: now, release: staleRelease, channel }, cachePath);
|
|
228
|
+
return { release: staleRelease, fresh: false };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { captureBrowserErrors } from './browser-errors.mjs';
|
|
4
|
+
|
|
5
|
+
/** Bounded W3C WebDriver checks with mandatory pre-navigation BiDi error capture. */
|
|
6
|
+
export async function webdriverCheck({ endpoint, url, steps = [], screenshot, waitMs = 5000 }) {
|
|
7
|
+
if (!Number.isInteger(waitMs) || waitMs < 1 || waitMs > 60000) throw Object.assign(Error('waitMs must be an integer from 1 to 60000'), { category: 'configuration' });
|
|
8
|
+
let deadline;
|
|
9
|
+
let phase = 'driver';
|
|
10
|
+
let stepIndex;
|
|
11
|
+
let currentStep;
|
|
12
|
+
const base = endpoint.replace(/\/$/, '');
|
|
13
|
+
async function request(path, method = 'GET', body) {
|
|
14
|
+
const response = await fetch(base + path, {
|
|
15
|
+
method, headers: { 'content-type': 'application/json' },
|
|
16
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
17
|
+
signal: AbortSignal.timeout(deadline ? Math.max(1, Math.min(20000, deadline - Date.now())) : 20000)
|
|
18
|
+
});
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
if (!response.ok || data.value?.error) throw Object.assign(Error(data.value?.message || `WebDriver HTTP ${response.status}`), { webdriverCode: data.value?.error });
|
|
21
|
+
return data.value;
|
|
22
|
+
}
|
|
23
|
+
const session = await request('/session', 'POST', { capabilities: { alwaysMatch: {
|
|
24
|
+
browserName: 'firefox', webSocketUrl: true, 'moz:firefoxOptions': { args: ['-headless'] }
|
|
25
|
+
} } });
|
|
26
|
+
const prefix = `/session/${session.sessionId}`;
|
|
27
|
+
let failure;
|
|
28
|
+
let capture;
|
|
29
|
+
let diagnostics;
|
|
30
|
+
try {
|
|
31
|
+
phase = 'capture';
|
|
32
|
+
capture = await captureBrowserErrors(session.capabilities.webSocketUrl);
|
|
33
|
+
await request(prefix + '/timeouts', 'POST', { implicit: 0, pageLoad: 15000, script: 5000 });
|
|
34
|
+
phase = 'navigation';
|
|
35
|
+
await request(prefix + '/url', 'POST', { url });
|
|
36
|
+
for (const [index, step] of steps.entries()) {
|
|
37
|
+
phase = 'assertion'; stepIndex = index; currentStep = step;
|
|
38
|
+
deadline = Date.now() + waitMs;
|
|
39
|
+
let id;
|
|
40
|
+
for (;;) {
|
|
41
|
+
try {
|
|
42
|
+
const element = await request(prefix + '/element', 'POST', { using: 'css selector', value: step.selector });
|
|
43
|
+
id = element['element-6066-11e4-a52e-4f735466cecf'];
|
|
44
|
+
if (step.text !== undefined) {
|
|
45
|
+
const text = await request(`${prefix}/element/${id}/text`);
|
|
46
|
+
if (!text.includes(step.text)) throw Object.assign(Error(`${step.selector}: expected ${JSON.stringify(step.text)}, got ${JSON.stringify(text)}`), { webdriverCode: 'text mismatch' });
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!['no such element', 'stale element reference', 'text mismatch'].includes(error.webdriverCode) || Date.now() >= deadline) throw error;
|
|
51
|
+
await new Promise(r => setTimeout(r, Math.min(100, Math.max(1, deadline - Date.now()))));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
deadline = undefined;
|
|
55
|
+
if (step.action === 'click') {
|
|
56
|
+
phase = 'interaction';
|
|
57
|
+
await request(`${prefix}/element/${id}/click`, 'POST', {});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
stepIndex = undefined; currentStep = undefined; phase = 'screenshot';
|
|
61
|
+
if (screenshot) {
|
|
62
|
+
const png = await request(prefix + '/screenshot');
|
|
63
|
+
mkdirSync(dirname(screenshot), { recursive: true });
|
|
64
|
+
writeFileSync(screenshot, Buffer.from(png, 'base64'));
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
failure = Object.assign(error, { category: phase, stepIndex, selector: currentStep?.selector, expected: currentStep?.text });
|
|
68
|
+
}
|
|
69
|
+
deadline = undefined;
|
|
70
|
+
if (capture) {
|
|
71
|
+
try {
|
|
72
|
+
diagnostics = await capture.inspect();
|
|
73
|
+
if (diagnostics.errors.length) failure ||= Object.assign(Error('Unexpected browser errors'), { category: 'browser-errors' });
|
|
74
|
+
} catch (error) { failure ||= Object.assign(error, { category: 'capture' }); }
|
|
75
|
+
finally { capture.close(); }
|
|
76
|
+
}
|
|
77
|
+
if (failure && diagnostics) Object.assign(failure, { browserErrors: diagnostics.errors, droppedBrowserErrors: diagnostics.dropped });
|
|
78
|
+
try { await request(prefix, 'DELETE'); }
|
|
79
|
+
catch (error) { failure ||= Object.assign(Error(`Session cleanup failed: ${error.message}`), { category: 'cleanup' }); }
|
|
80
|
+
if (failure) throw failure;
|
|
81
|
+
return { pass: true, engine: 'firefox-webdriver', browserVersion: session.capabilities.browserVersion, screenshot: screenshot || null, consoleChecked: true };
|
|
82
|
+
}
|
package/lib/workflow.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Canonical, non-executable workflow data. Models propose nodes and branches;
|
|
2
|
+
// harness policy remains authoritative. No renderer or process dependencies.
|
|
3
|
+
const list = v => Array.isArray(v) ? v : [];
|
|
4
|
+
const text = v => typeof v === 'string' && v.trim().length > 0;
|
|
5
|
+
const safe = v => text(v) && /^[a-z][a-z0-9_-]{0,39}$/.test(v) && !v.startsWith('gate_');
|
|
6
|
+
export function validateWorkflow(plan) {
|
|
7
|
+
if (plan.workflow === undefined) return []; // Legacy plans have a projection.
|
|
8
|
+
const errors = [], workflow = plan.workflow;
|
|
9
|
+
const fail = message => errors.push({ code: 'workflow', target: 'workflow', severity: 'blocking', message });
|
|
10
|
+
if (!workflow || typeof workflow !== 'object' || !Array.isArray(workflow.nodes) || !Array.isArray(workflow.recovery)) { fail('Workflow requires nodes and recovery arrays.'); return errors; }
|
|
11
|
+
if (!workflow.nodes.length || workflow.nodes.length > 80) fail('Workflow requires 1–80 nodes.');
|
|
12
|
+
if (workflow.recovery.length > 80) fail('Use at most 80 recovery transitions.');
|
|
13
|
+
const ids = new Set(), steps = new Set(list(plan.steps).map((s, i) => typeof s === 'string' ? `legacy_${i + 1}` : s.id));
|
|
14
|
+
const components = new Set(list(plan.design?.components).map(c => c?.id));
|
|
15
|
+
const checks = new Set(list(plan.checks).map(c => c.id));
|
|
16
|
+
const requirements = new Set(list(plan.acceptance).map(a => a.requirement));
|
|
17
|
+
for (const n of workflow.nodes) {
|
|
18
|
+
if (!n || !safe(n.id) || ids.has(n.id)) { fail('Node IDs must be unique safe IDs outside the gate_ namespace.'); continue; }
|
|
19
|
+
ids.add(n.id);
|
|
20
|
+
if (!text(n.title) || n.title.length > 500 || !['work', 'decision', 'check', 'artifact'].includes(n.type)) fail(`Invalid title/type: ${n.id}`);
|
|
21
|
+
for (const field of ['dependsOn', 'requirements', 'components', 'checks', 'inputs', 'artifacts', 'effects']) if (!Array.isArray(n[field]) || n[field].some(v => !text(v)) || new Set(n[field]).size !== n[field].length) fail(`${n.id}.${field} must be a unique string array.`);
|
|
22
|
+
if (list(n.requirements).some(id => !requirements.has(id)) || list(n.components).some(id => !components.has(id)) || list(n.checks).some(id => !checks.has(id))) fail(`Unknown requirement/component/check: ${n.id}`);
|
|
23
|
+
if (list(n.effects).some(effect => !['read', 'write', 'execute'].includes(effect))) fail(`Unknown permitted effect: ${n.id}`);
|
|
24
|
+
if (n.type === 'work' && !steps.has(n.step)) fail(`Work node needs a declared step: ${n.id}`);
|
|
25
|
+
if (n.type === 'check' && !list(n.checks).length) fail(`Check node needs check IDs: ${n.id}`);
|
|
26
|
+
if (n.type === 'decision' && !text(n.condition)) fail(`Decision needs a condition: ${n.id}`);
|
|
27
|
+
}
|
|
28
|
+
const visiting = new Set(), visited = new Set();
|
|
29
|
+
function visit(id) {
|
|
30
|
+
if (visiting.has(id)) { fail(`Dependency cycle at ${id}; use bounded recovery transitions instead.`); return; }
|
|
31
|
+
if (visited.has(id)) return;
|
|
32
|
+
visiting.add(id);
|
|
33
|
+
for (const dep of list(workflow.nodes.find(n => n?.id === id)?.dependsOn)) {
|
|
34
|
+
if (!ids.has(dep)) fail(`Unknown dependency ${dep}`); else visit(dep);
|
|
35
|
+
}
|
|
36
|
+
visiting.delete(id); visited.add(id);
|
|
37
|
+
}
|
|
38
|
+
for (const id of ids) visit(id);
|
|
39
|
+
for (const r of workflow.recovery) {
|
|
40
|
+
if (!r || !ids.has(r.from) || !ids.has(r.to) || !text(r.when) || !text(r.stop) || !Number.isInteger(r.maxAttempts) || r.maxAttempts < 1 || r.maxAttempts > 10) fail('Recovery needs known endpoints, condition, stop reason and maxAttempts 1–10.');
|
|
41
|
+
}
|
|
42
|
+
for (const step of steps) if (!workflow.nodes.some(n => n?.type === 'work' && n.step === step)) fail(`Workflow omits step ${step}`);
|
|
43
|
+
return errors;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workflowGraph(plan) {
|
|
47
|
+
const errors = validateWorkflow(plan);
|
|
48
|
+
if (errors.length) throw Error(errors.map(e => e.message).join('\n'));
|
|
49
|
+
const steps = list(plan.steps);
|
|
50
|
+
const proposed = plan.workflow ? structuredClone(plan.workflow.nodes) : steps.map((s, i) => ({
|
|
51
|
+
id: `step${i}`, step: typeof s === 'string' ? `legacy_${i + 1}` : s.id, title: typeof s === 'string' ? s : s.title, type: 'work',
|
|
52
|
+
dependsOn: typeof s === 'string' ? (i ? [`step${i - 1}`] : []) : list(s.dependsOn).map(id => `step${steps.findIndex(other => other?.id === id)}`),
|
|
53
|
+
requirements: [], components: [], checks: typeof s === 'string' ? [] : list(s.checks), inputs: [], artifacts: [], effects: ['write'],
|
|
54
|
+
}));
|
|
55
|
+
const gates = [
|
|
56
|
+
{ id: 'gate_design', title: 'Test + approve plan', type: 'gate' },
|
|
57
|
+
{ id: 'gate_verify', title: 'Fresh checks + coverage', type: 'gate' },
|
|
58
|
+
{ id: 'gate_review', title: 'Final snapshot review', type: 'gate' },
|
|
59
|
+
{ id: 'gate_deliver', title: 'Complete handoff', type: 'gate' },
|
|
60
|
+
];
|
|
61
|
+
const edges = [];
|
|
62
|
+
for (const n of proposed) for (const dep of n.dependsOn.length ? n.dependsOn : ['gate_design']) edges.push({ from: dep, to: n.id, kind: 'dependency' });
|
|
63
|
+
for (const n of proposed) if (!proposed.some(other => other.dependsOn.includes(n.id))) edges.push({ from: n.id, to: 'gate_verify', kind: 'dependency' });
|
|
64
|
+
if (!proposed.length) edges.push({ from: 'gate_design', to: 'gate_verify', kind: 'dependency' });
|
|
65
|
+
edges.push({ from: 'gate_verify', to: 'gate_review', kind: 'dependency' }, { from: 'gate_review', to: 'gate_deliver', kind: 'dependency' });
|
|
66
|
+
for (const r of list(plan.workflow?.recovery)) edges.push({ ...r, kind: 'recovery', label: `${r.when}; <=${r.maxAttempts}; stop: ${r.stop}` });
|
|
67
|
+
return { authored: Boolean(plan.workflow), nodes: [gates[0], ...proposed, ...gates.slice(1)], edges, semantics: 'Dependency DAG plus proposed bounded recovery; not an executable scheduler. Effects and conditions are declarations, not sandbox permissions.' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function workflowD2(plan, stepStatus = {}) {
|
|
71
|
+
const graph = workflowGraph(plan), steps = list(plan.steps);
|
|
72
|
+
const lines = ['direction: down'];
|
|
73
|
+
for (const n of graph.nodes) {
|
|
74
|
+
const index = steps.findIndex((s, i) => (typeof s === 'string' ? `legacy_${i + 1}` : s.id) === n.step);
|
|
75
|
+
const status = index < 0 ? '' : stepStatus[`step${index}`] || 'pending';
|
|
76
|
+
lines.push(`${n.id}: ${JSON.stringify(`${n.title}${status ? ` [${status}]` : ''}`)}`);
|
|
77
|
+
}
|
|
78
|
+
for (const e of graph.edges) lines.push(`${e.from} -> ${e.to}${e.label ? `: ${JSON.stringify(e.label)}` : ''}`);
|
|
79
|
+
return lines.join('\n') + '\n';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function architectureGraph(plan) {
|
|
83
|
+
return {
|
|
84
|
+
nodes: list(plan.design?.components).map(c => ({ id: c.id, title: c.responsibility, layer: c.layer, excludes: c.excludes, requirements: c.requirements })),
|
|
85
|
+
edges: list(plan.design?.components).flatMap(c => list(c.dependencies).map(to => ({ from: c.id, to, kind: 'dependency' }))),
|
|
86
|
+
ports: structuredClone(list(plan.design?.ports)),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { atomicJson } from './delivery.mjs';
|
|
4
|
+
|
|
5
|
+
export function lockWorkspace(cwd) {
|
|
6
|
+
const dir = join(cwd, '.harness');
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
const path = join(dir, 'workspace.lock');
|
|
9
|
+
let fd;
|
|
10
|
+
try { fd = openSync(path, 'wx'); }
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === 'EEXIST') throw Error('Workspace busy: another delivery operation is active. Retry after it finishes; never remove this lock without confirming the writer has stopped.');
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
try { writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() })); }
|
|
16
|
+
catch (error) { closeSync(fd); unlinkSync(path); throw error; }
|
|
17
|
+
return () => { closeSync(fd); unlinkSync(path); };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function activeReportPath(cwd) {
|
|
21
|
+
let pointer;
|
|
22
|
+
try { pointer = JSON.parse(readFileSync(join(cwd, '.harness', 'active.json'), 'utf8')); }
|
|
23
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
24
|
+
if (pointer?.version !== 1 || typeof pointer.path !== 'string' || isAbsolute(pointer.path)) throw Error('Invalid active workspace report');
|
|
25
|
+
const path = resolve(cwd, pointer.path);
|
|
26
|
+
const rel = relative(join(cwd, '.harness'), path);
|
|
27
|
+
if (rel.startsWith('..') || isAbsolute(rel) || !rel.endsWith('report.json')) throw Error('Invalid active workspace report path');
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Called only while holding the workspace operation lock.
|
|
32
|
+
export function selectReport(cwd, path) {
|
|
33
|
+
atomicJson(join(cwd, '.harness', 'active.json'), { version: 1, path: relative(cwd, path) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assertActiveReport(cwd, path) {
|
|
37
|
+
const active = activeReportPath(cwd);
|
|
38
|
+
if (active && resolve(active) !== resolve(path)) throw Error('Delivery run was superseded in this workspace. Inspect delivery_status and create a new plan explicitly; stale operations were not replayed.');
|
|
39
|
+
}
|
package/metadata.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,96 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carthagent",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"workspaces": [
|
|
5
|
+
"plugins/carthagent"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.19.0"
|
|
10
|
+
},
|
|
11
|
+
"description": "Evidence-driven software delivery, from plan to proof",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"carthagent",
|
|
14
|
+
"ai-agent",
|
|
15
|
+
"software-delivery",
|
|
16
|
+
"evidence-driven-delivery"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"ctg": "./bin/ctg.mjs"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/B-icy/Carthagent"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/B-icy/Carthagent#readme",
|
|
26
|
+
"bugs": "https://github.com/B-icy/Carthagent/issues",
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"docs",
|
|
30
|
+
"extensions",
|
|
31
|
+
"guidance",
|
|
32
|
+
"lib",
|
|
33
|
+
"plugins",
|
|
34
|
+
"!plugins/**/node_modules/**",
|
|
35
|
+
"themes",
|
|
36
|
+
"prompts",
|
|
37
|
+
"public",
|
|
38
|
+
"scenarios/**/*.json",
|
|
39
|
+
"scenarios/**/*.md",
|
|
40
|
+
"scenarios/**/*.py",
|
|
41
|
+
"scenarios/**/*.txt",
|
|
42
|
+
"skills",
|
|
43
|
+
"tests/*.mjs",
|
|
44
|
+
"tests/*.py",
|
|
45
|
+
"tests/fixtures/archive-plugin",
|
|
46
|
+
"tools",
|
|
47
|
+
"vendor",
|
|
48
|
+
"evaluate.mjs",
|
|
49
|
+
"metadata.json",
|
|
50
|
+
"server.mjs"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "node server.mjs",
|
|
54
|
+
"start": "node server.mjs",
|
|
55
|
+
"ctg": "node bin/ctg.mjs",
|
|
56
|
+
"check": "node tools/check-syntax.mjs",
|
|
57
|
+
"typecheck": "npm run typecheck:legacy && npm run typecheck:native",
|
|
58
|
+
"typecheck:legacy": "tsc --noEmit --skipLibCheck --target ES2023 --module NodeNext --moduleResolution NodeNext --types node extensions/delivery.ts extensions/experiential.ts extensions/subscriptions.ts extensions/codegraph.ts types/pi-coding-agent.d.ts",
|
|
59
|
+
"typecheck:native": "tsc -p plugins/carthagent/tsconfig.json",
|
|
60
|
+
"lint": "eslint . && npm run typecheck",
|
|
61
|
+
"quality": "npm run lint && npm run build && npm test",
|
|
62
|
+
"test:browser": "node tools/test-webdriver.mjs",
|
|
63
|
+
"test:native-host": "node tools/test-native-host.mjs",
|
|
64
|
+
"test:native-package": "node tools/test-native-package.mjs",
|
|
65
|
+
"test:native-controls": "node tools/test-native-controls.mjs",
|
|
66
|
+
"test:native-outcomes": "node tools/test-native-outcomes.mjs",
|
|
67
|
+
"test:native-pty": "node tools/test-native-pty.mjs",
|
|
68
|
+
"build": "npm run check && npm run typecheck",
|
|
69
|
+
"test": "node --test tests/*.test.mjs",
|
|
70
|
+
"test:python": "node tools/test-python.mjs"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@opencode/cli": "2.0.32",
|
|
74
|
+
"@opencode/client": "2.0.32",
|
|
75
|
+
"@opencode/plugin": "2.0.32",
|
|
76
|
+
"express": "^5.2.1",
|
|
77
|
+
"jiti": "^2.7.0",
|
|
78
|
+
"jsdom": "^30.0.1",
|
|
79
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
80
|
+
"web-tree-sitter": "^0.22.6"
|
|
81
|
+
},
|
|
82
|
+
"overrides": {
|
|
83
|
+
"seroval": "1.6.9"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@eslint/js": "^10.0.1",
|
|
87
|
+
"@types/node": "^24.3.0",
|
|
88
|
+
"eslint": "^10.11.0",
|
|
89
|
+
"globals": "^17.12.0",
|
|
90
|
+
"typescript": "^5.9.2",
|
|
91
|
+
"typescript-eslint": "8.57.0"
|
|
92
|
+
},
|
|
93
|
+
"allowScripts": {
|
|
94
|
+
"@opencode/cli@2.0.32": true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@carthagent/opencode-plugin",
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./tui": "./src/bootstrap.ts",
|
|
9
|
+
"./rpc": "./src/rpc.ts",
|
|
10
|
+
"./delivery": "./src/server/native-delivery.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@opencode/plugin": "2.0.32"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"@opencode/theme": "2.0.32",
|
|
17
|
+
"@opentui/core": "0.5.17",
|
|
18
|
+
"@opentui/solid": "0.5.17",
|
|
19
|
+
"solid-js": "1.9.12"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@opencode/theme": "2.0.32",
|
|
23
|
+
"@opentui/core": "0.5.17",
|
|
24
|
+
"@opentui/solid": "0.5.17",
|
|
25
|
+
"solid-js": "1.9.12",
|
|
26
|
+
"web-tree-sitter": "0.25.10"
|
|
27
|
+
}
|
|
28
|
+
}
|