@xoxoai/checkmate 0.6.7 → 0.7.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 +54 -257
- package/dist/checkmate.js +13375 -0
- package/package.json +30 -94
- package/plugin/.claude-plugin/plugin.json +10 -0
- package/plugin/bin/checkmate.ts +3 -0
- package/plugin/contracts/finding.schema.json +78 -0
- package/plugin/contracts/gap.schema.json +56 -0
- package/plugin/contracts/plan-candidate.schema.json +103 -0
- package/plugin/contracts/plan-delta.schema.json +63 -0
- package/plugin/contracts/plan-patch.schema.json +116 -0
- package/plugin/contracts/plan-request.schema.json +108 -0
- package/plugin/contracts/plan.schema.json +313 -0
- package/plugin/contracts/profile.schema.json +154 -0
- package/plugin/contracts/response.schema.json +25 -0
- package/plugin/contracts/result.schema.json +410 -0
- package/plugin/dist/checkmate.js +13012 -0
- package/plugin/package.json +4 -0
- package/plugin/references/browser.md +35 -0
- package/plugin/references/http.md +30 -0
- package/plugin/references/workflow.md +122 -0
- package/plugin/skills/execute/SKILL.md +92 -0
- package/plugin/skills/plan/SKILL.md +94 -0
- package/plugin/skills/report/SKILL.md +28 -0
- package/plugin/skills/revise/SKILL.md +43 -0
- package/plugin/skills/setup/SKILL.md +82 -0
- package/plugin/skills/status/SKILL.md +20 -0
- package/plugin/skills/ui/SKILL.md +26 -0
- package/plugin/src/actor.ts +21 -0
- package/plugin/src/cli.ts +1133 -0
- package/plugin/src/contracts.ts +342 -0
- package/plugin/src/index.ts +52 -0
- package/plugin/src/plans.ts +1028 -0
- package/plugin/src/render.ts +544 -0
- package/plugin/src/runs.ts +868 -0
- package/plugin/src/store.ts +384 -0
- package/plugin/src/targets.ts +205 -0
- package/plugin/src/types.ts +319 -0
- package/plugin/src/ui/assets/client.js +501 -0
- package/plugin/src/ui/assets/favicon.svg +1 -0
- package/plugin/src/ui/assets/fonts/inter-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/inter.woff2 +0 -0
- package/plugin/src/ui/assets/fonts/jetbrains-mono-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/jetbrains-mono.woff2 +0 -0
- package/plugin/src/ui/assets/fonts/space-grotesk-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/space-grotesk.woff2 +0 -0
- package/plugin/src/ui/assets/style.css +1926 -0
- package/plugin/src/ui/edit.ts +305 -0
- package/plugin/src/ui/forms.ts +209 -0
- package/plugin/src/ui/html.ts +130 -0
- package/plugin/src/ui/layout.ts +198 -0
- package/plugin/src/ui/lists.ts +645 -0
- package/plugin/src/ui/plan.ts +577 -0
- package/plugin/src/ui/run.ts +489 -0
- package/plugin/src/ui/server.ts +684 -0
- package/plugin/src/validators.d.ts +13 -0
- package/plugin/src/validators.js +1 -0
- package/plugin/src/views.ts +839 -0
- package/bin/checkmate.js +0 -13
- package/dist/ai/client.d.ts +0 -64
- package/dist/ai/client.d.ts.map +0 -1
- package/dist/ai/client.js +0 -313
- package/dist/ai/client.js.map +0 -1
- package/dist/ai/message-handler.d.ts +0 -9
- package/dist/ai/message-handler.d.ts.map +0 -1
- package/dist/ai/message-handler.js +0 -50
- package/dist/ai/message-handler.js.map +0 -1
- package/dist/ai/message-history.d.ts +0 -17
- package/dist/ai/message-history.d.ts.map +0 -1
- package/dist/ai/message-history.js +0 -45
- package/dist/ai/message-history.js.map +0 -1
- package/dist/ai/prompts.d.ts +0 -4
- package/dist/ai/prompts.d.ts.map +0 -1
- package/dist/ai/prompts.js +0 -24
- package/dist/ai/prompts.js.map +0 -1
- package/dist/ai/rate-limit-policy.d.ts +0 -9
- package/dist/ai/rate-limit-policy.d.ts.map +0 -1
- package/dist/ai/rate-limit-policy.js +0 -34
- package/dist/ai/rate-limit-policy.js.map +0 -1
- package/dist/ai/tool-response-handler.d.ts +0 -16
- package/dist/ai/tool-response-handler.d.ts.map +0 -1
- package/dist/ai/tool-response-handler.js +0 -85
- package/dist/ai/tool-response-handler.js.map +0 -1
- package/dist/ai/turn-processor.d.ts +0 -34
- package/dist/ai/turn-processor.d.ts.map +0 -1
- package/dist/ai/turn-processor.js +0 -107
- package/dist/ai/turn-processor.js.map +0 -1
- package/dist/api/allocate-run-identity.d.ts +0 -8
- package/dist/api/allocate-run-identity.d.ts.map +0 -1
- package/dist/api/allocate-run-identity.js +0 -10
- package/dist/api/allocate-run-identity.js.map +0 -1
- package/dist/api/execute-prepared-run.d.ts +0 -22
- package/dist/api/execute-prepared-run.d.ts.map +0 -1
- package/dist/api/execute-prepared-run.js +0 -184
- package/dist/api/execute-prepared-run.js.map +0 -1
- package/dist/api/index.d.ts +0 -25
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -69
- package/dist/api/index.js.map +0 -1
- package/dist/api/prepare-run.d.ts +0 -44
- package/dist/api/prepare-run.d.ts.map +0 -1
- package/dist/api/prepare-run.js +0 -261
- package/dist/api/prepare-run.js.map +0 -1
- package/dist/cli/diagnostic-relay.d.ts +0 -6
- package/dist/cli/diagnostic-relay.d.ts.map +0 -1
- package/dist/cli/diagnostic-relay.js +0 -6
- package/dist/cli/diagnostic-relay.js.map +0 -1
- package/dist/cli/main.d.ts +0 -21
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -166
- package/dist/cli/main.js.map +0 -1
- package/dist/cli/protocol.d.ts +0 -57
- package/dist/cli/protocol.d.ts.map +0 -1
- package/dist/cli/protocol.js +0 -116
- package/dist/cli/protocol.js.map +0 -1
- package/dist/cli/reconcile-result.d.ts +0 -13
- package/dist/cli/reconcile-result.d.ts.map +0 -1
- package/dist/cli/reconcile-result.js +0 -300
- package/dist/cli/reconcile-result.js.map +0 -1
- package/dist/cli/run-parent.d.ts +0 -37
- package/dist/cli/run-parent.d.ts.map +0 -1
- package/dist/cli/run-parent.js +0 -491
- package/dist/cli/run-parent.js.map +0 -1
- package/dist/cli/signals.d.ts +0 -36
- package/dist/cli/signals.d.ts.map +0 -1
- package/dist/cli/signals.js +0 -85
- package/dist/cli/signals.js.map +0 -1
- package/dist/cli/static-commands.d.ts +0 -11
- package/dist/cli/static-commands.d.ts.map +0 -1
- package/dist/cli/static-commands.js +0 -83
- package/dist/cli/static-commands.js.map +0 -1
- package/dist/cli/worker-stream-guard.d.ts +0 -20
- package/dist/cli/worker-stream-guard.d.ts.map +0 -1
- package/dist/cli/worker-stream-guard.js +0 -41
- package/dist/cli/worker-stream-guard.js.map +0 -1
- package/dist/cli/worker.d.ts +0 -3
- package/dist/cli/worker.d.ts.map +0 -1
- package/dist/cli/worker.js +0 -111
- package/dist/cli/worker.js.map +0 -1
- package/dist/config/ingestion.d.ts +0 -58
- package/dist/config/ingestion.d.ts.map +0 -1
- package/dist/config/ingestion.js +0 -283
- package/dist/config/ingestion.js.map +0 -1
- package/dist/config/manifest.d.ts +0 -20
- package/dist/config/manifest.d.ts.map +0 -1
- package/dist/config/manifest.js +0 -91
- package/dist/config/manifest.js.map +0 -1
- package/dist/config/model-egress.d.ts +0 -10
- package/dist/config/model-egress.d.ts.map +0 -1
- package/dist/config/model-egress.js +0 -62
- package/dist/config/model-egress.js.map +0 -1
- package/dist/config/package-resolution.d.ts +0 -4
- package/dist/config/package-resolution.d.ts.map +0 -1
- package/dist/config/package-resolution.js +0 -24
- package/dist/config/package-resolution.js.map +0 -1
- package/dist/config/policy.d.ts +0 -44
- package/dist/config/policy.d.ts.map +0 -1
- package/dist/config/policy.js +0 -141
- package/dist/config/policy.js.map +0 -1
- package/dist/config/record.d.ts +0 -4
- package/dist/config/record.d.ts.map +0 -1
- package/dist/config/record.js +0 -10
- package/dist/config/record.js.map +0 -1
- package/dist/config/secrets.d.ts +0 -11
- package/dist/config/secrets.d.ts.map +0 -1
- package/dist/config/secrets.js +0 -42
- package/dist/config/secrets.js.map +0 -1
- package/dist/contracts/diagnostics.d.ts +0 -4
- package/dist/contracts/diagnostics.d.ts.map +0 -1
- package/dist/contracts/diagnostics.js +0 -31
- package/dist/contracts/diagnostics.js.map +0 -1
- package/dist/contracts/serialize.d.ts +0 -2
- package/dist/contracts/serialize.d.ts.map +0 -1
- package/dist/contracts/serialize.js +0 -90
- package/dist/contracts/serialize.js.map +0 -1
- package/dist/contracts/types.d.ts +0 -316
- package/dist/contracts/types.d.ts.map +0 -1
- package/dist/contracts/types.js +0 -74
- package/dist/contracts/types.js.map +0 -1
- package/dist/contracts/validator.d.ts +0 -8
- package/dist/contracts/validator.d.ts.map +0 -1
- package/dist/contracts/validator.js +0 -71
- package/dist/contracts/validator.js.map +0 -1
- package/dist/driver.d.ts +0 -97
- package/dist/driver.d.ts.map +0 -1
- package/dist/driver.js +0 -24
- package/dist/driver.js.map +0 -1
- package/dist/drivers/descriptor.d.ts +0 -15
- package/dist/drivers/descriptor.d.ts.map +0 -1
- package/dist/drivers/descriptor.js +0 -122
- package/dist/drivers/descriptor.js.map +0 -1
- package/dist/drivers/loader.d.ts +0 -18
- package/dist/drivers/loader.d.ts.map +0 -1
- package/dist/drivers/loader.js +0 -97
- package/dist/drivers/loader.js.map +0 -1
- package/dist/drivers/web/checkmate-driver.json +0 -47
- package/dist/drivers/web/index.d.ts +0 -6
- package/dist/drivers/web/index.d.ts.map +0 -1
- package/dist/drivers/web/index.js +0 -27
- package/dist/drivers/web/index.js.map +0 -1
- package/dist/drivers/web/session.d.ts +0 -25
- package/dist/drivers/web/session.d.ts.map +0 -1
- package/dist/drivers/web/session.js +0 -132
- package/dist/drivers/web/session.js.map +0 -1
- package/dist/drivers/web/tools/network-request-recorder.d.ts +0 -46
- package/dist/drivers/web/tools/network-request-recorder.d.ts.map +0 -1
- package/dist/drivers/web/tools/network-request-recorder.js +0 -164
- package/dist/drivers/web/tools/network-request-recorder.js.map +0 -1
- package/dist/drivers/web/tools/screenshot-service.d.ts +0 -10
- package/dist/drivers/web/tools/screenshot-service.d.ts.map +0 -1
- package/dist/drivers/web/tools/screenshot-service.js +0 -23
- package/dist/drivers/web/tools/screenshot-service.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/index.d.ts +0 -4
- package/dist/drivers/web/tools/snapshot-filter/index.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/index.js +0 -4
- package/dist/drivers/web/tools/snapshot-filter/index.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts +0 -15
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js +0 -99
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts +0 -5
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js +0 -56
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts +0 -3
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js +0 -85
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-service.d.ts +0 -31
- package/dist/drivers/web/tools/snapshot-service.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-service.js +0 -96
- package/dist/drivers/web/tools/snapshot-service.js.map +0 -1
- package/dist/drivers/web/tools/tool.d.ts +0 -76
- package/dist/drivers/web/tools/tool.d.ts.map +0 -1
- package/dist/drivers/web/tools/tool.js +0 -562
- package/dist/drivers/web/tools/tool.js.map +0 -1
- package/dist/drivers/web/tools/transient-state-tracker.d.ts +0 -26
- package/dist/drivers/web/tools/transient-state-tracker.d.ts.map +0 -1
- package/dist/drivers/web/tools/transient-state-tracker.js +0 -301
- package/dist/drivers/web/tools/transient-state-tracker.js.map +0 -1
- package/dist/drivers/web/tools/types.d.ts +0 -11
- package/dist/drivers/web/tools/types.d.ts.map +0 -1
- package/dist/drivers/web/tools/types.js +0 -2
- package/dist/drivers/web/tools/types.js.map +0 -1
- package/dist/evidence/atomic-file.d.ts +0 -23
- package/dist/evidence/atomic-file.d.ts.map +0 -1
- package/dist/evidence/atomic-file.js +0 -104
- package/dist/evidence/atomic-file.js.map +0 -1
- package/dist/evidence/layout.d.ts +0 -30
- package/dist/evidence/layout.d.ts.map +0 -1
- package/dist/evidence/layout.js +0 -152
- package/dist/evidence/layout.js.map +0 -1
- package/dist/evidence/retention.d.ts +0 -5
- package/dist/evidence/retention.d.ts.map +0 -1
- package/dist/evidence/retention.js +0 -10
- package/dist/evidence/retention.js.map +0 -1
- package/dist/evidence/store.d.ts +0 -101
- package/dist/evidence/store.d.ts.map +0 -1
- package/dist/evidence/store.js +0 -375
- package/dist/evidence/store.js.map +0 -1
- package/dist/evidence/terminal-finalizer.d.ts +0 -19
- package/dist/evidence/terminal-finalizer.d.ts.map +0 -1
- package/dist/evidence/terminal-finalizer.js +0 -56
- package/dist/evidence/terminal-finalizer.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +0 -1
- package/dist/logging/invocation-logger.d.ts +0 -4
- package/dist/logging/invocation-logger.d.ts.map +0 -1
- package/dist/logging/invocation-logger.js +0 -25
- package/dist/logging/invocation-logger.js.map +0 -1
- package/dist/logging/level-logger.d.ts +0 -11
- package/dist/logging/level-logger.d.ts.map +0 -1
- package/dist/logging/level-logger.js +0 -82
- package/dist/logging/level-logger.js.map +0 -1
- package/dist/logging/types.d.ts +0 -8
- package/dist/logging/types.d.ts.map +0 -1
- package/dist/logging/types.js +0 -7
- package/dist/logging/types.js.map +0 -1
- package/dist/redaction/diagnostic-sanitizer.d.ts +0 -7
- package/dist/redaction/diagnostic-sanitizer.d.ts.map +0 -1
- package/dist/redaction/diagnostic-sanitizer.js +0 -42
- package/dist/redaction/diagnostic-sanitizer.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -40
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -176
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/redaction/scrub.d.ts +0 -29
- package/dist/redaction/scrub.d.ts.map +0 -1
- package/dist/redaction/scrub.js +0 -54
- package/dist/redaction/scrub.js.map +0 -1
- package/dist/runtime/config.d.ts +0 -16
- package/dist/runtime/config.d.ts.map +0 -1
- package/dist/runtime/config.js +0 -2
- package/dist/runtime/config.js.map +0 -1
- package/dist/runtime/driver-boundary.d.ts +0 -16
- package/dist/runtime/driver-boundary.d.ts.map +0 -1
- package/dist/runtime/driver-boundary.js +0 -71
- package/dist/runtime/driver-boundary.js.map +0 -1
- package/dist/runtime/driver-session.d.ts +0 -9
- package/dist/runtime/driver-session.d.ts.map +0 -1
- package/dist/runtime/driver-session.js +0 -64
- package/dist/runtime/driver-session.js.map +0 -1
- package/dist/runtime/internal-step-evidence.d.ts +0 -30
- package/dist/runtime/internal-step-evidence.d.ts.map +0 -1
- package/dist/runtime/internal-step-evidence.js +0 -82
- package/dist/runtime/internal-step-evidence.js.map +0 -1
- package/dist/runtime/result-adapter.d.ts +0 -9
- package/dist/runtime/result-adapter.d.ts.map +0 -1
- package/dist/runtime/result-adapter.js +0 -77
- package/dist/runtime/result-adapter.js.map +0 -1
- package/dist/runtime/runner.d.ts +0 -43
- package/dist/runtime/runner.d.ts.map +0 -1
- package/dist/runtime/runner.js +0 -72
- package/dist/runtime/runner.js.map +0 -1
- package/dist/runtime/scenario-control.d.ts +0 -40
- package/dist/runtime/scenario-control.d.ts.map +0 -1
- package/dist/runtime/scenario-control.js +0 -94
- package/dist/runtime/scenario-control.js.map +0 -1
- package/dist/runtime/scenario-runner.d.ts +0 -41
- package/dist/runtime/scenario-runner.d.ts.map +0 -1
- package/dist/runtime/scenario-runner.js +0 -274
- package/dist/runtime/scenario-runner.js.map +0 -1
- package/dist/runtime/scenario-state.d.ts +0 -55
- package/dist/runtime/scenario-state.d.ts.map +0 -1
- package/dist/runtime/scenario-state.js +0 -128
- package/dist/runtime/scenario-state.js.map +0 -1
- package/dist/runtime/step-execution.d.ts +0 -27
- package/dist/runtime/step-execution.d.ts.map +0 -1
- package/dist/runtime/step-execution.js +0 -143
- package/dist/runtime/step-execution.js.map +0 -1
- package/dist/runtime/text.d.ts +0 -10
- package/dist/runtime/text.d.ts.map +0 -1
- package/dist/runtime/text.js +0 -25
- package/dist/runtime/text.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -53
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -2
- package/dist/runtime/types.js.map +0 -1
- package/dist/runtime/usage-tracker.d.ts +0 -37
- package/dist/runtime/usage-tracker.d.ts.map +0 -1
- package/dist/runtime/usage-tracker.js +0 -110
- package/dist/runtime/usage-tracker.js.map +0 -1
- package/dist/tools/define-agent-tool.d.ts +0 -20
- package/dist/tools/define-agent-tool.d.ts.map +0 -1
- package/dist/tools/define-agent-tool.js +0 -30
- package/dist/tools/define-agent-tool.js.map +0 -1
- package/dist/tools/dispatcher.d.ts +0 -19
- package/dist/tools/dispatcher.d.ts.map +0 -1
- package/dist/tools/dispatcher.js +0 -91
- package/dist/tools/dispatcher.js.map +0 -1
- package/dist/tools/loop-detector.d.ts +0 -27
- package/dist/tools/loop-detector.d.ts.map +0 -1
- package/dist/tools/loop-detector.js +0 -76
- package/dist/tools/loop-detector.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -19
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -54
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/step/result-tool.d.ts +0 -7
- package/dist/tools/step/result-tool.d.ts.map +0 -1
- package/dist/tools/step/result-tool.js +0 -34
- package/dist/tools/step/result-tool.js.map +0 -1
- package/dist/tools/tool-contract.d.ts +0 -3
- package/dist/tools/tool-contract.d.ts.map +0 -1
- package/dist/tools/tool-contract.js +0 -2
- package/dist/tools/tool-contract.js.map +0 -1
- package/dist/tools/types.d.ts +0 -210
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -4
- package/dist/tools/types.js.map +0 -1
- package/docs/CLI.md +0 -59
- package/docs/CONFIGURATION.md +0 -105
- package/docs/DRIVERS.md +0 -79
- package/docs/EVIDENCE.md +0 -64
- package/schemas/checkmate-config.v1.json +0 -176
- package/schemas/describe-result.v1.json +0 -414
- package/schemas/driver-descriptor.v1.json +0 -66
- package/schemas/run-request.v1.json +0 -67
- package/schemas/run-result.v1.json +0 -333
- package/schemas/validation-result.v1.json +0 -86
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import type { Reply, Store } from './store.ts'
|
|
2
|
+
import type { Plan, Profile } from './types.ts'
|
|
3
|
+
import type { RequestedTool, TargetRequest } from './targets.ts'
|
|
4
|
+
import type { PlanTool } from './plans.ts'
|
|
5
|
+
import type { EvidenceInput } from './runs.ts'
|
|
6
|
+
import { closeStore, completed, openReadOnlyStore, openStore } from './store.ts'
|
|
7
|
+
import { requireChoice, requireCondition, revisionReference } from './contracts.ts'
|
|
8
|
+
import { openTarget, saveTarget, showTarget, showTargetRequest } from './targets.ts'
|
|
9
|
+
import {
|
|
10
|
+
adoptGaps,
|
|
11
|
+
answerGap,
|
|
12
|
+
approvePlan,
|
|
13
|
+
diffPlan,
|
|
14
|
+
editPlan,
|
|
15
|
+
gate,
|
|
16
|
+
openPlan,
|
|
17
|
+
patchPlan,
|
|
18
|
+
rejectPlan,
|
|
19
|
+
revisePlan,
|
|
20
|
+
savePlan,
|
|
21
|
+
showPending,
|
|
22
|
+
showPlanRequest,
|
|
23
|
+
} from './plans.ts'
|
|
24
|
+
import {
|
|
25
|
+
acceptResult,
|
|
26
|
+
beginRun,
|
|
27
|
+
checkAssertion,
|
|
28
|
+
closeTest,
|
|
29
|
+
finishRun,
|
|
30
|
+
recordFinding,
|
|
31
|
+
recordGap,
|
|
32
|
+
rejectResult,
|
|
33
|
+
resumeRun,
|
|
34
|
+
} from './runs.ts'
|
|
35
|
+
import {
|
|
36
|
+
evidenceBytes,
|
|
37
|
+
inbox,
|
|
38
|
+
operationLog,
|
|
39
|
+
planDetail,
|
|
40
|
+
planList,
|
|
41
|
+
planStatus,
|
|
42
|
+
roles,
|
|
43
|
+
runDetail,
|
|
44
|
+
runList,
|
|
45
|
+
} from './views.ts'
|
|
46
|
+
import { exportPlan, exportRun } from './render.ts'
|
|
47
|
+
import { startUi } from './ui/server.ts'
|
|
48
|
+
import { defaultActor } from './actor.ts'
|
|
49
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
50
|
+
import { dirname, resolve } from 'node:path'
|
|
51
|
+
|
|
52
|
+
type FlagKind = 'value' | 'list' | 'repeat' | 'switch'
|
|
53
|
+
type Args = Record<string, string | string[] | boolean>
|
|
54
|
+
type Context = { actor: string | undefined; stdin: () => unknown; text: () => string }
|
|
55
|
+
type Printed = { facts?: Record<string, unknown>; lines?: unknown[][] }
|
|
56
|
+
type Command = {
|
|
57
|
+
positionals: string[]
|
|
58
|
+
flags?: Record<string, FlagKind>
|
|
59
|
+
required?: string[]
|
|
60
|
+
write?: boolean
|
|
61
|
+
stdin?: boolean
|
|
62
|
+
readOnly?: boolean
|
|
63
|
+
run: (store: Store, args: Args, context: Context) => Reply
|
|
64
|
+
print?: (reply: Reply) => Printed
|
|
65
|
+
}
|
|
66
|
+
export type Io = { cwd: string; env: Record<string, string | undefined>; stdin: () => string }
|
|
67
|
+
export type Outcome = { code: number; output: string }
|
|
68
|
+
|
|
69
|
+
const testFields = [
|
|
70
|
+
'title',
|
|
71
|
+
'readiness',
|
|
72
|
+
'capability',
|
|
73
|
+
'operations',
|
|
74
|
+
'requirementIds',
|
|
75
|
+
'gapIds',
|
|
76
|
+
'dependsOn',
|
|
77
|
+
'preconditions',
|
|
78
|
+
'action',
|
|
79
|
+
'assertions',
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
function value(args: Args, name: string) {
|
|
83
|
+
const found = args[name]
|
|
84
|
+
return typeof found === 'string' ? found : undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function list(args: Args, name: string) {
|
|
88
|
+
const found = args[name]
|
|
89
|
+
return Array.isArray(found) ? found : undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function integer(args: Args, name: string) {
|
|
93
|
+
const text = value(args, name)
|
|
94
|
+
if (text === undefined) return undefined
|
|
95
|
+
requireCondition(/^[0-9]+$/.test(text), `Invalid --${name}: ${text}. Use a whole number.`)
|
|
96
|
+
return Number(text)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function evidence(args: Args, context: Context): EvidenceInput[] | undefined {
|
|
100
|
+
return list(args, 'evidence')?.map((spec) => {
|
|
101
|
+
const match = /^-(?::(.+))?$/.exec(spec)
|
|
102
|
+
return match ? { text: context.text(), kind: match[1] } : spec
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toolSpec(spec: string): RequestedTool {
|
|
107
|
+
const match = /^(.+?):(cli|mcp|skill):(.+)$/.exec(spec)
|
|
108
|
+
requireCondition(match, `Invalid --tool: ${spec}. Use <name>:<cli|mcp|skill>:<purpose>.`)
|
|
109
|
+
return { name: match[1], kind: match[2] as RequestedTool['kind'], purpose: match[3] }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function planRequestInput(args: Args, stdin: () => unknown) {
|
|
113
|
+
const objective = value(args, 'objective')
|
|
114
|
+
const sources = list(args, 'source')
|
|
115
|
+
const instructions = value(args, 'instructions')
|
|
116
|
+
if (objective === undefined) {
|
|
117
|
+
requireCondition(
|
|
118
|
+
!sources && instructions === undefined,
|
|
119
|
+
'Give --objective with --source and --instructions, or send the request on standard input'
|
|
120
|
+
)
|
|
121
|
+
return stdin()
|
|
122
|
+
}
|
|
123
|
+
return { objective, context: { sources: sources ?? [], ...(instructions === undefined ? {} : { instructions }) } }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function leftOutLines(reply: Reply) {
|
|
127
|
+
const leftOut = (reply.data.leftOut ?? []) as Array<{ testId: string; reason: string }>
|
|
128
|
+
return leftOut.map((item) => [item.testId, 'left_out', item.reason])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function short(sha256: unknown) {
|
|
132
|
+
return typeof sha256 === 'string' ? sha256.slice(0, 8) : sha256
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const commands: Record<string, Command> = {
|
|
136
|
+
init: {
|
|
137
|
+
positionals: [],
|
|
138
|
+
run: (store) => completed('.checkmate/checkmate.db', { path: store.path }),
|
|
139
|
+
},
|
|
140
|
+
inbox: {
|
|
141
|
+
positionals: [],
|
|
142
|
+
run: (store) => completed('inbox', inbox(store)),
|
|
143
|
+
print: (reply) => {
|
|
144
|
+
const waiting = reply.data as ReturnType<typeof inbox>
|
|
145
|
+
return {
|
|
146
|
+
facts: {
|
|
147
|
+
gaps: waiting.gaps.length,
|
|
148
|
+
plans: waiting.plans.length,
|
|
149
|
+
results: waiting.results.length,
|
|
150
|
+
requests: waiting.requests.length,
|
|
151
|
+
},
|
|
152
|
+
lines: [
|
|
153
|
+
...waiting.gaps.map((gap) =>
|
|
154
|
+
gap.runId
|
|
155
|
+
? [
|
|
156
|
+
`${gap.planId}@${gap.revision}/${gap.gapId}`,
|
|
157
|
+
'run_gap',
|
|
158
|
+
gap.severity,
|
|
159
|
+
gap.runId,
|
|
160
|
+
gap.question,
|
|
161
|
+
]
|
|
162
|
+
: [`${gap.planId}@${gap.revision}/${gap.gapId}`, 'gap', gap.severity, gap.question]
|
|
163
|
+
),
|
|
164
|
+
...waiting.plans.map((plan) => [
|
|
165
|
+
`${plan.planId}@${plan.revision}`,
|
|
166
|
+
'plan',
|
|
167
|
+
short(plan.sha256),
|
|
168
|
+
plan.summary,
|
|
169
|
+
]),
|
|
170
|
+
...waiting.results.map((result) => [
|
|
171
|
+
result.runId,
|
|
172
|
+
'result',
|
|
173
|
+
`${result.planId}@${result.revision}`,
|
|
174
|
+
result.build,
|
|
175
|
+
result.coverage,
|
|
176
|
+
Object.entries(result.counts).map(([outcome, count]) => `${outcome}=${count}`),
|
|
177
|
+
]),
|
|
178
|
+
...waiting.requests.map((request) => [
|
|
179
|
+
request.planId,
|
|
180
|
+
'requested',
|
|
181
|
+
request.target,
|
|
182
|
+
request.objective,
|
|
183
|
+
]),
|
|
184
|
+
],
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
status: {
|
|
189
|
+
positionals: ['planId'],
|
|
190
|
+
run: (store, args) => {
|
|
191
|
+
const planId = value(args, 'planId')!
|
|
192
|
+
return completed(planId, planStatus(store, planId))
|
|
193
|
+
},
|
|
194
|
+
print: (reply) => {
|
|
195
|
+
const status = reply.data as ReturnType<typeof planStatus>
|
|
196
|
+
return {
|
|
197
|
+
facts: {
|
|
198
|
+
state: status.state,
|
|
199
|
+
waitsOn: status.waitsOn,
|
|
200
|
+
revision: status.revision ?? undefined,
|
|
201
|
+
approval: status.approval ?? undefined,
|
|
202
|
+
run: status.latestRun?.runId,
|
|
203
|
+
},
|
|
204
|
+
lines: [
|
|
205
|
+
...status.why.map((line) => ['why', line]),
|
|
206
|
+
...(status.next ? [['next', status.next]] : []),
|
|
207
|
+
...status.gaps.map((gap) =>
|
|
208
|
+
gap.runId
|
|
209
|
+
? [gap.gapId, 'run_gap', gap.severity, gap.runId, gap.question]
|
|
210
|
+
: [gap.gapId, 'gap', gap.severity, gap.question]
|
|
211
|
+
),
|
|
212
|
+
...status.pendingDecisions.map((decision) => [
|
|
213
|
+
decision.id,
|
|
214
|
+
'pending',
|
|
215
|
+
decision.gapId,
|
|
216
|
+
decision.kind,
|
|
217
|
+
]),
|
|
218
|
+
...status.defects.map((defect) => [
|
|
219
|
+
defect.findingId,
|
|
220
|
+
'defect',
|
|
221
|
+
defect.runId,
|
|
222
|
+
defect.severity,
|
|
223
|
+
defect.assertionIds,
|
|
224
|
+
defect.title,
|
|
225
|
+
]),
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
'target open': {
|
|
231
|
+
positionals: ['targetId'],
|
|
232
|
+
flags: { 'base-url': 'value', capabilities: 'list', operations: 'list', tool: 'repeat' },
|
|
233
|
+
required: ['base-url', 'capabilities', 'operations'],
|
|
234
|
+
write: true,
|
|
235
|
+
run: (store, args, { actor }) =>
|
|
236
|
+
openTarget(store, actor, {
|
|
237
|
+
targetId: value(args, 'targetId')!,
|
|
238
|
+
baseUrl: value(args, 'base-url')!,
|
|
239
|
+
capabilities: list(args, 'capabilities') ?? [],
|
|
240
|
+
allowedOperations: list(args, 'operations') ?? [],
|
|
241
|
+
tools: (list(args, 'tool') ?? []).map(toolSpec),
|
|
242
|
+
}),
|
|
243
|
+
print: (reply) => ({ facts: { recorded: reply.data.recorded } }),
|
|
244
|
+
},
|
|
245
|
+
'target request': {
|
|
246
|
+
positionals: ['targetId'],
|
|
247
|
+
run: (store, args) => showTargetRequest(store, value(args, 'targetId')!),
|
|
248
|
+
print: (reply) => {
|
|
249
|
+
const request = reply.data.request as TargetRequest
|
|
250
|
+
return {
|
|
251
|
+
lines: [
|
|
252
|
+
['baseUrl', request.baseUrl],
|
|
253
|
+
['capabilities', request.capabilities],
|
|
254
|
+
['operations', request.allowedOperations],
|
|
255
|
+
...request.tools.map((tool) => ['tool', tool.name, tool.kind, tool.purpose]),
|
|
256
|
+
],
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
'target save': {
|
|
261
|
+
positionals: ['targetId'],
|
|
262
|
+
write: true,
|
|
263
|
+
stdin: true,
|
|
264
|
+
run: (store, args, { actor, stdin }) =>
|
|
265
|
+
saveTarget(store, actor, { targetId: value(args, 'targetId')!, profile: stdin() }),
|
|
266
|
+
print: (reply) => ({ facts: { sha256: short(reply.data.sha256), recorded: reply.data.recorded } }),
|
|
267
|
+
},
|
|
268
|
+
'target show': {
|
|
269
|
+
positionals: ['reference'],
|
|
270
|
+
run: (store, args) => showTarget(store, value(args, 'reference')!),
|
|
271
|
+
print: (reply) => {
|
|
272
|
+
const profile = reply.data.profile as Profile
|
|
273
|
+
const approvals = reply.data.approvals as { plan: string; result: string }
|
|
274
|
+
return {
|
|
275
|
+
facts: { sha256: short(reply.data.sha256), ready: profile.readyForLiveExecution },
|
|
276
|
+
lines: [
|
|
277
|
+
['baseUrl', profile.baseUrl],
|
|
278
|
+
['environment', profile.environment],
|
|
279
|
+
['capabilities', profile.capabilities],
|
|
280
|
+
['operations', profile.allowedOperations],
|
|
281
|
+
['approvals', `plan=${approvals.plan}`, `result=${approvals.result}`],
|
|
282
|
+
...profile.tools.map((tool) => [
|
|
283
|
+
'tool',
|
|
284
|
+
tool.name,
|
|
285
|
+
tool.kind,
|
|
286
|
+
tool.version ?? '-',
|
|
287
|
+
tool.reachable ? 'reachable' : 'unreachable',
|
|
288
|
+
tool.purpose,
|
|
289
|
+
]),
|
|
290
|
+
],
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
'plan open': {
|
|
295
|
+
positionals: ['planId'],
|
|
296
|
+
flags: { target: 'value', objective: 'value', source: 'repeat', instructions: 'value' },
|
|
297
|
+
required: ['target'],
|
|
298
|
+
write: true,
|
|
299
|
+
stdin: true,
|
|
300
|
+
run: (store, args, { actor, stdin }) =>
|
|
301
|
+
openPlan(store, actor, {
|
|
302
|
+
planId: value(args, 'planId')!,
|
|
303
|
+
target: value(args, 'target')!,
|
|
304
|
+
request: planRequestInput(args, stdin),
|
|
305
|
+
}),
|
|
306
|
+
print: (reply) => ({ facts: { target: reply.data.target, recorded: reply.data.recorded } }),
|
|
307
|
+
},
|
|
308
|
+
'plan request': {
|
|
309
|
+
positionals: ['planId'],
|
|
310
|
+
run: (store, args) => showPlanRequest(store, value(args, 'planId')!),
|
|
311
|
+
print: (reply) => {
|
|
312
|
+
const data = reply.data as ReturnType<typeof showPlanRequest>['data'] & {
|
|
313
|
+
target: string
|
|
314
|
+
objective: string
|
|
315
|
+
requirements: Plan['requirements']
|
|
316
|
+
sourceContext: Array<{ uri: string; content: string }>
|
|
317
|
+
context: { sources: string[]; instructions?: string }
|
|
318
|
+
tools: PlanTool[]
|
|
319
|
+
capabilities: string[]
|
|
320
|
+
allowedOperations: string[]
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
facts: { target: data.target },
|
|
324
|
+
lines: [
|
|
325
|
+
['objective', data.objective],
|
|
326
|
+
...data.requirements.map((requirement) => [requirement.id, requirement.text, requirement.source]),
|
|
327
|
+
...data.context.sources.map((source) => ['source', source]),
|
|
328
|
+
...(data.context.instructions ? [['instructions', data.context.instructions]] : []),
|
|
329
|
+
...data.sourceContext.map((context) => ['context', context.uri, context.content]),
|
|
330
|
+
...data.tools.map((tool) => ['tool', tool.name, tool.kind, tool.purpose]),
|
|
331
|
+
['capabilities', data.capabilities],
|
|
332
|
+
['operations', data.allowedOperations],
|
|
333
|
+
],
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
'plan save': {
|
|
338
|
+
positionals: ['planId'],
|
|
339
|
+
write: true,
|
|
340
|
+
stdin: true,
|
|
341
|
+
run: (store, args, { actor, stdin }) =>
|
|
342
|
+
savePlan(store, actor, { planId: value(args, 'planId')!, candidate: stdin() }),
|
|
343
|
+
print: (reply) => ({ facts: { sha256: short(reply.data.sha256) } }),
|
|
344
|
+
},
|
|
345
|
+
'plan show': {
|
|
346
|
+
positionals: ['reference'],
|
|
347
|
+
flags: { tests: 'list', fields: 'list', gaps: 'value' },
|
|
348
|
+
run: (store, args) => showPlan(store, args),
|
|
349
|
+
print: printPlan,
|
|
350
|
+
},
|
|
351
|
+
'plan list': {
|
|
352
|
+
positionals: [],
|
|
353
|
+
flags: { 'needs-decision': 'switch', 'awaiting-approval': 'switch', blocked: 'switch', 'waits-on': 'value' },
|
|
354
|
+
run: (store, args) => {
|
|
355
|
+
const waitsOn = value(args, 'waits-on')
|
|
356
|
+
if (waitsOn !== undefined) requireChoice(waitsOn, roles, 'role (--waits-on)')
|
|
357
|
+
const plans = planList(store).filter(
|
|
358
|
+
(plan) =>
|
|
359
|
+
(!args['needs-decision'] || plan.openBlockingGaps.length > 0) &&
|
|
360
|
+
(!args['awaiting-approval'] || plan.approval === 'pending') &&
|
|
361
|
+
(!args.blocked || plan.blocked.length > 0) &&
|
|
362
|
+
(waitsOn === undefined || plan.loop.waitsOn === waitsOn)
|
|
363
|
+
)
|
|
364
|
+
return completed('plans', { plans })
|
|
365
|
+
},
|
|
366
|
+
print: (reply) => ({
|
|
367
|
+
facts: { count: (reply.data.plans as unknown[]).length },
|
|
368
|
+
lines: (reply.data.plans as ReturnType<typeof planList>).map((plan) =>
|
|
369
|
+
plan.status === 'requested'
|
|
370
|
+
? [plan.planId, 'status=requested', `state=${plan.loop.state}`, `waitsOn=${plan.loop.waitsOn}`]
|
|
371
|
+
: [
|
|
372
|
+
`${plan.planId}@${plan.revision}`,
|
|
373
|
+
`gaps=${cell(plan.openBlockingGaps)}`,
|
|
374
|
+
`approval=${plan.approval}`,
|
|
375
|
+
`blocked=${cell(plan.blocked.map((block) => `${block.testId}<-${block.dependency}: ${block.cause}`))}`,
|
|
376
|
+
`state=${plan.loop.state}`,
|
|
377
|
+
`waitsOn=${plan.loop.waitsOn}`,
|
|
378
|
+
]
|
|
379
|
+
),
|
|
380
|
+
}),
|
|
381
|
+
},
|
|
382
|
+
'plan diff': {
|
|
383
|
+
positionals: ['planId', 'from', 'to'],
|
|
384
|
+
run: (store, args) =>
|
|
385
|
+
diffPlan(store, { planId: value(args, 'planId')!, from: value(args, 'from')!, to: value(args, 'to')! }),
|
|
386
|
+
print: (reply) => {
|
|
387
|
+
const patch = reply.data.patch as Array<{ op: string; path: string; value?: unknown }>
|
|
388
|
+
return {
|
|
389
|
+
facts: { operations: patch.length },
|
|
390
|
+
lines: [
|
|
391
|
+
...(reply.data.changes as string[]).map((change) => ['change', change]),
|
|
392
|
+
...patch.map((operation) =>
|
|
393
|
+
operation.op === 'remove'
|
|
394
|
+
? [operation.op, operation.path]
|
|
395
|
+
: [operation.op, operation.path, JSON.stringify(operation.value)]
|
|
396
|
+
),
|
|
397
|
+
],
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
'gap answer': {
|
|
402
|
+
positionals: ['planId', 'gapId'],
|
|
403
|
+
flags: { kind: 'value', value: 'value', source: 'value', rationale: 'value', 'expect-sha': 'value' },
|
|
404
|
+
required: ['kind', 'value', 'rationale'],
|
|
405
|
+
write: true,
|
|
406
|
+
run: (store, args, { actor }) =>
|
|
407
|
+
answerGap(store, actor, {
|
|
408
|
+
planId: value(args, 'planId')!,
|
|
409
|
+
gapId: value(args, 'gapId')!,
|
|
410
|
+
kind: value(args, 'kind')!,
|
|
411
|
+
value: value(args, 'value')!,
|
|
412
|
+
source: value(args, 'source') ?? null,
|
|
413
|
+
rationale: value(args, 'rationale')!,
|
|
414
|
+
expectSha256: value(args, 'expect-sha'),
|
|
415
|
+
}),
|
|
416
|
+
print: (reply) => {
|
|
417
|
+
const adopted = reply.data.adopted as { runId: string; revision: number; edit: string } | undefined
|
|
418
|
+
return {
|
|
419
|
+
facts: {
|
|
420
|
+
decision: reply.data.decisionId,
|
|
421
|
+
gap: reply.data.gapId,
|
|
422
|
+
from: adopted?.runId,
|
|
423
|
+
revision: adopted?.revision,
|
|
424
|
+
edit: adopted?.edit,
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
'plan pending': {
|
|
430
|
+
positionals: ['planId'],
|
|
431
|
+
run: (store, args) => showPending(store, value(args, 'planId')!),
|
|
432
|
+
print: (reply) => {
|
|
433
|
+
const data = reply.data as ReturnType<typeof showPending>['data'] & {
|
|
434
|
+
decisions: Plan['decisions']
|
|
435
|
+
gaps: Plan['gaps']
|
|
436
|
+
tests: Plan['tests']
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
facts: { sha256: short(data.sha256), decisions: data.decisions.length },
|
|
440
|
+
lines: [
|
|
441
|
+
...data.decisions.map((decision) => [decision.id, decision.gapId, decision.kind, decision.value]),
|
|
442
|
+
...data.gaps.map((gap) => [gap.id, gap.severity, gap.status, gap.question]),
|
|
443
|
+
...data.tests.flatMap(testLines),
|
|
444
|
+
],
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
'plan revise': {
|
|
449
|
+
positionals: ['planId'],
|
|
450
|
+
flags: { 'expect-sha': 'value' },
|
|
451
|
+
write: true,
|
|
452
|
+
stdin: true,
|
|
453
|
+
run: (store, args, { actor, stdin }) =>
|
|
454
|
+
revisePlan(store, actor, {
|
|
455
|
+
planId: value(args, 'planId')!,
|
|
456
|
+
delta: stdin(),
|
|
457
|
+
expectSha256: value(args, 'expect-sha'),
|
|
458
|
+
}),
|
|
459
|
+
print: (reply) => ({ facts: { sha256: short(reply.data.sha256) } }),
|
|
460
|
+
},
|
|
461
|
+
'plan edit': {
|
|
462
|
+
positionals: ['reference'],
|
|
463
|
+
flags: { patch: 'switch', 'expect-sha': 'value' },
|
|
464
|
+
write: true,
|
|
465
|
+
stdin: true,
|
|
466
|
+
run: (store, args, { actor, stdin }) => {
|
|
467
|
+
const reference = value(args, 'reference')!
|
|
468
|
+
const expectSha256 = value(args, 'expect-sha')
|
|
469
|
+
return args.patch
|
|
470
|
+
? patchPlan(store, actor, { reference, patch: stdin(), expectSha256 })
|
|
471
|
+
: editPlan(store, actor, { reference, candidate: stdin(), expectSha256 })
|
|
472
|
+
},
|
|
473
|
+
print: (reply) => ({ facts: { sha256: short(reply.data.sha256), edit: reply.data.edit } }),
|
|
474
|
+
},
|
|
475
|
+
'plan adopt-gaps': {
|
|
476
|
+
positionals: ['reference'],
|
|
477
|
+
flags: { run: 'value', gaps: 'list', 'expect-sha': 'value' },
|
|
478
|
+
required: ['run'],
|
|
479
|
+
write: true,
|
|
480
|
+
run: (store, args, { actor }) =>
|
|
481
|
+
adoptGaps(store, actor, {
|
|
482
|
+
reference: value(args, 'reference')!,
|
|
483
|
+
runId: value(args, 'run')!,
|
|
484
|
+
gapIds: list(args, 'gaps'),
|
|
485
|
+
expectSha256: value(args, 'expect-sha'),
|
|
486
|
+
}),
|
|
487
|
+
print: (reply) => ({
|
|
488
|
+
facts: { sha256: short(reply.data.sha256), edit: reply.data.edit, adopted: reply.data.adopted },
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
'plan approve': verdictCommand('reference', (store, actor, reference, rationale, expectSha256) =>
|
|
492
|
+
approvePlan(store, actor, { reference, rationale, expectSha256 })
|
|
493
|
+
),
|
|
494
|
+
'plan reject': verdictCommand('reference', (store, actor, reference, rationale, expectSha256) =>
|
|
495
|
+
rejectPlan(store, actor, { reference, rationale, expectSha256 })
|
|
496
|
+
),
|
|
497
|
+
'run gate': {
|
|
498
|
+
positionals: ['reference'],
|
|
499
|
+
flags: { tests: 'list', build: 'value', partial: 'value', 'retest-of': 'value' },
|
|
500
|
+
required: ['build'],
|
|
501
|
+
run: (store, args) =>
|
|
502
|
+
gate(store, {
|
|
503
|
+
reference: value(args, 'reference')!,
|
|
504
|
+
tests: list(args, 'tests'),
|
|
505
|
+
build: value(args, 'build')!,
|
|
506
|
+
partial: value(args, 'partial'),
|
|
507
|
+
retestOf: value(args, 'retest-of') ?? null,
|
|
508
|
+
}).reply,
|
|
509
|
+
print: (reply) => ({
|
|
510
|
+
facts: { sha256: short(reply.data.sha256), tests: reply.data.tests },
|
|
511
|
+
lines: leftOutLines(reply),
|
|
512
|
+
}),
|
|
513
|
+
},
|
|
514
|
+
'run begin': {
|
|
515
|
+
positionals: ['runId', 'reference'],
|
|
516
|
+
flags: {
|
|
517
|
+
tests: 'list',
|
|
518
|
+
build: 'value',
|
|
519
|
+
partial: 'value',
|
|
520
|
+
'on-new-blocker': 'value',
|
|
521
|
+
timeout: 'value',
|
|
522
|
+
'retest-of': 'value',
|
|
523
|
+
},
|
|
524
|
+
required: ['build'],
|
|
525
|
+
write: true,
|
|
526
|
+
run: (store, args, { actor }) =>
|
|
527
|
+
beginRun(store, actor, {
|
|
528
|
+
runId: value(args, 'runId')!,
|
|
529
|
+
reference: value(args, 'reference')!,
|
|
530
|
+
tests: list(args, 'tests'),
|
|
531
|
+
build: value(args, 'build')!,
|
|
532
|
+
partial: value(args, 'partial'),
|
|
533
|
+
onNewBlocker: value(args, 'on-new-blocker'),
|
|
534
|
+
timeoutSeconds: integer(args, 'timeout'),
|
|
535
|
+
retestOf: value(args, 'retest-of') ?? null,
|
|
536
|
+
}),
|
|
537
|
+
print: (reply) => ({
|
|
538
|
+
facts: {
|
|
539
|
+
tests: reply.data.tests,
|
|
540
|
+
startedAt: reply.data.startedAt,
|
|
541
|
+
browserSession: reply.data.browserSession,
|
|
542
|
+
deadlineSeconds: reply.data.deadlineSeconds,
|
|
543
|
+
evidenceDirectory: reply.data.evidenceDirectory,
|
|
544
|
+
},
|
|
545
|
+
lines: leftOutLines(reply),
|
|
546
|
+
}),
|
|
547
|
+
},
|
|
548
|
+
'run check': {
|
|
549
|
+
positionals: ['runId', 'testId', 'assertionId'],
|
|
550
|
+
flags: { result: 'value', observed: 'value', reason: 'value', evidence: 'repeat' },
|
|
551
|
+
required: ['result'],
|
|
552
|
+
write: true,
|
|
553
|
+
run: (store, args, context) =>
|
|
554
|
+
checkAssertion(store, context.actor, {
|
|
555
|
+
runId: value(args, 'runId')!,
|
|
556
|
+
testId: value(args, 'testId')!,
|
|
557
|
+
assertionId: value(args, 'assertionId')!,
|
|
558
|
+
result: value(args, 'result')!,
|
|
559
|
+
observed: value(args, 'observed'),
|
|
560
|
+
reason: value(args, 'reason'),
|
|
561
|
+
evidence: evidence(args, context),
|
|
562
|
+
}),
|
|
563
|
+
print: (reply) => ({
|
|
564
|
+
facts: { [`${reply.data.testId}.${reply.data.assertionId}`]: reply.data.result },
|
|
565
|
+
lines: (reply.data.evidenceIds as string[]).map((id) => [id, 'evidence']),
|
|
566
|
+
}),
|
|
567
|
+
},
|
|
568
|
+
'run close': {
|
|
569
|
+
positionals: ['runId', 'testId'],
|
|
570
|
+
flags: { outcome: 'value', reason: 'value', evidence: 'repeat' },
|
|
571
|
+
write: true,
|
|
572
|
+
run: (store, args, context) =>
|
|
573
|
+
closeTest(store, context.actor, {
|
|
574
|
+
runId: value(args, 'runId')!,
|
|
575
|
+
testId: value(args, 'testId')!,
|
|
576
|
+
outcome: value(args, 'outcome'),
|
|
577
|
+
reason: value(args, 'reason'),
|
|
578
|
+
evidence: evidence(args, context),
|
|
579
|
+
}),
|
|
580
|
+
print: (reply) => ({
|
|
581
|
+
facts: { [String(reply.data.testId)]: reply.data.outcome },
|
|
582
|
+
lines: reply.data.reason ? [['reason', reply.data.reason]] : [],
|
|
583
|
+
}),
|
|
584
|
+
},
|
|
585
|
+
'run finding': {
|
|
586
|
+
positionals: ['runId'],
|
|
587
|
+
write: true,
|
|
588
|
+
stdin: true,
|
|
589
|
+
run: (store, args, { actor, stdin }) =>
|
|
590
|
+
recordFinding(store, actor, { runId: value(args, 'runId')!, finding: stdin() }),
|
|
591
|
+
print: (reply) => ({ facts: { finding: reply.data.findingId, tests: reply.data.testIds } }),
|
|
592
|
+
},
|
|
593
|
+
'run gap': {
|
|
594
|
+
positionals: ['runId'],
|
|
595
|
+
write: true,
|
|
596
|
+
stdin: true,
|
|
597
|
+
run: (store, args, { actor, stdin }) => recordGap(store, actor, { runId: value(args, 'runId')!, gap: stdin() }),
|
|
598
|
+
print: (reply) => ({ facts: { gap: reply.data.gapId } }),
|
|
599
|
+
},
|
|
600
|
+
'run finish': {
|
|
601
|
+
positionals: ['runId'],
|
|
602
|
+
flags: { completion: 'value', cleanup: 'value', 'cleanup-reason': 'value' },
|
|
603
|
+
required: ['completion', 'cleanup', 'cleanup-reason'],
|
|
604
|
+
write: true,
|
|
605
|
+
run: (store, args, { actor }) =>
|
|
606
|
+
finishRun(store, actor, {
|
|
607
|
+
runId: value(args, 'runId')!,
|
|
608
|
+
completion: value(args, 'completion')!,
|
|
609
|
+
cleanup: value(args, 'cleanup')!,
|
|
610
|
+
cleanupReason: value(args, 'cleanup-reason')!,
|
|
611
|
+
}),
|
|
612
|
+
print: (reply) => ({
|
|
613
|
+
facts: { coverage: reply.data.coverage, sha256: short(reply.data.sha256) },
|
|
614
|
+
lines: Object.entries(reply.data.counts as Record<string, number>),
|
|
615
|
+
}),
|
|
616
|
+
},
|
|
617
|
+
'run resume': {
|
|
618
|
+
positionals: ['runId'],
|
|
619
|
+
flags: { error: 'list', reason: 'value' },
|
|
620
|
+
write: true,
|
|
621
|
+
run: (store, args, { actor }) =>
|
|
622
|
+
resumeRun(store, actor, {
|
|
623
|
+
runId: value(args, 'runId')!,
|
|
624
|
+
error: list(args, 'error'),
|
|
625
|
+
reason: value(args, 'reason'),
|
|
626
|
+
}),
|
|
627
|
+
print: (reply) => {
|
|
628
|
+
const data = reply.data as {
|
|
629
|
+
attempt: number
|
|
630
|
+
browserSession: string
|
|
631
|
+
evidenceDirectory: string
|
|
632
|
+
closed: string[]
|
|
633
|
+
tests: Array<{ testId: string; warning: string; assertions: Plan['tests'][number]['assertions'] }>
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
facts: {
|
|
637
|
+
attempt: data.attempt,
|
|
638
|
+
browserSession: data.browserSession,
|
|
639
|
+
evidenceDirectory: data.evidenceDirectory,
|
|
640
|
+
},
|
|
641
|
+
lines: [
|
|
642
|
+
...data.closed.map((id) => [id, 'error']),
|
|
643
|
+
...data.tests.flatMap((test) => [
|
|
644
|
+
[test.testId, 'unclosed', test.warning],
|
|
645
|
+
...test.assertions.map((assertion) => [
|
|
646
|
+
`${test.testId}.${assertion.id}`,
|
|
647
|
+
assertion.checkKind,
|
|
648
|
+
assertion.check,
|
|
649
|
+
]),
|
|
650
|
+
]),
|
|
651
|
+
],
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
'run show': {
|
|
656
|
+
positionals: ['runId'],
|
|
657
|
+
flags: { tests: 'list' },
|
|
658
|
+
run: (store, args) => {
|
|
659
|
+
const detail = runDetail(store, value(args, 'runId')!)
|
|
660
|
+
const selected = list(args, 'tests')
|
|
661
|
+
for (const id of selected ?? [])
|
|
662
|
+
requireCondition(
|
|
663
|
+
detail.tests.some((test) => test.testId === id),
|
|
664
|
+
`Test ${id} is not selected in run ${detail.runId}`
|
|
665
|
+
)
|
|
666
|
+
const tests = detail.tests.filter((test) => !selected || selected.includes(test.testId))
|
|
667
|
+
return completed(detail.runId, { ...detail, tests })
|
|
668
|
+
},
|
|
669
|
+
print: (reply) => {
|
|
670
|
+
const detail = reply.data as ReturnType<typeof runDetail>
|
|
671
|
+
return {
|
|
672
|
+
facts: {
|
|
673
|
+
state: detail.state,
|
|
674
|
+
plan: `${detail.planId}@${detail.revision}`,
|
|
675
|
+
build: detail.build,
|
|
676
|
+
completion: detail.result?.completion,
|
|
677
|
+
coverage: detail.result?.coverage.status,
|
|
678
|
+
acceptance: detail.result?.acceptance,
|
|
679
|
+
},
|
|
680
|
+
lines: [
|
|
681
|
+
...detail.tests.flatMap((test) => [
|
|
682
|
+
[test.testId, test.outcome ?? 'open', test.title, ...(test.reason ? [test.reason] : [])],
|
|
683
|
+
...test.assertions.map((assertion) => [
|
|
684
|
+
`${test.testId}.${assertion.assertionId}`,
|
|
685
|
+
assertion.result ?? 'unchecked',
|
|
686
|
+
assertion.observed ?? assertion.check,
|
|
687
|
+
...(assertion.reason ? [assertion.reason] : []),
|
|
688
|
+
]),
|
|
689
|
+
]),
|
|
690
|
+
...detail.findings.map((finding) => [
|
|
691
|
+
finding.id,
|
|
692
|
+
finding.type,
|
|
693
|
+
finding.severity,
|
|
694
|
+
finding.assertionIds,
|
|
695
|
+
finding.title,
|
|
696
|
+
]),
|
|
697
|
+
...detail.newGaps.map((gap) => [gap.id, gap.severity, gap.status, gap.question]),
|
|
698
|
+
],
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
'run list': {
|
|
703
|
+
positionals: [],
|
|
704
|
+
flags: { plan: 'value', build: 'value' },
|
|
705
|
+
run: (store, args) =>
|
|
706
|
+
completed('runs', { runs: runList(store, { planId: value(args, 'plan'), build: value(args, 'build') }) }),
|
|
707
|
+
print: (reply) => ({
|
|
708
|
+
facts: { count: (reply.data.runs as unknown[]).length },
|
|
709
|
+
lines: (reply.data.runs as ReturnType<typeof runList>).map((run) => [
|
|
710
|
+
run.runId,
|
|
711
|
+
`${run.planId}@${run.revision}`,
|
|
712
|
+
run.build,
|
|
713
|
+
run.state,
|
|
714
|
+
run.completion,
|
|
715
|
+
run.coverage,
|
|
716
|
+
run.acceptance,
|
|
717
|
+
]),
|
|
718
|
+
}),
|
|
719
|
+
},
|
|
720
|
+
'result accept': verdictCommand('runId', (store, actor, runId, rationale, expectSha256) =>
|
|
721
|
+
acceptResult(store, actor, { runId, rationale, expectSha256 })
|
|
722
|
+
),
|
|
723
|
+
'result reject': verdictCommand('runId', (store, actor, runId, rationale, expectSha256) =>
|
|
724
|
+
rejectResult(store, actor, { runId, rationale, expectSha256 })
|
|
725
|
+
),
|
|
726
|
+
'evidence get': {
|
|
727
|
+
positionals: ['evidenceId'],
|
|
728
|
+
flags: { out: 'value' },
|
|
729
|
+
required: ['out'],
|
|
730
|
+
run: (store, args) => {
|
|
731
|
+
const evidence = evidenceBytes(store, value(args, 'evidenceId')!)
|
|
732
|
+
const out = writeOutput(store, value(args, 'out')!, evidence.bytes)
|
|
733
|
+
const { bytes, ...data } = evidence
|
|
734
|
+
return completed(evidence.id, { ...data, out })
|
|
735
|
+
},
|
|
736
|
+
print: (reply) => ({ facts: { out: reply.data.out, size: reply.data.size, mediaType: reply.data.mediaType } }),
|
|
737
|
+
},
|
|
738
|
+
'export plan': exportCommand('reference', exportPlan),
|
|
739
|
+
'export run': exportCommand('runId', exportRun),
|
|
740
|
+
log: {
|
|
741
|
+
positionals: [],
|
|
742
|
+
flags: { subject: 'value', limit: 'value' },
|
|
743
|
+
run: (store, args) => {
|
|
744
|
+
const subject = value(args, 'subject')
|
|
745
|
+
requireCondition(
|
|
746
|
+
subject === undefined || subject === 'inbox' || /^(target|plan|run):./.test(subject),
|
|
747
|
+
`Invalid --subject: ${subject}. Use target:<targetId>, plan:<planId>, run:<runId>, or inbox.`
|
|
748
|
+
)
|
|
749
|
+
return completed('log', {
|
|
750
|
+
operations: operationLog(store, { subject, limit: integer(args, 'limit') }),
|
|
751
|
+
})
|
|
752
|
+
},
|
|
753
|
+
print: (reply) => ({
|
|
754
|
+
lines: (reply.data.operations as ReturnType<typeof operationLog>).map((operation) => [
|
|
755
|
+
operation.id,
|
|
756
|
+
operation.at,
|
|
757
|
+
operation.actor,
|
|
758
|
+
operation.command,
|
|
759
|
+
operation.status,
|
|
760
|
+
operation.subjects,
|
|
761
|
+
]),
|
|
762
|
+
}),
|
|
763
|
+
},
|
|
764
|
+
query: {
|
|
765
|
+
positionals: ['sql'],
|
|
766
|
+
readOnly: true,
|
|
767
|
+
run: (store, args) => {
|
|
768
|
+
const statement = store.db.prepare(value(args, 'sql')!)
|
|
769
|
+
const columns = statement.columns().map((column) => column.name)
|
|
770
|
+
const rows = statement.all().map((row) =>
|
|
771
|
+
Object.fromEntries(
|
|
772
|
+
columns.map((column) => {
|
|
773
|
+
const field = row[column]
|
|
774
|
+
return [column, field instanceof Uint8Array ? `<blob ${field.length} bytes>` : field]
|
|
775
|
+
})
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
return completed('query', { columns, rows })
|
|
779
|
+
},
|
|
780
|
+
print: (reply) => {
|
|
781
|
+
const columns = reply.data.columns as string[]
|
|
782
|
+
const rows = reply.data.rows as Array<Record<string, unknown>>
|
|
783
|
+
return {
|
|
784
|
+
facts: { rows: rows.length },
|
|
785
|
+
lines: [columns, ...rows.map((row) => columns.map((name) => row[name]))],
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function exportCommand(positional: string, render: (store: Store, subject: string, format: string) => string): Command {
|
|
792
|
+
return {
|
|
793
|
+
positionals: [positional],
|
|
794
|
+
flags: { format: 'value', out: 'value' },
|
|
795
|
+
required: ['format'],
|
|
796
|
+
run: (store, args) => {
|
|
797
|
+
const subject = value(args, positional)!
|
|
798
|
+
const format = value(args, 'format')!
|
|
799
|
+
const content = render(store, subject, format)
|
|
800
|
+
const out = value(args, 'out')
|
|
801
|
+
if (out === undefined) return completed(subject, { format, content })
|
|
802
|
+
return completed(subject, { format, out: writeOutput(store, out, content) })
|
|
803
|
+
},
|
|
804
|
+
print: (reply) => ({ facts: { out: reply.data.out } }),
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function verdictCommand(
|
|
809
|
+
positional: string,
|
|
810
|
+
decide: (
|
|
811
|
+
store: Store,
|
|
812
|
+
actor: string | undefined,
|
|
813
|
+
subject: string,
|
|
814
|
+
rationale: string,
|
|
815
|
+
expectSha256: string | undefined
|
|
816
|
+
) => Reply
|
|
817
|
+
): Command {
|
|
818
|
+
return {
|
|
819
|
+
positionals: [positional],
|
|
820
|
+
flags: { rationale: 'value', 'expect-sha': 'value' },
|
|
821
|
+
required: ['rationale'],
|
|
822
|
+
write: true,
|
|
823
|
+
run: (store, args, { actor }) =>
|
|
824
|
+
decide(store, actor, value(args, positional)!, value(args, 'rationale')!, value(args, 'expect-sha')),
|
|
825
|
+
print: (reply) => ({ facts: { verdict: reply.data.verdict, sha256: short(reply.data.sha256) } }),
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function showPlan(store: Store, args: Args) {
|
|
830
|
+
const { id, revision } = revisionReference(value(args, 'reference')!, 'plan reference')
|
|
831
|
+
const detail = planDetail(store, id, revision)
|
|
832
|
+
const subject = `${id}@${detail.revision}`
|
|
833
|
+
const tests = list(args, 'tests')
|
|
834
|
+
const fields = list(args, 'fields')
|
|
835
|
+
const gaps = requireChoice(value(args, 'gaps') ?? 'all', ['open', 'all'] as const, 'gaps filter (--gaps)')
|
|
836
|
+
if (!tests && !fields) {
|
|
837
|
+
const plan = {
|
|
838
|
+
...detail.plan,
|
|
839
|
+
gaps: detail.plan.gaps.filter((gap) => gaps === 'all' || gap.status === 'open'),
|
|
840
|
+
}
|
|
841
|
+
return completed(subject, { ...detail, plan })
|
|
842
|
+
}
|
|
843
|
+
for (const test of tests ?? [])
|
|
844
|
+
requireCondition(
|
|
845
|
+
detail.plan.tests.some((item) => item.id === test),
|
|
846
|
+
`Unknown test in ${subject}: ${test}`
|
|
847
|
+
)
|
|
848
|
+
for (const field of fields ?? [])
|
|
849
|
+
requireCondition(testFields.includes(field), `Unknown field: ${field}. Use: ${testFields.join(', ')}`)
|
|
850
|
+
const picked = detail.plan.tests
|
|
851
|
+
.filter((test) => !tests || tests.includes(test.id))
|
|
852
|
+
.map((test) =>
|
|
853
|
+
Object.fromEntries([
|
|
854
|
+
['id', test.id],
|
|
855
|
+
...(fields ?? ['readiness', 'capability', 'title']).map((field) => [
|
|
856
|
+
field,
|
|
857
|
+
test[field as keyof typeof test],
|
|
858
|
+
]),
|
|
859
|
+
])
|
|
860
|
+
)
|
|
861
|
+
return completed(subject, {
|
|
862
|
+
planId: id,
|
|
863
|
+
revision: detail.revision,
|
|
864
|
+
sha256: detail.sha256,
|
|
865
|
+
fields: fields ?? ['readiness', 'capability', 'title'],
|
|
866
|
+
tests: picked,
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function testLines(test: Plan['tests'][number]) {
|
|
871
|
+
return [
|
|
872
|
+
[test.id, test.readiness, test.capability, test.title],
|
|
873
|
+
[test.id, 'action', test.action],
|
|
874
|
+
...test.preconditions.map((precondition) => [test.id, 'precondition', precondition]),
|
|
875
|
+
...test.assertions.map((assertion) => [`${test.id}.${assertion.id}`, assertion.checkKind, assertion.check]),
|
|
876
|
+
]
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function printPlan(reply: Reply): Printed {
|
|
880
|
+
const facts = { sha256: short(reply.data.sha256) }
|
|
881
|
+
if (reply.data.fields) {
|
|
882
|
+
const fields = reply.data.fields as string[]
|
|
883
|
+
const tests = reply.data.tests as Array<Record<string, unknown>>
|
|
884
|
+
return {
|
|
885
|
+
facts,
|
|
886
|
+
lines: tests.flatMap((test) => [
|
|
887
|
+
[test.id, ...fields.filter((field) => field !== 'assertions').map((field) => test[field])],
|
|
888
|
+
...(fields.includes('assertions')
|
|
889
|
+
? (test.assertions as Plan['tests'][number]['assertions']).map((assertion) => [
|
|
890
|
+
`${test.id}.${assertion.id}`,
|
|
891
|
+
assertion.checkKind,
|
|
892
|
+
assertion.check,
|
|
893
|
+
])
|
|
894
|
+
: []),
|
|
895
|
+
]),
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const detail = reply.data as ReturnType<typeof planDetail>
|
|
899
|
+
const { plan } = detail
|
|
900
|
+
const coverage = new Map(plan.coverage.map((row) => [row.requirementId, row.status]))
|
|
901
|
+
return {
|
|
902
|
+
facts: {
|
|
903
|
+
...facts,
|
|
904
|
+
approval: detail.approval.state,
|
|
905
|
+
...(detail.latestRevision !== detail.revision ? { latest: detail.latestRevision } : {}),
|
|
906
|
+
},
|
|
907
|
+
lines: [
|
|
908
|
+
...plan.requirements.map((requirement) => [requirement.id, coverage.get(requirement.id), requirement.text]),
|
|
909
|
+
...plan.tests.map((test) => [test.id, test.readiness, test.capability, test.title]),
|
|
910
|
+
...plan.gaps.map((gap) => [gap.id, gap.severity, gap.status, gap.question]),
|
|
911
|
+
...plan.decisions.map((decision) => [decision.id, decision.gapId, decision.kind, decision.value]),
|
|
912
|
+
...detail.pendingDecisions.map((decision) => [
|
|
913
|
+
decision.id,
|
|
914
|
+
decision.gapId,
|
|
915
|
+
`pending ${decision.kind}`,
|
|
916
|
+
decision.value,
|
|
917
|
+
]),
|
|
918
|
+
],
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function writeOutput(store: Store, out: string, content: string | Uint8Array) {
|
|
923
|
+
const path = resolve(store.workspace, out)
|
|
924
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
925
|
+
try {
|
|
926
|
+
writeFileSync(path, content, { flag: 'wx' })
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (error instanceof Error && 'code' in error && error.code === 'EEXIST')
|
|
929
|
+
throw new Error(`Output file exists: ${out}. Choose a new path.`, { cause: error })
|
|
930
|
+
throw error
|
|
931
|
+
}
|
|
932
|
+
return out
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function cell(field: unknown): string {
|
|
936
|
+
if (field === null || field === undefined) return '-'
|
|
937
|
+
if (Array.isArray(field)) return field.length ? field.map(cell).join(',') : '-'
|
|
938
|
+
if (typeof field === 'object') return JSON.stringify(field)
|
|
939
|
+
return String(field).replace(/\\/g, '\\\\').replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n')
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function compact(reply: Reply, printed: Printed) {
|
|
943
|
+
const facts = Object.entries(printed.facts ?? {})
|
|
944
|
+
.filter(([, field]) => field !== undefined)
|
|
945
|
+
.map(([key, field]) => `${key}=${cell(field)}`)
|
|
946
|
+
return [
|
|
947
|
+
[reply.status, reply.subject, ...facts].join(' '),
|
|
948
|
+
...(printed.lines ?? []).map((line) => line.map(cell).join('\t')),
|
|
949
|
+
...reply.reasons.map((reason) => `reason: ${cell(reason)}`),
|
|
950
|
+
].join('\n')
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function format(reply: Reply, json: boolean, print?: Command['print']) {
|
|
954
|
+
if (json)
|
|
955
|
+
return JSON.stringify({ status: reply.status, subject: reply.subject, reasons: reply.reasons, ...reply.data })
|
|
956
|
+
return compact(reply, print ? print(reply) : {})
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const globalFlags: Record<string, FlagKind> = { workspace: 'value', json: 'switch' }
|
|
960
|
+
|
|
961
|
+
function commandName(argv: string[]) {
|
|
962
|
+
const words = argv.slice(0, 2).filter((word) => !word.startsWith('--'))
|
|
963
|
+
if (words.length === 2 && commands[words.join(' ')]) return words.join(' ')
|
|
964
|
+
if (commands[words[0]]) return words[0]
|
|
965
|
+
throw new Error(`Unknown command: ${words.join(' ') || '(none)'}. Run checkmate --help.`)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function parse(name: string, tokens: string[]) {
|
|
969
|
+
const command = commands[name]
|
|
970
|
+
const flags: Record<string, FlagKind> = {
|
|
971
|
+
...globalFlags,
|
|
972
|
+
...command.flags,
|
|
973
|
+
...(command.write ? { actor: 'value' as const } : {}),
|
|
974
|
+
}
|
|
975
|
+
const args: Args = {}
|
|
976
|
+
const positionals: string[] = []
|
|
977
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
978
|
+
const token = tokens[i]
|
|
979
|
+
if (!token.startsWith('--')) {
|
|
980
|
+
positionals.push(token)
|
|
981
|
+
continue
|
|
982
|
+
}
|
|
983
|
+
const flag = token.slice(2)
|
|
984
|
+
const kind = flags[flag]
|
|
985
|
+
if (!kind) throw new Error(`Unknown option ${token} for ${name}. ${usage(name)}`)
|
|
986
|
+
if (kind === 'switch') {
|
|
987
|
+
args[flag] = true
|
|
988
|
+
continue
|
|
989
|
+
}
|
|
990
|
+
const next = tokens[++i]
|
|
991
|
+
if (next === undefined || next.startsWith('--')) throw new Error(`Missing value for ${token}`)
|
|
992
|
+
if (kind === 'value') {
|
|
993
|
+
if (flag in args) throw new Error(`Option ${token} is given twice`)
|
|
994
|
+
args[flag] = next
|
|
995
|
+
} else if (kind === 'list') {
|
|
996
|
+
if (flag in args) throw new Error(`Option ${token} is given twice`)
|
|
997
|
+
args[flag] = next
|
|
998
|
+
.split(',')
|
|
999
|
+
.map((item) => item.trim())
|
|
1000
|
+
.filter(Boolean)
|
|
1001
|
+
} else args[flag] = [...((args[flag] as string[] | undefined) ?? []), next]
|
|
1002
|
+
}
|
|
1003
|
+
if (positionals.length !== command.positionals.length) throw new Error(usage(name))
|
|
1004
|
+
for (const flag of command.required ?? []) if (!(flag in args)) throw new Error(`Missing --${flag}. ${usage(name)}`)
|
|
1005
|
+
command.positionals.forEach((positional, i) => (args[positional] = positionals[i]))
|
|
1006
|
+
return args
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function usage(name: string) {
|
|
1010
|
+
const command = commands[name]
|
|
1011
|
+
const flags = Object.entries(command.flags ?? {}).map(([flag, kind]) => {
|
|
1012
|
+
const text = kind === 'switch' ? `--${flag}` : `--${flag} <${kind === 'list' ? 'a,b' : 'value'}>`
|
|
1013
|
+
return command.required?.includes(flag) ? text : `[${text}]`
|
|
1014
|
+
})
|
|
1015
|
+
return [
|
|
1016
|
+
'Usage: checkmate',
|
|
1017
|
+
name,
|
|
1018
|
+
...command.positionals.map((positional) => `<${positional}>`),
|
|
1019
|
+
...flags,
|
|
1020
|
+
...(command.write ? ['[--actor <name>]'] : []),
|
|
1021
|
+
...(command.stdin ? ['< payload.json'] : []),
|
|
1022
|
+
].join(' ')
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const uiUsage = 'Usage: checkmate ui [--port <value>]'
|
|
1026
|
+
|
|
1027
|
+
function help() {
|
|
1028
|
+
return [
|
|
1029
|
+
...Object.keys(commands).map((name) => usage(name).replace('Usage: ', '')),
|
|
1030
|
+
uiUsage.replace('Usage: ', ''),
|
|
1031
|
+
'Options for every command: --workspace <path>, --json',
|
|
1032
|
+
].join('\n')
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function version() {
|
|
1036
|
+
return JSON.parse(readFileSync(new URL('../.claude-plugin/plugin.json', import.meta.url), 'utf8')).version as string
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function readJson(text: string) {
|
|
1040
|
+
requireCondition(text.trim().length > 0, 'Missing JSON payload on standard input')
|
|
1041
|
+
try {
|
|
1042
|
+
return JSON.parse(text) as unknown
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
throw new Error(`Standard input is not valid JSON: ${(error as Error).message}`, { cause: error })
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export function run(argv: string[], io: Io): Outcome {
|
|
1049
|
+
if (argv[0] === '--version') return { code: 0, output: version() }
|
|
1050
|
+
if (!argv.length || argv[0] === '--help') return { code: 0, output: help() }
|
|
1051
|
+
const json = argv.includes('--json')
|
|
1052
|
+
let name = 'checkmate'
|
|
1053
|
+
let args: Args
|
|
1054
|
+
try {
|
|
1055
|
+
name = commandName(argv)
|
|
1056
|
+
args = parse(name, argv.slice(name.split(' ').length))
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
const reply: Reply = { status: 'invalid_request', subject: name, reasons: [(error as Error).message], data: {} }
|
|
1059
|
+
return { code: 1, output: format(reply, json) }
|
|
1060
|
+
}
|
|
1061
|
+
const command = commands[name]
|
|
1062
|
+
const subject = command.positionals.length && name !== 'query' ? String(args[command.positionals[0]]) : name
|
|
1063
|
+
const workspace = resolve(io.cwd, value(args, 'workspace') ?? '.')
|
|
1064
|
+
let store: Store | undefined
|
|
1065
|
+
try {
|
|
1066
|
+
store = command.readOnly ? openReadOnlyStore(workspace) : openStore(workspace)
|
|
1067
|
+
const actor = command.write ? (value(args, 'actor') ?? defaultActor(workspace, io.env)) : undefined
|
|
1068
|
+
const reply = command.run(store, args, { actor, stdin: () => readJson(io.stdin()), text: io.stdin })
|
|
1069
|
+
const code = reply.status === 'completed' || reply.status === 'needs_decision' ? 0 : 1
|
|
1070
|
+
if (!json && typeof reply.data.content === 'string')
|
|
1071
|
+
return { code, output: reply.data.content.replace(/\n$/, '') }
|
|
1072
|
+
return { code, output: format(reply, json, command.print) }
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
const reply: Reply = { status: 'rejected', subject, reasons: [(error as Error).message], data: {} }
|
|
1075
|
+
return { code: 1, output: format(reply, json) }
|
|
1076
|
+
} finally {
|
|
1077
|
+
if (store) closeStore(store)
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function uiOptions(tokens: string[], cwd: string) {
|
|
1082
|
+
const values: Record<string, string> = {}
|
|
1083
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1084
|
+
const token = tokens[i]
|
|
1085
|
+
if (token === '--json') continue
|
|
1086
|
+
const flag = token.startsWith('--') ? token.slice(2) : ''
|
|
1087
|
+
if (!['workspace', 'port'].includes(flag)) throw new Error(`Unknown argument ${token} for ui. ${uiUsage}`)
|
|
1088
|
+
const next = tokens[++i]
|
|
1089
|
+
if (next === undefined || next.startsWith('--')) throw new Error(`Missing value for ${token}`)
|
|
1090
|
+
if (flag in values) throw new Error(`Option ${token} is given twice`)
|
|
1091
|
+
values[flag] = next
|
|
1092
|
+
}
|
|
1093
|
+
const port = values.port === undefined ? undefined : Number(values.port)
|
|
1094
|
+
requireCondition(
|
|
1095
|
+
port === undefined || (/^[0-9]+$/.test(values.port) && port <= 65535),
|
|
1096
|
+
`Invalid --port: ${values.port}. Use 0 to 65535.`
|
|
1097
|
+
)
|
|
1098
|
+
return { workspace: resolve(cwd, values.workspace ?? '.'), port }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export async function serveUi(tokens: string[]) {
|
|
1102
|
+
const json = tokens.includes('--json')
|
|
1103
|
+
let options: ReturnType<typeof uiOptions>
|
|
1104
|
+
try {
|
|
1105
|
+
options = uiOptions(tokens, process.cwd())
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
const reply: Reply = { status: 'invalid_request', subject: 'ui', reasons: [(error as Error).message], data: {} }
|
|
1108
|
+
process.stdout.write(format(reply, json) + '\n')
|
|
1109
|
+
process.exitCode = 1
|
|
1110
|
+
return
|
|
1111
|
+
}
|
|
1112
|
+
const ui = await startUi(options)
|
|
1113
|
+
process.stdout.write(
|
|
1114
|
+
format(completed('ui', { url: ui.url }), json, (reply) => ({ facts: { url: reply.data.url } })) + '\n'
|
|
1115
|
+
)
|
|
1116
|
+
const stop = async () => {
|
|
1117
|
+
await ui.close()
|
|
1118
|
+
process.exit(0)
|
|
1119
|
+
}
|
|
1120
|
+
process.once('SIGINT', stop)
|
|
1121
|
+
process.once('SIGTERM', stop)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export async function main(argv: string[]) {
|
|
1125
|
+
if (argv[0] === 'ui') return serveUi(argv.slice(1))
|
|
1126
|
+
const outcome = run(argv, {
|
|
1127
|
+
cwd: process.cwd(),
|
|
1128
|
+
env: process.env,
|
|
1129
|
+
stdin: () => readFileSync(0, 'utf8'),
|
|
1130
|
+
})
|
|
1131
|
+
process.stdout.write(outcome.output + '\n')
|
|
1132
|
+
process.exitCode = outcome.code
|
|
1133
|
+
}
|