@gajae-code/coding-agent 0.14.2 → 0.15.0
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/CHANGELOG.md +120 -1
- package/dist/types/async/job-manager.d.ts +4 -1
- package/dist/types/autoresearch/dashboard.d.ts +29 -4
- package/dist/types/autoresearch/data-context.d.ts +25 -0
- package/dist/types/autoresearch/git.d.ts +65 -7
- package/dist/types/autoresearch/harness.d.ts +58 -0
- package/dist/types/autoresearch/prompts.d.ts +51 -0
- package/dist/types/autoresearch/runs.d.ts +173 -0
- package/dist/types/autoresearch/types.d.ts +9 -139
- package/dist/types/capability/types.d.ts +20 -0
- package/dist/types/cli/accounts-cli.d.ts +1 -0
- package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
- package/dist/types/config/autorouting-contract.d.ts +146 -0
- package/dist/types/config/autorouting-generator.d.ts +40 -0
- package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
- package/dist/types/config/autorouting.d.ts +35 -0
- package/dist/types/config/model-discovery-manager.d.ts +8 -1
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/provider-selection-policy.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/durability.d.ts +29 -0
- package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +14 -0
- package/dist/types/crash/index-store.d.ts +3 -0
- package/dist/types/crash/record-loader.d.ts +2 -0
- package/dist/types/crash/upstream/relay.d.ts +5 -3
- package/dist/types/cursor.d.ts +12 -0
- package/dist/types/defaults/gjc-defaults.d.ts +35 -1
- package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
- package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
- package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
- package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
- package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
- package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
- package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
- package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
- package/dist/types/hooks/skill-keywords.d.ts +1 -1
- package/dist/types/main.d.ts +0 -10
- package/dist/types/modes/acp/acp-agent.d.ts +13 -6
- package/dist/types/modes/components/model-selector.d.ts +14 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
- package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +9 -0
- package/dist/types/rlm/artifacts.d.ts +4 -4
- package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
- package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
- package/dist/types/runtime-mcp/config.d.ts +6 -0
- package/dist/types/runtime-mcp/manager.d.ts +10 -0
- package/dist/types/sdk/acp/adapter.d.ts +0 -4
- package/dist/types/sdk/broker/broker.d.ts +1 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
- package/dist/types/sdk/broker/session-index.d.ts +15 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +8 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
- package/dist/types/sdk/client/client.d.ts +39 -4
- package/dist/types/sdk/host/host.d.ts +1 -0
- package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
- package/dist/types/sdk/host/model-pin.d.ts +29 -0
- package/dist/types/sdk/host/session-runtime.d.ts +2 -1
- package/dist/types/sdk/lifecycle-session.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +27 -1
- package/dist/types/sdk/session.d.ts +11 -1
- package/dist/types/session/agent-session.d.ts +36 -6
- package/dist/types/session/artifacts.d.ts +16 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/session-manager.d.ts +84 -2
- package/dist/types/session/session-staging-paths.d.ts +3 -0
- package/dist/types/skill-state/canonical-skills.d.ts +1 -1
- package/dist/types/skill-state/workflow-hud.d.ts +12 -21
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
- package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +23 -0
- package/dist/types/task/executor.d.ts +35 -4
- package/dist/types/task/index.d.ts +17 -3
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +144 -0
- package/dist/types/tools/atomic-file-write.d.ts +72 -0
- package/dist/types/tools/descriptors.d.ts +1 -0
- package/dist/types/tools/index.d.ts +36 -1
- package/dist/types/tools/move-session.d.ts +40 -0
- package/dist/types/tools/python.d.ts +12 -0
- package/dist/types/tools/resource-gc.d.ts +0 -3
- package/dist/types/tools/search.d.ts +8 -1
- package/dist/types/tools/skill.d.ts +9 -4
- package/dist/types/tools/subagent.d.ts +33 -3
- package/dist/types/utils/xml-escape.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
- package/package.json +11 -12
- package/scripts/build-sdk-package-smoke.ts +7 -5
- package/scripts/check-autorouting-tier-map.ts +183 -0
- package/scripts/generate-sdk-operation-inventory.ts +12 -0
- package/scripts/generate-tool-catalog.ts +40 -5
- package/src/async/job-manager.ts +5 -2
- package/src/autoresearch/dashboard.ts +134 -314
- package/src/autoresearch/data-context.ts +35 -0
- package/src/autoresearch/git.ts +186 -26
- package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
- package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
- package/src/autoresearch/prompts/prompt.md +103 -0
- package/src/autoresearch/prompts.ts +126 -0
- package/src/autoresearch/runs.ts +708 -0
- package/src/autoresearch/types.ts +9 -150
- package/src/capability/index.ts +4 -4
- package/src/capability/types.ts +20 -0
- package/src/cli/accounts-cli.ts +21 -5
- package/src/cli/config-cli.ts +25 -1
- package/src/cli/fast-help.ts +1 -2
- package/src/cli/list-models.ts +5 -8
- package/src/cli/setup-cli.ts +8 -0
- package/src/cli.ts +2 -21
- package/src/commands/autoresearch.ts +33 -0
- package/src/commands/coordinator.ts +15 -0
- package/src/commands/launch.ts +2 -4
- package/src/commands/quick-lane.ts +1 -1
- package/src/commands/sdk.ts +24 -0
- package/src/commands/state.ts +1 -1
- package/src/config/autorouting-contract.ts +474 -0
- package/src/config/autorouting-generator.ts +238 -0
- package/src/config/autorouting-tier-map.ts +6108 -0
- package/src/config/autorouting.ts +120 -0
- package/src/config/model-discovery-manager.ts +42 -11
- package/src/config/model-registry.ts +348 -68
- package/src/config/provider-selection-policy.ts +60 -9
- package/src/config/settings-schema.ts +195 -6
- package/src/config/settings.ts +62 -1
- package/src/config.ts +2 -3
- package/src/coordinator/contract.ts +0 -1
- package/src/coordinator-mcp/codex-handoff.ts +68 -42
- package/src/coordinator-mcp/durability.ts +200 -0
- package/src/coordinator-mcp/event-webhook.ts +278 -0
- package/src/coordinator-mcp/question-state.ts +109 -46
- package/src/coordinator-mcp/server.ts +932 -504
- package/src/crash/index-store.ts +126 -22
- package/src/crash/record-loader.ts +7 -0
- package/src/crash/upstream/relay.ts +82 -34
- package/src/cursor.ts +84 -1
- package/src/debug/index.ts +26 -0
- package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
- package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
- package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
- package/src/defaults/gjc-defaults.ts +89 -2
- package/src/defaults/gjc-skills.generated.ts +24 -17
- package/src/discovery/builtin.ts +18 -5
- package/src/discovery/helpers.ts +6 -6
- package/src/edit/index.ts +5 -2
- package/src/eval/py/executor.ts +8 -0
- package/src/extensibility/extensions/loader.ts +135 -13
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/gjc-plugins/schema.ts +1 -1
- package/src/extensibility/runtime-skill-discovery.ts +2 -2
- package/src/extensibility/skill-management.ts +2 -3
- package/src/extensibility/skills.ts +16 -8
- package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
- package/src/gjc-runtime/gc-render.ts +0 -1
- package/src/gjc-runtime/gc-runtime.ts +1 -11
- package/src/gjc-runtime/launch-tmux.ts +3 -3
- package/src/gjc-runtime/psmux-detect.ts +2 -2
- package/src/gjc-runtime/python-transcript.ts +58 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -18
- package/src/gjc-runtime/session-layout.ts +18 -7
- package/src/gjc-runtime/session-state-sidecar.ts +58 -9
- package/src/gjc-runtime/state-renderer.ts +0 -38
- package/src/gjc-runtime/state-runtime.ts +43 -22
- package/src/gjc-runtime/state-schema.ts +1 -1
- package/src/gjc-runtime/tmux-common.ts +7 -7
- package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
- package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
- package/src/gjc-runtime/workflow-command-ref.ts +15 -11
- package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
- package/src/gjc-runtime/workflow-manifest.ts +43 -83
- package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
- package/src/hooks/skill-keywords.ts +3 -3
- package/src/hooks/skill-state.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +26 -24
- package/src/lsp/index.ts +25 -5
- package/src/main.ts +2 -27
- package/src/modes/acp/acp-agent.ts +200 -75
- package/src/modes/components/model-selector.ts +216 -13
- package/src/modes/components/oauth-selector.ts +5 -5
- package/src/modes/components/skill-hud/render.ts +173 -35
- package/src/modes/components/smart-routing-panel.ts +531 -0
- package/src/modes/components/tool-status-header.ts +6 -3
- package/src/modes/controllers/command-controller.ts +17 -7
- package/src/modes/controllers/input-controller.ts +44 -9
- package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +298 -65
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
- package/src/modes/types.ts +9 -1
- package/src/prompts/agents/planner.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/volatile-project-context.md +2 -1
- package/src/prompts/tools/move-session.md +15 -0
- package/src/prompts/tools/python.md +12 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/prompts/tools/skill.md +2 -2
- package/src/prompts/tools/task-summary.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/rlm/artifacts.ts +12 -10
- package/src/runtime/optional-runtime-services.ts +3 -2
- package/src/runtime/workspace-tree-service.ts +11 -3
- package/src/runtime-mcp/config.ts +8 -1
- package/src/runtime-mcp/manager.ts +33 -0
- package/src/sdk/acp/adapter.ts +48 -1
- package/src/sdk/broker/broker.ts +30 -2
- package/src/sdk/broker/discovery.ts +217 -2
- package/src/sdk/broker/lifecycle.ts +385 -59
- package/src/sdk/broker/session-index.ts +243 -48
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
- package/src/sdk/bus/index.ts +208 -11
- package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
- package/src/sdk/bus/reconciliation-store.ts +27 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
- package/src/sdk/client/client.ts +204 -22
- package/src/sdk/host/host.ts +16 -0
- package/src/sdk/host/internal-autorouting-state.ts +13 -0
- package/src/sdk/host/model-pin.ts +101 -0
- package/src/sdk/host/reverse-leases.ts +2 -1
- package/src/sdk/host/session-runtime.test.ts +224 -1
- package/src/sdk/host/session-runtime.ts +125 -37
- package/src/sdk/lifecycle/service.ts +3 -1
- package/src/sdk/lifecycle-session.ts +42 -1
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +375 -36
- package/src/sdk/session.ts +547 -47
- package/src/session/account-inventory.ts +1 -1
- package/src/session/agent-session.ts +765 -189
- package/src/session/artifacts.ts +234 -4
- package/src/session/internal/managed-session-storage.ts +91 -4
- package/src/session/provider-safety-stop-hint.ts +3 -2
- package/src/session/session-manager.ts +775 -16
- package/src/session/session-staging-paths.ts +8 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
- package/src/setup/host-plugin-setup.ts +2 -2
- package/src/skill-state/active-state.ts +2 -2
- package/src/skill-state/canonical-skills.ts +1 -1
- package/src/skill-state/initial-phase.ts +1 -1
- package/src/skill-state/workflow-hud.ts +41 -41
- package/src/skill-state/workflow-mutation-guard.ts +26 -7
- package/src/slash-commands/builtin-registry.ts +77 -0
- package/src/slash-commands/helpers/autorouting-status.ts +55 -0
- package/src/system-prompt.ts +102 -5
- package/src/task/discovery.ts +2 -3
- package/src/task/executor.ts +617 -43
- package/src/task/index.ts +262 -15
- package/src/task/receipt.ts +30 -0
- package/src/task/render.ts +11 -1
- package/src/task/types.ts +316 -0
- package/src/tools/atomic-file-write.ts +594 -0
- package/src/tools/bisect.ts +3 -2
- package/src/tools/descriptors.test.ts +3 -0
- package/src/tools/descriptors.ts +24 -0
- package/src/tools/image-gen.ts +5 -1
- package/src/tools/index.ts +62 -5
- package/src/tools/move-session.ts +107 -0
- package/src/tools/python.ts +205 -0
- package/src/tools/read.ts +88 -15
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resource-gc.ts +8 -177
- package/src/tools/search.ts +30 -6
- package/src/tools/skill-discovery.ts +1 -0
- package/src/tools/skill.ts +37 -7
- package/src/tools/subagent-render.ts +119 -13
- package/src/tools/subagent.ts +91 -74
- package/src/tools/tool-catalog.generated.ts +64 -2
- package/src/tools/write.ts +16 -12
- package/src/utils/xml-escape.ts +14 -0
- package/src/web/search/providers/codex.ts +2 -2
- package/src/workflow/workflow-intent-diff.ts +13 -12
- package/src/workflow/workflow-intent-report-consumer.ts +6 -6
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
- package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
- package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
- package/dist/types/gjc-runtime/team-store.d.ts +0 -168
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
- package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/src/autoresearch/command-resume.md +0 -14
- package/src/autoresearch/index.ts +0 -536
- package/src/autoresearch/prompt.md +0 -103
- package/src/autoresearch/resume-message.md +0 -10
- package/src/autoresearch/state.ts +0 -273
- package/src/autoresearch/storage.ts +0 -699
- package/src/autoresearch/tools/init-experiment.ts +0 -272
- package/src/autoresearch/tools/log-experiment.ts +0 -524
- package/src/autoresearch/tools/run-experiment.ts +0 -475
- package/src/autoresearch/tools/update-notes.ts +0 -109
- package/src/commands/rlm.ts +0 -19
- package/src/commands/team.ts +0 -238
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
- package/src/defaults/gjc/skills/team/SKILL.md +0 -467
- package/src/gjc-runtime/team-gc.ts +0 -49
- package/src/gjc-runtime/team-launch.ts +0 -404
- package/src/gjc-runtime/team-notify.ts +0 -326
- package/src/gjc-runtime/team-runtime.ts +0 -5923
- package/src/gjc-runtime/team-store.ts +0 -878
- package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
- package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
- package/src/gjc-runtime/team-workers.ts +0 -1123
- package/src/prompts/system/rlm-report-command.md +0 -1
- package/src/prompts/system/rlm-research.md +0 -23
- package/src/rlm/index.ts +0 -365
- package/src/rlm/preset.ts +0 -76
- package/src/rlm/python-tool.ts +0 -68
|
@@ -0,0 +1,1509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native implementation of `gjc autoresearch` — the session-scoped mission
|
|
3
|
+
* runtime for the autoresearch workflow skill.
|
|
4
|
+
*
|
|
5
|
+
* Everything the runtime persists lives under
|
|
6
|
+
* `<cwd>/.gjc/_session-{gjcSessionId}/autoresearch/` (`sessionAutoresearchDir`):
|
|
7
|
+
* the mission artifact (`mission.json`) and the append-only ledger
|
|
8
|
+
* (`ledger.jsonl`). The legacy global autoresearch store is intentionally
|
|
9
|
+
* dead: no code path in this module writes there, and every mutation routes
|
|
10
|
+
* through the sanctioned `.gjc/**` state-writer primitives, which structurally
|
|
11
|
+
* refuse targets outside the project `.gjc/` tree.
|
|
12
|
+
*
|
|
13
|
+
* Intake contract (AC-14..AC-16): two entrypoints write the one mission
|
|
14
|
+
* artifact. Spec intake (`intake --spec <path>`, or the bare `--spec` flag) consumes a persisted deep-interview
|
|
15
|
+
* spec and asks zero clarification questions; cold intake (positional goal or
|
|
16
|
+
* bare invocation) signals that goal/constraints/deliverables clarification must
|
|
17
|
+
* run before research begins and writes nothing. The mission mode is one of
|
|
18
|
+
* `web` / `mixed` / `data` and is always supplied explicitly — the write
|
|
19
|
+
* boundary REJECTS a missing or invalid mode and never infers one from the
|
|
20
|
+
* presence of a data file.
|
|
21
|
+
*
|
|
22
|
+
* The ledger is append-only and carries the six event kinds: `mission_created`,
|
|
23
|
+
* `mode_set`, `run_logged`, `verdict_issued`, `critic_recorded`,
|
|
24
|
+
* `mission_cleared`. A mission completes only on a structured best-effort
|
|
25
|
+
* verdict (status, evidence, caveats, evaluator); an optional critic pass
|
|
26
|
+
* records a `critic_receipt` whose evaluator identity is distinct from the
|
|
27
|
+
* mission agent.
|
|
28
|
+
*/
|
|
29
|
+
import * as crypto from "node:crypto";
|
|
30
|
+
import * as fs from "node:fs/promises";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
import { type AutoresearchDashboardInput, renderExpandedDashboard } from "../autoresearch/dashboard";
|
|
33
|
+
import { loadAutoresearchDataContext, type RlmDataContext } from "../autoresearch/data-context";
|
|
34
|
+
import { type EnsureAutoresearchBranchResult, ensureAutoresearchBranch } from "../autoresearch/git";
|
|
35
|
+
import { type ParsedHarnessOutput, parseHarnessOutput } from "../autoresearch/harness";
|
|
36
|
+
import { renderAutoresearchIteratePrompt, renderAutoresearchSetupPrompt } from "../autoresearch/prompts";
|
|
37
|
+
import {
|
|
38
|
+
AutoresearchRunsStore,
|
|
39
|
+
buildAutoresearchExperimentState,
|
|
40
|
+
createAutoresearchExperimentConfig,
|
|
41
|
+
type MetricDirection,
|
|
42
|
+
} from "../autoresearch/runs";
|
|
43
|
+
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
44
|
+
import { buildAutoresearchHudSummary } from "../skill-state/workflow-hud";
|
|
45
|
+
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
46
|
+
import { sessionAutoresearchDir } from "./session-layout";
|
|
47
|
+
import {
|
|
48
|
+
resolveGjcSessionForRead,
|
|
49
|
+
resolveGjcSessionForWrite,
|
|
50
|
+
SessionResolutionError,
|
|
51
|
+
writeSessionActivityMarker,
|
|
52
|
+
} from "./session-resolution";
|
|
53
|
+
import {
|
|
54
|
+
appendJsonl,
|
|
55
|
+
persistedStateRevision,
|
|
56
|
+
readExistingStateForMutation,
|
|
57
|
+
writeGuardedJsonAtomic,
|
|
58
|
+
} from "./state-writer";
|
|
59
|
+
import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
|
|
60
|
+
|
|
61
|
+
export type AutoresearchMode = "web" | "mixed" | "data";
|
|
62
|
+
export type AutoresearchIntakeKind = "handoff" | "cold";
|
|
63
|
+
|
|
64
|
+
export const AUTORESEARCH_MODES = new Set<AutoresearchMode>(["web", "mixed", "data"]);
|
|
65
|
+
|
|
66
|
+
export const AUTORESEARCH_LEDGER_EVENT_KINDS = [
|
|
67
|
+
"mission_created",
|
|
68
|
+
"mode_set",
|
|
69
|
+
"run_logged",
|
|
70
|
+
"verdict_issued",
|
|
71
|
+
"critic_recorded",
|
|
72
|
+
"mission_cleared",
|
|
73
|
+
] as const;
|
|
74
|
+
export type AutoresearchLedgerEventKind = (typeof AUTORESEARCH_LEDGER_EVENT_KINDS)[number];
|
|
75
|
+
|
|
76
|
+
/** The one mission artifact both intake entrypoints write. */
|
|
77
|
+
export interface AutoresearchMission {
|
|
78
|
+
objective: string;
|
|
79
|
+
mode: AutoresearchMode;
|
|
80
|
+
deliverables: string[];
|
|
81
|
+
constraints: string[];
|
|
82
|
+
slug: string;
|
|
83
|
+
intake: AutoresearchIntakeKind;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
updatedAt: string;
|
|
86
|
+
/** Absolute path of the deep-interview spec consumed by spec intake. */
|
|
87
|
+
specPath?: string;
|
|
88
|
+
handedOffAt?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Primary metric contract for the mission's harness.
|
|
91
|
+
*
|
|
92
|
+
* Optional: a mission that declares none keeps the historical default
|
|
93
|
+
* (`metric`, lower-is-better). When declared it MUST reach the experiment
|
|
94
|
+
* config -- otherwise a mission whose research contract is
|
|
95
|
+
* higher-is-better silently optimizes the wrong direction.
|
|
96
|
+
*/
|
|
97
|
+
primaryMetric?: string;
|
|
98
|
+
metricUnit?: string;
|
|
99
|
+
metricDirection?: MetricDirection;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AutoresearchPaths {
|
|
103
|
+
dir: string;
|
|
104
|
+
missionPath: string;
|
|
105
|
+
ledgerPath: string;
|
|
106
|
+
/** Quarantine root for retired missions: `<dir>/retired/<slug>.<timestamp>/`. */
|
|
107
|
+
retiredRoot: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Append-only session ledger row. */
|
|
111
|
+
export interface AutoresearchLedgerEvent {
|
|
112
|
+
eventId: string;
|
|
113
|
+
event: AutoresearchLedgerEventKind;
|
|
114
|
+
timestamp: string;
|
|
115
|
+
[field: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Optional per-mission critic pass receipt; evaluator is distinct from the mission agent. */
|
|
119
|
+
export interface AutoresearchCriticReceipt {
|
|
120
|
+
criticId: string;
|
|
121
|
+
status: Record<string, unknown>;
|
|
122
|
+
evidence: string[];
|
|
123
|
+
caveats: string[];
|
|
124
|
+
evaluator: string;
|
|
125
|
+
recordedAt: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Structured mission verdict: status is open data, not a pinned enum — terminality is deliberately deferred. */
|
|
129
|
+
export interface AutoresearchVerdictReceipt {
|
|
130
|
+
receiptId: string;
|
|
131
|
+
status: Record<string, unknown>;
|
|
132
|
+
evidence: string[];
|
|
133
|
+
caveats: string[];
|
|
134
|
+
evaluator: string;
|
|
135
|
+
issuedAt: string;
|
|
136
|
+
criticReceipt?: AutoresearchCriticReceipt;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface AutoresearchCommandResult {
|
|
140
|
+
status: number;
|
|
141
|
+
stdout?: string;
|
|
142
|
+
stderr?: string;
|
|
143
|
+
intake?: AutoresearchIntakeKind;
|
|
144
|
+
missionCreated?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface AutoresearchReadReceipt {
|
|
148
|
+
ok: true;
|
|
149
|
+
exists: boolean;
|
|
150
|
+
mission?: AutoresearchMission;
|
|
151
|
+
ledger: AutoresearchLedgerEvent[];
|
|
152
|
+
paths: AutoresearchPaths;
|
|
153
|
+
/**
|
|
154
|
+
* GJC session id the mission state was read from. Callers deriving
|
|
155
|
+
* session-scoped identity (notably the kernel owner id) MUST use this
|
|
156
|
+
* instead of re-resolving, so every path agrees on one scope.
|
|
157
|
+
*/
|
|
158
|
+
sessionId: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface AutoresearchWriteReceipt {
|
|
162
|
+
ok: true;
|
|
163
|
+
mission: AutoresearchMission;
|
|
164
|
+
missionPath: string;
|
|
165
|
+
intake: AutoresearchIntakeKind;
|
|
166
|
+
ledgerEvent?: AutoresearchLedgerEvent;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface AutoresearchClearReceipt {
|
|
170
|
+
ok: true;
|
|
171
|
+
cleared: boolean;
|
|
172
|
+
missionPath: string;
|
|
173
|
+
ledgerEvent: AutoresearchLedgerEvent;
|
|
174
|
+
/** Where the retired mission's artifacts were quarantined, when one existed. */
|
|
175
|
+
retiredTo?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface AutoresearchIntakeReceipt extends AutoresearchWriteReceipt {
|
|
179
|
+
specPath: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class AutoresearchCommandError extends CommandError {
|
|
183
|
+
constructor(exitStatus: number, message: string) {
|
|
184
|
+
super(exitStatus, message);
|
|
185
|
+
this.name = "AutoresearchCommandError";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** AC-16 write-boundary gate: mode must be explicitly supplied and valid. */
|
|
190
|
+
function assertAutoresearchMode(value: unknown, source: string): asserts value is AutoresearchMode {
|
|
191
|
+
if (typeof value !== "string" || !AUTORESEARCH_MODES.has(value as AutoresearchMode)) {
|
|
192
|
+
throw new AutoresearchCommandError(
|
|
193
|
+
2,
|
|
194
|
+
`autoresearch mission mode must be one of web, mixed, or data (${source}); received ${JSON.stringify(value)}. ` +
|
|
195
|
+
"Mode is never inferred from the presence of a data file — declare it explicitly at intake.",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertStructuredStatus(status: unknown, source: string): asserts status is Record<string, unknown> {
|
|
201
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) {
|
|
202
|
+
throw new AutoresearchCommandError(
|
|
203
|
+
2,
|
|
204
|
+
`autoresearch ${source} must be a structured object; received ${JSON.stringify(status)}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function requireStringArray(value: unknown, fieldName: string): string[] {
|
|
210
|
+
if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
|
|
211
|
+
throw new AutoresearchCommandError(2, `autoresearch ${fieldName} must be a string array`);
|
|
212
|
+
}
|
|
213
|
+
return dedupeStrings(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function dedupeStrings(values: readonly string[]): string[] {
|
|
217
|
+
const out: string[] = [];
|
|
218
|
+
const seen = new Set<string>();
|
|
219
|
+
for (const value of values) {
|
|
220
|
+
const trimmed = value.trim();
|
|
221
|
+
if (trimmed.length === 0 || seen.has(trimmed)) continue;
|
|
222
|
+
seen.add(trimmed);
|
|
223
|
+
out.push(trimmed);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isEnoent(error: unknown): boolean {
|
|
229
|
+
return (
|
|
230
|
+
typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ENOENT"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function getAutoresearchPaths(cwd: string, sessionId?: string | null): AutoresearchPaths {
|
|
235
|
+
const resolvedSessionId =
|
|
236
|
+
sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
237
|
+
const dir = sessionAutoresearchDir(cwd, resolvedSessionId);
|
|
238
|
+
return {
|
|
239
|
+
dir,
|
|
240
|
+
missionPath: path.join(dir, "mission.json"),
|
|
241
|
+
ledgerPath: path.join(dir, "ledger.jsonl"),
|
|
242
|
+
retiredRoot: path.join(dir, "retired"),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeAutoresearchMission(value: unknown): AutoresearchMission {
|
|
247
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
248
|
+
throw new AutoresearchCommandError(2, "autoresearch mission.json must contain a JSON object");
|
|
249
|
+
}
|
|
250
|
+
const record = value as Record<string, unknown>;
|
|
251
|
+
const objective = typeof record.objective === "string" ? record.objective.trim() : "";
|
|
252
|
+
const slug = typeof record.slug === "string" ? record.slug.trim() : "";
|
|
253
|
+
if (!objective) throw new AutoresearchCommandError(2, "autoresearch mission.json is missing objective");
|
|
254
|
+
if (!slug) throw new AutoresearchCommandError(2, "autoresearch mission.json is missing slug");
|
|
255
|
+
assertAutoresearchMode(record.mode, "mission.json");
|
|
256
|
+
const createdAt = typeof record.createdAt === "string" ? record.createdAt : "";
|
|
257
|
+
const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : createdAt;
|
|
258
|
+
const intake: AutoresearchIntakeKind = record.intake === "handoff" ? "handoff" : "cold";
|
|
259
|
+
return {
|
|
260
|
+
objective,
|
|
261
|
+
mode: record.mode,
|
|
262
|
+
deliverables: requireStringArray(record.deliverables ?? [], "mission.json deliverables"),
|
|
263
|
+
constraints: requireStringArray(record.constraints ?? [], "mission.json constraints"),
|
|
264
|
+
slug,
|
|
265
|
+
intake,
|
|
266
|
+
createdAt,
|
|
267
|
+
updatedAt,
|
|
268
|
+
...(typeof record.specPath === "string" && record.specPath.trim() !== "" ? { specPath: record.specPath } : {}),
|
|
269
|
+
...(typeof record.handedOffAt === "string" && record.handedOffAt.trim() !== ""
|
|
270
|
+
? { handedOffAt: record.handedOffAt }
|
|
271
|
+
: {}),
|
|
272
|
+
// Preserve the declared metric contract across read/write round-trips;
|
|
273
|
+
// dropping it here would silently reinstate the default direction.
|
|
274
|
+
...(typeof record.primaryMetric === "string" && record.primaryMetric.trim() !== ""
|
|
275
|
+
? { primaryMetric: record.primaryMetric.trim() }
|
|
276
|
+
: {}),
|
|
277
|
+
...(typeof record.metricUnit === "string" && record.metricUnit.trim() !== ""
|
|
278
|
+
? { metricUnit: record.metricUnit.trim() }
|
|
279
|
+
: {}),
|
|
280
|
+
...(record.metricDirection === "higher" || record.metricDirection === "lower"
|
|
281
|
+
? { metricDirection: record.metricDirection }
|
|
282
|
+
: {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function readAutoresearchMission(
|
|
287
|
+
cwd: string,
|
|
288
|
+
sessionId?: string | null,
|
|
289
|
+
): Promise<AutoresearchMission | null> {
|
|
290
|
+
const resolvedSessionId =
|
|
291
|
+
sessionId?.trim() ||
|
|
292
|
+
(await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
|
|
293
|
+
try {
|
|
294
|
+
return normalizeAutoresearchMission(
|
|
295
|
+
await Bun.file(getAutoresearchPaths(cwd, resolvedSessionId).missionPath).json(),
|
|
296
|
+
);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (isEnoent(error)) return null;
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function readAutoresearchLedger(
|
|
304
|
+
cwd: string,
|
|
305
|
+
sessionId?: string | null,
|
|
306
|
+
): Promise<AutoresearchLedgerEvent[]> {
|
|
307
|
+
const resolvedSessionId =
|
|
308
|
+
sessionId?.trim() ||
|
|
309
|
+
(await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
|
|
310
|
+
try {
|
|
311
|
+
const raw = await Bun.file(getAutoresearchPaths(cwd, resolvedSessionId).ledgerPath).text();
|
|
312
|
+
return raw
|
|
313
|
+
.split(/\r?\n/)
|
|
314
|
+
.map(line => line.trim())
|
|
315
|
+
.filter(line => line.length > 0)
|
|
316
|
+
.map(line => JSON.parse(line) as AutoresearchLedgerEvent);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (isEnoent(error)) return [];
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function appendAutoresearchLedger(
|
|
324
|
+
cwd: string,
|
|
325
|
+
payload: { event: AutoresearchLedgerEventKind } & Record<string, unknown>,
|
|
326
|
+
sessionId?: string | null,
|
|
327
|
+
): Promise<AutoresearchLedgerEvent> {
|
|
328
|
+
const resolvedSessionId =
|
|
329
|
+
sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
330
|
+
const paths = getAutoresearchPaths(cwd, resolvedSessionId);
|
|
331
|
+
const entry: AutoresearchLedgerEvent = {
|
|
332
|
+
eventId: crypto.randomUUID(),
|
|
333
|
+
...payload,
|
|
334
|
+
timestamp: new Date().toISOString(),
|
|
335
|
+
};
|
|
336
|
+
await appendJsonl(paths.ledgerPath, entry, {
|
|
337
|
+
cwd,
|
|
338
|
+
audit: {
|
|
339
|
+
category: "ledger",
|
|
340
|
+
verb: "append",
|
|
341
|
+
owner: "gjc-runtime",
|
|
342
|
+
skill: "autoresearch",
|
|
343
|
+
sessionId: resolvedSessionId,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
await writeSessionActivityMarker(cwd, resolvedSessionId, { writer: "autoresearch-runtime", path: paths.ledgerPath });
|
|
347
|
+
return entry;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function persistAutoresearchMission(input: {
|
|
351
|
+
cwd: string;
|
|
352
|
+
sessionId: string;
|
|
353
|
+
mission: AutoresearchMission;
|
|
354
|
+
}): Promise<AutoresearchMission> {
|
|
355
|
+
const paths = getAutoresearchPaths(input.cwd, input.sessionId);
|
|
356
|
+
const existingRead = await readExistingStateForMutation(paths.missionPath);
|
|
357
|
+
if (existingRead.kind === "corrupt") {
|
|
358
|
+
throw new AutoresearchCommandError(
|
|
359
|
+
2,
|
|
360
|
+
`existing autoresearch mission is corrupt or tampered (${existingRead.error}); refusing to overwrite ${paths.missionPath}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
await writeGuardedJsonAtomic(paths.missionPath, input.mission, {
|
|
364
|
+
cwd: input.cwd,
|
|
365
|
+
policy: "source",
|
|
366
|
+
expectedRevision: existingRead.kind === "valid" ? persistedStateRevision(existingRead.value) : undefined,
|
|
367
|
+
audit: {
|
|
368
|
+
category: "state",
|
|
369
|
+
verb: "write",
|
|
370
|
+
owner: "gjc-runtime",
|
|
371
|
+
skill: "autoresearch",
|
|
372
|
+
sessionId: input.sessionId,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
await writeSessionActivityMarker(input.cwd, input.sessionId, {
|
|
376
|
+
writer: "autoresearch-runtime",
|
|
377
|
+
path: paths.missionPath,
|
|
378
|
+
});
|
|
379
|
+
return input.mission;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ------------------------------ verbs ------------------------------ */
|
|
383
|
+
|
|
384
|
+
/** read verb: current mission + append-only ledger snapshot. */
|
|
385
|
+
export async function autoresearchRead(cwd: string, sessionId?: string | null): Promise<AutoresearchReadReceipt> {
|
|
386
|
+
const resolvedSessionId =
|
|
387
|
+
sessionId?.trim() ||
|
|
388
|
+
(await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
|
|
389
|
+
const mission = await readAutoresearchMission(cwd, resolvedSessionId);
|
|
390
|
+
const ledger = await readAutoresearchLedger(cwd, resolvedSessionId);
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
exists: mission !== null,
|
|
394
|
+
...(mission ? { mission } : {}),
|
|
395
|
+
ledger,
|
|
396
|
+
paths: getAutoresearchPaths(cwd, resolvedSessionId),
|
|
397
|
+
sessionId: resolvedSessionId,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Validate and normalize the optional primary-metric contract.
|
|
403
|
+
*
|
|
404
|
+
* Direction is validated at the write boundary exactly like mode: an
|
|
405
|
+
* unrecognized value is rejected rather than coerced, so a typo can never
|
|
406
|
+
* silently flip the mission to the default direction.
|
|
407
|
+
*/
|
|
408
|
+
function normalizeMetricContract(
|
|
409
|
+
input: { primaryMetric?: string; metricUnit?: string; metricDirection?: string },
|
|
410
|
+
context: string,
|
|
411
|
+
): Pick<AutoresearchMission, "primaryMetric" | "metricUnit" | "metricDirection"> {
|
|
412
|
+
const result: Pick<AutoresearchMission, "primaryMetric" | "metricUnit" | "metricDirection"> = {};
|
|
413
|
+
const primaryMetric = input.primaryMetric?.trim();
|
|
414
|
+
if (primaryMetric) result.primaryMetric = primaryMetric;
|
|
415
|
+
const metricUnit = input.metricUnit?.trim();
|
|
416
|
+
if (metricUnit) result.metricUnit = metricUnit;
|
|
417
|
+
const direction = input.metricDirection?.trim().toLowerCase();
|
|
418
|
+
if (direction !== undefined && direction !== "") {
|
|
419
|
+
if (direction !== "higher" && direction !== "lower") {
|
|
420
|
+
throw new AutoresearchCommandError(
|
|
421
|
+
2,
|
|
422
|
+
`${context} metric direction must be "higher" or "lower"; received ${JSON.stringify(input.metricDirection)}. It is never inferred.`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
result.metricDirection = direction;
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** write verb: persist the mission after cold-intake clarification. Mode is required. */
|
|
431
|
+
export async function autoresearchWrite(input: {
|
|
432
|
+
cwd: string;
|
|
433
|
+
objective: string;
|
|
434
|
+
mode: AutoresearchMode;
|
|
435
|
+
deliverables?: string[];
|
|
436
|
+
constraints?: string[];
|
|
437
|
+
slug: string;
|
|
438
|
+
sessionId?: string | null;
|
|
439
|
+
primaryMetric?: string;
|
|
440
|
+
metricUnit?: string;
|
|
441
|
+
metricDirection?: string;
|
|
442
|
+
}): Promise<AutoresearchWriteReceipt> {
|
|
443
|
+
const objective = input.objective.trim();
|
|
444
|
+
if (!objective) throw new AutoresearchCommandError(2, "autoresearch mission objective is required");
|
|
445
|
+
const slug = input.slug.trim();
|
|
446
|
+
if (!slug) throw new AutoresearchCommandError(2, "autoresearch mission slug is required");
|
|
447
|
+
assertSafePathComponent(slug, "slug");
|
|
448
|
+
// AC-16: hard fail at the write boundary; mode is never inferred.
|
|
449
|
+
assertAutoresearchMode(input.mode, "write intake");
|
|
450
|
+
const metric = normalizeMetricContract(input, "write intake");
|
|
451
|
+
const resolvedSessionId =
|
|
452
|
+
input.sessionId?.trim() ||
|
|
453
|
+
resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
454
|
+
const now = new Date().toISOString();
|
|
455
|
+
const paths = getAutoresearchPaths(input.cwd, resolvedSessionId);
|
|
456
|
+
const existing = await readAutoresearchMission(input.cwd, resolvedSessionId);
|
|
457
|
+
const mission: AutoresearchMission = {
|
|
458
|
+
objective,
|
|
459
|
+
mode: input.mode,
|
|
460
|
+
deliverables: dedupeStrings(input.deliverables ?? []),
|
|
461
|
+
constraints: dedupeStrings(input.constraints ?? []),
|
|
462
|
+
slug,
|
|
463
|
+
intake: "cold",
|
|
464
|
+
createdAt: existing?.createdAt ?? now,
|
|
465
|
+
updatedAt: now,
|
|
466
|
+
...metric,
|
|
467
|
+
};
|
|
468
|
+
await persistAutoresearchMission({ cwd: input.cwd, sessionId: resolvedSessionId, mission });
|
|
469
|
+
let ledgerEvent: AutoresearchLedgerEvent | undefined;
|
|
470
|
+
if (existing === null) {
|
|
471
|
+
ledgerEvent = await appendAutoresearchLedger(
|
|
472
|
+
input.cwd,
|
|
473
|
+
{ event: "mission_created", slug, mode: mission.mode, objective },
|
|
474
|
+
resolvedSessionId,
|
|
475
|
+
);
|
|
476
|
+
} else if (existing.mode !== mission.mode) {
|
|
477
|
+
ledgerEvent = await appendAutoresearchLedger(
|
|
478
|
+
input.cwd,
|
|
479
|
+
{ event: "mode_set", slug, mode: mission.mode, previousMode: existing.mode },
|
|
480
|
+
resolvedSessionId,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return { ok: true, mission, missionPath: paths.missionPath, intake: "cold", ledgerEvent };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Durable per-mission artifacts that live directly under the session
|
|
488
|
+
* autoresearch dir. `clear` must retire ALL of them, not just `mission.json`:
|
|
489
|
+
* leaving `ledger.jsonl` behind lets a successor mission inherit the previous
|
|
490
|
+
* mission's `verdict_issued` / `critic_recorded` rows. `runs.jsonl` and
|
|
491
|
+
* `experiment.json` leak run history and the metric contract, and `runs/`
|
|
492
|
+
* holds per-run artifacts the same way.
|
|
493
|
+
*/
|
|
494
|
+
const AUTORESEARCH_MISSION_ARTIFACTS = ["mission.json", "ledger.jsonl", "runs.jsonl", "experiment.json"] as const;
|
|
495
|
+
const AUTORESEARCH_RUNS_SUBDIR = "runs";
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* clear verb: retire the entire mission working set and record the mission clear.
|
|
499
|
+
*
|
|
500
|
+
* Artifacts are QUARANTINED to `<dir>/retired/<slug>.<timestamp>/`, never
|
|
501
|
+
* deleted, so a completed mission's evidence stays auditable while the
|
|
502
|
+
* successor mission starts from genuinely empty state. The `mission_cleared` row
|
|
503
|
+
* is appended to the OUTGOING ledger before it is moved, so the retired ledger
|
|
504
|
+
* is a complete record that ends with its own clear and the successor's ledger
|
|
505
|
+
* starts empty rather than the clear silently vanishing from the audit trail.
|
|
506
|
+
*/
|
|
507
|
+
export async function autoresearchClear(cwd: string, sessionId?: string | null): Promise<AutoresearchClearReceipt> {
|
|
508
|
+
const resolvedSessionId =
|
|
509
|
+
sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
510
|
+
const paths = getAutoresearchPaths(cwd, resolvedSessionId);
|
|
511
|
+
const existing = await readAutoresearchMission(cwd, resolvedSessionId);
|
|
512
|
+
// Close out the outgoing ledger first so the retired copy ends with its clear.
|
|
513
|
+
const ledgerEvent = await appendAutoresearchLedger(
|
|
514
|
+
cwd,
|
|
515
|
+
{ event: "mission_cleared", slug: existing?.slug ?? "" },
|
|
516
|
+
resolvedSessionId,
|
|
517
|
+
);
|
|
518
|
+
const retiredTo = existing ? await retireAutoresearchMissionArtifacts(paths, existing.slug) : undefined;
|
|
519
|
+
const deleted = existing !== null;
|
|
520
|
+
await reconcileAutoresearchState(cwd, existing, resolvedSessionId, { active: false, phase: "complete" });
|
|
521
|
+
return {
|
|
522
|
+
ok: true,
|
|
523
|
+
cleared: deleted,
|
|
524
|
+
missionPath: paths.missionPath,
|
|
525
|
+
ledgerEvent,
|
|
526
|
+
...(retiredTo ? { retiredTo } : {}),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Move every durable mission artifact aside into a uniquely-reserved directory.
|
|
532
|
+
*
|
|
533
|
+
* A timestamp alone collides when two clears land in the same millisecond,
|
|
534
|
+
* which would overwrite the earlier mission's evidence, so the directory is
|
|
535
|
+
* reserved with an exclusive mkdir and disambiguated with a counter.
|
|
536
|
+
*/
|
|
537
|
+
async function retireAutoresearchMissionArtifacts(paths: AutoresearchPaths, slug: string): Promise<string> {
|
|
538
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
539
|
+
await fs.mkdir(paths.retiredRoot, { recursive: true });
|
|
540
|
+
const base = path.join(paths.retiredRoot, `${slug}.${stamp}`);
|
|
541
|
+
let retiredTo = base;
|
|
542
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
543
|
+
retiredTo = attempt === 1 ? base : `${base}-${attempt}`;
|
|
544
|
+
try {
|
|
545
|
+
await fs.mkdir(retiredTo);
|
|
546
|
+
break;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (const name of AUTORESEARCH_MISSION_ARTIFACTS) {
|
|
552
|
+
await moveIfPresent(path.join(paths.dir, name), path.join(retiredTo, name));
|
|
553
|
+
}
|
|
554
|
+
await moveIfPresent(path.join(paths.dir, AUTORESEARCH_RUNS_SUBDIR), path.join(retiredTo, AUTORESEARCH_RUNS_SUBDIR));
|
|
555
|
+
return retiredTo;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function moveIfPresent(from: string, to: string): Promise<void> {
|
|
559
|
+
try {
|
|
560
|
+
await fs.rename(from, to);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
if (!isEnoent(error)) throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* ---------------------------- spec intake ---------------------------- */
|
|
567
|
+
|
|
568
|
+
const AUTORESEARCH_MODE_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-mode\s*:\s*(web|mixed|data)\s*$/i;
|
|
569
|
+
const AUTORESEARCH_METRIC_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-metric\s*:\s*(.+?)\s*$/i;
|
|
570
|
+
const AUTORESEARCH_METRIC_UNIT_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-metric-unit\s*:\s*(.+?)\s*$/i;
|
|
571
|
+
const AUTORESEARCH_METRIC_DIRECTION_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-metric-direction\s*:\s*(.+?)\s*$/i;
|
|
572
|
+
const HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
573
|
+
const BULLET_RE = /^[-*]\s+(.+)$/;
|
|
574
|
+
const ACCEPTANCE_CRITERIA_DELIVERABLE_RE = /^[-*]\s+\[[ xX]\]\s+(.+)$/;
|
|
575
|
+
const INTERVIEW_ID_RE = /^[-*]\s*Interview ID:\s*(.+)$/i;
|
|
576
|
+
|
|
577
|
+
interface ParsedAutoresearchSpec {
|
|
578
|
+
objective: string;
|
|
579
|
+
mode: AutoresearchMode;
|
|
580
|
+
deliverables: string[];
|
|
581
|
+
constraints: string[];
|
|
582
|
+
slug: string;
|
|
583
|
+
primaryMetric?: string;
|
|
584
|
+
metricUnit?: string;
|
|
585
|
+
metricDirection?: MetricDirection;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function sectionBullets(lines: string[], sectionNames: readonly string[]): string[] {
|
|
589
|
+
const wanted = new Set(sectionNames.map(name => name.toLowerCase()));
|
|
590
|
+
let current: string | null = null;
|
|
591
|
+
const bullets: string[] = [];
|
|
592
|
+
for (const line of lines) {
|
|
593
|
+
const trimmed = line.trim();
|
|
594
|
+
const heading = HEADING_RE.exec(trimmed);
|
|
595
|
+
if (heading) {
|
|
596
|
+
current = heading[1]!.trim().toLowerCase();
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (current === null || !wanted.has(current)) continue;
|
|
600
|
+
const bullet = BULLET_RE.exec(trimmed);
|
|
601
|
+
if (!bullet) continue;
|
|
602
|
+
const text = bullet[1]!.trim();
|
|
603
|
+
if (text) bullets.push(text);
|
|
604
|
+
}
|
|
605
|
+
return dedupeStrings(bullets);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function sanitizeSpecSlug(value: string): string {
|
|
609
|
+
const normalized = value
|
|
610
|
+
.trim()
|
|
611
|
+
.toLowerCase()
|
|
612
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
613
|
+
.replace(/^-+|-+$/g, "");
|
|
614
|
+
if (normalized === "") {
|
|
615
|
+
throw new AutoresearchCommandError(2, "autoresearch spec intake could not derive a mission slug from the spec");
|
|
616
|
+
}
|
|
617
|
+
return normalized;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Parse a persisted deep-interview spec into the mission fields. The spec MUST
|
|
622
|
+
* declare its mission mode explicitly with a line like `autoresearch-mode: web`
|
|
623
|
+
* (one of web, mixed, data); a missing or invalid declaration is a hard fail —
|
|
624
|
+
* mode is never inferred from spec structure or incidental files.
|
|
625
|
+
*/
|
|
626
|
+
function parseAutoresearchSpec(specText: string, specPath: string): ParsedAutoresearchSpec {
|
|
627
|
+
const lines = specText.split(/\r?\n/);
|
|
628
|
+
let mode: AutoresearchMode | undefined;
|
|
629
|
+
for (const line of lines) {
|
|
630
|
+
const match = AUTORESEARCH_MODE_DECLARATION_RE.exec(line.trim());
|
|
631
|
+
if (match) {
|
|
632
|
+
mode = match[1]!.toLowerCase() as AutoresearchMode;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!mode) {
|
|
637
|
+
throw new AutoresearchCommandError(
|
|
638
|
+
2,
|
|
639
|
+
`autoresearch spec intake requires the spec to declare its mission mode explicitly with a line like "autoresearch-mode: web" (one of web, mixed, data) at ${specPath}. ` +
|
|
640
|
+
"Mode is never inferred from the presence of a data file.",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
// Optional metric contract. Same declaration style as the mode line; the
|
|
644
|
+
// direction is validated rather than coerced.
|
|
645
|
+
const firstMatch = (re: RegExp): string | undefined => {
|
|
646
|
+
for (const line of lines) {
|
|
647
|
+
const found = re.exec(line.trim());
|
|
648
|
+
if (found) return found[1];
|
|
649
|
+
}
|
|
650
|
+
return undefined;
|
|
651
|
+
};
|
|
652
|
+
const metric = normalizeMetricContract(
|
|
653
|
+
{
|
|
654
|
+
primaryMetric: firstMatch(AUTORESEARCH_METRIC_DECLARATION_RE),
|
|
655
|
+
metricUnit: firstMatch(AUTORESEARCH_METRIC_UNIT_DECLARATION_RE),
|
|
656
|
+
metricDirection: firstMatch(AUTORESEARCH_METRIC_DIRECTION_DECLARATION_RE),
|
|
657
|
+
},
|
|
658
|
+
`autoresearch spec intake at ${specPath}`,
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const h1 = lines.find(line => /^#\s+\S/.test(line.trim()));
|
|
662
|
+
const objective =
|
|
663
|
+
(h1 ?? lines.find(line => line.trim() !== ""))?.trim().replace(/^#\s+/, "") ?? path.basename(specPath);
|
|
664
|
+
const deliverables = sectionBullets(lines, ["deliverables"]);
|
|
665
|
+
const acceptanceCriteria = sectionBullets(lines, ["acceptance criteria"]);
|
|
666
|
+
const finalDeliverables =
|
|
667
|
+
deliverables.length > 0
|
|
668
|
+
? deliverables
|
|
669
|
+
: acceptanceCriteria.map(text => ACCEPTANCE_CRITERIA_DELIVERABLE_RE.exec(text)?.[1]?.trim() ?? text);
|
|
670
|
+
const constraints = sectionBullets(lines, ["constraints"]);
|
|
671
|
+
const interviewIdLine = lines.find(line => INTERVIEW_ID_RE.test(line.trim()));
|
|
672
|
+
const interviewId = interviewIdLine ? INTERVIEW_ID_RE.exec(interviewIdLine.trim())?.[1]?.trim() : undefined;
|
|
673
|
+
const slug = sanitizeSpecSlug(interviewId ?? path.basename(specPath, path.extname(specPath)));
|
|
674
|
+
return {
|
|
675
|
+
objective,
|
|
676
|
+
mode,
|
|
677
|
+
deliverables: finalDeliverables,
|
|
678
|
+
constraints,
|
|
679
|
+
slug,
|
|
680
|
+
...metric,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** Spec intake (`intake --spec <path>` or bare `--spec`): read the spec, write the mission, ask zero questions. The persisted intake kind stays "handoff" (seeded by a deep-interview handoff). */
|
|
685
|
+
export async function autoresearchIntake(input: {
|
|
686
|
+
cwd: string;
|
|
687
|
+
specPath: string;
|
|
688
|
+
sessionId?: string | null;
|
|
689
|
+
}): Promise<AutoresearchIntakeReceipt> {
|
|
690
|
+
const resolvedSpecPath = path.resolve(input.cwd, input.specPath);
|
|
691
|
+
let specText: string;
|
|
692
|
+
try {
|
|
693
|
+
specText = await fs.readFile(resolvedSpecPath, "utf-8");
|
|
694
|
+
} catch (error) {
|
|
695
|
+
throw new AutoresearchCommandError(
|
|
696
|
+
2,
|
|
697
|
+
`autoresearch spec intake could not read spec at ${resolvedSpecPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const parsed = parseAutoresearchSpec(specText, resolvedSpecPath);
|
|
701
|
+
const resolvedSessionId =
|
|
702
|
+
input.sessionId?.trim() ||
|
|
703
|
+
resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
704
|
+
const now = new Date().toISOString();
|
|
705
|
+
const paths = getAutoresearchPaths(input.cwd, resolvedSessionId);
|
|
706
|
+
const existing = await readAutoresearchMission(input.cwd, resolvedSessionId);
|
|
707
|
+
const mission: AutoresearchMission = {
|
|
708
|
+
objective: parsed.objective,
|
|
709
|
+
mode: parsed.mode,
|
|
710
|
+
deliverables: parsed.deliverables,
|
|
711
|
+
constraints: parsed.constraints,
|
|
712
|
+
slug: parsed.slug,
|
|
713
|
+
intake: "handoff",
|
|
714
|
+
createdAt: existing?.createdAt ?? now,
|
|
715
|
+
updatedAt: now,
|
|
716
|
+
specPath: resolvedSpecPath,
|
|
717
|
+
handedOffAt: now,
|
|
718
|
+
...(parsed.primaryMetric === undefined ? {} : { primaryMetric: parsed.primaryMetric }),
|
|
719
|
+
...(parsed.metricUnit === undefined ? {} : { metricUnit: parsed.metricUnit }),
|
|
720
|
+
...(parsed.metricDirection === undefined ? {} : { metricDirection: parsed.metricDirection }),
|
|
721
|
+
};
|
|
722
|
+
await persistAutoresearchMission({ cwd: input.cwd, sessionId: resolvedSessionId, mission });
|
|
723
|
+
let ledgerEvent: AutoresearchLedgerEvent | undefined;
|
|
724
|
+
if (existing === null) {
|
|
725
|
+
ledgerEvent = await appendAutoresearchLedger(
|
|
726
|
+
input.cwd,
|
|
727
|
+
{
|
|
728
|
+
event: "mission_created",
|
|
729
|
+
slug: mission.slug,
|
|
730
|
+
mode: mission.mode,
|
|
731
|
+
objective: mission.objective,
|
|
732
|
+
specPath: resolvedSpecPath,
|
|
733
|
+
},
|
|
734
|
+
resolvedSessionId,
|
|
735
|
+
);
|
|
736
|
+
} else if (existing.mode !== mission.mode) {
|
|
737
|
+
ledgerEvent = await appendAutoresearchLedger(
|
|
738
|
+
input.cwd,
|
|
739
|
+
{ event: "mode_set", slug: mission.slug, mode: mission.mode, previousMode: existing.mode },
|
|
740
|
+
resolvedSessionId,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
ok: true,
|
|
745
|
+
mission,
|
|
746
|
+
missionPath: paths.missionPath,
|
|
747
|
+
intake: "handoff",
|
|
748
|
+
specPath: resolvedSpecPath,
|
|
749
|
+
ledgerEvent,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* ------------------------------ run/verdict ------------------------------ */
|
|
754
|
+
|
|
755
|
+
/** Record one experiment run in the ledger. */
|
|
756
|
+
export async function autoresearchLogRun(input: {
|
|
757
|
+
cwd: string;
|
|
758
|
+
runId: string;
|
|
759
|
+
status: string;
|
|
760
|
+
description: string;
|
|
761
|
+
metric?: number;
|
|
762
|
+
slug?: string;
|
|
763
|
+
sessionId?: string | null;
|
|
764
|
+
}): Promise<AutoresearchLedgerEvent> {
|
|
765
|
+
const runId = input.runId.trim();
|
|
766
|
+
if (!runId) throw new AutoresearchCommandError(2, "autoresearch run_id is required");
|
|
767
|
+
const status = input.status.trim();
|
|
768
|
+
if (!status) throw new AutoresearchCommandError(2, "autoresearch run status is required");
|
|
769
|
+
const description = input.description.trim();
|
|
770
|
+
if (!description) throw new AutoresearchCommandError(2, "autoresearch run description is required");
|
|
771
|
+
if (status !== "keep" && status !== "discard" && status !== "crash" && status !== "checks_failed") {
|
|
772
|
+
throw new AutoresearchCommandError(2, "autoresearch run status must be keep, discard, crash, or checks_failed");
|
|
773
|
+
}
|
|
774
|
+
const sessionId =
|
|
775
|
+
input.sessionId?.trim() ||
|
|
776
|
+
resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
777
|
+
const store = await autoresearchRunsStore(input.cwd, sessionId);
|
|
778
|
+
if (!store.config) throw new AutoresearchCommandError(2, "autoresearch run logging requires an active mission");
|
|
779
|
+
// Reuse the caller-supplied pending run identity when it exists so runs.jsonl and
|
|
780
|
+
// the ledger reference the same run; otherwise synthesize one record.
|
|
781
|
+
const existing = store.listRuns().find(run => run.runId === runId);
|
|
782
|
+
let targetRunId: string;
|
|
783
|
+
if (existing && existing.status === null && existing.abandonedAt === null) {
|
|
784
|
+
targetRunId = existing.runId;
|
|
785
|
+
} else {
|
|
786
|
+
const started = await store.startRun({ command: "research observation" });
|
|
787
|
+
targetRunId = started.runId;
|
|
788
|
+
}
|
|
789
|
+
await store.completeRun(targetRunId, {
|
|
790
|
+
exitCode: status === "crash" || status === "checks_failed" ? 1 : 0,
|
|
791
|
+
timedOut: false,
|
|
792
|
+
});
|
|
793
|
+
await store.logRun(targetRunId, {
|
|
794
|
+
status,
|
|
795
|
+
description,
|
|
796
|
+
...(input.metric === undefined ? {} : { metric: input.metric }),
|
|
797
|
+
});
|
|
798
|
+
const ledgerEvent = await appendAutoresearchLedger(
|
|
799
|
+
input.cwd,
|
|
800
|
+
{
|
|
801
|
+
event: "run_logged",
|
|
802
|
+
run_id: targetRunId,
|
|
803
|
+
status,
|
|
804
|
+
description,
|
|
805
|
+
...(input.slug?.trim() ? { slug: input.slug.trim() } : {}),
|
|
806
|
+
...(typeof input.metric === "number" && Number.isFinite(input.metric) ? { metric: input.metric } : {}),
|
|
807
|
+
},
|
|
808
|
+
sessionId,
|
|
809
|
+
);
|
|
810
|
+
const mission = await readAutoresearchMission(input.cwd, sessionId);
|
|
811
|
+
if (mission) await reconcileAutoresearchState(input.cwd, mission, sessionId, { phase: "research" });
|
|
812
|
+
return ledgerEvent;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Record the optional per-mission critic pass; its evaluator is distinct from the mission agent. */
|
|
816
|
+
export async function autoresearchRecordCritic(input: {
|
|
817
|
+
cwd: string;
|
|
818
|
+
status: Record<string, unknown>;
|
|
819
|
+
evidence: string[];
|
|
820
|
+
caveats: string[];
|
|
821
|
+
evaluator: string;
|
|
822
|
+
slug?: string;
|
|
823
|
+
sessionId?: string | null;
|
|
824
|
+
}): Promise<AutoresearchCriticReceipt> {
|
|
825
|
+
assertStructuredStatus(input.status, "critic status");
|
|
826
|
+
const evidence = requireStringArray(input.evidence, "critic evidence");
|
|
827
|
+
const caveats = requireStringArray(input.caveats, "critic caveats");
|
|
828
|
+
const evaluator = input.evaluator.trim();
|
|
829
|
+
if (!evaluator) throw new AutoresearchCommandError(2, "autoresearch critic evaluator is required");
|
|
830
|
+
const receipt: AutoresearchCriticReceipt = {
|
|
831
|
+
criticId: crypto.randomUUID(),
|
|
832
|
+
status: input.status,
|
|
833
|
+
evidence,
|
|
834
|
+
caveats,
|
|
835
|
+
evaluator,
|
|
836
|
+
recordedAt: new Date().toISOString(),
|
|
837
|
+
};
|
|
838
|
+
await appendAutoresearchLedger(
|
|
839
|
+
input.cwd,
|
|
840
|
+
{
|
|
841
|
+
event: "critic_recorded",
|
|
842
|
+
...(input.slug?.trim() ? { slug: input.slug.trim() } : {}),
|
|
843
|
+
criticReceipt: receipt,
|
|
844
|
+
},
|
|
845
|
+
input.sessionId,
|
|
846
|
+
);
|
|
847
|
+
return receipt;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Issue the mission verdict; an optional critic receipt rides along with its own evaluator identity. */
|
|
851
|
+
export async function autoresearchIssueVerdict(input: {
|
|
852
|
+
cwd: string;
|
|
853
|
+
status: Record<string, unknown>;
|
|
854
|
+
evidence: string[];
|
|
855
|
+
caveats: string[];
|
|
856
|
+
evaluator: string;
|
|
857
|
+
criticReceipt?: AutoresearchCriticReceipt;
|
|
858
|
+
slug?: string;
|
|
859
|
+
sessionId?: string | null;
|
|
860
|
+
}): Promise<AutoresearchVerdictReceipt> {
|
|
861
|
+
assertStructuredStatus(input.status, "verdict status");
|
|
862
|
+
const evidence = requireStringArray(input.evidence, "verdict evidence");
|
|
863
|
+
const caveats = requireStringArray(input.caveats, "verdict caveats");
|
|
864
|
+
const evaluator = input.evaluator.trim();
|
|
865
|
+
if (!evaluator) throw new AutoresearchCommandError(2, "autoresearch verdict evaluator is required");
|
|
866
|
+
const sessionId =
|
|
867
|
+
input.sessionId?.trim() ||
|
|
868
|
+
resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
869
|
+
const ledger = await readAutoresearchLedger(input.cwd, sessionId);
|
|
870
|
+
const latestCritic = [...ledger].reverse().find(event => event.event === "critic_recorded" && event.criticReceipt)
|
|
871
|
+
?.criticReceipt as AutoresearchCriticReceipt | undefined;
|
|
872
|
+
const receipt: AutoresearchVerdictReceipt = {
|
|
873
|
+
receiptId: crypto.randomUUID(),
|
|
874
|
+
status: input.status,
|
|
875
|
+
evidence,
|
|
876
|
+
caveats,
|
|
877
|
+
evaluator,
|
|
878
|
+
issuedAt: new Date().toISOString(),
|
|
879
|
+
...((input.criticReceipt ?? latestCritic) ? { criticReceipt: input.criticReceipt ?? latestCritic } : {}),
|
|
880
|
+
};
|
|
881
|
+
await appendAutoresearchLedger(
|
|
882
|
+
input.cwd,
|
|
883
|
+
{
|
|
884
|
+
event: "verdict_issued",
|
|
885
|
+
...(input.slug?.trim() ? { slug: input.slug.trim() } : {}),
|
|
886
|
+
verdictReceipt: receipt,
|
|
887
|
+
},
|
|
888
|
+
sessionId,
|
|
889
|
+
);
|
|
890
|
+
const mission = await readAutoresearchMission(input.cwd, sessionId);
|
|
891
|
+
if (mission) await reconcileAutoresearchState(input.cwd, mission, sessionId, { phase: "verdict" });
|
|
892
|
+
return receipt;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* ------------------------------ CLI dispatch ------------------------------ */
|
|
896
|
+
|
|
897
|
+
function renderAutoresearchHelp(): string {
|
|
898
|
+
return [
|
|
899
|
+
"Run native GJC Autoresearch workflow commands",
|
|
900
|
+
"",
|
|
901
|
+
"USAGE",
|
|
902
|
+
" $ gjc autoresearch [--spec <path>] [--json] [goal...]",
|
|
903
|
+
" $ gjc autoresearch intake --spec <path> [--json]",
|
|
904
|
+
" $ gjc autoresearch read [--json]",
|
|
905
|
+
" $ gjc autoresearch clear [--json]",
|
|
906
|
+
" $ gjc autoresearch write --goal <goal> --mode <web|mixed|data> --slug <slug> [--deliverable <text>] [--constraint <text>] [--json]",
|
|
907
|
+
" $ gjc autoresearch log-run --run-id <id> --status <status> --description <text> [--metric <number>] [--json]",
|
|
908
|
+
" $ gjc autoresearch verdict --status-json <object> --evidence <text> --evaluator <id> [--caveat <text>] [--json]",
|
|
909
|
+
" $ gjc autoresearch critic --status-json <object> --evidence <text> --evaluator <id> [--caveat <text>] [--json]",
|
|
910
|
+
"",
|
|
911
|
+
"INTAKE",
|
|
912
|
+
" intake --spec Spec intake: read a persisted deep-interview spec and start research",
|
|
913
|
+
" with zero clarification questions. The spec must declare its mission",
|
|
914
|
+
" mode explicitly (a line like `autoresearch-mode: web`).",
|
|
915
|
+
" positional goal Cold intake: signals that goal/constraints/deliverables clarification",
|
|
916
|
+
" must run before research begins.",
|
|
917
|
+
" bare invocation Cold intake (no goal text).",
|
|
918
|
+
" write Persist a clarified cold-intake mission. Mode and slug are required.",
|
|
919
|
+
" read Read the current mission and append-only ledger.",
|
|
920
|
+
" clear Clear the mission artifact and retire its working set.",
|
|
921
|
+
" log-run Append one research run receipt to the mission ledger.",
|
|
922
|
+
" verdict Append the structured mission verdict receipt.",
|
|
923
|
+
" critic Append an optional structured critic receipt.",
|
|
924
|
+
" --json Output a machine-readable receipt.",
|
|
925
|
+
"",
|
|
926
|
+
"STATE",
|
|
927
|
+
" Mission/ledger/verdict state persists under .gjc/_session-{sessionid}/autoresearch/.",
|
|
928
|
+
" The global autoresearch store is not written.",
|
|
929
|
+
"",
|
|
930
|
+
"EXAMPLES",
|
|
931
|
+
" $ gjc autoresearch --spec .gjc/_session-abc/specs/deep-interview-my-mission.md --json",
|
|
932
|
+
' $ gjc autoresearch "Optimize the tokenizer throughput"',
|
|
933
|
+
" $ gjc autoresearch",
|
|
934
|
+
"",
|
|
935
|
+
].join("\n");
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function repeatedFlagValues(args: readonly string[], flag: string): string[] {
|
|
939
|
+
const values: string[] = [];
|
|
940
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
941
|
+
if (args[index] !== flag) continue;
|
|
942
|
+
const value = args[index + 1];
|
|
943
|
+
if (!value || value.startsWith("--")) throw new AutoresearchCommandError(2, `${flag} requires a non-empty value`);
|
|
944
|
+
values.push(value);
|
|
945
|
+
index += 1;
|
|
946
|
+
}
|
|
947
|
+
return values;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function assertOnlyAutoresearchFlags(args: readonly string[], allowed: readonly string[]): void {
|
|
951
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
952
|
+
const arg = args[index]!;
|
|
953
|
+
if (!arg.startsWith("--")) continue;
|
|
954
|
+
if (!allowed.includes(arg)) throw new AutoresearchCommandError(2, `unknown flag for gjc autoresearch: ${arg}`);
|
|
955
|
+
if (arg !== "--json") index += 1;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function requiredFlagValue(args: readonly string[], flag: string): string {
|
|
960
|
+
const value = flagValue(args, flag)?.trim();
|
|
961
|
+
if (!value) throw new AutoresearchCommandError(2, `${flag} requires a non-empty value`);
|
|
962
|
+
return value;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function jsonObjectFlag(args: readonly string[], flag: string): Record<string, unknown> {
|
|
966
|
+
const raw = requiredFlagValue(args, flag);
|
|
967
|
+
try {
|
|
968
|
+
const value = JSON.parse(raw) as unknown;
|
|
969
|
+
assertStructuredStatus(value, flag);
|
|
970
|
+
return value;
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (error instanceof AutoresearchCommandError) throw error;
|
|
973
|
+
throw new AutoresearchCommandError(2, `${flag} must be a JSON object`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function extractPositionalGoal(args: readonly string[]): string {
|
|
978
|
+
const parts: string[] = [];
|
|
979
|
+
let skipNext = false;
|
|
980
|
+
for (const arg of args) {
|
|
981
|
+
if (skipNext) {
|
|
982
|
+
skipNext = false;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (arg === "--spec") {
|
|
986
|
+
skipNext = true;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (arg === "--json") continue;
|
|
990
|
+
if (arg.startsWith("-")) {
|
|
991
|
+
throw new AutoresearchCommandError(2, `unknown flag for gjc autoresearch: ${arg}`);
|
|
992
|
+
}
|
|
993
|
+
parts.push(arg);
|
|
994
|
+
}
|
|
995
|
+
return parts.join(" ").trim();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function renderSpecIntakeText(receipt: AutoresearchIntakeReceipt): string {
|
|
999
|
+
return [
|
|
1000
|
+
`autoresearch intake=handoff slug=${receipt.mission.slug}`,
|
|
1001
|
+
`mode=${receipt.mission.mode}`,
|
|
1002
|
+
`objective=${receipt.mission.objective}`,
|
|
1003
|
+
`spec_path=${receipt.specPath}`,
|
|
1004
|
+
`mission_path=${receipt.missionPath}`,
|
|
1005
|
+
"research may begin; zero clarification questions.",
|
|
1006
|
+
"",
|
|
1007
|
+
].join("\n");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function renderColdIntakeText(goal: string): string {
|
|
1011
|
+
return [
|
|
1012
|
+
"autoresearch intake=cold clarification_required=true",
|
|
1013
|
+
...(goal ? [`goal=${goal}`] : []),
|
|
1014
|
+
"next=run goal, constraints, and deliverables clarification before research begins",
|
|
1015
|
+
"",
|
|
1016
|
+
].join("\n");
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Reconcile the session-scoped active-state/HUD row after a mission write
|
|
1021
|
+
* (skill "autoresearch" is a plain active-state skill until the canonical slot
|
|
1022
|
+
* swap; the entry and snapshot are the generic per-skill machinery).
|
|
1023
|
+
* Best-effort: a HUD sync failure never changes command semantics.
|
|
1024
|
+
*/
|
|
1025
|
+
async function reconcileAutoresearchState(
|
|
1026
|
+
cwd: string,
|
|
1027
|
+
mission: AutoresearchMission | null,
|
|
1028
|
+
sessionId?: string,
|
|
1029
|
+
options: { active?: boolean; phase?: "intake" | "research" | "verdict" | "complete" } = {},
|
|
1030
|
+
): Promise<void> {
|
|
1031
|
+
const resolvedSessionId =
|
|
1032
|
+
sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
1033
|
+
try {
|
|
1034
|
+
const ledger = await readAutoresearchLedger(cwd, resolvedSessionId);
|
|
1035
|
+
const store = await AutoresearchRunsStore.open(cwd, resolvedSessionId);
|
|
1036
|
+
const latestVerdict = [...ledger]
|
|
1037
|
+
.reverse()
|
|
1038
|
+
.find(event => event.event === "verdict_issued" && event.verdictReceipt);
|
|
1039
|
+
const verdictReceipt = latestVerdict?.verdictReceipt as AutoresearchVerdictReceipt | undefined;
|
|
1040
|
+
const verdictStatus = verdictReceipt?.status;
|
|
1041
|
+
const verdict = verdictStatus
|
|
1042
|
+
? typeof verdictStatus.verdict === "string"
|
|
1043
|
+
? verdictStatus.verdict
|
|
1044
|
+
: typeof verdictStatus.status === "string"
|
|
1045
|
+
? verdictStatus.status
|
|
1046
|
+
: undefined
|
|
1047
|
+
: undefined;
|
|
1048
|
+
const hud = mission
|
|
1049
|
+
? buildAutoresearchHudSummary({
|
|
1050
|
+
phase: options.phase ?? "research",
|
|
1051
|
+
mode: mission.mode,
|
|
1052
|
+
intake: mission.intake,
|
|
1053
|
+
slug: mission.slug,
|
|
1054
|
+
specPath: mission.specPath,
|
|
1055
|
+
verdict,
|
|
1056
|
+
experimentStatuses: store.listLoggedRuns().flatMap(run => (run.status ? [run.status] : [])),
|
|
1057
|
+
updatedAt: new Date().toISOString(),
|
|
1058
|
+
})
|
|
1059
|
+
: undefined;
|
|
1060
|
+
await syncSkillActiveState({
|
|
1061
|
+
cwd,
|
|
1062
|
+
skill: "autoresearch",
|
|
1063
|
+
active: options.active ?? true,
|
|
1064
|
+
phase: options.phase ?? "research",
|
|
1065
|
+
sessionId: resolvedSessionId,
|
|
1066
|
+
source: "gjc-autoresearch-native",
|
|
1067
|
+
hud: hud ?? {
|
|
1068
|
+
version: 1,
|
|
1069
|
+
summary: "autoresearch mission cleared",
|
|
1070
|
+
chips: [],
|
|
1071
|
+
updated_at: new Date().toISOString(),
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
} catch {
|
|
1075
|
+
// HUD sync is best-effort and must not change command semantics.
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export async function runNativeAutoresearchCommand(
|
|
1080
|
+
args: string[],
|
|
1081
|
+
cwd = process.cwd(),
|
|
1082
|
+
): Promise<AutoresearchCommandResult> {
|
|
1083
|
+
try {
|
|
1084
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
1085
|
+
return { status: 0, stdout: renderAutoresearchHelp() };
|
|
1086
|
+
}
|
|
1087
|
+
const verb = args[0];
|
|
1088
|
+
if (verb === "read") {
|
|
1089
|
+
assertOnlyAutoresearchFlags(args.slice(1), ["--json"]);
|
|
1090
|
+
const receipt = await autoresearchRead(cwd);
|
|
1091
|
+
return {
|
|
1092
|
+
status: 0,
|
|
1093
|
+
stdout: hasFlag(args, "--json")
|
|
1094
|
+
? renderCliWriteReceipt({
|
|
1095
|
+
ok: receipt.ok,
|
|
1096
|
+
exists: receipt.exists,
|
|
1097
|
+
...(receipt.mission ? { mission: receipt.mission } : {}),
|
|
1098
|
+
ledger: receipt.ledger,
|
|
1099
|
+
paths: receipt.paths,
|
|
1100
|
+
})
|
|
1101
|
+
: `autoresearch exists=${receipt.exists}\nmission_path=${receipt.paths.missionPath}\nledger_path=${receipt.paths.ledgerPath}\n`,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (verb === "clear") {
|
|
1105
|
+
assertOnlyAutoresearchFlags(args.slice(1), ["--json"]);
|
|
1106
|
+
const receipt = await autoresearchClear(cwd);
|
|
1107
|
+
return {
|
|
1108
|
+
status: 0,
|
|
1109
|
+
stdout: hasFlag(args, "--json")
|
|
1110
|
+
? renderCliWriteReceipt({
|
|
1111
|
+
ok: true,
|
|
1112
|
+
cleared: receipt.cleared,
|
|
1113
|
+
mission_path: receipt.missionPath,
|
|
1114
|
+
...(receipt.retiredTo ? { retired_to: receipt.retiredTo } : {}),
|
|
1115
|
+
ledger_event: receipt.ledgerEvent.event,
|
|
1116
|
+
})
|
|
1117
|
+
: `autoresearch cleared=${receipt.cleared}\nmission_path=${receipt.missionPath}\n${
|
|
1118
|
+
receipt.retiredTo ? `retired_to=${receipt.retiredTo}\n` : ""
|
|
1119
|
+
}`,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (verb === "write") {
|
|
1123
|
+
assertOnlyAutoresearchFlags(args.slice(1), [
|
|
1124
|
+
"--goal",
|
|
1125
|
+
"--mode",
|
|
1126
|
+
"--slug",
|
|
1127
|
+
"--deliverable",
|
|
1128
|
+
"--constraint",
|
|
1129
|
+
"--primary-metric",
|
|
1130
|
+
"--metric-unit",
|
|
1131
|
+
"--metric-direction",
|
|
1132
|
+
"--json",
|
|
1133
|
+
]);
|
|
1134
|
+
const objective = flagValue(args, "--goal");
|
|
1135
|
+
const mode = flagValue(args, "--mode");
|
|
1136
|
+
const slug = flagValue(args, "--slug");
|
|
1137
|
+
if (objective === undefined || mode === undefined || slug === undefined) {
|
|
1138
|
+
throw new AutoresearchCommandError(2, "write requires --goal, --mode, and --slug");
|
|
1139
|
+
}
|
|
1140
|
+
const receipt = await autoresearchWrite({
|
|
1141
|
+
cwd,
|
|
1142
|
+
objective,
|
|
1143
|
+
mode: mode as AutoresearchMode,
|
|
1144
|
+
slug,
|
|
1145
|
+
deliverables: repeatedFlagValues(args, "--deliverable"),
|
|
1146
|
+
constraints: repeatedFlagValues(args, "--constraint"),
|
|
1147
|
+
primaryMetric: flagValue(args, "--primary-metric"),
|
|
1148
|
+
metricUnit: flagValue(args, "--metric-unit"),
|
|
1149
|
+
metricDirection: flagValue(args, "--metric-direction"),
|
|
1150
|
+
});
|
|
1151
|
+
await reconcileAutoresearchState(cwd, receipt.mission);
|
|
1152
|
+
return {
|
|
1153
|
+
status: 0,
|
|
1154
|
+
intake: "cold",
|
|
1155
|
+
missionCreated: true,
|
|
1156
|
+
stdout: hasFlag(args, "--json")
|
|
1157
|
+
? renderCliWriteReceipt({
|
|
1158
|
+
ok: true,
|
|
1159
|
+
intake: receipt.intake,
|
|
1160
|
+
mission: receipt.mission,
|
|
1161
|
+
mission_path: receipt.missionPath,
|
|
1162
|
+
ledger_event: receipt.ledgerEvent?.event,
|
|
1163
|
+
})
|
|
1164
|
+
: `autoresearch intake=cold slug=${receipt.mission.slug}\nmode=${receipt.mission.mode}\nmission_path=${receipt.missionPath}\n`,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
if (verb === "log-run") {
|
|
1168
|
+
assertOnlyAutoresearchFlags(args.slice(1), [
|
|
1169
|
+
"--run-id",
|
|
1170
|
+
"--status",
|
|
1171
|
+
"--description",
|
|
1172
|
+
"--metric",
|
|
1173
|
+
"--slug",
|
|
1174
|
+
"--json",
|
|
1175
|
+
]);
|
|
1176
|
+
const metricRaw = flagValue(args, "--metric");
|
|
1177
|
+
const metric = metricRaw === undefined ? undefined : Number(metricRaw);
|
|
1178
|
+
if (metricRaw !== undefined && !Number.isFinite(metric)) {
|
|
1179
|
+
throw new AutoresearchCommandError(2, "--metric must be a finite number");
|
|
1180
|
+
}
|
|
1181
|
+
const event = await autoresearchLogRun({
|
|
1182
|
+
cwd,
|
|
1183
|
+
runId: requiredFlagValue(args, "--run-id"),
|
|
1184
|
+
status: requiredFlagValue(args, "--status"),
|
|
1185
|
+
description: requiredFlagValue(args, "--description"),
|
|
1186
|
+
metric,
|
|
1187
|
+
slug: flagValue(args, "--slug"),
|
|
1188
|
+
});
|
|
1189
|
+
return { status: 0, stdout: renderCliWriteReceipt({ ok: true, ledger_event: event }) };
|
|
1190
|
+
}
|
|
1191
|
+
if (verb === "critic" || verb === "verdict") {
|
|
1192
|
+
assertOnlyAutoresearchFlags(args.slice(1), [
|
|
1193
|
+
"--status-json",
|
|
1194
|
+
"--evidence",
|
|
1195
|
+
"--caveat",
|
|
1196
|
+
"--evaluator",
|
|
1197
|
+
"--slug",
|
|
1198
|
+
"--json",
|
|
1199
|
+
]);
|
|
1200
|
+
const input = {
|
|
1201
|
+
cwd,
|
|
1202
|
+
status: jsonObjectFlag(args, "--status-json"),
|
|
1203
|
+
evidence: repeatedFlagValues(args, "--evidence"),
|
|
1204
|
+
caveats: repeatedFlagValues(args, "--caveat"),
|
|
1205
|
+
evaluator: requiredFlagValue(args, "--evaluator"),
|
|
1206
|
+
slug: flagValue(args, "--slug"),
|
|
1207
|
+
};
|
|
1208
|
+
const receipt =
|
|
1209
|
+
verb === "critic" ? await autoresearchRecordCritic(input) : await autoresearchIssueVerdict(input);
|
|
1210
|
+
return { status: 0, stdout: renderCliWriteReceipt({ ok: true, receipt }) };
|
|
1211
|
+
}
|
|
1212
|
+
if (verb === "report") {
|
|
1213
|
+
throw new AutoresearchCommandError(
|
|
1214
|
+
2,
|
|
1215
|
+
'unknown verb "report" — the report verb was removed; missions end at verdict and the ledger is the record',
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
if (verb === "handoff") {
|
|
1219
|
+
throw new AutoresearchCommandError(
|
|
1220
|
+
2,
|
|
1221
|
+
'unknown verb "handoff" — for spec intake use `gjc autoresearch intake --spec <path>`; for workflow handoff invoke the next skill directly (/skill:<callee>) or run `gjc state autoresearch handoff --to <callee>`',
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
if (verb === "intake") {
|
|
1225
|
+
assertOnlyAutoresearchFlags(args.slice(1), ["--spec", "--json"]);
|
|
1226
|
+
const intakeSpecPath = requiredFlagValue(args, "--spec").trim();
|
|
1227
|
+
if (intakeSpecPath === "") {
|
|
1228
|
+
return { status: 2, stderr: "--spec requires a non-empty path\n" };
|
|
1229
|
+
}
|
|
1230
|
+
const json = hasFlag(args, "--json");
|
|
1231
|
+
const receipt = await autoresearchIntake({ cwd, specPath: intakeSpecPath });
|
|
1232
|
+
await reconcileAutoresearchState(cwd, receipt.mission);
|
|
1233
|
+
return {
|
|
1234
|
+
status: 0,
|
|
1235
|
+
intake: "handoff",
|
|
1236
|
+
missionCreated: true,
|
|
1237
|
+
stdout: json
|
|
1238
|
+
? renderCliWriteReceipt({
|
|
1239
|
+
ok: true,
|
|
1240
|
+
intake: "handoff",
|
|
1241
|
+
mission: receipt.mission,
|
|
1242
|
+
mission_path: receipt.missionPath,
|
|
1243
|
+
spec_path: receipt.specPath,
|
|
1244
|
+
ledger_event: receipt.ledgerEvent?.event,
|
|
1245
|
+
})
|
|
1246
|
+
: renderSpecIntakeText(receipt),
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const specPath = flagValue(args, "--spec");
|
|
1250
|
+
if (specPath !== undefined) {
|
|
1251
|
+
if (specPath.trim() === "") {
|
|
1252
|
+
return { status: 2, stderr: "--spec requires a non-empty path\n" };
|
|
1253
|
+
}
|
|
1254
|
+
const json = hasFlag(args, "--json");
|
|
1255
|
+
const receipt = await autoresearchIntake({ cwd, specPath: specPath.trim() });
|
|
1256
|
+
await reconcileAutoresearchState(cwd, receipt.mission);
|
|
1257
|
+
return {
|
|
1258
|
+
status: 0,
|
|
1259
|
+
intake: "handoff",
|
|
1260
|
+
missionCreated: true,
|
|
1261
|
+
stdout: json
|
|
1262
|
+
? renderCliWriteReceipt({
|
|
1263
|
+
ok: true,
|
|
1264
|
+
intake: "handoff",
|
|
1265
|
+
mission: receipt.mission,
|
|
1266
|
+
mission_path: receipt.missionPath,
|
|
1267
|
+
spec_path: receipt.specPath,
|
|
1268
|
+
ledger_event: receipt.ledgerEvent?.event,
|
|
1269
|
+
})
|
|
1270
|
+
: renderSpecIntakeText(receipt),
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
const goal = extractPositionalGoal(args);
|
|
1274
|
+
const json = hasFlag(args, "--json");
|
|
1275
|
+
const payload: Record<string, unknown> = {
|
|
1276
|
+
ok: true,
|
|
1277
|
+
intake: "cold",
|
|
1278
|
+
clarification_required: true,
|
|
1279
|
+
...(goal ? { goal } : {}),
|
|
1280
|
+
next: "run goal, constraints, and deliverables clarification before research begins",
|
|
1281
|
+
};
|
|
1282
|
+
return {
|
|
1283
|
+
status: 0,
|
|
1284
|
+
intake: "cold",
|
|
1285
|
+
stdout: json ? renderCliWriteReceipt(payload) : renderColdIntakeText(goal),
|
|
1286
|
+
};
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
1289
|
+
if (error instanceof SessionResolutionError) return { status: 1, stderr: `${error.message}\n` };
|
|
1290
|
+
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/* ------------------------------ capability surface ------------------------------ */
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Rebuilt autoresearch capability surface. Each capability is reachable from
|
|
1298
|
+
* the runtime through a thin, mission-state-aware wrapper. All are read-only
|
|
1299
|
+
* with respect to the ledger — none append events; the verbs above remain the
|
|
1300
|
+
* only writers.
|
|
1301
|
+
*/
|
|
1302
|
+
|
|
1303
|
+
/** 1. Branch isolation: ensure the worktree is on an `autoresearch/*` branch. */
|
|
1304
|
+
export function autoresearchBranchIsolation(cwd: string, goal: string): Promise<EnsureAutoresearchBranchResult> {
|
|
1305
|
+
return ensureAutoresearchBranch(cwd, goal);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** 2. Harness contract: parse `METRIC`/`ASI` lines out of captured harness output. */
|
|
1309
|
+
export function autoresearchHarnessOutput(output: string, primaryMetricName?: string): ParsedHarnessOutput {
|
|
1310
|
+
return parseHarnessOutput(output, primaryMetricName);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* 3. Durable run storage: open the session-scoped JSONL run store, seeding an
|
|
1315
|
+
* in-memory experiment config from the mission when none is persisted yet.
|
|
1316
|
+
*/
|
|
1317
|
+
export async function autoresearchRunsStore(cwd: string, sessionId?: string | null): Promise<AutoresearchRunsStore> {
|
|
1318
|
+
const store = await AutoresearchRunsStore.open(cwd, sessionId);
|
|
1319
|
+
if (store.config === null) {
|
|
1320
|
+
const mission = await readAutoresearchMission(cwd, sessionId);
|
|
1321
|
+
if (mission) {
|
|
1322
|
+
store.setInMemoryConfig(
|
|
1323
|
+
createAutoresearchExperimentConfig({
|
|
1324
|
+
name: mission.slug,
|
|
1325
|
+
goal: mission.objective,
|
|
1326
|
+
// A declared metric contract must win over the default, or a
|
|
1327
|
+
// higher-is-better mission optimizes backwards.
|
|
1328
|
+
primaryMetric: mission.primaryMetric ?? "metric",
|
|
1329
|
+
...(mission.metricUnit === undefined ? {} : { metricUnit: mission.metricUnit }),
|
|
1330
|
+
...(mission.metricDirection === undefined ? {} : { direction: mission.metricDirection }),
|
|
1331
|
+
branch: null,
|
|
1332
|
+
}),
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return store;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** 4. TUI dashboard: render the run table (collapsed + expanded) from persisted state. */
|
|
1340
|
+
export async function autoresearchDashboardText(cwd: string, sessionId?: string | null, width = 120): Promise<string> {
|
|
1341
|
+
const store = await autoresearchRunsStore(cwd, sessionId);
|
|
1342
|
+
const mission = await readAutoresearchMission(cwd, sessionId);
|
|
1343
|
+
if (!store.config) return "autoresearch: no experiment config yet";
|
|
1344
|
+
const state = buildAutoresearchExperimentState(store.config, store.listLoggedRuns());
|
|
1345
|
+
const pendingRun = store.getPendingRun();
|
|
1346
|
+
const input: AutoresearchDashboardInput = {
|
|
1347
|
+
state,
|
|
1348
|
+
runtime: {
|
|
1349
|
+
modeOn: mission !== null,
|
|
1350
|
+
running: null,
|
|
1351
|
+
pendingRun: pendingRun
|
|
1352
|
+
? {
|
|
1353
|
+
runNumber: pendingRun.runNumber,
|
|
1354
|
+
passed: pendingRun.exitCode === 0 && !pendingRun.timedOut,
|
|
1355
|
+
parsedPrimary: pendingRun.metric,
|
|
1356
|
+
}
|
|
1357
|
+
: null,
|
|
1358
|
+
},
|
|
1359
|
+
};
|
|
1360
|
+
return renderExpandedDashboard(input, width);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export async function autoresearchDataContext(
|
|
1364
|
+
cwd: string,
|
|
1365
|
+
dataFlag?: string,
|
|
1366
|
+
sessionId?: string | null,
|
|
1367
|
+
): Promise<RlmDataContext | null> {
|
|
1368
|
+
const mission = await readAutoresearchMission(cwd, sessionId);
|
|
1369
|
+
if (!mission) return null;
|
|
1370
|
+
return loadAutoresearchDataContext({
|
|
1371
|
+
cwd,
|
|
1372
|
+
mode: mission.mode,
|
|
1373
|
+
dataFlag,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/** 8. Two-phase prompts: Phase 1 (harness setup) rendered from mission/branch state. */
|
|
1378
|
+
export async function autoresearchSetupPrompt(
|
|
1379
|
+
baseSystemPrompt: string,
|
|
1380
|
+
cwd: string,
|
|
1381
|
+
goal?: string,
|
|
1382
|
+
sessionId?: string | null,
|
|
1383
|
+
): Promise<string> {
|
|
1384
|
+
const mission = await readAutoresearchMission(cwd, sessionId);
|
|
1385
|
+
const branch = await ensureAutoresearchBranch(cwd, goal ?? mission?.objective ?? "").then(result =>
|
|
1386
|
+
result.ok ? result.branchName : null,
|
|
1387
|
+
);
|
|
1388
|
+
return renderAutoresearchSetupPrompt({
|
|
1389
|
+
baseSystemPrompt,
|
|
1390
|
+
goal: goal ?? mission?.objective ?? "",
|
|
1391
|
+
workingDir: cwd,
|
|
1392
|
+
branch,
|
|
1393
|
+
baselineWarning: null,
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** 8. Two-phase prompts: Phase 2 (iterate) rendered from the persisted experiment state. */
|
|
1398
|
+
export async function autoresearchIteratePrompt(
|
|
1399
|
+
baseSystemPrompt: string,
|
|
1400
|
+
cwd: string,
|
|
1401
|
+
sessionId?: string | null,
|
|
1402
|
+
): Promise<string> {
|
|
1403
|
+
const store = await autoresearchRunsStore(cwd, sessionId);
|
|
1404
|
+
const mission = await readAutoresearchMission(cwd, sessionId);
|
|
1405
|
+
if (!store.config) {
|
|
1406
|
+
return renderAutoresearchIteratePrompt({
|
|
1407
|
+
baseSystemPrompt,
|
|
1408
|
+
goal: mission?.objective ?? "",
|
|
1409
|
+
workingDir: cwd,
|
|
1410
|
+
branch: null,
|
|
1411
|
+
baselineCommit: null,
|
|
1412
|
+
metricName: "metric",
|
|
1413
|
+
metricUnit: "",
|
|
1414
|
+
notes: "",
|
|
1415
|
+
currentSegment: 1,
|
|
1416
|
+
currentSegmentRunCount: 0,
|
|
1417
|
+
baselineMetric: null,
|
|
1418
|
+
bestMetric: null,
|
|
1419
|
+
bestRunNumber: null,
|
|
1420
|
+
recentRuns: [],
|
|
1421
|
+
unjustifiedRuns: [],
|
|
1422
|
+
pendingRun: null,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const state = buildAutoresearchExperimentState(store.config, store.listLoggedRuns());
|
|
1426
|
+
const current = state.results.filter(result => result.segment === state.currentSegment);
|
|
1427
|
+
const recentRuns = current.slice(-3).map(result => {
|
|
1428
|
+
const asiSummary = summarizePromptAsi(result.asi);
|
|
1429
|
+
return {
|
|
1430
|
+
run_number: result.runNumber,
|
|
1431
|
+
status: result.status,
|
|
1432
|
+
metric_display: formatPromptMetricValue(result.metric, state.metricUnit),
|
|
1433
|
+
description: result.description,
|
|
1434
|
+
has_asi_summary: Boolean(asiSummary),
|
|
1435
|
+
asi_summary: asiSummary ?? "",
|
|
1436
|
+
has_deviations: result.scopeDeviations.length > 0,
|
|
1437
|
+
deviations: result.scopeDeviations.join(", "),
|
|
1438
|
+
justified: Boolean(result.justification),
|
|
1439
|
+
flagged: result.flagged,
|
|
1440
|
+
flagged_reason: result.flaggedReason ?? "",
|
|
1441
|
+
};
|
|
1442
|
+
});
|
|
1443
|
+
const unjustifiedRuns = current
|
|
1444
|
+
.filter(
|
|
1445
|
+
result =>
|
|
1446
|
+
result.status === "keep" && !result.flagged && result.scopeDeviations.length > 0 && !result.justification,
|
|
1447
|
+
)
|
|
1448
|
+
.slice(-3)
|
|
1449
|
+
.map(result => ({ run_number: result.runNumber, paths: result.scopeDeviations.join(", ") }));
|
|
1450
|
+
const pendingRun = store.getPendingRun();
|
|
1451
|
+
const bestRun = findPromptBestRun(state);
|
|
1452
|
+
return renderAutoresearchIteratePrompt({
|
|
1453
|
+
baseSystemPrompt,
|
|
1454
|
+
goal: store.config.goal ?? mission?.objective ?? "",
|
|
1455
|
+
workingDir: cwd,
|
|
1456
|
+
branch: store.config.branch,
|
|
1457
|
+
baselineCommit: store.config.baselineCommit,
|
|
1458
|
+
metricName: state.metricName,
|
|
1459
|
+
metricUnit: state.metricUnit,
|
|
1460
|
+
notes: state.notes,
|
|
1461
|
+
currentSegment: state.currentSegment + 1,
|
|
1462
|
+
currentSegmentRunCount: current.length,
|
|
1463
|
+
baselineMetric: state.bestMetric,
|
|
1464
|
+
bestMetric: bestRun?.metric ?? state.bestMetric,
|
|
1465
|
+
bestRunNumber: bestRun?.runNumber ?? null,
|
|
1466
|
+
recentRuns,
|
|
1467
|
+
unjustifiedRuns,
|
|
1468
|
+
pendingRun: pendingRun
|
|
1469
|
+
? {
|
|
1470
|
+
runNumber: pendingRun.runNumber,
|
|
1471
|
+
command: pendingRun.command,
|
|
1472
|
+
parsedPrimary: pendingRun.metric,
|
|
1473
|
+
passed: pendingRun.exitCode === 0 && !pendingRun.timedOut,
|
|
1474
|
+
}
|
|
1475
|
+
: null,
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function summarizePromptAsi(asi: unknown): string | null {
|
|
1480
|
+
if (!asi || typeof asi !== "object") return null;
|
|
1481
|
+
const record = asi as Record<string, unknown>;
|
|
1482
|
+
const hypothesis = typeof record.hypothesis === "string" ? record.hypothesis.trim() : "";
|
|
1483
|
+
const rollback = typeof record.rollback_reason === "string" ? record.rollback_reason.trim() : "";
|
|
1484
|
+
const next = typeof record.next_action_hint === "string" ? record.next_action_hint.trim() : "";
|
|
1485
|
+
const summary = [hypothesis, rollback, next].filter(part => part.length > 0).join(" | ");
|
|
1486
|
+
return summary.length > 0 ? summary.slice(0, 220) : null;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function formatPromptMetricValue(value: number | null, unit: string): string {
|
|
1490
|
+
if (value === null) return `n/a${unit ? ` ${unit}` : ""}`;
|
|
1491
|
+
if (Number.isInteger(value)) return `${value}${unit}`;
|
|
1492
|
+
return `${value.toFixed(2)}${unit}`;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function findPromptBestRun(state: {
|
|
1496
|
+
results: Array<{ segment: number; status: string; metric: number | null; flagged: boolean; runNumber: number }>;
|
|
1497
|
+
currentSegment: number;
|
|
1498
|
+
bestDirection: "lower" | "higher";
|
|
1499
|
+
}): { metric: number; runNumber: number } | null {
|
|
1500
|
+
let best: { metric: number; runNumber: number } | null = null;
|
|
1501
|
+
for (const result of state.results) {
|
|
1502
|
+
if (result.segment !== state.currentSegment || result.status !== "keep" || result.flagged) continue;
|
|
1503
|
+
if (result.metric === null) continue;
|
|
1504
|
+
if (!best || (state.bestDirection === "lower" ? result.metric < best.metric : result.metric > best.metric)) {
|
|
1505
|
+
best = { metric: result.metric, runNumber: result.runNumber };
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return best;
|
|
1509
|
+
}
|