heyspex 0.1.0 → 0.2.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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
// Research tool handlers: heyspex_save_research, promote_research,
|
|
2
|
+
// promote_research_to_diagnose, split_research, complete_research, active_research
|
|
3
|
+
// (the execute-level logic lives in research-tools.mjs and siblings). Extracted from cli.mjs under
|
|
4
|
+
// [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
|
|
5
|
+
|
|
6
|
+
import { executeCompleteResearch, loadResearchFocus } from './research-focus.mjs';
|
|
7
|
+
import { allocateRandomVaultId, collectOccupiedIdsFromSpecs } from '../../scripts/vault-id-lib.mjs';
|
|
8
|
+
import { findSpecFile, parseFrontmatter } from './vault-io.mjs';
|
|
9
|
+
import { executeSplitResearch } from './split-research.mjs';
|
|
10
|
+
import { executePromoteResearchToDiagnose } from './promote-research-to-diagnose.mjs';
|
|
11
|
+
import { executePromoteResearch } from './promote-research.mjs';
|
|
12
|
+
import { consoleFetch, enrichConsoleFailurePayloadAsync } from './console-transport.mjs';
|
|
13
|
+
import { consoleErrorCode, consoleErrorMessage } from './console-fetch.mjs';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { basename, join, relative, isAbsolute, resolve, sep } from 'node:path';
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { readViaSelector, resolveReadTier, resolveVaultWriteMode } from './read-path.mjs';
|
|
18
|
+
import { detectAgentIdentity, getConsoleUrl } from './mcp-config.mjs';
|
|
19
|
+
import { collectOccupiedKeysEitherKind, mintFamilyKey, mintFileKey, parseSpecId } from './spec-id-lib.mjs';
|
|
20
|
+
import { yamlScalar } from './yaml-scalar.mjs';
|
|
21
|
+
import { measureDiskLanding } from './tool-helpers.mjs';
|
|
22
|
+
import { assertPathUnderRoot } from '../../scripts/lib/shared-path-helpers.mjs';
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* /hs-promote-research — research → milestone promotion with AI-assisted epic
|
|
27
|
+
* stubs. Implementation lives in `promote-research.mjs`; this wrapper supplies
|
|
28
|
+
* the console-fetch dep so the AI proposer call routes through console (Haiku
|
|
29
|
+
* 4.5 via tool-use, matching [[HS-TASK-TAOD]]'s pattern).
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Cloud-tier reads ([[HS-KR16-CWPX-TASK-promotion-reads-through-the-cloud-tier]]):
|
|
33
|
+
* `tier` is resolved ONCE per call here (`resolveReadTier()`) and threaded
|
|
34
|
+
* into `executePromoteResearch`'s deps — never re-resolved downstream.
|
|
35
|
+
* `overrides` lets a test inject `{ tier, consoleUrl, consoleFetch,
|
|
36
|
+
* resolveReadTier }` directly so it never opens a socket; every production
|
|
37
|
+
* call site (tool-dispatch.mjs) passes none, so the real `resolveReadTier()`
|
|
38
|
+
* / `getConsoleUrl()` / `consoleFetch` run unchanged.
|
|
39
|
+
*
|
|
40
|
+
* Cloud-tier WRITES ([[HS-BYU4-CWPX-TASK-promotion-writes-go-through-the-console]]):
|
|
41
|
+
* `writeMode` is resolved here on the same once-per-call rule —
|
|
42
|
+
* `resolveVaultWriteMode()` is the write-side twin of `resolveReadTier()`, and
|
|
43
|
+
* `handleSaveResearch` in this same file already trusts it. This is the seam
|
|
44
|
+
* that makes the cloud write path reachable: the execute phase defaults to
|
|
45
|
+
* `deps.writeMode ?? 'local'`, so without this line a cloud-reliant session
|
|
46
|
+
* would still mint ids locally and write milestone/epic files to a disk the
|
|
47
|
+
* console's index has never heard of.
|
|
48
|
+
*/
|
|
49
|
+
export async function handlePromoteResearch(vaultRoot, args = {}, overrides = {}) {
|
|
50
|
+
try {
|
|
51
|
+
const doResolveReadTier = overrides.resolveReadTier ?? resolveReadTier;
|
|
52
|
+
const tier = overrides.tier ?? (await doResolveReadTier());
|
|
53
|
+
const doResolveWriteMode = overrides.resolveVaultWriteMode ?? resolveVaultWriteMode;
|
|
54
|
+
const writeMode = overrides.writeMode ?? (await doResolveWriteMode());
|
|
55
|
+
const consoleUrl = overrides.consoleUrl ?? getConsoleUrl();
|
|
56
|
+
const doConsoleFetch = overrides.consoleFetch ?? consoleFetch;
|
|
57
|
+
const result = await executePromoteResearch(vaultRoot, args, {
|
|
58
|
+
consoleFetch: (path, options) => doConsoleFetch(path, options),
|
|
59
|
+
tier,
|
|
60
|
+
writeMode,
|
|
61
|
+
consoleUrl,
|
|
62
|
+
});
|
|
63
|
+
if (result?.error) {
|
|
64
|
+
return {
|
|
65
|
+
isError: true,
|
|
66
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
isError: true,
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
error: 'PROMOTE_RESEARCH_EXCEPTION',
|
|
81
|
+
message: err?.message ?? String(err),
|
|
82
|
+
},
|
|
83
|
+
null,
|
|
84
|
+
2,
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* heyspex_promote_research_to_diagnose — one research investigation → one
|
|
94
|
+
* HS-DIAG-* diagnose spec ([[HS-MSHY-TASK-research-diagnose-link-send-bugs-to-the]]).
|
|
95
|
+
* Research-side sibling of handlePromoteCaptureToDiagnose. Single-phase;
|
|
96
|
+
* writes the diagnose + the research's produces_diagnose linkage
|
|
97
|
+
* transactionally.
|
|
98
|
+
*/
|
|
99
|
+
export async function handlePromoteResearchToDiagnose(vaultRoot, args = {}) {
|
|
100
|
+
try {
|
|
101
|
+
const result = await executePromoteResearchToDiagnose(vaultRoot, args, {});
|
|
102
|
+
if (result?.error) {
|
|
103
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
104
|
+
}
|
|
105
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
isError: true,
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify(
|
|
113
|
+
{ error: 'PROMOTE_RESEARCH_TO_DIAGNOSE_EXCEPTION', message: err?.message ?? String(err) },
|
|
114
|
+
null,
|
|
115
|
+
2,
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* /hs-split-research — split a research doc into N child research docs.
|
|
125
|
+
* Implementation lives in `split-research.mjs`; single-phase tool (no AI),
|
|
126
|
+
* caller provides body_slice + sources_captures partitions.
|
|
127
|
+
*
|
|
128
|
+
* `tier` / `writeMode` are resolved HERE, once per call, and threaded down —
|
|
129
|
+
* the same rule {@link handlePromoteResearch} follows. In cloud write mode the
|
|
130
|
+
* children are POSTed first and every sibling cross-link is PUT against the
|
|
131
|
+
* basenames those POSTs returned.
|
|
132
|
+
*/
|
|
133
|
+
export async function handleSplitResearch(vaultRoot, args = {}, overrides = {}) {
|
|
134
|
+
try {
|
|
135
|
+
const doResolveReadTier = overrides.resolveReadTier ?? resolveReadTier;
|
|
136
|
+
const tier = overrides.tier ?? (await doResolveReadTier());
|
|
137
|
+
const doResolveWriteMode = overrides.resolveVaultWriteMode ?? resolveVaultWriteMode;
|
|
138
|
+
const writeMode = overrides.writeMode ?? (await doResolveWriteMode());
|
|
139
|
+
const consoleUrl = overrides.consoleUrl ?? getConsoleUrl();
|
|
140
|
+
const doConsoleFetch = overrides.consoleFetch ?? consoleFetch;
|
|
141
|
+
const result = await executeSplitResearch(vaultRoot, args, {
|
|
142
|
+
consoleFetch: (path, options) => doConsoleFetch(path, options),
|
|
143
|
+
tier,
|
|
144
|
+
writeMode,
|
|
145
|
+
consoleUrl,
|
|
146
|
+
});
|
|
147
|
+
if (result?.error) {
|
|
148
|
+
return {
|
|
149
|
+
isError: true,
|
|
150
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
155
|
+
};
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
isError: true,
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: JSON.stringify(
|
|
163
|
+
{
|
|
164
|
+
error: 'SPLIT_RESEARCH_EXCEPTION',
|
|
165
|
+
message: err?.message ?? String(err),
|
|
166
|
+
},
|
|
167
|
+
null,
|
|
168
|
+
2,
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function handleActiveResearch(vaultRoot) {
|
|
177
|
+
const focus = loadResearchFocus(vaultRoot);
|
|
178
|
+
if (!focus?.spec_path) {
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: 'text',
|
|
183
|
+
text: JSON.stringify(
|
|
184
|
+
{
|
|
185
|
+
focus: null,
|
|
186
|
+
message:
|
|
187
|
+
'No research pinned. Open an idea in console and click Promote on the doc toolbar, or pass source_idea_id + scope to heyspex_complete_research directly.',
|
|
188
|
+
},
|
|
189
|
+
null,
|
|
190
|
+
2,
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: 'text', text: JSON.stringify({ focus }, null, 2) }],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function handleCompleteResearch(vaultRoot, args) {
|
|
202
|
+
const result = executeCompleteResearch(vaultRoot, args, {
|
|
203
|
+
allocateRandomVaultId,
|
|
204
|
+
findSpecFile,
|
|
205
|
+
});
|
|
206
|
+
if (result.error) {
|
|
207
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
208
|
+
}
|
|
209
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// `research_type` enum — mirrors schemas/heyspex-frontmatter.schema.json and
|
|
213
|
+
// [[entity-model]] §research ([[HS-120I-TASK-implement-research-type-field-end-to-end]]).
|
|
214
|
+
export const RESEARCH_TYPE_VALUES = ['new-feature', 'enhancement', 'diagnose', 'code-audit', 'reference'];
|
|
215
|
+
|
|
216
|
+
export const DEFAULT_RESEARCH_TYPE = 'reference';
|
|
217
|
+
|
|
218
|
+
// `scope` enum — mirrors schemas/heyspex-frontmatter.schema.json and
|
|
219
|
+
// [[entity-model]] §research ([[HS-NGD1-TASK-consolidate-and-fix-the-research-scope-field]]).
|
|
220
|
+
// Same values + same default as `heyspex_promote_capture` and `heyspex_complete_research`.
|
|
221
|
+
export const RESEARCH_SCOPE_VALUES = ['internal', 'codebase', 'external', 'all'];
|
|
222
|
+
|
|
223
|
+
export const DEFAULT_RESEARCH_SCOPE = 'internal';
|
|
224
|
+
|
|
225
|
+
export async function handleSaveResearch(vaultRoot, args) {
|
|
226
|
+
const { title, summary, content, tags = [], related = [], sources = [], research_type, scope } = args;
|
|
227
|
+
|
|
228
|
+
if (!title || !summary || !content) {
|
|
229
|
+
return {
|
|
230
|
+
isError: true,
|
|
231
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'MISSING_REQUIRED_FIELDS', message: 'title, summary, and content are required' }) }],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Optional, defaults to 'reference' — never written blank. Seam for a future
|
|
236
|
+
// kind-based suggestion is the `research_type` arg itself (a later task can
|
|
237
|
+
// pass a computed suggestion here); no inference is performed by this tool
|
|
238
|
+
// (heyspex_save_research has no source capture to derive a kind from —
|
|
239
|
+
// unlike heyspex_promote_capture, which does, see research-type-suggest.mjs).
|
|
240
|
+
if (research_type !== undefined && research_type !== null && !RESEARCH_TYPE_VALUES.includes(research_type)) {
|
|
241
|
+
return {
|
|
242
|
+
isError: true,
|
|
243
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: `research_type ${JSON.stringify(research_type)} must be one of: ${RESEARCH_TYPE_VALUES.join(', ')}` }) }],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// HS-MVC1 AC3: track whether the caller supplied research_type at all, so
|
|
247
|
+
// the response can carry a NON-BLOCKING warning when it defaulted (save
|
|
248
|
+
// still proceeds — modeled on the `warning: "TARGET_RESEARCH_CLOSED"`
|
|
249
|
+
// pattern in promote-capture.mjs; this is not a hard gate).
|
|
250
|
+
const researchTypeWasOmitted = research_type === undefined || research_type === null;
|
|
251
|
+
const resolvedResearchType = RESEARCH_TYPE_VALUES.includes(research_type) ? research_type : DEFAULT_RESEARCH_TYPE;
|
|
252
|
+
|
|
253
|
+
// Optional, defaults to 'internal' — never written blank. NOT required (consistency
|
|
254
|
+
// fix only, no grace window) — [[HS-NGD1-TASK-consolidate-and-fix-the-research-scope-field]].
|
|
255
|
+
if (scope !== undefined && scope !== null && !RESEARCH_SCOPE_VALUES.includes(scope)) {
|
|
256
|
+
return {
|
|
257
|
+
isError: true,
|
|
258
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: `scope ${JSON.stringify(scope)} must be one of: ${RESEARCH_SCOPE_VALUES.join(', ')}` }) }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const resolvedScope = RESEARCH_SCOPE_VALUES.includes(scope) ? scope : DEFAULT_RESEARCH_SCOPE;
|
|
262
|
+
|
|
263
|
+
// Vault-root path guard (HS-AI1T) — in BOTH write modes, before any tier or
|
|
264
|
+
// network resolution: a relative / drive-relative root is refused before
|
|
265
|
+
// anything touches disk. The directory itself is created only on the local
|
|
266
|
+
// write path below (a cloud-reliant machine may have no vault clone at all,
|
|
267
|
+
// and must not grow a phantom `specs/research/` on every save).
|
|
268
|
+
const researchDir = join(vaultRoot, 'specs', 'research');
|
|
269
|
+
if (!existsSync(researchDir)) {
|
|
270
|
+
try {
|
|
271
|
+
assertPathUnderRoot(vaultRoot, researchDir, 'research-handlers write');
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return {
|
|
274
|
+
isError: true,
|
|
275
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'BAD_VAULT_ROOT', message: err.message }) }],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Shared between the two write paths: the acting agent (`assisted_by`
|
|
281
|
+
// auto-stamp, [[HS-EPIC-IKIM-authorship-provenance-frontmatter-owner]] AC #2),
|
|
282
|
+
// the cleaned `related` ids, and the markdown body.
|
|
283
|
+
const actingAgent = detectAgentIdentity(args.agent, args.session_id).agent;
|
|
284
|
+
const relatedIds = related.map((r) => String(r).replace(/^\[\[|\]\]$/g, ''));
|
|
285
|
+
const bodyMarkdown = buildResearchBodyMarkdown({ title, summary, content, sources });
|
|
286
|
+
|
|
287
|
+
// WHERE THE WRITE GOES ([[HS-DJWP-MCPC-TASK]] closed the read half; this is
|
|
288
|
+
// the write half). `vaultWriteMode` comes from the same dev-mode resolver
|
|
289
|
+
// every read tier trusts: `cloud` for prod and dev+cloud, `local` only for
|
|
290
|
+
// dev+local. A disk write in cloud mode is a split brain — a file the
|
|
291
|
+
// console's index has never heard of, on a machine that may not even hold a
|
|
292
|
+
// clone — so cloud mode files through the console exactly like
|
|
293
|
+
// heyspex_file_spec does (POST /api/specs, server-minted id).
|
|
294
|
+
const writeMode = await resolveVaultWriteMode();
|
|
295
|
+
if (writeMode === 'cloud') {
|
|
296
|
+
return saveResearchViaConsole({
|
|
297
|
+
vaultRoot,
|
|
298
|
+
title,
|
|
299
|
+
summary,
|
|
300
|
+
tags,
|
|
301
|
+
relatedIds,
|
|
302
|
+
researchType: resolvedResearchType,
|
|
303
|
+
scope: resolvedScope,
|
|
304
|
+
bodyMarkdown,
|
|
305
|
+
actingAgent,
|
|
306
|
+
researchTypeWasOmitted,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!existsSync(researchDir)) {
|
|
311
|
+
mkdirSync(researchDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// [[HS-DJWP-MCPC-TASK]] "Occupied-id scans" family: id allocation needs the
|
|
315
|
+
// occupied set BEFORE the write — in cloud reliance that comes from `GET
|
|
316
|
+
// /api/vault/occupied-ids?bucket=research` instead of scanning
|
|
317
|
+
// `specs/research` on disk.
|
|
318
|
+
const tier = await resolveReadTier();
|
|
319
|
+
const consoleUrl = getConsoleUrl();
|
|
320
|
+
let occupiedResearch;
|
|
321
|
+
try {
|
|
322
|
+
occupiedResearch = await readViaSelector({
|
|
323
|
+
tier,
|
|
324
|
+
diskRead: () => {
|
|
325
|
+
const existingFiles = readdirSync(researchDir).filter(f => f.endsWith('.md') && !f.endsWith('.review.md'));
|
|
326
|
+
const scanSpecs = [];
|
|
327
|
+
for (const file of existingFiles) {
|
|
328
|
+
try {
|
|
329
|
+
const filePath = join(researchDir, file);
|
|
330
|
+
const fileContent = readFileSync(filePath, 'utf8');
|
|
331
|
+
const { frontmatter } = parseFrontmatter(fileContent);
|
|
332
|
+
scanSpecs.push({
|
|
333
|
+
id: String(frontmatter.id ?? ''),
|
|
334
|
+
basename: file.replace(/\.md$/i, ''),
|
|
335
|
+
type: 'research',
|
|
336
|
+
});
|
|
337
|
+
} catch {
|
|
338
|
+
/* skip unreadable */
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return collectOccupiedIdsFromSpecs(scanSpecs).research;
|
|
342
|
+
},
|
|
343
|
+
neonRead: async () => {
|
|
344
|
+
const res = await consoleFetch('/api/vault/occupied-ids?bucket=research');
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
throw new Error(`GET /api/vault/occupied-ids?bucket=research returned ${res.status}: ${res.error || ''}`);
|
|
347
|
+
}
|
|
348
|
+
return new Set(Array.isArray(res.body?.ids) ? res.body.ids : []);
|
|
349
|
+
},
|
|
350
|
+
routeLabel: 'GET /api/vault/occupied-ids?bucket=research',
|
|
351
|
+
consoleUrl,
|
|
352
|
+
});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
return {
|
|
355
|
+
isError: true,
|
|
356
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'CONSOLE_UNREACHABLE_CLOUD_MODE', message: String(err?.message ?? err) }) }],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// Era-3 file-key-first ([[naming-v3-7-rulings-hs-namespace-kept]]): the
|
|
360
|
+
// FAMILY token still comes from the legacy allocator's `HS-<family>-RES`
|
|
361
|
+
// draw (parentless research — no family to inherit); `id:` keys onto the
|
|
362
|
+
// file key alone.
|
|
363
|
+
const allocatedFamilyId = allocateRandomVaultId('research', occupiedResearch);
|
|
364
|
+
const familyKey = parseSpecId(allocatedFamilyId)?.familyKey ?? mintFamilyKey(vaultRoot);
|
|
365
|
+
// Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
|
|
366
|
+
// and (V3) the anchor for `id:` itself.
|
|
367
|
+
// Disk occupancy ([[HS-FJWL]]): cloud write mode returned above (the
|
|
368
|
+
// `writeMode === 'cloud'` branch files through the console, which mints the
|
|
369
|
+
// id itself), so this line only ever runs on a disk-backed vault.
|
|
370
|
+
const fileKey = mintFileKey(collectOccupiedKeysEitherKind(vaultRoot));
|
|
371
|
+
const researchId = `HS-${fileKey}`;
|
|
372
|
+
|
|
373
|
+
const slug = researchSlug(title);
|
|
374
|
+
|
|
375
|
+
const filename = `${researchId}-${familyKey}-RES-${slug}.md`;
|
|
376
|
+
const filepath = join(researchDir, filename);
|
|
377
|
+
|
|
378
|
+
// Build frontmatter
|
|
379
|
+
const today = new Date().toISOString().split('T')[0];
|
|
380
|
+
const relatedLinks = relatedIds.map((cleanId) => ` - "[[${cleanId}]]"`).join('\n');
|
|
381
|
+
const tagsArray = tags.length > 0 ? `[${tags.join(', ')}]` : '[]';
|
|
382
|
+
|
|
383
|
+
// `assisted_by` auto-stamp: a research doc filed via MCP is agent-created, so
|
|
384
|
+
// stamp the acting agent. `owner` stays the vault owner (no authenticated user
|
|
385
|
+
// on this direct-disk path). Omit-when-empty: no line when no agent is detected.
|
|
386
|
+
const assistedByBlock = actingAgent ? `assisted_by:\n - ${yamlScalar(actingAgent)}\n` : '';
|
|
387
|
+
|
|
388
|
+
const fileContent = `---
|
|
389
|
+
id: ${researchId}
|
|
390
|
+
file_id: ${fileKey}
|
|
391
|
+
family: ${familyKey}
|
|
392
|
+
name: ${yamlScalar(title)}
|
|
393
|
+
type: research
|
|
394
|
+
research_type: ${resolvedResearchType}
|
|
395
|
+
scope: ${resolvedScope}
|
|
396
|
+
status: open
|
|
397
|
+
owner: pwher
|
|
398
|
+
created: ${today}
|
|
399
|
+
updated: ${today}
|
|
400
|
+
${assistedByBlock}tags: ${tagsArray}
|
|
401
|
+
${relatedLinks ? `related:\n${relatedLinks}` : 'related: []'}
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
${bodyMarkdown}`;
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
writeFileSync(filepath, fileContent, 'utf8');
|
|
408
|
+
} catch (e) {
|
|
409
|
+
return {
|
|
410
|
+
isError: true,
|
|
411
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'WRITE_FAILED', message: e.message }) }],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Report on the RESULT, not the request ([[HS-IOJW-5YDH-DIAG]] sweep): stat
|
|
416
|
+
// the file before claiming it saved.
|
|
417
|
+
const landed = measureDiskLanding(vaultRoot, filepath);
|
|
418
|
+
if (!landed.on_disk) {
|
|
419
|
+
return {
|
|
420
|
+
isError: true,
|
|
421
|
+
content: [{
|
|
422
|
+
type: 'text',
|
|
423
|
+
text: JSON.stringify({
|
|
424
|
+
error: 'WRITE_NOT_ON_DISK',
|
|
425
|
+
message: `writeFileSync reported success but no file exists at ${filepath}. Treat the research as unsaved.`,
|
|
426
|
+
research_id: researchId,
|
|
427
|
+
landed,
|
|
428
|
+
}, null, 2),
|
|
429
|
+
}],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = {
|
|
434
|
+
success: true,
|
|
435
|
+
research_id: researchId,
|
|
436
|
+
filename,
|
|
437
|
+
filepath: relative(vaultRoot, filepath),
|
|
438
|
+
landed,
|
|
439
|
+
message: `Research saved to specs/research/${filename}`,
|
|
440
|
+
next_steps: researchNextSteps(slug),
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// HS-MVC1 AC3: non-blocking warning — the save already succeeded above
|
|
444
|
+
// (resolvedResearchType was written to disk either way); this only informs
|
|
445
|
+
// the caller that research_type was not supplied so it defaulted, rather
|
|
446
|
+
// than silently going unnoticed.
|
|
447
|
+
if (researchTypeWasOmitted) attachResearchTypeWarning(result);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The filename slug — same rule on both write paths. */
|
|
455
|
+
function researchSlug(title) {
|
|
456
|
+
return String(title)
|
|
457
|
+
.toLowerCase()
|
|
458
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
459
|
+
.replace(/^-|-$/g, '')
|
|
460
|
+
.slice(0, 40);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Markdown after the frontmatter: title, summary, content, optional sources. */
|
|
464
|
+
export function buildResearchBodyMarkdown({ title, summary, content, sources = [] }) {
|
|
465
|
+
let sourcesSection = '';
|
|
466
|
+
if (sources.length > 0) {
|
|
467
|
+
sourcesSection = '\n## Sources\n\n';
|
|
468
|
+
for (const src of sources) {
|
|
469
|
+
if (src.url && src.title) {
|
|
470
|
+
sourcesSection += `- [${src.title}](${src.url})${src.description ? ` — ${src.description}` : ''}\n`;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return `# ${title}\n\n${summary}\n\n${content}\n${sourcesSection}\n`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function researchNextSteps(linkTarget) {
|
|
478
|
+
return [
|
|
479
|
+
`Add [[${linkTarget}]] wikilinks to related specs`,
|
|
480
|
+
`Update the research file as implementation progresses`,
|
|
481
|
+
// Research is evergreen ([[research-is-evergreen]]) — it never closes.
|
|
482
|
+
`Add \`enhances:\` to bind to a milestone (or \`bound_epic:\` when an epic is filed) — research stays status: open`,
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// HS-MVC1 AC3: non-blocking warning — the save already succeeded; this only
|
|
487
|
+
// informs the caller that research_type was not supplied so it defaulted.
|
|
488
|
+
function attachResearchTypeWarning(result) {
|
|
489
|
+
result.warning = 'RESEARCH_TYPE_MISSING';
|
|
490
|
+
result.warning_message = `research_type was not supplied — defaulted to "${DEFAULT_RESEARCH_TYPE}". Consider passing an explicit research_type (${RESEARCH_TYPE_VALUES.join(', ')}) next time; this does not block the save.`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Cloud write mode: file the research doc through the console, the same call
|
|
495
|
+
* heyspex_file_spec makes (filing-tools.mjs `handleFileSpec` — POST /api/specs,
|
|
496
|
+
* idempotency-key header, no timeout retry against a remote console per
|
|
497
|
+
* [[HS-2FQY]]'s double-mint guard). The console mints the id and the family
|
|
498
|
+
* (`createSpec`: `research` -> `specs/research/`, `RES`), so nothing is
|
|
499
|
+
* allocated client-side and no occupied-id scan is needed.
|
|
500
|
+
*
|
|
501
|
+
* Returns the SAME result shape as the disk path — `success`, `research_id`,
|
|
502
|
+
* `filename`, `filepath`, `landed`, `message`, `next_steps` (+ the
|
|
503
|
+
* research_type warning) — so callers and the `/hs-res` flow read one shape.
|
|
504
|
+
* `filepath` is the console's vault-relative path (`specs/research/...`);
|
|
505
|
+
* `landed.tier` is `cloud` and `landed.on_disk` is `false`, by construction:
|
|
506
|
+
* nothing was written to this machine.
|
|
507
|
+
*/
|
|
508
|
+
async function saveResearchViaConsole({
|
|
509
|
+
vaultRoot,
|
|
510
|
+
title,
|
|
511
|
+
summary,
|
|
512
|
+
tags,
|
|
513
|
+
relatedIds,
|
|
514
|
+
researchType,
|
|
515
|
+
scope,
|
|
516
|
+
bodyMarkdown,
|
|
517
|
+
actingAgent,
|
|
518
|
+
researchTypeWasOmitted,
|
|
519
|
+
}) {
|
|
520
|
+
// No `status:` — the console seeds `open` and refuses an out-of-band status
|
|
521
|
+
// on create ([[HS-ISZP]]); only the disk path writes it itself.
|
|
522
|
+
const frontmatter = {
|
|
523
|
+
research_type: researchType,
|
|
524
|
+
scope,
|
|
525
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
526
|
+
...(relatedIds.length > 0 ? { related: relatedIds.map((id) => `[[${id}]]`) } : {}),
|
|
527
|
+
};
|
|
528
|
+
const body = {
|
|
529
|
+
type: 'research',
|
|
530
|
+
name: title,
|
|
531
|
+
summary,
|
|
532
|
+
frontmatter,
|
|
533
|
+
body: bodyMarkdown,
|
|
534
|
+
...(actingAgent ? { acting_agent: actingAgent } : {}),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
let result;
|
|
538
|
+
try {
|
|
539
|
+
result = await consoleFetch('/api/specs', {
|
|
540
|
+
method: 'POST',
|
|
541
|
+
body,
|
|
542
|
+
headers: { 'idempotency-key': randomUUID() },
|
|
543
|
+
retryOnTimeout: false,
|
|
544
|
+
});
|
|
545
|
+
} catch (err) {
|
|
546
|
+
return {
|
|
547
|
+
isError: true,
|
|
548
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'SAVE_RESEARCH_FAILED', message: String(err?.message ?? err) }) }],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!result.ok) {
|
|
553
|
+
const payload = await enrichConsoleFailurePayloadAsync(result, {
|
|
554
|
+
error: consoleErrorCode(result, 'SAVE_RESEARCH_FAILED'),
|
|
555
|
+
message: consoleErrorMessage(result, `POST /api/specs returned ${result.status}`),
|
|
556
|
+
status: result.status,
|
|
557
|
+
url: result.url,
|
|
558
|
+
console_response: result.body,
|
|
559
|
+
attempted: { type: 'research', name: title },
|
|
560
|
+
write_mode: 'cloud',
|
|
561
|
+
...(result.errorKind === 'timeout'
|
|
562
|
+
? {
|
|
563
|
+
hint:
|
|
564
|
+
'The create may still have completed server-side after this client timed out. '
|
|
565
|
+
+ 'Search for a research doc with this exact name before re-saving — a blind re-save can mint a duplicate id ([[HS-2FQY]]).',
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
isError: true,
|
|
571
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// POST returns { spec: { id, relPath, ... }, write, warnings? }.
|
|
576
|
+
const spec = result.body?.spec ?? null;
|
|
577
|
+
const researchId = typeof spec?.id === 'string' && spec.id ? spec.id : null;
|
|
578
|
+
const relPath = spec?.relPath ?? spec?.path ?? null;
|
|
579
|
+
if (!researchId) {
|
|
580
|
+
return {
|
|
581
|
+
isError: true,
|
|
582
|
+
content: [{
|
|
583
|
+
type: 'text',
|
|
584
|
+
text: JSON.stringify({
|
|
585
|
+
error: 'SAVE_RESEARCH_MALFORMED_RESPONSE',
|
|
586
|
+
message: `POST /api/specs answered ${result.status} but carried no spec.id — treat the research as unsaved and check the console before re-saving.`,
|
|
587
|
+
url: result.url,
|
|
588
|
+
console_response: result.body,
|
|
589
|
+
}, null, 2),
|
|
590
|
+
}],
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const filename = relPath ? basename(String(relPath)) : null;
|
|
594
|
+
const consoleWrite = result.body?.write ?? null;
|
|
595
|
+
const consoleUrl = result.url ?? getConsoleUrl();
|
|
596
|
+
const landed = {
|
|
597
|
+
tier: 'cloud',
|
|
598
|
+
reported_by: consoleWrite ? 'console' : 'inferred_from_console_url',
|
|
599
|
+
console_url: consoleUrl,
|
|
600
|
+
rel_path: relPath,
|
|
601
|
+
abs_path: null,
|
|
602
|
+
on_disk: false,
|
|
603
|
+
in_local_vault: false,
|
|
604
|
+
vault_root: vaultRoot ?? null,
|
|
605
|
+
console_write: consoleWrite,
|
|
606
|
+
};
|
|
607
|
+
const out = {
|
|
608
|
+
success: true,
|
|
609
|
+
research_id: researchId,
|
|
610
|
+
filename,
|
|
611
|
+
filepath: relPath,
|
|
612
|
+
landed,
|
|
613
|
+
message: `Research saved through the console at ${consoleUrl}${relPath ? ` (${relPath})` : ''} — cloud write mode, nothing written to local disk`,
|
|
614
|
+
next_steps: researchNextSteps(filename ? filename.replace(/\.md$/i, '') : researchId),
|
|
615
|
+
...(Array.isArray(result.body?.warnings) && result.body.warnings.length > 0
|
|
616
|
+
? { console_warnings: result.body.warnings }
|
|
617
|
+
: {}),
|
|
618
|
+
};
|
|
619
|
+
if (researchTypeWasOmitted) attachResearchTypeWarning(out);
|
|
620
|
+
return {
|
|
621
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* out of scope — that is [[HS-X5GJ-RES-EU6J]], a separate feature built on top of this fix).
|
|
16
16
|
* - `heyspex_firecrawl_research` is the on-demand execution surface that makes the configured
|
|
17
17
|
* Firecrawl backend REACHABLE from an interactive turn — it is a thin MCP wrapper around
|
|
18
|
-
* dev-factory/
|
|
18
|
+
* dev-factory/spexcrawl/interactive-firecrawl-tool.mjs, which itself wraps the
|
|
19
19
|
* existing, self-tested default-backends.mjs/firecrawl-adapter.mjs implementation (no
|
|
20
20
|
* search/scrape logic is reimplemented here).
|
|
21
21
|
*
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { listConfigLeaves } from './config-settings.mjs';
|
|
26
26
|
// The deep-research backend is loaded LAZILY through the bridge, never imported
|
|
27
|
-
// statically — a static import here pulls 25 SpexFleet /
|
|
27
|
+
// statically — a static import here pulls 25 SpexFleet / spexcrawl files into
|
|
28
28
|
// the MCP server's package closure. See dev-factory-bridge.mjs for the full
|
|
29
29
|
// rationale ([[HS-ZJSP-0JNK-TASK-ship-the-heyspex-mcp-server-as-an]]).
|
|
30
30
|
import { loadDreamResearch } from './dev-factory-bridge.mjs';
|
|
@@ -65,7 +65,7 @@ export async function executeResolveResearchPlan(vaultRoot) {
|
|
|
65
65
|
'reports an explicit { downgraded: true, reason } if the backend is unavailable — never substitute ' +
|
|
66
66
|
'web-native silently. IMPORTANT (research-routing-hard-rule-sonnet, "Sonnet is the fallback, ' +
|
|
67
67
|
'Firecrawl is the default"): once the fetch succeeds, hand the gathered text to DeepSeek for ' +
|
|
68
|
-
'synthesis (dev-factory/
|
|
68
|
+
'synthesis (dev-factory/spexcrawl/synthesize-cli.mjs, or the `hs-res` skill for anything ' +
|
|
69
69
|
'beyond a single lookup) — do NOT reason over/summarize the results yourself. Sonnet doing its ' +
|
|
70
70
|
'own synthesis is the fallback branch only, used after an explicit unreachability report and ' +
|
|
71
71
|
'never silently.'
|
|
@@ -85,7 +85,7 @@ export async function executeResolveResearchPlan(vaultRoot) {
|
|
|
85
85
|
/**
|
|
86
86
|
* Run one Firecrawl search or scrape for an interactive agent turn (rung 0 `sequential`) —
|
|
87
87
|
* the on-demand execution surface for whatever heyspex_resolve_research_plan resolved.
|
|
88
|
-
* Delegates entirely to dev-factory/
|
|
88
|
+
* Delegates entirely to dev-factory/spexcrawl/interactive-firecrawl-tool.mjs; this
|
|
89
89
|
* function only maps the MCP snake_case args onto that module's contract.
|
|
90
90
|
*/
|
|
91
91
|
export async function executeFirecrawlResearch(args = {}) {
|