carthagent 1.0.7 → 1.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +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 +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/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 +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- 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 +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +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 +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +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 +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +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 +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- 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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** One inventory for native OpenCode slash/palette discovery and dispatch. */
|
|
2
|
+
export type CommandId =
|
|
3
|
+
| "help" | "resume" | "new" | "restart" | "theme" | "model" | "thinking"
|
|
4
|
+
| "compact" | "autocompact" | "review" | "export" | "copy" | "stats"
|
|
5
|
+
| "clear" | "abort" | "raw" | "guide" | "workflow" | "architecture" | "evidence"
|
|
6
|
+
| "login" | "account" | "usage" | "billing" | "logout-cloud" | "archive" | "demo" | "voyages" | "quests"
|
|
7
|
+
| "ctg-workspace" | "ctg-open" | "ctg-preview" | "ctg-review" | "ctg-worktree" | "ctg-task";
|
|
8
|
+
|
|
9
|
+
export interface CarthagentCommand {
|
|
10
|
+
id: CommandId;
|
|
11
|
+
name: string;
|
|
12
|
+
aliases?: string[];
|
|
13
|
+
args?: string;
|
|
14
|
+
/** Keep slash input eligible for command dispatch when the composer is submitted. */
|
|
15
|
+
submitSlash?: boolean;
|
|
16
|
+
description: string;
|
|
17
|
+
hidden?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const COMMANDS: readonly CarthagentCommand[] = [
|
|
21
|
+
{ id: "help", name: "help", aliases: ["ctg-help"], submitSlash: true, description: "List Carthagent commands and native shortcuts" },
|
|
22
|
+
{ id: "resume", name: "resume", aliases: ["sessions"], args: "[id|name]", description: "Search and resume native sessions; legacy archives are read-only" },
|
|
23
|
+
{ id: "restart", name: "restart", aliases: ["kill"], description: "Recover the selected session by interrupting its active turn" },
|
|
24
|
+
{ id: "theme", name: "theme", args: "[name]", description: "Pick or set a Carthagent-compatible theme" },
|
|
25
|
+
{ id: "model", name: "model", args: "[name]", description: "Search and select a provider model" },
|
|
26
|
+
{ id: "thinking", name: "thinking", description: "Choose a thinking variant when the model supports it" },
|
|
27
|
+
{ id: "compact", name: "compact", args: "[notes]", description: "Compact the selected session, optionally with notes" },
|
|
28
|
+
{ id: "autocompact", name: "autocompact", args: "[on|off]", description: "Toggle automatic compaction or explicitly enable or disable it for this session" },
|
|
29
|
+
{ id: "review", name: "review", aliases: ["review-mode"], args: "[ask|yes|no|status]", description: "Review the selected session or inspect its review default" },
|
|
30
|
+
{ id: "export", name: "export", args: "[file]", description: "Export the selected session as HTML" },
|
|
31
|
+
{ id: "copy", name: "copy", description: "Copy the full last assistant response" },
|
|
32
|
+
{ id: "stats", name: "stats", description: "Show native session token and cost stats" },
|
|
33
|
+
{ id: "clear", name: "clear", description: "Clear the visible transcript for this session without deleting history" },
|
|
34
|
+
{ id: "abort", name: "abort", description: "Interrupt the selected session" },
|
|
35
|
+
{ id: "raw", name: "raw", args: "<prompt>", description: "Send text without Carthagent delivery framing" },
|
|
36
|
+
{ id: "guide", name: "guide", args: "[task]", description: "Toggle framing or start a delivery-framed prompt" },
|
|
37
|
+
{ id: "workflow", name: "workflow", description: "Open the delivery workspace workflow view" },
|
|
38
|
+
{ id: "architecture", name: "architecture", description: "Open the delivery workspace architecture view" },
|
|
39
|
+
{ id: "evidence", name: "evidence", description: "Open observed delivery evidence" },
|
|
40
|
+
{ id: "login", name: "login", aliases: ["auth"], description: "Connect Ship, Claude, xAI, Codex/OpenAI, or Gemini" },
|
|
41
|
+
{ id: "account", name: "account", aliases: ["cloud"], description: "Show Ship account, credits, plan, and sessions" },
|
|
42
|
+
{ id: "voyages", name: "voyages", description: "Open Voyages profile, wallet, equipment, shop, and daily quests" },
|
|
43
|
+
{ id: "quests", name: "quests", description: "Open daily coding quests; evidence requires separate explicit consent" },
|
|
44
|
+
{ id: "usage", name: "usage", description: "Show recent Ship usage from the control plane" },
|
|
45
|
+
{ id: "billing", name: "billing", args: "[checkout|portal]", description: "Open Ship checkout or billing portal" },
|
|
46
|
+
{ id: "logout-cloud", name: "logout-cloud", args: "[session-id]", description: "Revoke a Ship session or sign out this profile" },
|
|
47
|
+
{ id: "archive", name: "archive", args: "[search <query>|read <id>|export <id>]", description: "Browse bounded read-only archived sessions" },
|
|
48
|
+
{ id: "demo", name: "demo", description: "Run a scripted, provider-free delivery walkthrough" },
|
|
49
|
+
{ id: "ctg-workspace", name: "ctg-workspace", args: "[refresh]", description: "Browse and open local CTG workspace projects, tasks, and sessions" },
|
|
50
|
+
{ id: "ctg-open", name: "ctg-open", args: "[search]", description: "Search local CTG sessions and open one in native OpenCode" },
|
|
51
|
+
{ id: "ctg-preview", name: "ctg-preview", description: "Preview CTG delivery evidence for the selected session" },
|
|
52
|
+
{ id: "ctg-review", name: "ctg-review", description: "Inspect CTG review status for the selected session" },
|
|
53
|
+
{ id: "ctg-worktree", name: "ctg-worktree", args: "<project-id> <name>", description: "Create a local CTG worktree" },
|
|
54
|
+
{ id: "ctg-task", name: "ctg-task", args: "<task>", description: "Create a CTG task with a native worktree and session" },
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
export function matchCommand(query: string, commands: readonly CarthagentCommand[] = COMMANDS) {
|
|
58
|
+
const token = query.replace(/^\//, "").toLowerCase();
|
|
59
|
+
if (!token || token.includes(" ")) return [];
|
|
60
|
+
return commands.filter((item) => item.name.startsWith(token) || item.aliases?.some((alias) => alias.startsWith(token)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveCommand(token: string, commands: readonly CarthagentCommand[] = COMMANDS) {
|
|
64
|
+
const normalized = token.replace(/^\//, "").toLowerCase();
|
|
65
|
+
return commands.find((item) => item.name === normalized || item.aliases?.includes(normalized));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Host-owned commands take priority. These slash names are verified against
|
|
69
|
+
// the V2.0.32 keybind command inventory and are intentionally not shadowed.
|
|
70
|
+
export const NATIVE_COMMAND_COLLISIONS = ["themes", "settings"] as const;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native dialogs resolve before their delayed refocus of the saved renderable.
|
|
3
|
+
* Observe actual action-list focus, then reconcile after focus() has finished
|
|
4
|
+
* installing its input handlers. A fixed timeout from compose can run too early.
|
|
5
|
+
*/
|
|
6
|
+
export function createDailyEditorFocus({ state, editor, defer = queueMicrotask }) {
|
|
7
|
+
let actions;
|
|
8
|
+
let generation = 0;
|
|
9
|
+
let disposed = false;
|
|
10
|
+
const permissions = () => {
|
|
11
|
+
const current = state();
|
|
12
|
+
const enabled = !disposed && !current.busy && current.mode === 'base';
|
|
13
|
+
return {
|
|
14
|
+
editor: enabled && current.phase === 'edit' && current.hasDraft && current.editorFocus,
|
|
15
|
+
actions: enabled && (current.phase !== 'edit' || !current.editorFocus),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const reconcile = () => {
|
|
19
|
+
if (disposed) return;
|
|
20
|
+
// Read synchronously too, so a Solid effect subscribes to ownership changes.
|
|
21
|
+
permissions();
|
|
22
|
+
const ticket = ++generation;
|
|
23
|
+
defer(() => {
|
|
24
|
+
if (disposed || ticket !== generation) return;
|
|
25
|
+
// Never capture ownership or the editor across a modal/phase/Tab change.
|
|
26
|
+
const allowed = permissions();
|
|
27
|
+
const input = editor();
|
|
28
|
+
if (!allowed.editor && input && !input.isDestroyed && input.focused) input.blur();
|
|
29
|
+
if (!allowed.actions && actions && !actions.isDestroyed && actions.focused) actions.blur();
|
|
30
|
+
const target = allowed.editor ? input : allowed.actions ? actions : undefined;
|
|
31
|
+
if (target && !target.isDestroyed && !target.focused) target.focus();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
reconcile,
|
|
36
|
+
editorEnabled: () => permissions().editor,
|
|
37
|
+
actionsEnabled: () => permissions().actions,
|
|
38
|
+
attachActions(renderable) {
|
|
39
|
+
actions?.off('focused', reconcile);
|
|
40
|
+
actions = renderable;
|
|
41
|
+
if (disposed) return;
|
|
42
|
+
actions.on('focused', reconcile);
|
|
43
|
+
reconcile();
|
|
44
|
+
},
|
|
45
|
+
dispose() {
|
|
46
|
+
disposed = true;
|
|
47
|
+
generation++;
|
|
48
|
+
actions?.off('focused', reconcile);
|
|
49
|
+
actions = undefined;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { voyagesEventAction } from './voyages-state.mjs';
|
|
2
|
+
|
|
3
|
+
export const dailyCancellationNotice = 'Evidence/consent canceled because the account, connection or location changed. Nothing was resent automatically. Refresh and review again; check the recovery journal for any request already started.';
|
|
4
|
+
|
|
5
|
+
/** Daily-page invalidation only; retain the shared credential/location policy.
|
|
6
|
+
* Reset the native index before either replay removal or protected-state clearing
|
|
7
|
+
* can clamp a former consent selection onto Back. Never focus a renderable here.
|
|
8
|
+
*/
|
|
9
|
+
export function handleDailyQuestsEvent({ controller, event, location, changedLocation = false, interrupted = false, actions, clearReplay, closeDialog, notice }) {
|
|
10
|
+
const action = changedLocation ? String(event.type) === 'server.disconnected' ? 'clear' : 'refresh' : voyagesEventAction(event, location);
|
|
11
|
+
if (action === 'ignore') return;
|
|
12
|
+
if (actions && !actions.isDestroyed) actions.setSelectedIndex(0);
|
|
13
|
+
if (interrupted) notice(dailyCancellationNotice);
|
|
14
|
+
clearReplay();
|
|
15
|
+
controller.invalidate();
|
|
16
|
+
closeDialog();
|
|
17
|
+
if (action === 'refresh') return controller.refresh();
|
|
18
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { dailyRequest } from '../daily-evidence.mjs';
|
|
2
|
+
|
|
3
|
+
const copy = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
4
|
+
function freeze(value) {
|
|
5
|
+
if (value && typeof value === 'object') {
|
|
6
|
+
Object.values(value).forEach(freeze);
|
|
7
|
+
Object.freeze(value);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
const localError = code => ({ type: 'dailyQuestError', data: { code } });
|
|
12
|
+
const failureMessage = code => `${code}. Refresh required; no success assumed. Sign in again if access expired or was revoked.`;
|
|
13
|
+
|
|
14
|
+
/** Native promise RPC errors are { type, message, data }. Never render messages. */
|
|
15
|
+
export function dailyErrorCode(error) {
|
|
16
|
+
const code = error?.type === 'dailyQuestError' ? error.data?.code : undefined;
|
|
17
|
+
return typeof code === 'string' && code.length >= 1 && code.length <= 64 && !/[^a-z_]/.test(code) ? code : 'daily_request_failed';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Processing status is not a coding assessment, nor independent verification. */
|
|
21
|
+
export function dailySubmissionLabel(submission) {
|
|
22
|
+
if (!submission) return 'No submission';
|
|
23
|
+
if (submission.status === 'queued') return 'Queued (not completed)';
|
|
24
|
+
if (submission.status === 'grading') return 'Grading (not completed)';
|
|
25
|
+
if (submission.status === 'failed') return 'Processing failed (not a coding assessment)';
|
|
26
|
+
if (submission.status === 'graded') {
|
|
27
|
+
if (submission.decision === 'assessed_complete') return 'Classifier-assessed complete (not independently verified)';
|
|
28
|
+
if (submission.decision === 'not_met') return 'Graded: not met';
|
|
29
|
+
if (submission.decision === 'needs_evidence') return 'Graded: needs evidence';
|
|
30
|
+
return 'Graded: assessment unavailable';
|
|
31
|
+
}
|
|
32
|
+
return 'Submission status unavailable';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Offline UI transaction controller. Only explicit submit/retry can upload.
|
|
36
|
+
* Dialog callbacks receive detached copies plus a live() lifetime predicate.
|
|
37
|
+
* Cancel/back/invalidate synchronously revoke any outstanding dialog callback.
|
|
38
|
+
*/
|
|
39
|
+
export function createDailyQuestsState({ api, changed = (_state) => {}, timeoutSignal = () => AbortSignal.timeout(15_000) }) {
|
|
40
|
+
let generation = 0;
|
|
41
|
+
let disposed = false;
|
|
42
|
+
let controller = new AbortController();
|
|
43
|
+
const state = { snapshot: undefined, detail: undefined, busy: false, error: '', draft: undefined, preview: undefined, phase: 'overview' };
|
|
44
|
+
const emit = () => { if (!disposed) changed(copy(state)); };
|
|
45
|
+
const clearDraft = () => { state.draft = undefined; state.preview = undefined; state.phase = 'overview'; };
|
|
46
|
+
const clearProtected = () => { state.snapshot = undefined; state.detail = undefined; clearDraft(); };
|
|
47
|
+
const revoke = () => {
|
|
48
|
+
generation++;
|
|
49
|
+
controller.abort();
|
|
50
|
+
controller = new AbortController();
|
|
51
|
+
state.busy = false;
|
|
52
|
+
state.error = '';
|
|
53
|
+
};
|
|
54
|
+
const invalidate = () => { revoke(); clearProtected(); emit(); };
|
|
55
|
+
const networkSignal = signal => AbortSignal.any([signal, timeoutSignal()]);
|
|
56
|
+
const savedRecord = id => state.snapshot?.records.find(record => record.id === id);
|
|
57
|
+
const knownQuest = id => state.snapshot?.slate.items.some(quest => quest.id === id)
|
|
58
|
+
|| state.snapshot?.records.some(record => record.questId === id);
|
|
59
|
+
|
|
60
|
+
async function loadSnapshot(tx, expectedAccount = tx.accountKey) {
|
|
61
|
+
if (!tx.live()) return false;
|
|
62
|
+
const snapshot = copy(await api.snapshot(networkSignal(tx.signal)));
|
|
63
|
+
if (!tx.live()) return false;
|
|
64
|
+
if (!snapshot?.accountKey || (expectedAccount != null && snapshot.accountKey !== expectedAccount)) {
|
|
65
|
+
throw localError('daily_account_changed');
|
|
66
|
+
}
|
|
67
|
+
state.snapshot = snapshot;
|
|
68
|
+
emit();
|
|
69
|
+
return tx.live();
|
|
70
|
+
}
|
|
71
|
+
async function loadDetail(id, tx) {
|
|
72
|
+
if (!tx.live()) return;
|
|
73
|
+
const detail = copy(await api.detail({ id }, networkSignal(tx.signal)));
|
|
74
|
+
if (!tx.live()) return;
|
|
75
|
+
if (detail?.id !== id) throw localError('daily_request_failed');
|
|
76
|
+
// The detail RPC reconciles acceptance and erases raw journal payloads.
|
|
77
|
+
// Drop the matching in-memory copy before exposing the accepted detail.
|
|
78
|
+
if (detail.latestSubmission && state.snapshot) {
|
|
79
|
+
const submission = detail.latestSubmission;
|
|
80
|
+
const previous = savedRecord(submission.id);
|
|
81
|
+
const record = { id: submission.id, questId: detail.id, createdAt: previous?.createdAt ?? submission.createdAt, request: null, submission: copy(submission) };
|
|
82
|
+
state.snapshot.records = state.snapshot.records.filter(item => item.id !== submission.id);
|
|
83
|
+
state.snapshot.records.push(record);
|
|
84
|
+
}
|
|
85
|
+
state.detail = detail;
|
|
86
|
+
emit();
|
|
87
|
+
}
|
|
88
|
+
async function run(action) {
|
|
89
|
+
if (disposed || state.busy) return;
|
|
90
|
+
const current = generation;
|
|
91
|
+
const signal = controller.signal;
|
|
92
|
+
const tx = { signal, accountKey: state.snapshot?.accountKey, live: () => !disposed && generation === current && !signal.aborted };
|
|
93
|
+
state.busy = true;
|
|
94
|
+
state.error = '';
|
|
95
|
+
emit();
|
|
96
|
+
try { if (tx.live()) await action(tx); }
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (tx.live()) {
|
|
99
|
+
clearProtected();
|
|
100
|
+
state.error = failureMessage(dailyErrorCode(error));
|
|
101
|
+
emit();
|
|
102
|
+
// Read the durable journal only. Never replay a write, assume success,
|
|
103
|
+
// or resurrect raw evidence from a callback after an unknown outcome.
|
|
104
|
+
try { await loadSnapshot(tx); }
|
|
105
|
+
catch (reconcileError) {
|
|
106
|
+
if (tx.live()) {
|
|
107
|
+
clearProtected();
|
|
108
|
+
if (dailyErrorCode(reconcileError) === 'daily_account_changed') state.error = failureMessage('daily_account_changed');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
if (tx.live()) { state.busy = false; emit(); }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function refreshedWrite(tx, questId) {
|
|
117
|
+
// Immediately drop all copies of raw evidence; GET is authoritative.
|
|
118
|
+
clearProtected();
|
|
119
|
+
emit();
|
|
120
|
+
if (await loadSnapshot(tx)) await loadDetail(questId, tx);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
invalidate,
|
|
125
|
+
dispose() { if (!disposed) { invalidate(); disposed = true; controller.abort(); } },
|
|
126
|
+
refresh() {
|
|
127
|
+
return run(async tx => { clearProtected(); emit(); await loadSnapshot(tx, null); });
|
|
128
|
+
},
|
|
129
|
+
select(id) {
|
|
130
|
+
return run(async tx => {
|
|
131
|
+
if (!knownQuest(id)) return;
|
|
132
|
+
clearDraft(); state.detail = undefined; emit();
|
|
133
|
+
await loadDetail(id, tx);
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
compose(makeDraft) {
|
|
137
|
+
return run(async tx => {
|
|
138
|
+
const detail = copy(state.detail);
|
|
139
|
+
const accountKey = state.snapshot?.accountKey;
|
|
140
|
+
if (!accountKey || !detail || detail.status !== 'open' || !knownQuest(detail.id)) return;
|
|
141
|
+
clearDraft(); emit();
|
|
142
|
+
if (!tx.live()) return;
|
|
143
|
+
const draft = await makeDraft(copy(detail), tx.live, tx.signal);
|
|
144
|
+
if (!draft || !tx.live() || state.snapshot?.accountKey !== accountKey || state.detail?.id !== detail.id) return;
|
|
145
|
+
state.draft = { accountKey, questId: detail.id, id: draft.id, text: JSON.stringify(draft.evidence, null, 2), provenance: typeof draft.provenance === 'string' ? draft.provenance : '' };
|
|
146
|
+
state.phase = 'edit';
|
|
147
|
+
emit();
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
edit(text) {
|
|
151
|
+
if (disposed || state.busy || !state.draft || typeof text !== 'string') return;
|
|
152
|
+
state.draft.text = text; state.preview = undefined; state.phase = 'edit'; state.error = ''; emit();
|
|
153
|
+
},
|
|
154
|
+
preview() {
|
|
155
|
+
if (disposed || state.busy || !state.draft) return;
|
|
156
|
+
state.preview = undefined; state.phase = 'edit'; state.error = '';
|
|
157
|
+
try {
|
|
158
|
+
const request = dailyRequest(state.draft.id, JSON.parse(state.draft.text));
|
|
159
|
+
state.draft.text = JSON.stringify(request.evidence, null, 2);
|
|
160
|
+
state.preview = freeze(copy(request)); state.phase = 'preview';
|
|
161
|
+
} catch { state.error = 'invalid_daily_evidence. Correct the evidence before previewing or submitting.'; }
|
|
162
|
+
emit();
|
|
163
|
+
},
|
|
164
|
+
backToEdit() {
|
|
165
|
+
if (disposed || !state.draft) return;
|
|
166
|
+
if (state.busy) revoke();
|
|
167
|
+
state.preview = undefined; state.phase = 'edit'; state.error = ''; emit();
|
|
168
|
+
},
|
|
169
|
+
cancelDraft() {
|
|
170
|
+
if (disposed) return;
|
|
171
|
+
if (state.busy) revoke();
|
|
172
|
+
clearDraft(); state.error = ''; emit();
|
|
173
|
+
},
|
|
174
|
+
submit(confirm) {
|
|
175
|
+
return run(async tx => {
|
|
176
|
+
if (!state.draft || !state.preview || !state.detail || state.detail.status !== 'open') return;
|
|
177
|
+
const draft = copy(state.draft);
|
|
178
|
+
if (draft.accountKey !== tx.accountKey || draft.questId !== state.detail.id) return;
|
|
179
|
+
const request = freeze(copy(state.preview));
|
|
180
|
+
const shown = { quest: copy(state.detail), request: freeze(copy(request)) };
|
|
181
|
+
if (await confirm(shown, tx.live) !== true || !tx.live()) return;
|
|
182
|
+
if (state.draft?.id !== draft.id || state.snapshot?.accountKey !== draft.accountKey || !state.preview) return;
|
|
183
|
+
await api.submit({ accountKey: draft.accountKey, questId: draft.questId, ...copy(request) }, networkSignal(tx.signal));
|
|
184
|
+
if (tx.live()) await refreshedWrite(tx, draft.questId);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
poll(id) {
|
|
188
|
+
return run(async tx => {
|
|
189
|
+
const record = savedRecord(id);
|
|
190
|
+
const questId = record?.questId ?? (state.detail?.latestSubmission?.id === id ? state.detail.id : undefined);
|
|
191
|
+
if (!questId || !tx.accountKey) return;
|
|
192
|
+
clearDraft(); state.detail = undefined; emit();
|
|
193
|
+
if (!tx.live()) return;
|
|
194
|
+
await api.poll({ id }, networkSignal(tx.signal));
|
|
195
|
+
if (await loadSnapshot(tx)) await loadDetail(questId, tx);
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
retry(id, confirm) {
|
|
199
|
+
return run(async tx => {
|
|
200
|
+
const record = copy(savedRecord(id));
|
|
201
|
+
if (!record?.request || !tx.accountKey) return;
|
|
202
|
+
const shown = freeze(copy(record));
|
|
203
|
+
if (await confirm(shown, tx.live) !== true || !tx.live()) return;
|
|
204
|
+
await api.retry({ accountKey: tx.accountKey, id: record.id }, networkSignal(tx.signal));
|
|
205
|
+
if (tx.live()) await refreshedWrite(tx, record.questId);
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
forget(id, confirm) {
|
|
209
|
+
return run(async tx => {
|
|
210
|
+
const record = copy(savedRecord(id));
|
|
211
|
+
if (!record?.request || !tx.accountKey) return;
|
|
212
|
+
if (await confirm(freeze(copy(record)), tx.live) !== true || !tx.live()) return;
|
|
213
|
+
await api.forgetPayload({ accountKey: tx.accountKey, id: record.id }, networkSignal(tx.signal));
|
|
214
|
+
if (tx.live()) await refreshedWrite(tx, record.questId);
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from "solid-js";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import type { ScrollBoxRenderable, SelectRenderable, TextareaRenderable } from "@opentui/core";
|
|
5
|
+
import { usePlugin } from "@opencode/plugin/tui";
|
|
6
|
+
import { clickSelect } from "./select-mouse.js";
|
|
7
|
+
import { dailyQuestsClient, dailySourceClient } from "./client.js";
|
|
8
|
+
import type { DailyEvidence, DailyQuestDetail, DailySnapshot, DailySubmission, DailyRecord } from "../daily-quests-rpc.js";
|
|
9
|
+
import type { DailySourceReport } from "../daily-source-rpc.js";
|
|
10
|
+
import { createDailyQuestsState, dailySubmissionLabel } from "./daily-quests-state.mjs";
|
|
11
|
+
import { handleDailyQuestsEvent } from "./daily-quests-events.mjs";
|
|
12
|
+
import { syncDailyEditor } from "./daily-editor.mjs";
|
|
13
|
+
import { createDailyEditorFocus } from "./daily-editor-focus.mjs";
|
|
14
|
+
|
|
15
|
+
type Request = NonNullable<DailyRecord['request']>;
|
|
16
|
+
type View = {
|
|
17
|
+
snapshot?: DailySnapshot; detail?: DailyQuestDetail; busy: boolean; error: string;
|
|
18
|
+
draft?: { accountKey: string; questId: string; id: string; text: string; provenance: string };
|
|
19
|
+
preview?: Request; phase: "overview" | "edit" | "preview";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Stable opaque references, not proof of authorship. Never send a local path. */
|
|
23
|
+
function reference(...parts: string[]) {
|
|
24
|
+
const h = createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
25
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const privacy = "Only the exact preview below is sent to Carthagent. External JEV receives the revision, goal, change, checks, findings and server rubric, not project/outcome/run IDs. No transcript or whole project is uploaded. Review and remove secrets yourself; automatic secret detection is not provided.\n\nBackend raw evidence: pending up to 24 hours, erased on terminal processing (outages can delay cleanup). Metadata: 90 days. Provider policy applies; immediate provider/backup deletion is not promised. Unknown-send recovery stores this exact payload in a private local journal until acceptance is known or for up to 24 hours on the next journal access, not a background deletion timer. You can erase its local payload while retaining the submission ID.\n\nClassifier assessment is not independent verification. No coins, XP, distance, inventory or other rewards. Signing in or running Ship is not consent.";
|
|
29
|
+
|
|
30
|
+
export function DailyQuestsPage(props: { sessionID?: string; questID?: string }) {
|
|
31
|
+
const context = usePlugin();
|
|
32
|
+
const location = () => (props.sessionID ? context.data.session.get(props.sessionID)?.location : undefined) ?? context.location ?? context.data.location.default();
|
|
33
|
+
const [view, setView] = createSignal<View>({ busy: false, error: "", phase: "overview" });
|
|
34
|
+
const [editorFocus, setEditorFocus] = createSignal(true);
|
|
35
|
+
const [replayID, setReplayID] = createSignal<string>();
|
|
36
|
+
const [cancellationNotice, setCancellationNotice] = createSignal("");
|
|
37
|
+
const controller = createDailyQuestsState({ api: dailyQuestsClient(context, location), changed: (next: View) => setView(next) });
|
|
38
|
+
const sourceApi = dailySourceClient(context, location);
|
|
39
|
+
let summary: ScrollBoxRenderable | undefined;
|
|
40
|
+
let editor: TextareaRenderable | undefined;
|
|
41
|
+
let actions: SelectRenderable | undefined;
|
|
42
|
+
const focus = createDailyEditorFocus({
|
|
43
|
+
state: () => ({ phase: view().phase, hasDraft: !!view().draft, busy: view().busy, editorFocus: editorFocus(), mode: context.keymap.mode.current() }),
|
|
44
|
+
editor: () => editor,
|
|
45
|
+
});
|
|
46
|
+
createEffect(() => focus.reconcile());
|
|
47
|
+
const preview = () => { if (view().phase === "edit") syncDailyEditor(editor, controller); controller.preview(); };
|
|
48
|
+
let activeDialog: symbol | undefined;
|
|
49
|
+
const ownDialog = async <T,>(show: () => Promise<T>): Promise<T> => {
|
|
50
|
+
const token = Symbol(); activeDialog = token;
|
|
51
|
+
try { return await show(); } finally { if (activeDialog === token) activeDialog = undefined; }
|
|
52
|
+
};
|
|
53
|
+
const closeOwnDialog = () => {
|
|
54
|
+
if (!activeDialog) return;
|
|
55
|
+
activeDialog = undefined;
|
|
56
|
+
context.ui.dialog.clear();
|
|
57
|
+
};
|
|
58
|
+
const confirm = (title: string, message: string, label: string) => ownDialog(() => context.ui.dialog.confirm({ title, message, label: { confirm: label, cancel: "Cancel" } }));
|
|
59
|
+
context.keymap.layer(() => ({ commands: [
|
|
60
|
+
{ bind: "ctrl+u", title: "Scroll daily quest preview up", run: () => { summary?.scrollBy(-1); } },
|
|
61
|
+
{ bind: "ctrl+d", title: "Scroll daily quest preview down", run: () => { summary?.scrollBy(1); } },
|
|
62
|
+
{ bind: "ctrl+g", title: "Validate daily evidence preview", enabled: () => view().phase === "edit" && !view().busy, run: preview },
|
|
63
|
+
{ bind: "tab", title: "Switch evidence editor / actions", enabled: () => view().phase === "edit" && !view().busy, run: () => { setEditorFocus((focused) => !focused); } },
|
|
64
|
+
] }));
|
|
65
|
+
|
|
66
|
+
const chooseProjection = async (source: DailySourceReport, sessionID: string, live: () => boolean): Promise<DailyEvidence | undefined> => {
|
|
67
|
+
let goal = false, change = false;
|
|
68
|
+
const checks = new Set<string>(), findings = new Set<string>();
|
|
69
|
+
while (live()) {
|
|
70
|
+
const selected = await ownDialog(() => context.ui.dialog.select({
|
|
71
|
+
title: "Select local evidence · nothing included by default",
|
|
72
|
+
options: [
|
|
73
|
+
{ title: "Done selecting", value: "done", description: "Open editable JSON; nothing is sent yet" },
|
|
74
|
+
{ title: `${goal ? "Remove" : "Include"} recorded goal`, value: "goal", description: "May contain private task text" },
|
|
75
|
+
{ title: `${change ? "Remove" : "Include"} recorded change`, value: "change", description: source.change ? "Recorded claim, not independently verified" : "Not recorded; remains empty" },
|
|
76
|
+
...source.checks.map((c) => ({ title: `${checks.has(c.id) ? "Remove" : "Include"} check ${c.id}`, value: `check:${c.id}`, description: `${c.after} · ${c.after === "passed" ? "fresh" : "not fresh"} · includes output tail, potentially sensitive; before not recorded` })),
|
|
77
|
+
...source.findings.map((f) => ({ title: `${findings.has(f.id) ? "Remove" : "Include"} finding ${f.id}`, value: `finding:${f.id}`, description: "Includes issue, evidence and resolution text; model/user-authored claim" })),
|
|
78
|
+
],
|
|
79
|
+
}));
|
|
80
|
+
if (!live() || selected === undefined) return;
|
|
81
|
+
if (selected === "done") break;
|
|
82
|
+
if (selected === "goal") goal = !goal;
|
|
83
|
+
else if (selected === "change") change = !change;
|
|
84
|
+
else {
|
|
85
|
+
const isCheck = selected.startsWith("check:");
|
|
86
|
+
const id = selected.slice(isCheck ? 6 : 8), set = isCheck ? checks : findings;
|
|
87
|
+
if (set.has(id)) set.delete(id); else set.add(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!live()) return;
|
|
91
|
+
const session = context.data.session.get(sessionID);
|
|
92
|
+
const project = reference("daily-project-v1", session?.projectID ?? location().directory);
|
|
93
|
+
return {
|
|
94
|
+
projectId: project, outcomeId: reference("daily-outcome-v1", project, source.runId), runId: source.runId,
|
|
95
|
+
revision: source.revision, goal: goal ? source.goal : "", change: change ? source.change : "",
|
|
96
|
+
checks: source.checks.filter((c) => checks.has(c.id)).map(({ id, before, after, revision, result }) => ({ name: id, before, after, revision, result })),
|
|
97
|
+
findings: source.findings.filter((f) => findings.has(f.id)).map(({ issue, evidence, resolution }) => ({ issue, evidence, resolution })),
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const compose = () => controller.compose(async (_quest: DailyQuestDetail, live: () => boolean, signal: AbortSignal) => {
|
|
102
|
+
const kind = await ownDialog(() => context.ui.dialog.select({ title: "Choose evidence source", options: [
|
|
103
|
+
{ title: "Select actual local Ship report", value: "local", description: "Owned session only; choose individual checks/findings, no transcript" },
|
|
104
|
+
{ title: "Write a structured manual claim", value: "manual", description: "Blank evidence, not a claim that checks ran; edit every field" },
|
|
105
|
+
] }));
|
|
106
|
+
if (!kind || !live()) return;
|
|
107
|
+
if (kind === "manual") {
|
|
108
|
+
const project = reference("daily-project-v1", props.sessionID ? context.data.session.get(props.sessionID)?.projectID ?? location().directory : location().directory);
|
|
109
|
+
return { id: randomUUID(), evidence: { projectId: project, outcomeId: randomUUID(), runId: randomUUID(), revision: "", goal: "", change: "", checks: [], findings: [] }, provenance: "Manual user claim. Enter a genuine 40/64-hex revision. Empty evidence does not establish completion. Keep project/outcome/run IDs when revising the same outcome; they do not prove authorship." };
|
|
110
|
+
}
|
|
111
|
+
const sessions = context.data.session.list().filter((s) => s.location.directory === location().directory);
|
|
112
|
+
const sessionID = props.sessionID ?? await ownDialog(() => context.ui.dialog.select({ title: "Choose local Ship session", options: sessions.map((s) => ({ title: s.title ?? s.id, value: s.id, description: s.id })) }));
|
|
113
|
+
if (!sessionID || !live()) return;
|
|
114
|
+
const { source } = await sourceApi.report({ sessionID }, AbortSignal.any([signal, AbortSignal.timeout(15_000)]));
|
|
115
|
+
if (!live()) return;
|
|
116
|
+
if (!source) throw new Error("daily_source_unavailable");
|
|
117
|
+
const evidence = await chooseProjection(source, sessionID, live);
|
|
118
|
+
if (!evidence || !live()) return;
|
|
119
|
+
return { id: randomUUID(), evidence, provenance: `Local Ship session ${sessionID} · run ${source.runId} · plan revision ${source.planRevision} · ${source.freshness}. Source revision is the actual workspace fingerprint, not the plan revision. Earlier failed-before execution is not recorded and is never inferred. Selected text is editable and remains a user claim, not independent verification.${source.truncated ? " Source projection is truncated; inspect and correct missing details before submitting." : ""}` };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const submit = () => controller.submit(({ quest, request }: { quest: DailyQuestDetail; request: Request }) => confirm(
|
|
123
|
+
"Explicit consent for this one JEV submission",
|
|
124
|
+
`${quest.definition.title}\nSubmission ${request.id}\nSend only the exact JSON shown in the scrollable preview to Carthagent for external JEV grading? Review/redact before consent. Raw backend evidence: pending up to 24h, erased on terminal processing; metadata 90d. Outages can delay cleanup. Provider/backup deletion is not guaranteed. No rewards or independent verification.\n\nCancel to review the preview and full retention notice again. This authorizes this submission only.`,
|
|
125
|
+
"Consent and send",
|
|
126
|
+
));
|
|
127
|
+
const retry = (id: string) => controller.retry(id, (record: DailyRecord) => confirm("Retry the SAME consented submission?", `Submission ${record.id}\nReplay only the exact saved ID/body shown in the scrollable recovery preview to Carthagent/external JEV under the same retention notice? Poll first when possible. This cannot edit evidence or create a replacement ID. Cancel keeps it pending.`, "Replay same request"));
|
|
128
|
+
const forget = (id: string) => controller.forget(id, (record: DailyRecord) => confirm("Erase local recovery payload?", `Submission ${record.id}\nErase the selected evidence from the local journal. Keep the stable submission/quest IDs for read-only status checks. Exact replay will no longer be possible. This does not cancel a server job, withdraw evidence already sent, or promise provider/backup deletion.`, "Erase local payload"));
|
|
129
|
+
|
|
130
|
+
const statusText = (submission: DailySubmission) => `${dailySubmissionLabel(submission)}\nSubmission ${submission.id} · provider attempts ${submission.attempts}/4\nCreated ${submission.createdAt}${submission.nextAttemptAt ? ` · next attempt ${submission.nextAttemptAt}` : ""}${submission.finishedAt ? ` · finished ${submission.finishedAt}` : ""}${submission.errorCode ? `\nProcessing code: ${submission.errorCode}` : ""}${submission.model ? `\nModel ${submission.model} · rubric ${submission.rubricVersion}` : ""}${submission.scores ? `\nUncalibrated rubric scores (not confidence): ${Object.entries(submission.scores).map(([id, score]) => `${id}: ${score}`).join(" · ")}` : ""}`;
|
|
131
|
+
const options = () => {
|
|
132
|
+
const state = view(), detail = state.detail;
|
|
133
|
+
const basic = [
|
|
134
|
+
{ name: "Refresh daily assignments and recovery journal", description: "Read-only; never uploads or replays evidence", value: "refresh" },
|
|
135
|
+
{ name: "Sign in / reconnect", description: "Native Carthagent Ship device sign-in; not grading consent", value: "signin" },
|
|
136
|
+
{ name: "Back to Voyages", description: "Discard unsent editor contents", value: "back" },
|
|
137
|
+
];
|
|
138
|
+
if (!state.snapshot) return basic;
|
|
139
|
+
return [...basic,
|
|
140
|
+
...(replayID() ? [
|
|
141
|
+
{ name: "Confirm exact saved replay…", description: "Review the scrollable recovery preview first; explicit same-ID replay", value: "confirm-replay" },
|
|
142
|
+
{ name: "Close recovery preview", description: "No replay; keep saved payload", value: "close-replay" },
|
|
143
|
+
] : []),
|
|
144
|
+
...state.snapshot.slate.items.map((q) => ({ name: `${q.definition.title} · ${q.status}`, description: `${q.definition.kind} · UTC window ends ${q.endsAt}`, value: `quest:${q.id}` })),
|
|
145
|
+
...(detail?.status === "open" && !state.draft ? [{ name: "Choose evidence for selected quest", description: "Select local report data or write a manual claim; no upload", value: "compose" }] : []),
|
|
146
|
+
...(state.phase === "edit" ? [{ name: "Validate and preview exact payload", description: "Ctrl+G; canonical UTF-8 limits, no network", value: "preview" }] : []),
|
|
147
|
+
...(state.phase === "preview" ? [
|
|
148
|
+
{ name: "Consent and submit this preview…", description: "Fresh explicit consent dialog; no automatic acceptance or rewards", value: "submit" },
|
|
149
|
+
{ name: "Edit / redact evidence again", description: "Invalidates the preview; requires validation and fresh consent", value: "edit" },
|
|
150
|
+
] : []),
|
|
151
|
+
...(state.draft ? [{ name: "Discard unsent evidence", description: "Clear local editor; no submission", value: "cancel" }] : []),
|
|
152
|
+
...(detail?.latestSubmission ? [{ name: "Poll selected quest's latest submission", description: "Read-only authoritative status; no provider dispatch", value: `poll:${detail.latestSubmission.id}` }] : []),
|
|
153
|
+
...state.snapshot.records.flatMap((r) => [
|
|
154
|
+
{ name: `Poll saved ${r.id}`, description: r.submission ? dailySubmissionLabel(r.submission) : "Outcome unknown; stable ID retained, including previous UTC days", value: `poll:${r.id}` },
|
|
155
|
+
...(r.request ? [
|
|
156
|
+
{ name: `Review exact replay ${r.id}`, description: "Saved same ID/body; explicit confirmation, never automatic", value: `retry:${r.id}` },
|
|
157
|
+
{ name: `Erase local payload ${r.id}`, description: "Keep ID for polling; cannot replay afterward", value: `forget:${r.id}` },
|
|
158
|
+
] : []),
|
|
159
|
+
]),
|
|
160
|
+
];
|
|
161
|
+
};
|
|
162
|
+
const select = (value?: string) => {
|
|
163
|
+
// A stale native refocus must not turn editor Enter into an action.
|
|
164
|
+
if (!value || !focus.actionsEnabled()) return;
|
|
165
|
+
if (value === "back") { context.ui.router.navigate({ type: "plugin", name: "voyages", data: { ...(props.sessionID ? { sessionID: props.sessionID } : {}) } }); return; }
|
|
166
|
+
if (value === "signin") { controller.invalidate(); closeOwnDialog(); context.keymap.dispatch("provider.connect"); return; }
|
|
167
|
+
if (value === "refresh") { setReplayID(undefined); void controller.refresh(); return; }
|
|
168
|
+
if (value === "compose") { setCancellationNotice(""); setEditorFocus(true); void compose(); return; }
|
|
169
|
+
if (value === "preview") { preview(); return; }
|
|
170
|
+
if (value === "submit") { void submit(); return; }
|
|
171
|
+
if (value === "edit") { setEditorFocus(true); controller.backToEdit(); return; }
|
|
172
|
+
if (value === "cancel") { controller.cancelDraft(); return; }
|
|
173
|
+
if (value === "close-replay") { setReplayID(undefined); return; }
|
|
174
|
+
if (value === "confirm-replay") { const id = replayID(); if (id) { void retry(id).finally(() => setReplayID(undefined)); } return; }
|
|
175
|
+
const colon = value.indexOf(":"), id = value.slice(colon + 1);
|
|
176
|
+
if (value.startsWith("quest:")) void controller.select(id);
|
|
177
|
+
if (value.startsWith("poll:")) void controller.poll(id);
|
|
178
|
+
if (value.startsWith("retry:")) { setCancellationNotice(""); controller.cancelDraft(); setReplayID(id); }
|
|
179
|
+
if (value.startsWith("forget:")) void forget(id);
|
|
180
|
+
};
|
|
181
|
+
onMount(() => {
|
|
182
|
+
let binding = JSON.stringify(location());
|
|
183
|
+
const stop = context.data.listen(({ details }) => {
|
|
184
|
+
const nextBinding = JSON.stringify(location());
|
|
185
|
+
const changedLocation = binding !== nextBinding;
|
|
186
|
+
binding = nextBinding;
|
|
187
|
+
void handleDailyQuestsEvent({
|
|
188
|
+
controller, event: details, location: location(), changedLocation, actions,
|
|
189
|
+
interrupted: !!(view().draft || view().preview || replayID() || activeDialog),
|
|
190
|
+
clearReplay: () => setReplayID(undefined), closeDialog: closeOwnDialog, notice: setCancellationNotice,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
// A navigator link selects only an assignment in the authenticated slate.
|
|
194
|
+
// It never composes evidence or opens a consent dialog.
|
|
195
|
+
void controller.refresh().then(() => { if (props.questID) return controller.select(props.questID); });
|
|
196
|
+
// The router reuses this page when another rail quest is opened.
|
|
197
|
+
createEffect(on(() => props.questID, (questID) => { if (questID) void controller.select(questID); }, { defer: true }));
|
|
198
|
+
onCleanup(() => { focus.dispose(); stop(); actions = undefined; controller.dispose(); closeOwnDialog(); });
|
|
199
|
+
});
|
|
200
|
+
return <box flexDirection="column" flexGrow={1} padding={1}>
|
|
201
|
+
<text flexShrink={0} fg={context.theme.text.action.primary.base}>CARTHAGENT · DAILY CODING QUESTS</text>
|
|
202
|
+
<scrollbox ref={summary} flexBasis={0} flexGrow={1} minHeight={1} contentOptions={{ flexDirection: "column" }}>
|
|
203
|
+
<text flexShrink={0} fg={context.theme.text.muted}>Classifier-assessed submitted evidence, not independent verification. No rewards. Refresh/poll never uploads evidence.</text>
|
|
204
|
+
<Show when={cancellationNotice()}><text flexShrink={0} fg={context.theme.text.feedback.warning.base}>{cancellationNotice()}</text></Show>
|
|
205
|
+
<Show when={view().busy}><text flexShrink={0} fg={context.theme.text.feedback.warning.base}>Awaiting response / explicit selection or consent. Other actions locked.</text></Show>
|
|
206
|
+
<Show when={view().error}><text flexShrink={0} fg={context.theme.text.feedback.error.base}>{view().error}</text></Show>
|
|
207
|
+
<Show when={view().snapshot} fallback={<text flexShrink={0} fg={context.theme.text.muted}>Protected assignments and evidence hidden until successful authentication and refresh.</text>}>{(snapshot) => <box flexDirection="column" flexShrink={0}>
|
|
208
|
+
<text flexShrink={0} fg={context.theme.text.base}>UTC day {snapshot().slate.day} · closes {snapshot().slate.endsAt} · policy {snapshot().slate.policyVersion}</text>
|
|
209
|
+
<text flexShrink={0} fg={context.theme.text.muted}>New submissions close at the server's UTC boundary. Accepted jobs can finish afterward. Select a quest below for its actual criteria.</text>
|
|
210
|
+
<For each={snapshot().records}>{(r) => <text flexShrink={0} fg={context.theme.text.muted}>{r.submission ? statusText(r.submission) : `Outcome unknown / no confirmed acceptance · submission ${r.id} · quest ${r.questId}${r.request ? " · exact replay payload available" : " · payload erased; poll ID only"}`}</text>}</For>
|
|
211
|
+
</box>}</Show>
|
|
212
|
+
<Show when={view().detail}>{(detail) => <box flexDirection="column" flexShrink={0}>
|
|
213
|
+
<text flexShrink={0} fg={context.theme.text.action.primary.base}>{detail().definition.title} · {detail().status}</text>
|
|
214
|
+
<text flexShrink={0} fg={context.theme.text.muted}>Quest {detail().id} · {detail().definition.kind} · ends {detail().endsAt} · rubric {detail().definition.rubricVersion}</text>
|
|
215
|
+
<For each={detail().definition.criteria}>{(c) => <text flexShrink={0} fg={context.theme.text.base}>{c.id}: {c.text}</text>}</For>
|
|
216
|
+
<Show when={detail().latestSubmission}>{(s) => <text flexShrink={0} fg={context.theme.text.base}>{statusText(s())}</text>}</Show>
|
|
217
|
+
</box>}</Show>
|
|
218
|
+
<Show when={view().draft}><text flexShrink={0} fg={context.theme.text.muted}>{view().draft?.provenance}</text></Show>
|
|
219
|
+
<Show when={view().phase === "preview" && view().preview}>
|
|
220
|
+
<text flexShrink={0} fg={context.theme.text.base}>{privacy}</text>
|
|
221
|
+
<text flexShrink={0} fg={context.theme.text.base}>{`Exact request for quest ${view().draft?.questId}:\n${JSON.stringify(view().preview, null, 2)}`}</text>
|
|
222
|
+
</Show>
|
|
223
|
+
<Show when={view().snapshot?.records.find((r) => r.id === replayID() && r.request)}>{(record) => <box flexDirection="column" flexShrink={0}>
|
|
224
|
+
<text flexShrink={0} fg={context.theme.text.base}>{privacy}</text>
|
|
225
|
+
<text flexShrink={0} fg={context.theme.text.base}>{`Exact saved recovery request · quest ${record().questId}:\n${JSON.stringify(record().request, null, 2)}`}</text>
|
|
226
|
+
</box>}</Show>
|
|
227
|
+
</scrollbox>
|
|
228
|
+
<Show when={view().phase === "edit" && view().draft}>
|
|
229
|
+
<text flexShrink={0} fg={context.theme.text.faint}>Ctrl+J newline · Tab actions · Ctrl+G preview, never sends</text>
|
|
230
|
+
<textarea ref={editor} flexBasis={0} flexGrow={1} minHeight={2} initialValue={view().draft?.text ?? ""} focused={focus.editorEnabled()} onContentChange={() => syncDailyEditor(editor, controller)} keyBindings={[{ name: "g", ctrl: true, action: "submit" }]} onSubmit={preview} />
|
|
231
|
+
</Show>
|
|
232
|
+
<text flexShrink={0} fg={context.theme.text.faint}>↑/↓ Enter · PgUp/Dn · Ctrl+U/D details · Cancel: no send</text>
|
|
233
|
+
<select ref={(node) => { actions = node; focus.attachActions(node); }} onMouseUp={(event) => { if (focus.actionsEnabled()) clickSelect(actions, event); }} flexBasis={0} flexGrow={1} flexShrink={0} minHeight={3} showScrollIndicator options={options()} focused={focus.actionsEnabled()} keyBindings={[{ name: "pageup", action: "move-up-fast" }, { name: "pagedown", action: "move-down-fast" }]} onSelect={(_index, option) => select(option?.value)} />
|
|
234
|
+
</box>;
|
|
235
|
+
}
|